Skip to main content

proof_engine/render/
glyph_3d_renderer.rs

1//! Instanced 3D glyph rendering with PBR G-buffer output.
2//!
3//! Groups glyphs by character, issues one instanced draw call per unique character.
4//! Outputs to a G-buffer: albedo, normal, metallic+roughness, emission.
5//! The existing deferred pipeline resolves lighting including SVOGI.
6
7use glam::{Vec2, Vec3, Vec4, Mat4};
8use std::collections::HashMap;
9
10use crate::glyph::glyph_mesh::GlyphMeshCache;
11use crate::glyph::glyph_materials::GlyphMaterial;
12
13// ── Instance data ───────────────────────────────────────────────────────────
14
15/// Per-instance data for 3D glyph rendering. 80 bytes, GPU-friendly.
16#[repr(C)]
17#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
18pub struct Glyph3DInstance {
19    /// Column-major 4×4 model matrix.
20    pub model_matrix: [f32; 16],
21    /// RGBA base color.
22    pub base_color: [f32; 4],
23    /// Emission intensity.
24    pub emission: f32,
25    /// Metallic factor [0,1].
26    pub metallic: f32,
27    /// Roughness factor [0,1].
28    pub roughness: f32,
29    /// Animation phase (for per-glyph time offset).
30    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
48// ── Batch ───────────────────────────────────────────────────────────────────
49
50/// A batch of instances sharing the same character mesh.
51pub 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// ── Render config ───────────────────────────────────────────────────────────
65
66#[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
87// ── Renderer ────────────────────────────────────────────────────────────────
88
89pub 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    /// Clear instance lists. Call at the start of each frame.
107    pub fn begin_frame(&mut self) {
108        self.per_char_instances.clear();
109        self.batches.clear();
110        self.frame_count += 1;
111    }
112
113    /// Queue a 3D glyph for rendering.
114    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    /// Build sorted batches from submitted instances.
121    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    /// Get the sorted batches for draw call submission.
139    pub fn batches(&self) -> &[Glyph3DBatch] {
140        &self.batches
141    }
142
143    /// Total instance count across all batches.
144    pub fn total_instances(&self) -> usize {
145        self.batches.iter().map(|b| b.instances.len()).sum()
146    }
147
148    /// Number of draw calls (one per unique character).
149    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
158/// Apply per-glyph rotation variation and scale pulsing.
159pub fn per_glyph_transform(base: Mat4, phase: f32, config: &Render3DConfig) -> Mat4 {
160    // Hash phase to get a deterministic but varied rotation offset
161    let hash = (phase * 12345.6789).sin() * 43758.5453;
162    let rot_offset = (hash.fract() - 0.5) * config.rotation_variation;
163
164    // Scale pulse
165    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
174// ── GLSL Shaders ────────────────────────────────────────────────────────────
175
176/// Vertex shader for 3D PBR glyph rendering.
177pub 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
226/// Fragment shader: G-buffer output for deferred PBR.
227pub 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
259// ── VAO layout description ──────────────────────────────────────────────────
260
261/// Describes how to set up the vertex attribute layout for 3D glyph rendering.
262/// This is a reference for the OpenGL setup code, not executable.
263pub struct Glyph3DVaoLayout;
264
265impl Glyph3DVaoLayout {
266    /// Vertex stride (Vertex3D: 3+3+2 floats = 32 bytes)
267    pub const VERTEX_STRIDE: i32 = 32;
268    /// Instance stride (Glyph3DInstance: 16+4+1+1+1+1 floats = 96 bytes)
269    pub const INSTANCE_STRIDE: i32 = 96;
270
271    /// Per-vertex attributes (from vertex buffer, divisor = 0)
272    pub const VERTEX_ATTRIBS: [(u32, i32, i32); 3] = [
273        (0, 3, 0),   // position: vec3 @ offset 0
274        (1, 3, 12),  // normal: vec3 @ offset 12
275        (2, 2, 24),  // uv: vec2 @ offset 24
276    ];
277
278    /// Per-instance attributes (from instance buffer, divisor = 1)
279    pub const INSTANCE_ATTRIBS: [(u32, i32, i32); 9] = [
280        (3, 4, 0),   // model_col0: vec4 @ offset 0
281        (4, 4, 16),  // model_col1: vec4 @ offset 16
282        (5, 4, 32),  // model_col2: vec4 @ offset 32
283        (6, 4, 48),  // model_col3: vec4 @ offset 48
284        (7, 4, 64),  // base_color: vec4 @ offset 64
285        (8, 1, 80),  // emission: float @ offset 80
286        (9, 1, 84),  // metallic: float @ offset 84
287        (10, 1, 88), // roughness: float @ offset 88
288        (11, 1, 92), // anim_phase: float @ offset 92
289    ];
290}
291
292// ── Tests ───────────────────────────────────────────────────────────────────
293
294#[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); // A and B
315        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        // Different phases should produce different transforms
342        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}