Skip to main content

proof_engine/svogi/
integration.rs

1use glam::{Vec3, Vec4};
2use super::octree::{SparseVoxelOctree, VoxelData, VoxelGrid, Aabb};
3use super::atlas::BrickAtlas;
4use super::voxelize::{Triangle, VoxelizeConfig, voxelize_triangles};
5use super::inject::{LightSource, ShadowMap, inject_direct_light, inject_emissive};
6use super::propagate::{PropagationConfig, propagate_light};
7use super::cone_trace::{ConeTraceConfig, diffuse_gi, specular_gi, ambient_occlusion};
8
9/// Update rate for the SVOGI system.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum UpdateRate {
12    EveryFrame,
13    EveryNFrames(u32),
14    OnDemand,
15}
16
17/// Configuration for the SVOGI system.
18#[derive(Debug, Clone)]
19pub struct SvogiConfig {
20    pub resolution: u32,
21    pub max_depth: u8,
22    pub gi_intensity: f32,
23    pub ao_intensity: f32,
24    pub bounce_count: u32,
25    pub update_rate: UpdateRate,
26}
27
28impl Default for SvogiConfig {
29    fn default() -> Self {
30        Self {
31            resolution: 64,
32            max_depth: 6,
33            gi_intensity: 1.0,
34            ao_intensity: 1.0,
35            bounce_count: 2,
36            update_rate: UpdateRate::EveryFrame,
37        }
38    }
39}
40
41/// Debug visualization modes.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum SvogiDebugView {
44    Voxels,
45    Radiance,
46    Normals,
47    AO,
48    SHBands,
49    ConeDirections,
50}
51
52/// Statistics for the SVOGI system.
53#[derive(Debug, Clone, Default)]
54pub struct SvogiStats {
55    pub voxel_count: usize,
56    pub update_time_ms: f32,
57    pub trace_time_ms: f32,
58    pub memory_mb: f32,
59}
60
61/// The main SVOGI system integrating all components.
62pub struct SvogiSystem {
63    pub octree: SparseVoxelOctree,
64    pub atlas: BrickAtlas,
65    pub config: SvogiConfig,
66    grid: Option<VoxelGrid>,
67    frame_counter: u32,
68    dirty_regions: Vec<Aabb>,
69    world_bounds: Aabb,
70}
71
72impl SvogiSystem {
73    /// Initialize the SVOGI system.
74    pub fn init(world_bounds: Aabb, config: SvogiConfig) -> Self {
75        let octree = SparseVoxelOctree::new(world_bounds, config.max_depth);
76        let atlas_size = glam::UVec3::splat(config.resolution * 2);
77        let atlas = BrickAtlas::new(atlas_size, 4);
78
79        Self {
80            octree,
81            atlas,
82            config,
83            grid: None,
84            frame_counter: 0,
85            dirty_regions: Vec::new(),
86            world_bounds,
87        }
88    }
89
90    /// Voxelize a scene from triangles.
91    pub fn voxelize_scene(&mut self, scene_triangles: &[Triangle]) {
92        let vox_config = VoxelizeConfig {
93            resolution: self.config.resolution,
94            world_bounds: self.world_bounds,
95            conservative: false,
96        };
97        let grid = voxelize_triangles(scene_triangles, &vox_config);
98        self.octree = SparseVoxelOctree::build_from_voxel_grid(&grid, self.world_bounds);
99        self.grid = Some(grid);
100    }
101
102    /// Inject direct lighting from light sources and shadow maps.
103    pub fn inject_lights(&mut self, lights: &[LightSource], shadow_maps: &[ShadowMap]) {
104        if let Some(ref mut grid) = self.grid {
105            inject_direct_light(
106                grid,
107                lights,
108                shadow_maps,
109                self.world_bounds.min,
110                self.world_bounds.size(),
111            );
112            inject_emissive(grid);
113            // Rebuild octree from updated grid
114            self.octree = SparseVoxelOctree::build_from_voxel_grid(grid, self.world_bounds);
115        }
116    }
117
118    /// Propagate light for the given number of iterations.
119    pub fn propagate(&mut self, iterations: u32) {
120        if let Some(ref mut grid) = self.grid {
121            let config = PropagationConfig {
122                iterations,
123                damping: 0.8,
124                flux_weight: 1.0,
125            };
126            propagate_light(grid, &config);
127            self.octree = SparseVoxelOctree::build_from_voxel_grid(grid, self.world_bounds);
128        }
129    }
130
131    /// Compute per-pixel GI from G-buffer data.
132    pub fn apply_gi(
133        &self,
134        gbuffer_positions: &[Vec3],
135        gbuffer_normals: &[Vec3],
136        gbuffer_albedo: &[Vec3],
137    ) -> Vec<Vec4> {
138        let config = ConeTraceConfig {
139            max_distance: self.world_bounds.size().length() * 0.5,
140            step_multiplier: 1.0,
141            ao_weight: self.config.ao_intensity,
142            gi_weight: self.config.gi_intensity,
143        };
144
145        let mut result = Vec::with_capacity(gbuffer_positions.len());
146
147        for i in 0..gbuffer_positions.len() {
148            let pos = gbuffer_positions[i];
149            let normal = gbuffer_normals[i];
150            let albedo = gbuffer_albedo[i];
151
152            if normal.length_squared() < 0.01 {
153                result.push(Vec4::ZERO);
154                continue;
155            }
156
157            let gi = diffuse_gi(&self.octree, pos, normal, &config);
158            let ao = ambient_occlusion(&self.octree, pos, normal, &config);
159
160            let final_color = albedo * (gi + Vec3::splat(ao * 0.1));
161            result.push(Vec4::new(final_color.x, final_color.y, final_color.z, 1.0));
162        }
163
164        result
165    }
166
167    /// Full frame update: voxelize if needed, inject, propagate.
168    pub fn update(
169        &mut self,
170        dt: f32,
171        scene_triangles: &[Triangle],
172        lights: &[LightSource],
173        shadow_maps: &[ShadowMap],
174    ) {
175        self.frame_counter += 1;
176
177        let should_update = match self.config.update_rate {
178            UpdateRate::EveryFrame => true,
179            UpdateRate::EveryNFrames(n) => self.frame_counter % n == 0,
180            UpdateRate::OnDemand => !self.dirty_regions.is_empty(),
181        };
182
183        if !should_update {
184            return;
185        }
186
187        // Voxelize
188        if self.dirty_regions.is_empty() {
189            self.voxelize_scene(scene_triangles);
190        } else {
191            // Partial revoxelization: only for dirty regions
192            // For simplicity, revoxelize the whole scene but only if dirty
193            self.voxelize_scene(scene_triangles);
194            self.dirty_regions.clear();
195        }
196
197        // Inject lighting
198        self.inject_lights(lights, shadow_maps);
199
200        // Propagate bounced lighting
201        self.propagate(self.config.bounce_count);
202    }
203
204    /// Mark a region as dirty for incremental updates.
205    pub fn mark_dirty(&mut self, region: Aabb) {
206        self.dirty_regions.push(region);
207    }
208
209    /// Render debug visualization.
210    pub fn render_debug(&self, view: SvogiDebugView, camera_pos: Vec3) -> Vec<(Vec3, Vec4)> {
211        let mut points = Vec::new();
212
213        for (pos, data) in self.octree.iter_leaves() {
214            let color = match view {
215                SvogiDebugView::Voxels => {
216                    Vec4::new(1.0, 1.0, 1.0, data.opacity)
217                }
218                SvogiDebugView::Radiance => {
219                    data.radiance
220                }
221                SvogiDebugView::Normals => {
222                    Vec4::new(
223                        data.normal.x * 0.5 + 0.5,
224                        data.normal.y * 0.5 + 0.5,
225                        data.normal.z * 0.5 + 0.5,
226                        1.0,
227                    )
228                }
229                SvogiDebugView::AO => {
230                    let config = ConeTraceConfig::default();
231                    let ao = ambient_occlusion(&self.octree, pos, data.normal, &config);
232                    Vec4::new(ao, ao, ao, 1.0)
233                }
234                SvogiDebugView::SHBands => {
235                    // Visualize first 3 SH bands as RGB
236                    Vec4::new(
237                        data.sh_coeffs[0].abs(),
238                        if data.sh_coeffs.len() > 1 { data.sh_coeffs[1].abs() } else { 0.0 },
239                        if data.sh_coeffs.len() > 2 { data.sh_coeffs[2].abs() } else { 0.0 },
240                        1.0,
241                    )
242                }
243                SvogiDebugView::ConeDirections => {
244                    // Color based on normal direction
245                    let dir_color = (data.normal + Vec3::ONE) * 0.5;
246                    Vec4::new(dir_color.x, dir_color.y, dir_color.z, 1.0)
247                }
248            };
249
250            points.push((pos, color));
251        }
252
253        // Sort by distance from camera (back to front for transparency)
254        points.sort_by(|a, b| {
255            let da = (a.0 - camera_pos).length_squared();
256            let db = (b.0 - camera_pos).length_squared();
257            db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
258        });
259
260        points
261    }
262
263    /// Get statistics.
264    pub fn stats(&self) -> SvogiStats {
265        SvogiStats {
266            // Occupied voxels only. leaf_count() also counts the empty root
267            // of an unused octree, so an empty scene reported 1 voxel per
268            // cascade.
269            voxel_count: self.octree.iter_leaves().count(),
270            update_time_ms: 0.0,
271            trace_time_ms: 0.0,
272            memory_mb: self.octree.memory_usage() as f32 / (1024.0 * 1024.0),
273        }
274    }
275}
276
277/// Cascaded SVOGI for multi-resolution coverage.
278pub struct CascadedSvogi {
279    pub cascades: Vec<SvogiSystem>,
280}
281
282impl CascadedSvogi {
283    /// Create nested cascades. Each cascade covers a larger area at lower resolution.
284    pub fn new(cascade_count: u32, base_resolution: u32, world_size: f32) -> Self {
285        let mut cascades = Vec::with_capacity(cascade_count as usize);
286
287        for i in 0..cascade_count {
288            let scale = 2.0f32.powi(i as i32);
289            let half_size = world_size * scale * 0.5;
290            let bounds = Aabb::new(
291                Vec3::splat(-half_size),
292                Vec3::splat(half_size),
293            );
294            let config = SvogiConfig {
295                resolution: base_resolution,
296                max_depth: (base_resolution as f32).log2() as u8,
297                gi_intensity: 1.0 / scale, // Farther cascades contribute less
298                ao_intensity: 1.0 / scale,
299                bounce_count: (2.0 / scale).max(1.0) as u32,
300                update_rate: if i == 0 {
301                    UpdateRate::EveryFrame
302                } else {
303                    UpdateRate::EveryNFrames(1 << i)
304                },
305            };
306            cascades.push(SvogiSystem::init(bounds, config));
307        }
308
309        Self { cascades }
310    }
311
312    /// Update all cascades.
313    pub fn update(
314        &mut self,
315        dt: f32,
316        scene_triangles: &[Triangle],
317        lights: &[LightSource],
318        shadow_maps: &[ShadowMap],
319    ) {
320        for cascade in &mut self.cascades {
321            cascade.update(dt, scene_triangles, lights, shadow_maps);
322        }
323    }
324
325    /// Sample GI from the appropriate cascade based on distance.
326    pub fn sample_gi(&self, position: Vec3, normal: Vec3) -> Vec3 {
327        let config = ConeTraceConfig::default();
328        let mut total_gi = Vec3::ZERO;
329        let mut weight_sum = 0.0f32;
330
331        for (i, cascade) in self.cascades.iter().enumerate() {
332            if cascade.world_bounds.contains(position) {
333                let gi = diffuse_gi(&cascade.octree, position, normal, &config);
334                let weight = 1.0 / (i as f32 + 1.0);
335                total_gi += gi * weight;
336                weight_sum += weight;
337            }
338        }
339
340        if weight_sum > 0.0 {
341            total_gi / weight_sum
342        } else {
343            Vec3::ZERO
344        }
345    }
346
347    /// Get combined stats.
348    pub fn stats(&self) -> SvogiStats {
349        let mut combined = SvogiStats::default();
350        for cascade in &self.cascades {
351            let s = cascade.stats();
352            combined.voxel_count += s.voxel_count;
353            combined.memory_mb += s.memory_mb;
354        }
355        combined
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::svogi::inject::{DirectionalLight, LightSource};
363
364    fn make_test_triangles() -> Vec<Triangle> {
365        vec![
366            Triangle {
367                v0: Vec3::new(1.0, 1.0, 1.0),
368                v1: Vec3::new(3.0, 1.0, 1.0),
369                v2: Vec3::new(2.0, 3.0, 1.0),
370                normal: Vec3::Z,
371                color: Vec4::new(1.0, 0.0, 0.0, 1.0),
372                emission: Vec4::ZERO,
373            },
374            Triangle {
375                v0: Vec3::new(5.0, 5.0, 5.0),
376                v1: Vec3::new(7.0, 5.0, 5.0),
377                v2: Vec3::new(6.0, 7.0, 5.0),
378                normal: Vec3::Z,
379                color: Vec4::new(0.0, 1.0, 0.0, 1.0),
380                emission: Vec4::ZERO,
381            },
382        ]
383    }
384
385    #[test]
386    fn test_init_system() {
387        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(16.0));
388        let config = SvogiConfig::default();
389        let system = SvogiSystem::init(bounds, config);
390        assert_eq!(system.octree.node_count(), 1); // Just root
391    }
392
393    #[test]
394    fn test_voxelize_scene() {
395        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
396        let config = SvogiConfig {
397            resolution: 8,
398            max_depth: 3,
399            ..Default::default()
400        };
401        let mut system = SvogiSystem::init(bounds, config);
402        system.voxelize_scene(&make_test_triangles());
403        assert!(system.octree.node_count() > 1);
404    }
405
406    #[test]
407    fn test_full_pipeline_nonzero_gi() {
408        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
409        let config = SvogiConfig {
410            resolution: 8,
411            max_depth: 3,
412            gi_intensity: 1.0,
413            ao_intensity: 1.0,
414            bounce_count: 1,
415            update_rate: UpdateRate::EveryFrame,
416        };
417        let mut system = SvogiSystem::init(bounds, config);
418
419        let triangles = make_test_triangles();
420        // The test triangles face +Z. A light shining along -Y grazes them
421        // (N . L = 0), so the old test lit nothing; shine along -Z instead.
422        let light = LightSource::Directional(DirectionalLight {
423            direction: Vec3::new(0.0, 0.0, -1.0),
424            color: Vec3::ONE,
425            intensity: 2.0,
426        });
427
428        system.voxelize_scene(&triangles);
429        system.inject_lights(&[light], &[]);
430        system.propagate(2);
431
432        // Check that the octree has data
433        let leaves: Vec<_> = system.octree.iter_leaves().collect();
434        assert!(!leaves.is_empty(), "Should have voxels after pipeline");
435
436        // Check at least some radiance exists
437        let has_radiance = leaves.iter().any(|(_, d)| d.radiance.x > 0.0 || d.radiance.y > 0.0 || d.radiance.z > 0.0);
438        assert!(has_radiance, "Some voxels should have non-zero radiance after injection");
439    }
440
441    #[test]
442    fn test_apply_gi() {
443        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
444        let config = SvogiConfig {
445            resolution: 8,
446            max_depth: 3,
447            ..Default::default()
448        };
449        let mut system = SvogiSystem::init(bounds, config);
450        system.voxelize_scene(&make_test_triangles());
451
452        let positions = vec![Vec3::new(2.0, 2.0, 2.0)];
453        let normals = vec![Vec3::Y];
454        let albedos = vec![Vec3::ONE];
455        let result = system.apply_gi(&positions, &normals, &albedos);
456        assert_eq!(result.len(), 1);
457    }
458
459    #[test]
460    fn test_mark_dirty() {
461        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
462        let config = SvogiConfig {
463            resolution: 8,
464            max_depth: 3,
465            update_rate: UpdateRate::OnDemand,
466            ..Default::default()
467        };
468        let mut system = SvogiSystem::init(bounds, config);
469        system.mark_dirty(Aabb::new(Vec3::ZERO, Vec3::splat(4.0)));
470        assert_eq!(system.dirty_regions.len(), 1);
471    }
472
473    #[test]
474    fn test_debug_views() {
475        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
476        let config = SvogiConfig {
477            resolution: 8,
478            max_depth: 3,
479            ..Default::default()
480        };
481        let mut system = SvogiSystem::init(bounds, config);
482        system.voxelize_scene(&make_test_triangles());
483
484        for view in &[
485            SvogiDebugView::Voxels,
486            SvogiDebugView::Radiance,
487            SvogiDebugView::Normals,
488            SvogiDebugView::SHBands,
489            SvogiDebugView::ConeDirections,
490        ] {
491            let points = system.render_debug(*view, Vec3::splat(4.0));
492            // Just verify it doesn't panic
493            let _ = points;
494        }
495    }
496
497    #[test]
498    fn test_stats() {
499        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
500        let config = SvogiConfig {
501            resolution: 8,
502            max_depth: 3,
503            ..Default::default()
504        };
505        let mut system = SvogiSystem::init(bounds, config);
506        system.voxelize_scene(&make_test_triangles());
507        let stats = system.stats();
508        assert!(stats.voxel_count > 0);
509        assert!(stats.memory_mb > 0.0);
510    }
511
512    #[test]
513    fn test_cascaded_svogi() {
514        let cascaded = CascadedSvogi::new(3, 8, 16.0);
515        assert_eq!(cascaded.cascades.len(), 3);
516
517        let stats = cascaded.stats();
518        assert_eq!(stats.voxel_count, 0); // No voxels yet
519    }
520
521    #[test]
522    fn test_cascaded_sample_gi() {
523        let mut cascaded = CascadedSvogi::new(2, 8, 16.0);
524
525        // Voxelize first cascade
526        let triangles = vec![Triangle {
527            v0: Vec3::new(-2.0, -2.0, 0.0),
528            v1: Vec3::new(2.0, -2.0, 0.0),
529            v2: Vec3::new(0.0, 2.0, 0.0),
530            normal: Vec3::Z,
531            color: Vec4::new(1.0, 0.0, 0.0, 1.0),
532            emission: Vec4::ZERO,
533        }];
534        cascaded.cascades[0].voxelize_scene(&triangles);
535
536        let gi = cascaded.sample_gi(Vec3::ZERO, Vec3::Y);
537        // Just verify it runs without panic
538        let _ = gi;
539    }
540
541    #[test]
542    fn test_update_rate_every_n_frames() {
543        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
544        let config = SvogiConfig {
545            resolution: 4,
546            max_depth: 2,
547            update_rate: UpdateRate::EveryNFrames(3),
548            bounce_count: 1,
549            ..Default::default()
550        };
551        let mut system = SvogiSystem::init(bounds, config);
552        let tris = make_test_triangles();
553
554        // Frame 1: should not update (counter = 1)
555        system.update(0.016, &tris, &[], &[]);
556        let count_1 = system.octree.node_count();
557
558        // Frame 2: should not update (counter = 2)
559        system.update(0.016, &tris, &[], &[]);
560
561        // Frame 3: should update (counter = 3, 3%3 == 0)
562        system.update(0.016, &tris, &[], &[]);
563        let count_3 = system.octree.node_count();
564
565        assert!(count_3 > count_1 || count_3 >= 1);
566    }
567}