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 LAST_CHAR: u32 = 126;
const NUM_GLYPHS: u32 = LAST_CHAR - FIRST_CHAR + 1;
const FONT_PX: f32 = 16.0;
const FONT_PX: f32 = 124.0;
struct GlyphMetrics
{
@@ -58,7 +58,10 @@ impl FontAtlas
let cell_y = row * cell_h;
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: [
(cell_x + cell_w) as f32 / atlas_w as f32,
(cell_y + cell_h) as f32 / atlas_h as f32,
@@ -129,8 +132,8 @@ impl FontAtlas
label: Some("Font Atlas Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
@@ -156,6 +159,24 @@ impl FontAtlas
/// `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
/// `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(
&self,
text: &str,
@@ -214,12 +235,18 @@ impl FontAtlas
let bl = centre - right * half_char_w - up * half_char_h;
verts.extend_from_slice(&[
TextVertex { position: tl.to_array(), uv: g.uv_min },
TextVertex {
position: tl.to_array(),
uv: g.uv_min,
},
TextVertex {
position: tr.to_array(),
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 {
position: bl.to_array(),
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::entity::EntityHandle;
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
use super::text_pipeline::TextVertex;
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
thread_local! {
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 global::{
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 types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};

View File

@@ -65,40 +65,40 @@ impl TextPipeline
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
});
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new(
std::mem::size_of::<ViewProjUniform>() as u64,
),
},
count: None,
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
ViewProjUniform,
>()
as u64),
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text ViewProj Buffer"),

View File

@@ -37,9 +37,9 @@ fn vs_main(in: VertexIn) -> VertexOut
fn fs_main(in: VertexOut) -> @location(0) vec4<f32>
{
let coverage = textureSample(glyph_atlas, glyph_sampler, in.uv).r;
if coverage < 0.5
if coverage < 0.004
{
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::world::World;
const BUBBLE_WIDTH: f32 = 2.2;
const BUBBLE_HEIGHT: f32 = 1.1;
const BODY_FRAC: f32 = 0.78;
const MAX_BUBBLE_WIDTH: f32 = 8.0;
const MIN_BUBBLE_WIDTH: f32 = 0.5;
const TAIL_HEIGHT: f32 = 0.242;
const CORNER_R: f32 = 0.18;
const BORDER_W: f32 = 0.06;
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 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 LINE_SPACING: f32 = 0.01;
@@ -43,60 +43,6 @@ pub fn dialog_bubble_render_system(
None => continue,
};
let body_half_h = BUBBLE_HEIGHT * BODY_FRAC * 0.5;
let tail_height = BUBBLE_HEIGHT * (1.0 - BODY_FRAC);
let anchor = character_pos + Vec3::Y * (HEIGHT_OFFSET + body_half_h);
let to_camera = camera_pos - anchor;
let forward = if to_camera.length_squared() > 1e-6
{
to_camera.normalize()
}
else
{
Vec3::Z
};
let up_ref = Vec3::Y;
let right = if forward.abs().dot(up_ref) > 0.99
{
Vec3::X
}
else
{
up_ref.cross(forward).normalize()
};
let up = forward.cross(right).normalize();
let half_w = BUBBLE_WIDTH * 0.5;
let total_down = body_half_h + tail_height;
let tl = anchor - right * half_w + up * body_half_h;
let tr = anchor + right * half_w + up * body_half_h;
let br = anchor + right * half_w - up * total_down;
let bl = anchor - right * half_w - up * total_down;
let vertices = [
BillboardVertex { position: tl.to_array(), uv: [0.0, 0.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 {
view_proj: view_proj.to_cols_array_2d(),
size: [BUBBLE_WIDTH, BUBBLE_HEIGHT],
body_frac: BODY_FRAC,
corner_r: CORNER_R,
border_w: BORDER_W,
_pad1: [0.0; 3],
fill_color: FILL_COLOR,
border_color: BORDER_COLOR,
_pad2: [0.0; 32],
};
calls.push(BillboardDrawCall { vertices, uniforms });
let text = match world
.dialog_bubbles
.with(bubble_entity, |b| b.current_text.clone())
@@ -105,12 +51,101 @@ pub fn dialog_bubble_render_system(
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 (draw_call, text_verts) = with_font_atlas(|atlas| {
let char_w = CHAR_WORLD_HEIGHT * atlas.aspect();
let text_verts = with_font_atlas(|atlas| {
atlas.build_bubble_text(
// 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 to_camera = camera_pos - anchor;
let forward = if to_camera.length_squared() > 1e-6
{
to_camera.normalize()
}
else
{
Vec3::Z
};
let up_ref = Vec3::Y;
let right = if forward.abs().dot(up_ref) > 0.99
{
Vec3::X
}
else
{
up_ref.cross(forward).normalize()
};
let up = forward.cross(right).normalize();
let half_w = bubble_width * 0.5;
let total_down = body_half_h + TAIL_HEIGHT;
let tl = anchor - right * half_w + up * body_half_h;
let tr = anchor + right * half_w + up * body_half_h;
let br = anchor + right * half_w - up * total_down;
let bl = anchor - right * half_w - up * total_down;
let vertices = [
BillboardVertex {
position: tl.to_array(),
uv: [0.0, 0.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 {
view_proj: view_proj.to_cols_array_2d(),
size: [bubble_width, bubble_height],
body_frac,
corner_r: CORNER_R,
border_w: BORDER_W,
_pad1: [0.0; 3],
fill_color: FILL_COLOR,
border_color: BORDER_COLOR,
_pad2: [0.0; 32],
};
let inner_half_w = bubble_width * 0.5 - border_world - TEXT_PADDING;
let inner_top_y = body_half_h - border_world - TEXT_PADDING;
let text_verts = atlas.build_bubble_text(
&text,
anchor,
right,
@@ -119,9 +154,12 @@ pub fn dialog_bubble_render_system(
inner_top_y,
CHAR_WORLD_HEIGHT,
LINE_SPACING,
)
);
(BillboardDrawCall { vertices, uniforms }, text_verts)
});
calls.push(draw_call);
all_text.extend(text_verts);
}