1use glam::{Vec2, Vec3, Vec4, Mat4};
8use std::collections::HashMap;
9
10use crate::glyph::glyph_mesh::GlyphMeshCache;
11use crate::glyph::glyph_materials::GlyphMaterial;
12
13#[repr(C)]
17#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
18pub struct Glyph3DInstance {
19 pub model_matrix: [f32; 16],
21 pub base_color: [f32; 4],
23 pub emission: f32,
25 pub metallic: f32,
27 pub roughness: f32,
29 pub animation_phase: f32,
31}
32
33const _: () = assert!(std::mem::size_of::<Glyph3DInstance>() == 96);
34
35impl Glyph3DInstance {
36 pub fn new(transform: Mat4, material: &GlyphMaterial, phase: f32) -> Self {
37 Self {
38 model_matrix: transform.to_cols_array(),
39 base_color: material.base_color,
40 emission: material.emission,
41 metallic: material.metallic,
42 roughness: material.roughness,
43 animation_phase: phase,
44 }
45 }
46}
47
48pub struct Glyph3DBatch {
52 pub character: char,
53 pub instances: Vec<Glyph3DInstance>,
54}
55
56impl Glyph3DBatch {
57 pub fn instance_count(&self) -> u32 { self.instances.len() as u32 }
58
59 pub fn instance_bytes(&self) -> &[u8] {
60 bytemuck::cast_slice(&self.instances)
61 }
62}
63
64#[derive(Clone, Debug)]
67pub struct Render3DConfig {
68 pub enable_3d: bool,
69 pub extrusion_depth: f32,
70 pub rotation_variation: f32,
71 pub scale_pulse_amount: f32,
72 pub bevel: bool,
73}
74
75impl Default for Render3DConfig {
76 fn default() -> Self {
77 Self {
78 enable_3d: true,
79 extrusion_depth: 0.3,
80 rotation_variation: 0.05,
81 scale_pulse_amount: 0.02,
82 bevel: false,
83 }
84 }
85}
86
87pub struct Glyph3DRenderer {
90 per_char_instances: HashMap<char, Vec<Glyph3DInstance>>,
91 batches: Vec<Glyph3DBatch>,
92 pub config: Render3DConfig,
93 frame_count: u64,
94}
95
96impl Glyph3DRenderer {
97 pub fn new(config: Render3DConfig) -> Self {
98 Self {
99 per_char_instances: HashMap::new(),
100 batches: Vec::new(),
101 config,
102 frame_count: 0,
103 }
104 }
105
106 pub fn begin_frame(&mut self) {
108 self.per_char_instances.clear();
109 self.batches.clear();
110 self.frame_count += 1;
111 }
112
113 pub fn submit_glyph(&mut self, ch: char, transform: Mat4, material: &GlyphMaterial, phase: f32) {
115 let final_transform = per_glyph_transform(transform, phase, &self.config);
116 let instance = Glyph3DInstance::new(final_transform, material, phase);
117 self.per_char_instances.entry(ch).or_default().push(instance);
118 }
119
120 pub fn build_batches(&mut self) {
122 self.batches.clear();
123 let mut chars: Vec<char> = self.per_char_instances.keys().copied().collect();
124 chars.sort();
125
126 for ch in chars {
127 if let Some(instances) = self.per_char_instances.get(&ch) {
128 if !instances.is_empty() {
129 self.batches.push(Glyph3DBatch {
130 character: ch,
131 instances: instances.clone(),
132 });
133 }
134 }
135 }
136 }
137
138 pub fn batches(&self) -> &[Glyph3DBatch] {
140 &self.batches
141 }
142
143 pub fn total_instances(&self) -> usize {
145 self.batches.iter().map(|b| b.instances.len()).sum()
146 }
147
148 pub fn draw_call_count(&self) -> usize {
150 self.batches.len()
151 }
152}
153
154impl Default for Glyph3DRenderer {
155 fn default() -> Self { Self::new(Render3DConfig::default()) }
156}
157
158pub fn per_glyph_transform(base: Mat4, phase: f32, config: &Render3DConfig) -> Mat4 {
160 let hash = (phase * 12345.6789).sin() * 43758.5453;
162 let rot_offset = (hash.fract() - 0.5) * config.rotation_variation;
163
164 let pulse = 1.0 + (phase * 3.0).sin() * config.scale_pulse_amount;
166
167 let rotation = Mat4::from_rotation_z(rot_offset)
168 * Mat4::from_rotation_y(rot_offset * 0.5);
169 let scale = Mat4::from_scale(Vec3::splat(pulse));
170
171 base * rotation * scale
172}
173
174pub const GLYPH_3D_VERT: &str = r#"
178#version 330 core
179
180// Per-vertex
181layout(location = 0) in vec3 a_position;
182layout(location = 1) in vec3 a_normal;
183layout(location = 2) in vec2 a_uv;
184
185// Per-instance (model matrix uses 4 attribute slots)
186layout(location = 3) in vec4 i_model_col0;
187layout(location = 4) in vec4 i_model_col1;
188layout(location = 5) in vec4 i_model_col2;
189layout(location = 6) in vec4 i_model_col3;
190layout(location = 7) in vec4 i_base_color;
191layout(location = 8) in float i_emission;
192layout(location = 9) in float i_metallic;
193layout(location = 10) in float i_roughness;
194layout(location = 11) in float i_anim_phase;
195
196uniform mat4 u_view_proj;
197uniform float u_time;
198
199out vec3 v_world_pos;
200out vec3 v_world_normal;
201out vec2 v_uv;
202out vec4 v_base_color;
203out float v_emission;
204out float v_metallic;
205out float v_roughness;
206out float v_anim_phase;
207
208void main() {
209 mat4 model = mat4(i_model_col0, i_model_col1, i_model_col2, i_model_col3);
210 vec4 world_pos = model * vec4(a_position, 1.0);
211 mat3 normal_mat = transpose(inverse(mat3(model)));
212
213 gl_Position = u_view_proj * world_pos;
214
215 v_world_pos = world_pos.xyz;
216 v_world_normal = normalize(normal_mat * a_normal);
217 v_uv = a_uv;
218 v_base_color = i_base_color;
219 v_emission = i_emission;
220 v_metallic = i_metallic;
221 v_roughness = i_roughness;
222 v_anim_phase = i_anim_phase;
223}
224"#;
225
226pub const GLYPH_3D_FRAG: &str = r#"
228#version 330 core
229
230in vec3 v_world_pos;
231in vec3 v_world_normal;
232in vec2 v_uv;
233in vec4 v_base_color;
234in float v_emission;
235in float v_metallic;
236in float v_roughness;
237in float v_anim_phase;
238
239uniform float u_time;
240
241// G-buffer outputs
242layout(location = 0) out vec4 o_albedo; // RGB albedo + alpha
243layout(location = 1) out vec4 o_normal; // world-space normal (RGB) + unused
244layout(location = 2) out vec4 o_material; // R=metallic, G=roughness, B=subsurface, A=unused
245layout(location = 3) out vec4 o_emission; // RGB emission color + intensity
246
247void main() {
248 // Emission pulsing based on animation phase
249 float pulse = 1.0 + sin(v_anim_phase * 3.14159 + u_time * 2.0) * 0.1;
250 float final_emission = v_emission * pulse;
251
252 o_albedo = v_base_color;
253 o_normal = vec4(normalize(v_world_normal) * 0.5 + 0.5, 1.0);
254 o_material = vec4(v_metallic, v_roughness, 0.0, 1.0);
255 o_emission = vec4(v_base_color.rgb * final_emission, final_emission);
256}
257"#;
258
259pub struct Glyph3DVaoLayout;
264
265impl Glyph3DVaoLayout {
266 pub const VERTEX_STRIDE: i32 = 32;
268 pub const INSTANCE_STRIDE: i32 = 96;
270
271 pub const VERTEX_ATTRIBS: [(u32, i32, i32); 3] = [
273 (0, 3, 0), (1, 3, 12), (2, 2, 24), ];
277
278 pub const INSTANCE_ATTRIBS: [(u32, i32, i32); 9] = [
280 (3, 4, 0), (4, 4, 16), (5, 4, 32), (6, 4, 48), (7, 4, 64), (8, 1, 80), (9, 1, 84), (10, 1, 88), (11, 1, 92), ];
290}
291
292#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn instance_size() {
300 assert_eq!(std::mem::size_of::<Glyph3DInstance>(), 96);
301 }
302
303 #[test]
304 fn submit_and_batch() {
305 let mut renderer = Glyph3DRenderer::default();
306 renderer.begin_frame();
307
308 let mat = GlyphMaterial::player();
309 renderer.submit_glyph('A', Mat4::IDENTITY, &mat, 0.0);
310 renderer.submit_glyph('A', Mat4::IDENTITY, &mat, 0.5);
311 renderer.submit_glyph('B', Mat4::IDENTITY, &mat, 0.0);
312
313 renderer.build_batches();
314 assert_eq!(renderer.draw_call_count(), 2); assert_eq!(renderer.total_instances(), 3);
316 }
317
318 #[test]
319 fn batches_sorted_by_character() {
320 let mut renderer = Glyph3DRenderer::default();
321 renderer.begin_frame();
322
323 let mat = GlyphMaterial::player();
324 renderer.submit_glyph('Z', Mat4::IDENTITY, &mat, 0.0);
325 renderer.submit_glyph('A', Mat4::IDENTITY, &mat, 0.0);
326
327 renderer.build_batches();
328 assert_eq!(renderer.batches()[0].character, 'A');
329 assert_eq!(renderer.batches()[1].character, 'Z');
330 }
331
332 #[test]
333 fn per_glyph_transform_applies_variation() {
334 let config = Render3DConfig {
335 rotation_variation: 0.1,
336 scale_pulse_amount: 0.05,
337 ..Render3DConfig::default()
338 };
339 let t1 = per_glyph_transform(Mat4::IDENTITY, 0.0, &config);
340 let t2 = per_glyph_transform(Mat4::IDENTITY, 1.0, &config);
341 assert_ne!(t1.to_cols_array(), t2.to_cols_array());
343 }
344
345 #[test]
346 fn begin_frame_clears() {
347 let mut renderer = Glyph3DRenderer::default();
348 renderer.submit_glyph('X', Mat4::IDENTITY, &GlyphMaterial::player(), 0.0);
349 renderer.build_batches();
350 assert_eq!(renderer.total_instances(), 1);
351
352 renderer.begin_frame();
353 renderer.build_batches();
354 assert_eq!(renderer.total_instances(), 0);
355 }
356}