text fixing
This commit is contained in:
@@ -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]],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -65,8 +65,7 @@ impl TextPipeline
|
||||
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
|
||||
});
|
||||
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Text Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
@@ -75,9 +74,10 @@ impl TextPipeline
|
||||
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,
|
||||
),
|
||||
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
|
||||
ViewProjUniform,
|
||||
>()
|
||||
as u64),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,8 +43,43 @@ 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 text = match world
|
||||
.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 to_camera = camera_pos - anchor;
|
||||
@@ -68,8 +103,8 @@ pub fn dialog_bubble_render_system(
|
||||
};
|
||||
let up = forward.cross(right).normalize();
|
||||
|
||||
let half_w = BUBBLE_WIDTH * 0.5;
|
||||
let total_down = body_half_h + tail_height;
|
||||
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;
|
||||
@@ -77,16 +112,28 @@ pub fn dialog_bubble_render_system(
|
||||
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] },
|
||||
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,
|
||||
size: [bubble_width, bubble_height],
|
||||
body_frac,
|
||||
corner_r: CORNER_R,
|
||||
border_w: BORDER_W,
|
||||
_pad1: [0.0; 3],
|
||||
@@ -95,22 +142,10 @@ pub fn dialog_bubble_render_system(
|
||||
_pad2: [0.0; 32],
|
||||
};
|
||||
|
||||
calls.push(BillboardDrawCall { vertices, uniforms });
|
||||
|
||||
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_half_w = bubble_width * 0.5 - border_world - TEXT_PADDING;
|
||||
let inner_top_y = body_half_h - border_world - TEXT_PADDING;
|
||||
|
||||
let text_verts = with_font_atlas(|atlas| {
|
||||
atlas.build_bubble_text(
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user