diff --git a/crates/comet_renderer/src/renderer2d.rs b/crates/comet_renderer/src/renderer2d.rs index 049acd0..9bcdfd5 100644 --- a/crates/comet_renderer/src/renderer2d.rs +++ b/crates/comet_renderer/src/renderer2d.rs @@ -36,6 +36,7 @@ pub struct Renderer2D<'a> { num_indices: u32, clear_color: Color, diffuse_texture: Texture, + diffuse_bind_group_layout: wgpu::BindGroupLayout, diffuse_bind_group: wgpu::BindGroup, graphic_resource_manager: GraphicResourceManager, camera: RenderCamera, @@ -286,6 +287,7 @@ impl<'a> Renderer2D<'a> { num_indices, clear_color, diffuse_texture, + diffuse_bind_group_layout: texture_bind_group_layout, diffuse_bind_group, graphic_resource_manager, camera, @@ -614,34 +616,10 @@ impl<'a> Renderer2D<'a> { self.diffuse_bind_group = diffuse_bind_group; } - fn set_texture_atlas(&mut self) { - self.diffuse_texture = Texture::from_image(&self.device, &self.queue, self.graphic_resource_manager.texture_atlas().atlas(), None, false).unwrap(); - - let texture_bind_group_layout = - self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("texture_bind_group_layout"), - }); - + fn switch_texture(&mut self, to: Texture) { + self.diffuse_texture = to; let diffuse_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &texture_bind_group_layout, + layout: &self.diffuse_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -658,49 +636,13 @@ impl<'a> Renderer2D<'a> { self.diffuse_bind_group = diffuse_bind_group; } + fn set_texture_atlas(&mut self) { + self.switch_texture(Texture::from_image(&self.device, &self.queue, self.graphic_resource_manager.texture_atlas().atlas(), None, false).unwrap()); + } + fn set_font_atlas(&mut self, font: String) { let font_atlas = self.graphic_resource_manager.fonts().iter().find(|f| f.name() == font).unwrap(); - self.diffuse_texture = Texture::from_image(&self.device, &self.queue, font_atlas.glyphs().atlas(), None, false).unwrap(); - - let texture_bind_group_layout = - self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("texture_bind_group_layout"), - }); - - let diffuse_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &texture_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&self.diffuse_texture.view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&self.diffuse_texture.sampler), - }, - ], - label: Some("diffuse_bind_group"), - }); - - self.diffuse_bind_group = diffuse_bind_group; + self.switch_texture(Texture::from_image(&self.device, &self.queue, font_atlas.glyphs().atlas(), None, false).unwrap()); } fn get_project_root() -> std::io::Result { @@ -821,10 +763,10 @@ impl<'a> Renderer2D<'a> { ((dim_x as f32/ self.config.width as f32) * 0.5, (dim_y as f32/ self.config.height as f32) * 0.5); let vertices: &mut Vec = &mut vec![ - Vertex :: new ( [-bound_x + position.x(), bound_y + position.y(), 0.0 + position.z()], [region.x0(), region.y0()], [0.0, 0.0, 0.0, 0.0] ), - Vertex :: new ( [-bound_x + position.x(), -bound_y + position.y(), 0.0 + position.z()], [region.x0(), region.y1()], [0.0, 0.0, 0.0, 0.0] ), - Vertex :: new ( [ bound_x + position.x(), -bound_y + position.y(), 0.0 + position.z()], [region.x1(), region.y1()], [0.0, 0.0, 0.0, 0.0] ) , - Vertex :: new ( [ bound_x + position.x(), bound_y + position.y(), 0.0 + position.z()], [region.x1(), region.y0()], [0.0, 0.0, 0.0, 0.0] ) + Vertex :: new ( [-bound_x + position.x(), bound_y + position.y(), 0.0 + position.z()], [region.u0(), region.v0()], [0.0, 0.0, 0.0, 0.0] ), + Vertex :: new ( [-bound_x + position.x(), -bound_y + position.y(), 0.0 + position.z()], [region.u0(), region.v1()], [0.0, 0.0, 0.0, 0.0] ), + Vertex :: new ( [ bound_x + position.x(), -bound_y + position.y(), 0.0 + position.z()], [region.u1(), region.v1()], [0.0, 0.0, 0.0, 0.0] ) , + Vertex :: new ( [ bound_x + position.x(), bound_y + position.y(), 0.0 + position.z()], [region.u1(), region.v0()], [0.0, 0.0, 0.0, 0.0] ) ]; let buffer_size = self.vertex_data.len() as u16; @@ -838,17 +780,56 @@ impl<'a> Renderer2D<'a> { } /// A function to draw text at a given position. - pub fn draw_text_at(&mut self, text: &str, font: String, position: Point3) { + pub fn draw_text_at(&mut self, text: &str, font: String, size: f32, position: Point3) { self.set_font_atlas(font.clone()); - let mut x = position.x(); - let mut y = position.y(); + let screen_position = Point3::new(position.x()/self.config.width as f32, position.y()/self.config.height as f32, position.z()); + let scale_factor = size / self.graphic_resource_manager.fonts().iter().find(|f| f.name() == font).unwrap().size(); - for c in text.chars() { - self.draw_texture_at(font.clone() + &c.to_string(), Point3::new(x, y, position.z())); + let line_height = (self.graphic_resource_manager.fonts().iter().find(|f| f.name() == font).unwrap().line_height() / self.config.height as f32) * scale_factor; + let lines = text.split("\n").collect::>(); + + let mut x_offset = 0.0; + let mut y_offset = 0.0; + + for line in lines { + for c in line.chars() { + let region = self.get_glyph_region(c, font.clone()); + let (dim_x, dim_y) = region.dimensions(); + + let w = (dim_x as f32 / self.config.width as f32) * scale_factor; + let h = (dim_y as f32 / self.config.height as f32) * scale_factor; + + let offset_x_px = (region.offset_x() / self.config.width as f32) * scale_factor; + let offset_y_px = (region.offset_y() / self.config.height as f32) * scale_factor; + + let glyph_left = screen_position.x() + x_offset + offset_x_px; + let glyph_top = screen_position.y() - offset_y_px - y_offset; + let glyph_right = glyph_left + w; + let glyph_bottom = glyph_top - h; + + let vertices: &mut Vec = &mut vec![ + Vertex::new([ glyph_left, glyph_top, screen_position.z() ], [region.u0(), region.v0()], [0.0; 4]), + Vertex::new([ glyph_left, glyph_bottom, screen_position.z() ], [region.u0(), region.v1()], [0.0; 4]), + Vertex::new([ glyph_right, glyph_bottom, screen_position.z() ], [region.u1(), region.v1()], [0.0; 4]), + Vertex::new([ glyph_right, glyph_top, screen_position.z() ], [region.u1(), region.v0()], [0.0; 4]), + ]; + + let buffer_size = self.vertex_data.len() as u16; + let indices: &mut Vec = &mut vec![ + buffer_size, buffer_size + 1, buffer_size + 3, + buffer_size + 1, buffer_size + 2, buffer_size + 3, + ]; + + x_offset += (region.advance() / self.config.width as f32) * scale_factor; + + self.push_to_buffers(vertices, indices); + } + + y_offset += line_height; + x_offset = 0.0; } - self.set_texture_atlas(); } fn find_priority_camera(&self, cameras: Vec) -> usize { @@ -953,10 +934,10 @@ impl<'a> Renderer2D<'a> { let buffer_size = vertex_buffer.len() as u16; vertex_buffer.append(&mut vec![ - Vertex :: new ( [-bound_x + position.x(), bound_y + position.y(), 0.0], [region.x0(), region.y0()], [0.0, 0.0, 0.0, 0.0] ), - Vertex :: new ( [-bound_x + position.x(), -bound_y + position.y(), 0.0], [region.x0(), region.y1()], [0.0, 0.0, 0.0, 0.0] ), - Vertex :: new ( [ bound_x + position.x(), -bound_y + position.y(), 0.0], [region.x1(), region.y1()], [0.0, 0.0, 0.0, 0.0] ) , - Vertex :: new ( [ bound_x + position.x(), bound_y + position.y(), 0.0], [region.x1(), region.y0()], [0.0, 0.0, 0.0, 0.0] ) + Vertex :: new ( [-bound_x + position.x(), bound_y + position.y(), 0.0], [region.u0(), region.v0()], [0.0, 0.0, 0.0, 0.0] ), + Vertex :: new ( [-bound_x + position.x(), -bound_y + position.y(), 0.0], [region.u0(), region.v1()], [0.0, 0.0, 0.0, 0.0] ), + Vertex :: new ( [ bound_x + position.x(), -bound_y + position.y(), 0.0], [region.u1(), region.v1()], [0.0, 0.0, 0.0, 0.0] ) , + Vertex :: new ( [ bound_x + position.x(), bound_y + position.y(), 0.0], [region.u1(), region.v0()], [0.0, 0.0, 0.0, 0.0] ) ]); index_buffer.append(&mut vec![ @@ -1125,10 +1106,10 @@ impl<'a> Renderer2D<'a> { let buffer_size = vertex_buffer.len() as u16; vertex_buffer.append(&mut vec![ - Vertex::new([-bound_x + position.x(), bound_y + position.y(), 0.0], [region.x0(), region.y0()], [0.0, 0.0, 0.0, 0.0]), - Vertex::new([-bound_x + position.x(), -bound_y + position.y(), 0.0], [region.x0(), region.y1()], [0.0, 0.0, 0.0, 0.0]), - Vertex::new([bound_x + position.x(), -bound_y + position.y(), 0.0], [region.x1(), region.y1()], [0.0, 0.0, 0.0, 0.0]), - Vertex::new([bound_x + position.x(), bound_y + position.y(), 0.0], [region.x1(), region.y0()], [0.0, 0.0, 0.0, 0.0]) + Vertex::new([-bound_x + position.x(), bound_y + position.y(), 0.0], [region.u0(), region.v0()], [0.0, 0.0, 0.0, 0.0]), + Vertex::new([-bound_x + position.x(), -bound_y + position.y(), 0.0], [region.u0(), region.v1()], [0.0, 0.0, 0.0, 0.0]), + Vertex::new([bound_x + position.x(), -bound_y + position.y(), 0.0], [region.u1(), region.v1()], [0.0, 0.0, 0.0, 0.0]), + Vertex::new([bound_x + position.x(), bound_y + position.y(), 0.0], [region.u1(), region.v0()], [0.0, 0.0, 0.0, 0.0]) ]); index_buffer.append(&mut vec![ diff --git a/crates/comet_resources/src/font.rs b/crates/comet_resources/src/font.rs index 5215bd6..a7c4748 100644 --- a/crates/comet_resources/src/font.rs +++ b/crates/comet_resources/src/font.rs @@ -1,17 +1,31 @@ use image::{DynamicImage, Rgba, RgbaImage}; use ab_glyph::{FontArc, PxScale, ScaleFont, Glyph, point, Font as AbFont}; +use comet_log::debug; use crate::texture_atlas::{TextureAtlas, TextureRegion}; +pub struct GlyphData { + pub name: String, + pub render: DynamicImage, + pub advance: f32, + pub offset_x: f32, + pub offset_y: f32, +} + pub struct Font { name: String, + size: f32, + line_height: f32, glyphs: TextureAtlas, } impl Font { pub fn new(path: &str, size: f32) -> Self { + let (glyphs, line_height) = Self::generate_atlas(path, size); Font { name: path.to_string(), - glyphs: Self::generate_atlas(path, size) + size, + line_height, + glyphs } } @@ -19,6 +33,14 @@ impl Font { &self.name } + pub fn size(&self) -> f32 { + self.size + } + + pub fn line_height(&self) -> f32 { + self.line_height + } + pub fn glyphs(&self) -> &TextureAtlas { &self.glyphs } @@ -27,31 +49,41 @@ impl Font { self.glyphs.textures().get(&ch.to_string()) } - fn generate_atlas(path: &str, size: f32) -> TextureAtlas { + fn generate_atlas(path: &str, size: f32) -> (TextureAtlas, f32) { let font_data = std::fs::read(path).expect("Failed to read font file"); let font = FontArc::try_from_vec(font_data).expect("Failed to load font"); let scale = PxScale::from(size); let scaled_font = font.as_scaled(scale); - let mut names = Vec::new(); - let mut images = Vec::new(); + let mut glyphs: Vec = Vec::new(); for code_point in 0x0020..=0x007E { if let Some(ch) = std::char::from_u32(code_point) { - if font.glyph_id(ch).0 == 0 { + let glyph_id = font.glyph_id(ch); + if glyph_id.0 == 0 { continue; } - names.push(ch.to_string()); + if ch == ' ' { + let advance = scaled_font.h_advance(glyph_id); + glyphs.push(GlyphData { + name: ch.to_string(), + render: DynamicImage::new_rgba8(0, 0), // no bitmap + advance, + offset_x: 0.0, + offset_y: 0.0, + }); + continue; + } let glyph = Glyph { - id: font.glyph_id(ch), + id: glyph_id, scale, position: point(0.0, 0.0), }; - if let Some(outline) = scaled_font.outline_glyph(glyph) { + if let Some(outline) = scaled_font.outline_glyph(glyph.clone()) { let bounds = outline.px_bounds(); let width = bounds.width().ceil() as u32; let height = bounds.height().ceil() as u32; @@ -70,11 +102,19 @@ impl Font { image.put_pixel(x, y, Rgba([255, 255, 255, alpha])); }); - images.push(DynamicImage::ImageRgba8(image)); + glyphs.push( + GlyphData { + name: ch.to_string(), + render: DynamicImage::ImageRgba8(image), + advance: scaled_font.h_advance(glyph_id), + offset_x: bounds.min.x, + offset_y: bounds.min.y, + } + ) } } } - TextureAtlas::from_textures(names, images) + (TextureAtlas::from_glyphs(glyphs), scaled_font.ascent() - scaled_font.descent()) } } \ No newline at end of file diff --git a/crates/comet_resources/src/texture_atlas.rs b/crates/comet_resources/src/texture_atlas.rs index 9002212..d4e1fc6 100644 --- a/crates/comet_resources/src/texture_atlas.rs +++ b/crates/comet_resources/src/texture_atlas.rs @@ -4,47 +4,66 @@ use std::time::Instant; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat}; use comet_log::*; use wgpu::{Device, FilterMode, TextureFormat, TextureUsages}; +use crate::font::GlyphData; use crate::Texture; #[derive(Debug)] pub struct TextureRegion { - x0: f32, - y0: f32, - x1: f32, - y1: f32, - dimensions: (u32, u32) + u0: f32, + v0: f32, + u1: f32, + v1: f32, + dimensions: (u32, u32), + advance: f32, + offset_x: f32, + offset_y: f32, } impl TextureRegion { - pub fn new(x0: f32, y0: f32, x1: f32, y1: f32, dimensions: (u32, u32)) -> Self { + pub fn new(u0: f32, v0: f32, u1: f32, v1: f32, dimensions: (u32, u32), advance: f32, offset_x: f32, offset_y: f32) -> Self { Self { - x0, - y0, - x1, - y1, - dimensions + u0, + v0, + u1, + v1, + dimensions, + advance, + offset_x, + offset_y, } } - pub fn x0(&self) -> f32 { - self.x0 + pub fn u0(&self) -> f32 { + self.u0 } - pub fn x1(&self) -> f32 { - self.x1 + pub fn u1(&self) -> f32 { + self.u1 } - pub fn y0(&self) -> f32 { - self.y0 + pub fn v0(&self) -> f32 { + self.v0 } - pub fn y1(&self) -> f32 { - self.y1 + pub fn v1(&self) -> f32 { + self.v1 } pub fn dimensions(&self) -> (u32, u32) { self.dimensions } + + pub fn advance(&self) -> f32 { + self.advance + } + + pub fn offset_x(&self) -> f32 { + self.offset_x + } + + pub fn offset_y(&self) -> f32 { + self.offset_y + } } #[derive(Debug)] @@ -152,7 +171,10 @@ impl TextureAtlas { y_offset as f32 / height as f32, (x_offset + texture.width()) as f32 / width as f32, (y_offset + texture.height()) as f32 / height as f32, - texture.dimensions() + texture.dimensions(), + 0.0, + 0.0, + 0.0 )); x_offset += texture.width(); } @@ -201,7 +223,10 @@ impl TextureAtlas { y_offset as f32 / height as f32, (x_offset + texture.width()) as f32 / width as f32, (y_offset + texture.height()) as f32 / height as f32, - texture.dimensions() + texture.dimensions(), + 0.0, + 0.0, + 0.0 )); x_offset += texture.width(); } @@ -214,6 +239,58 @@ impl TextureAtlas { } } + pub fn from_glyphs(mut glyphs: Vec) -> Self { + glyphs.sort_by(|a, b| b.render.height().cmp(&a.render.height())); + + let height = Self::calculate_atlas_height( + &glyphs.iter().map(|g| g.render.clone()).collect::>() + ); + let width = Self::calculate_atlas_width( + &glyphs.iter().map(|g| g.render.clone()).collect::>() + ); + + let mut base = DynamicImage::new_rgba8(width, height); + let mut regions = HashMap::new(); + let mut current_row_height = glyphs[0].render.height(); + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; + + for g in glyphs.iter() { + let glyph_w = g.render.width(); + let glyph_h = g.render.height(); + + if glyph_h != current_row_height { + y_offset += current_row_height; + x_offset = 0; + current_row_height = glyph_h; + } + + Self::insert_texture_at(&mut base, &g.render, x_offset, y_offset); + + let u0 = x_offset as f32 / width as f32; + let v0 = y_offset as f32 / height as f32; + let u1 = (x_offset + glyph_w) as f32 / width as f32; + let v1 = (y_offset + glyph_h) as f32 / height as f32; + + let region = TextureRegion::new( + u0, v0, u1, v1, + (glyph_w, glyph_h), + g.advance, + g.offset_x, + g.offset_y, + ); + + regions.insert(g.name.clone(), region); + + x_offset += glyph_w; + } + + TextureAtlas { + atlas: base, + textures: regions, + } + } + pub fn atlas(&self) -> &DynamicImage { &self.atlas }