diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml index 4d787f23c6be0227b4cc403bdb348ca24e826b82..4b5ffdfdd09c3886eee2cb843f572d2e63bdeeab 100644 --- a/kayak_font/Cargo.toml +++ b/kayak_font/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] anyhow = { version = "1.0" } bevy = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" } +bytemuck = "1.7.2" +crevice = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" } serde = "1.0" serde_json = "1.0" serde_path_to_error = "0.1" diff --git a/kayak_font/examples/bevy.rs b/kayak_font/examples/bevy.rs index 25968860628eed43893d69a43184350cbedf8a9c..1fa0e5a6b101202f235dcda432d6b1a28fcb78ff 100644 --- a/kayak_font/examples/bevy.rs +++ b/kayak_font/examples/bevy.rs @@ -1,13 +1,31 @@ use bevy::{ + math::Vec2, prelude::{App as BevyApp, AssetServer, Commands, Handle, Res}, + render2::{camera::OrthographicCameraBundle, color::Color}, window::WindowDescriptor, PipelinedDefaultPlugins, }; use kayak_font::{KayakFont, KayakFontPlugin}; +mod renderer; +use renderer::FontRenderPlugin; +use renderer::Text; + fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font"); - dbg!(font_handle); + + commands + .spawn() + .insert(Text { + color: Color::WHITE, + content: "Hello World!".into(), + font_size: 32.0, + position: Vec2::new(5.0, 5.0), + size: Vec2::new(100.0, 100.0), + }) + .insert(font_handle); } fn main() { @@ -20,6 +38,7 @@ fn main() { }) .add_plugins(PipelinedDefaultPlugins) .add_plugin(KayakFontPlugin) + .add_plugin(FontRenderPlugin) .add_startup_system(startup) .run(); } diff --git a/kayak_font/examples/renderer/extract.rs b/kayak_font/examples/renderer/extract.rs new file mode 100644 index 0000000000000000000000000000000000000000..2900f192f62848298ebb1c930c4f133928fa84fe --- /dev/null +++ b/kayak_font/examples/renderer/extract.rs @@ -0,0 +1,47 @@ +use bevy::{ + prelude::{Assets, Commands, Handle, Query, Res}, + sprite2::Rect, +}; +use kayak_font::{CoordinateSystem, KayakFont}; + +use super::{ + pipeline::{ExtractCharBundle, ExtractedChar}, + Text, +}; + +pub fn extract( + mut commands: Commands, + fonts: Res<Assets<KayakFont>>, + texts: Query<(&Text, &Handle<KayakFont>)>, +) { + let mut extracted_texts = Vec::new(); + + for (text, font_handle) in texts.iter() { + if let Some(font) = fonts.get(font_handle) { + let layouts = font.get_layout( + CoordinateSystem::PositiveYUp, + text.position, + &text.content, + text.font_size, + ); + + for layout in layouts { + extracted_texts.push(ExtractCharBundle { + extracted_quad: ExtractedChar { + font_handle: Some(font_handle.clone()), + rect: Rect { + min: layout.position, + max: layout.position + layout.size, + }, + color: text.color, + vertex_index: 0, + char_id: font.get_char_id(layout.content).unwrap(), + z_index: 0.0, + }, + }); + } + } + } + + commands.spawn_batch(extracted_texts); +} diff --git a/kayak_font/examples/renderer/mod.rs b/kayak_font/examples/renderer/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ca7cc81a07904a8172663f116efec9b1843e803a --- /dev/null +++ b/kayak_font/examples/renderer/mod.rs @@ -0,0 +1,63 @@ +use bevy::{ + core_pipeline::Transparent2d, + prelude::{Assets, HandleUntyped, Plugin, Res, ResMut}, + reflect::TypeUuid, + render2::{ + render_asset::RenderAssets, + render_phase::DrawFunctions, + render_resource::Shader, + renderer::{RenderDevice, RenderQueue}, + texture::Image, + RenderApp, RenderStage, + }, +}; +use kayak_font::FontTextureCache; + +use self::pipeline::{DrawUI, FontPipeline, QuadMeta}; + +mod extract; +pub mod pipeline; +mod text; + +pub use text::*; + +pub const FONT_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450); + +pub struct FontRenderPlugin; + +impl Plugin for FontRenderPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap(); + let unified_shader = Shader::from_wgsl(include_str!("shader.wgsl")); + shaders.set_untracked(FONT_SHADER_HANDLE, unified_shader); + + let render_app = app.sub_app(RenderApp); + render_app + .init_resource::<QuadMeta>() + .init_resource::<FontPipeline>() + .add_system_to_stage(RenderStage::Extract, extract::extract) + .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads) + .add_system_to_stage(RenderStage::Queue, pipeline::queue_quads) + .add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture); + + let draw_quad = DrawUI::new(&mut render_app.world); + + render_app + .world + .get_resource::<DrawFunctions<Transparent2d>>() + .unwrap() + .write() + .add(draw_quad); + } +} + +fn create_and_update_font_cache_texture( + device: Res<RenderDevice>, + queue: Res<RenderQueue>, + pipeline: Res<FontPipeline>, + mut font_texture_cache: ResMut<FontTextureCache>, + images: Res<RenderAssets<Image>>, +) { + font_texture_cache.process_new(&device, &queue, pipeline.into_inner(), &images); +} diff --git a/kayak_font/examples/renderer/pipeline.rs b/kayak_font/examples/renderer/pipeline.rs new file mode 100644 index 0000000000000000000000000000000000000000..8807dd337e41e3af1840c09c75701ae2eeae358a --- /dev/null +++ b/kayak_font/examples/renderer/pipeline.rs @@ -0,0 +1,395 @@ +use bevy::{ + core::FloatOrd, + core_pipeline::Transparent2d, + ecs::system::{ + lifetimeless::{Read, SQuery, SRes}, + SystemState, + }, + math::{const_vec3, Mat4, Quat, Vec2, Vec3, Vec4}, + prelude::{Bundle, Component, Entity, FromWorld, Handle, Query, Res, ResMut, World}, + render2::{ + color::Color, + render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass}, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent, + BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages, + BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, FragmentState, FrontFace, + MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache, + RenderPipelineDescriptor, Shader, ShaderStages, TextureFormat, TextureSampleType, + TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, + VertexStepMode, + }, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage}, + view::{ViewUniformOffset, ViewUniforms}, + }, + sprite2::Rect, +}; +use bytemuck::{Pod, Zeroable}; +use crevice::std140::AsStd140; +use kayak_font::{FontRenderingPipeline, FontTextureCache, KayakFont}; + +use super::FONT_SHADER_HANDLE; + +pub struct FontPipeline { + view_layout: BindGroupLayout, + pub(crate) font_image_layout: BindGroupLayout, + pipeline: CachedPipelineId, + empty_font_texture: (GpuImage, BindGroup), +} + +const QUAD_VERTEX_POSITIONS: &[Vec3] = &[ + const_vec3!([0.0, 0.0, 0.0]), + const_vec3!([1.0, 1.0, 0.0]), + const_vec3!([0.0, 1.0, 0.0]), + const_vec3!([0.0, 0.0, 0.0]), + const_vec3!([1.0, 0.0, 0.0]), + const_vec3!([1.0, 1.0, 0.0]), +]; + +impl FontRenderingPipeline for FontPipeline { + fn get_font_image_layout(&self) -> &BindGroupLayout { + &self.font_image_layout + } +} + +impl FromWorld for FontPipeline { + fn from_world(world: &mut World) -> Self { + let world = world.cell(); + let render_device = world.get_resource::<RenderDevice>().unwrap(); + let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap(); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + // TODO: change this to ViewUniform::std140_size_static once crevice fixes this! + // Context: https://github.com/LPGhatguy/crevice/issues/29 + min_binding_size: BufferSize::new(144), + }, + count: None, + }], + label: Some("ui_view_layout"), + }); + + // Used by fonts + let font_image_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2Array, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + ], + label: Some("text_image_layout"), + }); + + let vertex_buffer_layout = VertexBufferLayout { + array_stride: 60, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 12, + shader_location: 1, + }, + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 28, + shader_location: 2, + }, + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 44, + shader_location: 3, + }, + ], + }; + + let empty_font_texture = FontTextureCache::get_empty(&render_device, &font_image_layout); + + let pipeline_desc = RenderPipelineDescriptor { + vertex: VertexState { + shader: FONT_SHADER_HANDLE.typed::<Shader>(), + entry_point: "vertex".into(), + shader_defs: vec![], + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: FONT_SHADER_HANDLE.typed::<Shader>(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::SrcAlpha, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + }], + }), + layout: Some(vec![view_layout.clone(), font_image_layout.clone()]), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + polygon_mode: PolygonMode::Fill, + clamp_depth: false, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("font_pipeline".into()), + }; + + FontPipeline { + pipeline: pipeline_cache.queue(pipeline_desc), + view_layout, + font_image_layout, + empty_font_texture, + } + } +} + +#[derive(Debug, Bundle)] +pub struct ExtractCharBundle { + pub(crate) extracted_quad: ExtractedChar, +} + +#[derive(Debug, Component, Clone)] +pub struct ExtractedChar { + pub rect: Rect, + pub color: Color, + pub vertex_index: usize, + pub char_id: u32, + pub z_index: f32, + pub font_handle: Option<Handle<KayakFont>>, +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct QuadVertex { + pub position: [f32; 3], + pub color: [f32; 4], + pub uv: [f32; 4], + pub pos_size: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, AsStd140)] +struct QuadType { + pub t: i32, +} + +pub struct QuadMeta { + vertices: BufferVec<QuadVertex>, + view_bind_group: Option<BindGroup>, +} + +impl Default for QuadMeta { + fn default() -> Self { + Self { + vertices: BufferVec::new(BufferUsages::VERTEX), + view_bind_group: None, + } + } +} + +pub fn prepare_quads( + render_device: Res<RenderDevice>, + render_queue: Res<RenderQueue>, + mut sprite_meta: ResMut<QuadMeta>, + mut extracted_quads: Query<&mut ExtractedChar>, +) { + let extracted_sprite_len = extracted_quads.iter_mut().len(); + // don't create buffers when there are no quads + if extracted_sprite_len == 0 { + return; + } + + sprite_meta.vertices.clear(); + sprite_meta.vertices.reserve( + extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(), + &render_device, + ); + + for (i, mut extracted_sprite) in extracted_quads.iter_mut().enumerate() { + let sprite_rect = extracted_sprite.rect; + let color = extracted_sprite.color.as_linear_rgba_f32(); + + let uv_min = Vec2::ZERO; + let uv_max = Vec2::ONE; + + let bottom_left = Vec4::new(uv_min.x, uv_max.y, extracted_sprite.char_id as f32, 0.0); + let top_left = Vec4::new(uv_min.x, uv_min.y, extracted_sprite.char_id as f32, 0.0); + let top_right = Vec4::new(uv_max.x, uv_min.y, extracted_sprite.char_id as f32, 0.0); + let bottom_right = Vec4::new(uv_max.x, uv_max.y, extracted_sprite.char_id as f32, 0.0); + + let uvs: [[f32; 4]; 6] = [ + bottom_left.into(), + top_right.into(), + top_left.into(), + bottom_left.into(), + bottom_right.into(), + top_right.into(), + ]; + + extracted_sprite.vertex_index = i; + for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() { + let world = Mat4::from_scale_rotation_translation( + sprite_rect.size().extend(1.0), + Quat::default(), + sprite_rect.min.extend(0.0), + ); + let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate(); + sprite_meta.vertices.push(QuadVertex { + position: final_position.into(), + color, + uv: uvs[index], + pos_size: [ + sprite_rect.min.x, + sprite_rect.min.y, + sprite_rect.size().x, + sprite_rect.size().y, + ], + }); + } + } + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); +} + +pub fn queue_quads( + draw_functions: Res<DrawFunctions<Transparent2d>>, + render_device: Res<RenderDevice>, + mut sprite_meta: ResMut<QuadMeta>, + view_uniforms: Res<ViewUniforms>, + quad_pipeline: Res<FontPipeline>, + mut extracted_sprites: Query<(Entity, &ExtractedChar)>, + mut views: Query<&mut RenderPhase<Transparent2d>>, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding, + }], + label: Some("quad_view_bind_group"), + layout: &quad_pipeline.view_layout, + })); + + let draw_quad = draw_functions.read().get_id::<DrawUI>().unwrap(); + for mut transparent_phase in views.iter_mut() { + for (entity, quad) in extracted_sprites.iter_mut() { + transparent_phase.add(Transparent2d { + draw_function: draw_quad, + pipeline: quad_pipeline.pipeline, + entity, + sort_key: FloatOrd(quad.z_index), + }); + } + } + } +} + +pub struct DrawUI { + params: SystemState<( + SRes<QuadMeta>, + SRes<FontPipeline>, + SRes<RenderPipelineCache>, + SRes<FontTextureCache>, + SQuery<Read<ViewUniformOffset>>, + SQuery<Read<ExtractedChar>>, + )>, +} + +impl DrawUI { + pub fn new(world: &mut World) -> Self { + Self { + params: SystemState::new(world), + } + } +} + +impl Draw<Transparent2d> for DrawUI { + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &Transparent2d, + ) { + let (quad_meta, unified_pipeline, pipelines, font_texture_cache, views, quads) = + self.params.get(world); + + let view_uniform = views.get(view).unwrap(); + let quad_meta = quad_meta.into_inner(); + let extracted_quad = quads.get(item.entity).unwrap(); + if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) { + pass.set_render_pipeline(pipeline); + pass.set_vertex_buffer(0, quad_meta.vertices.buffer().unwrap().slice(..)); + pass.set_bind_group( + 0, + quad_meta.view_bind_group.as_ref().unwrap(), + &[view_uniform.offset], + ); + + let unified_pipeline = unified_pipeline.into_inner(); + if let Some(font_handle) = extracted_quad.font_handle.as_ref() { + if let Some(image_bindings) = + font_texture_cache.into_inner().get_binding(font_handle) + { + pass.set_bind_group(1, image_bindings, &[]); + } else { + pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]); + } + } else { + pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]); + } + + pass.draw( + (extracted_quad.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32 + ..((extracted_quad.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32, + 0..1, + ); + } + } +} diff --git a/kayak_font/examples/renderer/shader.wgsl b/kayak_font/examples/renderer/shader.wgsl new file mode 100644 index 0000000000000000000000000000000000000000..097890a21b4d880e4b2b3fe10a5c25ee0bfa411a --- /dev/null +++ b/kayak_font/examples/renderer/shader.wgsl @@ -0,0 +1,52 @@ +[[block]] +struct View { + view_proj: mat4x4<f32>; + world_position: vec3<f32>; +}; +[[group(0), binding(0)]] +var<uniform> view: View; + +struct VertexOutput { + [[builtin(position)]] position: vec4<f32>; + [[location(0)]] color: vec4<f32>; + [[location(1)]] uv: vec3<f32>; + [[location(2)]] pos: vec2<f32>; + [[location(3)]] size: vec2<f32>; + [[location(4)]] screen_position: vec2<f32>; + [[location(5)]] border_radius: f32; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3<f32>, + [[location(1)]] vertex_color: vec4<f32>, + [[location(2)]] vertex_uv: vec4<f32>, + [[location(3)]] vertex_pos_size: vec4<f32>, +) -> VertexOutput { + var out: VertexOutput; + out.color = vertex_color; + out.pos = vertex_pos_size.xy; + out.position = view.view_proj * vec4<f32>(vertex_position, 1.0); + out.screen_position = (view.view_proj * vec4<f32>(vertex_position, 1.0)).xy; + out.uv = vertex_uv.xyz; + out.size = vertex_pos_size.zw; + out.border_radius = vertex_uv.w; + return out; +} + +[[group(1), binding(0)]] +var font_texture: texture_2d_array<f32>; +[[group(1), binding(1)]] +var font_sampler: sampler; + +[[stage(fragment)]] +fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> { + var px_range = 2.5; + var tex_dimensions = textureDimensions(font_texture); + var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y)); + var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, in.uv.y), i32(in.uv.z)); + var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b)); + var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy)); + var a = clamp(sig_dist + 0.5, 0.0, 1.0); + return vec4<f32>(in.color.rgb, a); +} \ No newline at end of file diff --git a/kayak_font/examples/renderer/text.rs b/kayak_font/examples/renderer/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..845e2e47696dd0b1b2d9f7d2969ca0b4fd5efad1 --- /dev/null +++ b/kayak_font/examples/renderer/text.rs @@ -0,0 +1,10 @@ +use bevy::{math::Vec2, prelude::Component, render2::color::Color}; + +#[derive(Component)] +pub struct Text { + pub content: String, + pub position: Vec2, + pub size: Vec2, + pub font_size: f32, + pub color: Color, +} diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs index b50fedf55a56ec26649d734ece2a59838a0e93be..000aeb6c1ef35d279553fc95f37e47f852dff3a4 100644 --- a/kayak_font/src/font.rs +++ b/kayak_font/src/font.rs @@ -25,6 +25,12 @@ pub struct LayoutRect { pub content: char, } +#[derive(Debug, Clone, Copy)] +pub enum CoordinateSystem { + PositiveYUp, + PositiveYDown, +} + impl KayakFont { pub fn new(sdf: Sdf, atlas_image: Handle<Image>) -> Self { Self { @@ -46,7 +52,13 @@ impl KayakFont { self.char_ids.get(&c).and_then(|id| Some(*id)) } - pub fn get_layout(&self, position: Vec2, content: &String, font_size: f32) -> Vec<LayoutRect> { + pub fn get_layout( + &self, + axis_alignment: CoordinateSystem, + position: Vec2, + content: &String, + font_size: f32, + ) -> Vec<LayoutRect> { let mut positions_and_size = Vec::new(); let max_glyph_size = self.sdf.max_glyph_size(); let font_ratio = font_size / self.sdf.atlas.size; @@ -66,8 +78,13 @@ impl KayakFont { None => (0.0, 0.0, 0.0, 0.0), }; + let shift_sign = match axis_alignment { + CoordinateSystem::PositiveYDown => -1.0, + CoordinateSystem::PositiveYUp => 1.0, + }; + let position_x = position.x + x + left * font_size; - let position_y = (position.y + (-top * font_size)) + font_size; + let position_y = (position.y + (shift_sign * top * font_size)) + font_size; positions_and_size.push(LayoutRect { position: Vec2::new(position_x, position_y),