text fixing

This commit is contained in:
Jonas H
2026-03-29 10:47:10 +02:00
parent 75a046d92a
commit 909ae8612a
6 changed files with 172 additions and 106 deletions

View File

@@ -10,7 +10,7 @@ const ATLAS_ROWS: u32 = 8;
const FIRST_CHAR: u32 = 32; const FIRST_CHAR: u32 = 32;
const LAST_CHAR: u32 = 126; const LAST_CHAR: u32 = 126;
const NUM_GLYPHS: u32 = LAST_CHAR - FIRST_CHAR + 1; const NUM_GLYPHS: u32 = LAST_CHAR - FIRST_CHAR + 1;
const FONT_PX: f32 = 16.0; const FONT_PX: f32 = 124.0;
struct GlyphMetrics struct GlyphMetrics
{ {
@@ -58,7 +58,10 @@ impl FontAtlas
let cell_y = row * cell_h; let cell_y = row * cell_h;
glyphs.push(GlyphMetrics { glyphs.push(GlyphMetrics {
uv_min: [cell_x as f32 / atlas_w as f32, cell_y as f32 / atlas_h as f32], uv_min: [
cell_x as f32 / atlas_w as f32,
cell_y as f32 / atlas_h as f32,
],
uv_max: [ uv_max: [
(cell_x + cell_w) as f32 / atlas_w as f32, (cell_x + cell_w) as f32 / atlas_w as f32,
(cell_y + cell_h) as f32 / atlas_h as f32, (cell_y + cell_h) as f32 / atlas_h as f32,
@@ -129,8 +132,8 @@ impl FontAtlas
label: Some("Font Atlas Sampler"), label: Some("Font Atlas Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest, mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Linear,
..Default::default() ..Default::default()
}); });
@@ -156,6 +159,24 @@ impl FontAtlas
/// `inner_top_y` y-offset (up-axis) of the top edge of the text area /// `inner_top_y` y-offset (up-axis) of the top edge of the text area
/// `char_world_h` world-space height of one character cell /// `char_world_h` world-space height of one character cell
/// `line_spacing` extra vertical gap between lines in world units /// `line_spacing` extra vertical gap between lines in world units
/// Returns `(n_lines, max_chars_in_any_line)` for the given text wrapped at `inner_half_w`.
pub fn measure_text(&self, text: &str, char_world_h: f32, inner_half_w: f32) -> (usize, usize)
{
let char_w = char_world_h * self.aspect();
let chars_per_line = ((inner_half_w * 2.0) / char_w).floor() as usize;
if chars_per_line == 0
{
return (1, 0);
}
let lines = word_wrap(text, chars_per_line);
let n_lines = lines.len().max(1);
let max_chars = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
(n_lines, max_chars)
}
pub fn build_bubble_text( pub fn build_bubble_text(
&self, &self,
text: &str, text: &str,
@@ -214,12 +235,18 @@ impl FontAtlas
let bl = centre - right * half_char_w - up * half_char_h; let bl = centre - right * half_char_w - up * half_char_h;
verts.extend_from_slice(&[ verts.extend_from_slice(&[
TextVertex { position: tl.to_array(), uv: g.uv_min }, TextVertex {
position: tl.to_array(),
uv: g.uv_min,
},
TextVertex { TextVertex {
position: tr.to_array(), position: tr.to_array(),
uv: [g.uv_max[0], g.uv_min[1]], uv: [g.uv_max[0], g.uv_min[1]],
}, },
TextVertex { position: br.to_array(), uv: g.uv_max }, TextVertex {
position: br.to_array(),
uv: g.uv_max,
},
TextVertex { TextVertex {
position: bl.to_array(), position: bl.to_array(),
uv: [g.uv_min[0], g.uv_max[1]], uv: [g.uv_min[0], g.uv_max[1]],

View File

@@ -9,8 +9,8 @@ use std::cell::RefCell;
use crate::debug::DebugMode; use crate::debug::DebugMode;
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
use super::text_pipeline::TextVertex; use super::text_pipeline::TextVertex;
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
thread_local! { thread_local! {
static GLOBAL_RENDERER: RefCell<Option<Renderer>> = RefCell::new(None); static GLOBAL_RENDERER: RefCell<Option<Renderer>> = RefCell::new(None);

View File

@@ -15,7 +15,8 @@ pub use billboard::{BillboardDrawCall, BillboardPipeline};
pub use font_atlas::FontAtlas; pub use font_atlas::FontAtlas;
pub use global::{ pub use global::{
aspect_ratio, init, init_snow_light_accumulation, render, set_selected_entity, set_snow_depth, aspect_ratio, init, init_snow_light_accumulation, render, set_selected_entity, set_snow_depth,
set_terrain_data, update_spotlights, with_device, with_font_atlas, with_queue, with_surface_format, set_terrain_data, update_spotlights, with_device, with_font_atlas, with_queue,
with_surface_format,
}; };
pub use text_pipeline::TextVertex; pub use text_pipeline::TextVertex;
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};

View File

@@ -65,8 +65,7 @@ impl TextPipeline
source: wgpu::ShaderSource::Wgsl(shader_src.into()), source: wgpu::ShaderSource::Wgsl(shader_src.into()),
}); });
let bind_group_layout = let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Bind Group Layout"), label: Some("Text Bind Group Layout"),
entries: &[ entries: &[
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
@@ -75,9 +74,10 @@ impl TextPipeline
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false, has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new( min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
std::mem::size_of::<ViewProjUniform>() as u64, ViewProjUniform,
), >()
as u64),
}, },
count: None, count: None,
}, },

View File

@@ -37,9 +37,9 @@ fn vs_main(in: VertexIn) -> VertexOut
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> fn fs_main(in: VertexOut) -> @location(0) vec4<f32>
{ {
let coverage = textureSample(glyph_atlas, glyph_sampler, in.uv).r; let coverage = textureSample(glyph_atlas, glyph_sampler, in.uv).r;
if coverage < 0.5 if coverage < 0.004
{ {
discard; discard;
} }
return vec4<f32>(1.0, 1.0, 1.0, 1.0); return vec4<f32>(1.0, 1.0, 1.0, coverage);
} }

View File

@@ -4,9 +4,9 @@ use crate::render::billboard::{BillboardDrawCall, BillboardVertex, BubbleUniform
use crate::render::{with_font_atlas, TextVertex}; use crate::render::{with_font_atlas, TextVertex};
use crate::world::World; use crate::world::World;
const BUBBLE_WIDTH: f32 = 2.2; const MAX_BUBBLE_WIDTH: f32 = 8.0;
const BUBBLE_HEIGHT: f32 = 1.1; const MIN_BUBBLE_WIDTH: f32 = 0.5;
const BODY_FRAC: f32 = 0.78; const TAIL_HEIGHT: f32 = 0.242;
const CORNER_R: f32 = 0.18; const CORNER_R: f32 = 0.18;
const BORDER_W: f32 = 0.06; const BORDER_W: f32 = 0.06;
const HEIGHT_OFFSET: f32 = 8.2; const HEIGHT_OFFSET: f32 = 8.2;
@@ -14,7 +14,7 @@ const HEIGHT_OFFSET: f32 = 8.2;
const FILL_COLOR: [f32; 4] = [0.05, 0.05, 0.05, 1.0]; const FILL_COLOR: [f32; 4] = [0.05, 0.05, 0.05, 1.0];
const BORDER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; const BORDER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const CHAR_WORLD_HEIGHT: f32 = 0.36; const CHAR_WORLD_HEIGHT: f32 = 0.84;
const TEXT_PADDING: f32 = 0.06; const TEXT_PADDING: f32 = 0.06;
const LINE_SPACING: f32 = 0.01; const LINE_SPACING: f32 = 0.01;
@@ -43,8 +43,43 @@ pub fn dialog_bubble_render_system(
None => continue, None => continue,
}; };
let body_half_h = BUBBLE_HEIGHT * BODY_FRAC * 0.5; let text = match world
let tail_height = BUBBLE_HEIGHT * (1.0 - BODY_FRAC); .dialog_bubbles
.with(bubble_entity, |b| b.current_text.clone())
{
Some(t) => t,
None => continue,
};
let (draw_call, text_verts) = with_font_atlas(|atlas| {
let char_w = CHAR_WORLD_HEIGHT * atlas.aspect();
// First-pass: measure text at max width using a conservative padding estimate.
// BORDER_W is applied against body_height (the smaller dimension); TAIL_HEIGHT
// is a safe lower-bound for body_height, giving a slight over-estimate of padding.
let approx_pad = BORDER_W * TAIL_HEIGHT + TEXT_PADDING;
let max_inner_half_w = ((MAX_BUBBLE_WIDTH - 2.0 * approx_pad) * 0.5).max(char_w);
let (n_lines, max_line_chars) =
atlas.measure_text(&text, CHAR_WORLD_HEIGHT, max_inner_half_w);
// Compute body height analytically.
// border_world = BORDER_W * body_height (body is the smaller dimension)
// body_height = text_h + 2 * (border_world + TEXT_PADDING)
// → body_height * (1 - 2*BORDER_W) = text_h + 2*TEXT_PADDING
let text_h = n_lines as f32 * CHAR_WORLD_HEIGHT
+ n_lines.saturating_sub(1) as f32 * LINE_SPACING;
let body_height = (text_h + 2.0 * TEXT_PADDING) / (1.0 - 2.0 * BORDER_W);
// Bubble width: fit the longest line exactly, then clamp to [MIN, MAX].
let border_world = BORDER_W * body_height;
let needed_width = max_line_chars as f32 * char_w + 2.0 * (border_world + TEXT_PADDING);
let bubble_width = needed_width.clamp(MIN_BUBBLE_WIDTH, MAX_BUBBLE_WIDTH);
let bubble_height = body_height + TAIL_HEIGHT;
let body_frac = body_height / bubble_height;
// Billboard orientation
let body_half_h = body_height * 0.5;
let anchor = character_pos + Vec3::Y * (HEIGHT_OFFSET + body_half_h); let anchor = character_pos + Vec3::Y * (HEIGHT_OFFSET + body_half_h);
let to_camera = camera_pos - anchor; let to_camera = camera_pos - anchor;
@@ -68,8 +103,8 @@ pub fn dialog_bubble_render_system(
}; };
let up = forward.cross(right).normalize(); let up = forward.cross(right).normalize();
let half_w = BUBBLE_WIDTH * 0.5; let half_w = bubble_width * 0.5;
let total_down = body_half_h + tail_height; let total_down = body_half_h + TAIL_HEIGHT;
let tl = anchor - right * half_w + up * body_half_h; let tl = anchor - right * half_w + up * body_half_h;
let tr = anchor + right * half_w + up * body_half_h; let tr = anchor + right * half_w + up * body_half_h;
@@ -77,16 +112,28 @@ pub fn dialog_bubble_render_system(
let bl = anchor - right * half_w - up * total_down; let bl = anchor - right * half_w - up * total_down;
let vertices = [ let vertices = [
BillboardVertex { position: tl.to_array(), uv: [0.0, 0.0] }, BillboardVertex {
BillboardVertex { position: tr.to_array(), uv: [1.0, 0.0] }, position: tl.to_array(),
BillboardVertex { position: br.to_array(), uv: [1.0, 1.0] }, uv: [0.0, 0.0],
BillboardVertex { position: bl.to_array(), uv: [0.0, 1.0] }, },
BillboardVertex {
position: tr.to_array(),
uv: [1.0, 0.0],
},
BillboardVertex {
position: br.to_array(),
uv: [1.0, 1.0],
},
BillboardVertex {
position: bl.to_array(),
uv: [0.0, 1.0],
},
]; ];
let uniforms = BubbleUniforms { let uniforms = BubbleUniforms {
view_proj: view_proj.to_cols_array_2d(), view_proj: view_proj.to_cols_array_2d(),
size: [BUBBLE_WIDTH, BUBBLE_HEIGHT], size: [bubble_width, bubble_height],
body_frac: BODY_FRAC, body_frac,
corner_r: CORNER_R, corner_r: CORNER_R,
border_w: BORDER_W, border_w: BORDER_W,
_pad1: [0.0; 3], _pad1: [0.0; 3],
@@ -95,22 +142,10 @@ pub fn dialog_bubble_render_system(
_pad2: [0.0; 32], _pad2: [0.0; 32],
}; };
calls.push(BillboardDrawCall { vertices, uniforms }); let inner_half_w = bubble_width * 0.5 - border_world - TEXT_PADDING;
let text = match world
.dialog_bubbles
.with(bubble_entity, |b| b.current_text.clone())
{
Some(t) => t,
None => continue,
};
let border_world = BORDER_W * BUBBLE_WIDTH.min(BUBBLE_HEIGHT * BODY_FRAC);
let inner_half_w = BUBBLE_WIDTH * 0.5 - border_world - TEXT_PADDING;
let inner_top_y = body_half_h - border_world - TEXT_PADDING; let inner_top_y = body_half_h - border_world - TEXT_PADDING;
let text_verts = with_font_atlas(|atlas| { let text_verts = atlas.build_bubble_text(
atlas.build_bubble_text(
&text, &text,
anchor, anchor,
right, right,
@@ -119,9 +154,12 @@ pub fn dialog_bubble_render_system(
inner_top_y, inner_top_y,
CHAR_WORLD_HEIGHT, CHAR_WORLD_HEIGHT,
LINE_SPACING, LINE_SPACING,
) );
(BillboardDrawCall { vertices, uniforms }, text_verts)
}); });
calls.push(draw_call);
all_text.extend(text_verts); all_text.extend(text_verts);
} }