From 21de41eedda8adc6585ab0b590e6d5e41d8f8e6e Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Tue, 18 Aug 2026 02:58:59 -0400 Subject: Tinkering with TrueType font rendering --- crates/teleia/Cargo.toml | 1 + crates/teleia/src/assets/shaders/slug/frag.glsl | 72 +++++++++++++++ crates/teleia/src/assets/shaders/slug/vert.glsl | 4 + crates/teleia/src/font.rs | 112 +++++++++++++++++++++++- 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 crates/teleia/src/assets/shaders/slug/frag.glsl create mode 100644 crates/teleia/src/assets/shaders/slug/vert.glsl (limited to 'crates') diff --git a/crates/teleia/Cargo.toml b/crates/teleia/Cargo.toml index d5777a3..26c08a9 100644 --- a/crates/teleia/Cargo.toml +++ b/crates/teleia/Cargo.toml @@ -32,6 +32,7 @@ parry3d = "0.18.0" # collision detection nalgebra = {version = "0.33.2", features = ["convert-glam029"]} # linear algebra library for rapier3d byteorder = "1.5.0" # read numbers in particular endianness base64 = "0.22.1" # encode/decode base64 +ttf-parser = "0.25.1" # ttf fonts [target.'cfg(target_arch = "wasm32")'.dependencies] winit = {version = "=0.29.15", features = ["serde"]} # windowing and events diff --git a/crates/teleia/src/assets/shaders/slug/frag.glsl b/crates/teleia/src/assets/shaders/slug/frag.glsl new file mode 100644 index 0000000..206cb63 --- /dev/null +++ b/crates/teleia/src/assets/shaders/slug/frag.glsl @@ -0,0 +1,72 @@ +uniform sampler2D texture_data; +uniform int curves_len; + +struct curve { vec2 p1, p2, p3; }; +curve get_curve(int i) { + float tx = float(i * 2) / 4096.0; + float txn = float(i * 2 + 1) / 4096.0; + vec4 t1 = texture(texture_data, vec2(tx, 0.0)); + vec4 t2 = texture(texture_data, vec2(txn, 0.0)); + curve ret; + ret.p1 = t1.xy; + ret.p2 = t1.zw; + ret.p3 = t2.xy; + return ret; +} +vec2 compute_curve(curve c, float t) { + float invt = 1.0 - t; + return invt * invt * c.p1 + 2.0 * t * invt * c.p2 + t * t * c.p3; +} +ivec2 lookup_table_curve(curve c) { + bool yp1 = c.p1.y > 0.0; + bool yp2 = c.p2.y > 0.0; + bool yp3 = c.p3.y > 0.0; + int shift = (yp1 ? 2 : 0) + (yp2 ? 4 : 0) + (yp3 ? 8 : 0); + int res = 0x2e74 >> shift; + return ivec2(res & 1, (res >> 1) & 1); +} +vec2 roots_curve(curve c) { + vec2 a = c.p1 - c.p2 * 2.0 + c.p3; + vec2 b = c.p1 - c.p2; + float ra = 1.0 / a.y; + float rb = 0.5 / b.y; + float d = sqrt(max(b.y * b.y - a.y * c.p1.y, 0.0)); + float t1 = (b.y - d) * ra; + float t2 = (b.y + d) * ra; + if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = c.p1.y * rb; + return vec2(t1, t2); +} + +void main() { + vec2 c = vertex_texcoord - vec2(0.5, 0.5); + int winding = 0; + for (int i = 0; i < curves_len; ++i) { + curve b = get_curve(i); + if (distance(c, b.p1) < 0.01 || distance(c, b.p2) < 0.01 || distance(c, b.p3) < 0.01) { + frag_color = vec4(0.0, 1.0, 0.0, 1.0); + return; + } + b.p1 -= c; + b.p2 -= c; + b.p3 -= c; + ivec2 code = lookup_table_curve(b); + vec2 roots = roots_curve(b); + if (code.x > 0) { + vec2 horiz = compute_curve(b, roots.x); + if (horiz.x >= 0.0) { + winding += 1; + } + } + if (code.y > 0) { + vec2 horiz = compute_curve(b, roots.y); + if (horiz.x >= 0.0) { + winding -= 1; + } + } + } + if (winding != 0) { + frag_color = vec4(1.0, 1.0, 1.0, 1.0); + } else { + discard; + } +} diff --git a/crates/teleia/src/assets/shaders/slug/vert.glsl b/crates/teleia/src/assets/shaders/slug/vert.glsl new file mode 100644 index 0000000..e324f7e --- /dev/null +++ b/crates/teleia/src/assets/shaders/slug/vert.glsl @@ -0,0 +1,4 @@ +void main() +{ + default_main(); +} \ No newline at end of file diff --git a/crates/teleia/src/font.rs b/crates/teleia/src/font.rs index d97e96c..0b1dce7 100644 --- a/crates/teleia/src/font.rs +++ b/crates/teleia/src/font.rs @@ -1,4 +1,4 @@ -use crate::{context, mesh, state, texture}; +use crate::{context, mesh, shader, state, texture, utils}; use glow::HasContext; pub struct BitmapParams<'color> { @@ -192,3 +192,113 @@ impl Bitmap { self.render_text_parameterized(ctx, st, text, params); } } + +struct GlyphOutliner { + pos: (f32, f32), + data: Vec<((f32, f32), (f32, f32), (f32, f32))>, +} +impl GlyphOutliner { + fn new() -> Self { + Self { + pos: (0.0, 0.0), + data: Vec::new(), + } + } + fn add_curve(&mut self, p1: (f32, f32), p2: (f32, f32), p3: (f32, f32)) { + self.data.push(((p1.0, p1.1), (p2.0, p2.1), (p3.0, p3.1)),); + } +} +impl ttf_parser::OutlineBuilder for GlyphOutliner { + fn move_to(&mut self, x: f32, y: f32) { + self.pos = (x, y); + } + fn line_to(&mut self, x: f32, y: f32) { + self.add_curve(self.pos, (x, y), (x, y)); + self.pos = (x, y); + } + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.add_curve(self.pos, (x1, y1), (x, y)); + self.pos = (x, y); + } + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + panic!("cubic"); + // self.add_curve(self.pos, ((x1 + x2) / 2.0, (y1 + y2) / 2.0), (x, y)); + // self.pos = (x, y); + } + fn close(&mut self) {} +} + +pub struct Truetype { + shader: shader::Shader, + tex_bezier: texture::Texture, + face: ttf_parser::Face<'static>, +} +impl Truetype { + pub fn new(ctx: &context::Context, bs: &[u8]) -> utils::Erm { + let shader = shader::Shader::new(ctx, + include_str!("assets/shaders/slug/vert.glsl"), + include_str!("assets/shaders/slug/frag.glsl"), + ); + let mut v: Vec = Vec::new(); + v.extend_from_slice(bs); + let data = Box::leak(Box::new(v)); + let tex_bezier = texture::Texture::new_empty(ctx); + let face = ttf_parser::Face::parse(&*data, 0)?; + Ok(Self { + shader, + tex_bezier, + face, + }) + } + pub fn render_glyph(&self, + ctx: &context::Context, st: &mut state::State, + pos: glam::Vec2, sz: f32, + char: char + ) -> utils::Erm<()> { + let mut outliner = GlyphOutliner::new(); + let g = self.face.glyph_index(char).unwrap(); + let bound = self.face.outline_glyph(g, &mut outliner).unwrap(); + log::info!("glyph {:?} bound: {:?}", g, bound); + let width = (bound.x_max - bound.x_min) as f32; + let height = (bound.y_max - bound.y_min) as f32; + let curves_len = outliner.data.len() as i32; + let mut bytes = Vec::new(); + let scale_point = |bound: &ttf_parser::Rect, p: (f32, f32)| { + ((p.0 - bound.x_min as f32) / width - 0.5, + (p.1 - bound.y_min as f32) / height - 0.5) + }; + fn extend_with_point(bytes: &mut Vec, p: (f32, f32)) { + bytes.extend_from_slice(&p.0.to_ne_bytes()); + bytes.extend_from_slice(&p.1.to_ne_bytes()); + } + for (p1, p2, p3) in outliner.data { + extend_with_point(&mut bytes, scale_point(&bound, p1)); + extend_with_point(&mut bytes, scale_point(&bound, p2)); + extend_with_point(&mut bytes, scale_point(&bound, p3)); + extend_with_point(&mut bytes, (0.0, 0.0)); + } + bytes.resize(4096 * 4 * 4, 0); + self.tex_bezier.bind(ctx); + unsafe { + ctx.gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::RGBA32F as i32, + 4096, + 1, + 0, + glow::RGBA, + glow::FLOAT, + Some(&bytes), + ); + } + st.bind_2d(ctx, &self.shader); + self.shader.set_position_2d(ctx, st, + &pos, + &glam::Vec2::new(sz, height * sz / width) + ); + self.shader.set_i32(ctx, "curves_len", curves_len); + st.mesh_square.render(ctx); + Ok(()) + } +} -- cgit v1.3.1