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            voxel_count: self.octree.leaf_count(),
267            update_time_ms: 0.0,
268            trace_time_ms: 0.0,
269            memory_mb: self.octree.memory_usage() as f32 / (1024.0 * 1024.0),
270        }
271    }
272}
273
274/// Cascaded SVOGI for multi-resolution coverage.
275pub struct CascadedSvogi {
276    pub cascades: Vec<SvogiSystem>,
277}
278
279impl CascadedSvogi {
280    /// Create nested cascades. Each cascade covers a larger area at lower resolution.
281    pub fn new(cascade_count: u32, base_resolution: u32, world_size: f32) -> Self {
282        let mut cascades = Vec::with_capacity(cascade_count as usize);
283
284        for i in 0..cascade_count {
285            let scale = 2.0f32.powi(i as i32);
286            let half_size = world_size * scale * 0.5;
287            let bounds = Aabb::new(
288                Vec3::splat(-half_size),
289                Vec3::splat(half_size),
290            );
291            let config = SvogiConfig {
292                resolution: base_resolution,
293                max_depth: (base_resolution as f32).log2() as u8,
294                gi_intensity: 1.0 / scale, // Farther cascades contribute less
295                ao_intensity: 1.0 / scale,
296                bounce_count: (2.0 / scale).max(1.0) as u32,
297                update_rate: if i == 0 {
298                    UpdateRate::EveryFrame
299                } else {
300                    UpdateRate::EveryNFrames(1 << i)
301                },
302            };
303            cascades.push(SvogiSystem::init(bounds, config));
304        }
305
306        Self { cascades }
307    }
308
309    /// Update all cascades.
310    pub fn update(
311        &mut self,
312        dt: f32,
313        scene_triangles: &[Triangle],
314        lights: &[LightSource],
315        shadow_maps: &[ShadowMap],
316    ) {
317        for cascade in &mut self.cascades {
318            cascade.update(dt, scene_triangles, lights, shadow_maps);
319        }
320    }
321
322    /// Sample GI from the appropriate cascade based on distance.
323    pub fn sample_gi(&self, position: Vec3, normal: Vec3) -> Vec3 {
324        let config = ConeTraceConfig::default();
325        let mut total_gi = Vec3::ZERO;
326        let mut weight_sum = 0.0f32;
327
328        for (i, cascade) in self.cascades.iter().enumerate() {
329            if cascade.world_bounds.contains(position) {
330                let gi = diffuse_gi(&cascade.octree, position, normal, &config);
331                let weight = 1.0 / (i as f32 + 1.0);
332                total_gi += gi * weight;
333                weight_sum += weight;
334            }
335        }
336
337        if weight_sum > 0.0 {
338            total_gi / weight_sum
339        } else {
340            Vec3::ZERO
341        }
342    }
343
344    /// Get combined stats.
345    pub fn stats(&self) -> SvogiStats {
346        let mut combined = SvogiStats::default();
347        for cascade in &self.cascades {
348            let s = cascade.stats();
349            combined.voxel_count += s.voxel_count;
350            combined.memory_mb += s.memory_mb;
351        }
352        combined
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::svogi::inject::{DirectionalLight, LightSource};
360
361    fn make_test_triangles() -> Vec<Triangle> {
362        vec![
363            Triangle {
364                v0: Vec3::new(1.0, 1.0, 1.0),
365                v1: Vec3::new(3.0, 1.0, 1.0),
366                v2: Vec3::new(2.0, 3.0, 1.0),
367                normal: Vec3::Z,
368                color: Vec4::new(1.0, 0.0, 0.0, 1.0),
369                emission: Vec4::ZERO,
370            },
371            Triangle {
372                v0: Vec3::new(5.0, 5.0, 5.0),
373                v1: Vec3::new(7.0, 5.0, 5.0),
374                v2: Vec3::new(6.0, 7.0, 5.0),
375                normal: Vec3::Z,
376                color: Vec4::new(0.0, 1.0, 0.0, 1.0),
377                emission: Vec4::ZERO,
378            },
379        ]
380    }
381
382    #[test]
383    fn test_init_system() {
384        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(16.0));
385        let config = SvogiConfig::default();
386        let system = SvogiSystem::init(bounds, config);
387        assert_eq!(system.octree.node_count(), 1); // Just root
388    }
389
390    #[test]
391    fn test_voxelize_scene() {
392        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
393        let config = SvogiConfig {
394            resolution: 8,
395            max_depth: 3,
396            ..Default::default()
397        };
398        let mut system = SvogiSystem::init(bounds, config);
399        system.voxelize_scene(&make_test_triangles());
400        assert!(system.octree.node_count() > 1);
401    }
402
403    #[test]
404    fn test_full_pipeline_nonzero_gi() {
405        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
406        let config = SvogiConfig {
407            resolution: 8,
408            max_depth: 3,
409            gi_intensity: 1.0,
410            ao_intensity: 1.0,
411            bounce_count: 1,
412            update_rate: UpdateRate::EveryFrame,
413        };
414        let mut system = SvogiSystem::init(bounds, config);
415
416        let triangles = make_test_triangles();
417        let light = LightSource::Directional(DirectionalLight {
418            direction: Vec3::new(0.0, -1.0, 0.0),
419            color: Vec3::ONE,
420            intensity: 2.0,
421        });
422
423        system.voxelize_scene(&triangles);
424        system.inject_lights(&[light], &[]);
425        system.propagate(2);
426
427        // Check that the octree has data
428        let leaves: Vec<_> = system.octree.iter_leaves().collect();
429        assert!(!leaves.is_empty(), "Should have voxels after pipeline");
430
431        // Check at least some radiance exists
432        let has_radiance = leaves.iter().any(|(_, d)| d.radiance.x > 0.0 || d.radiance.y > 0.0 || d.radiance.z > 0.0);
433        assert!(has_radiance, "Some voxels should have non-zero radiance after injection");
434    }
435
436    #[test]
437    fn test_apply_gi() {
438        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
439        let config = SvogiConfig {
440            resolution: 8,
441            max_depth: 3,
442            ..Default::default()
443        };
444        let mut system = SvogiSystem::init(bounds, config);
445        system.voxelize_scene(&make_test_triangles());
446
447        let positions = vec![Vec3::new(2.0, 2.0, 2.0)];
448        let normals = vec![Vec3::Y];
449        let albedos = vec![Vec3::ONE];
450        let result = system.apply_gi(&positions, &normals, &albedos);
451        assert_eq!(result.len(), 1);
452    }
453
454    #[test]
455    fn test_mark_dirty() {
456        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
457        let config = SvogiConfig {
458            resolution: 8,
459            max_depth: 3,
460            update_rate: UpdateRate::OnDemand,
461            ..Default::default()
462        };
463        let mut system = SvogiSystem::init(bounds, config);
464        system.mark_dirty(Aabb::new(Vec3::ZERO, Vec3::splat(4.0)));
465        assert_eq!(system.dirty_regions.len(), 1);
466    }
467
468    #[test]
469    fn test_debug_views() {
470        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
471        let config = SvogiConfig {
472            resolution: 8,
473            max_depth: 3,
474            ..Default::default()
475        };
476        let mut system = SvogiSystem::init(bounds, config);
477        system.voxelize_scene(&make_test_triangles());
478
479        for view in &[
480            SvogiDebugView::Voxels,
481            SvogiDebugView::Radiance,
482            SvogiDebugView::Normals,
483            SvogiDebugView::SHBands,
484            SvogiDebugView::ConeDirections,
485        ] {
486            let points = system.render_debug(*view, Vec3::splat(4.0));
487            // Just verify it doesn't panic
488            let _ = points;
489        }
490    }
491
492    #[test]
493    fn test_stats() {
494        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
495        let config = SvogiConfig {
496            resolution: 8,
497            max_depth: 3,
498            ..Default::default()
499        };
500        let mut system = SvogiSystem::init(bounds, config);
501        system.voxelize_scene(&make_test_triangles());
502        let stats = system.stats();
503        assert!(stats.voxel_count > 0);
504        assert!(stats.memory_mb > 0.0);
505    }
506
507    #[test]
508    fn test_cascaded_svogi() {
509        let cascaded = CascadedSvogi::new(3, 8, 16.0);
510        assert_eq!(cascaded.cascades.len(), 3);
511
512        let stats = cascaded.stats();
513        assert_eq!(stats.voxel_count, 0); // No voxels yet
514    }
515
516    #[test]
517    fn test_cascaded_sample_gi() {
518        let mut cascaded = CascadedSvogi::new(2, 8, 16.0);
519
520        // Voxelize first cascade
521        let triangles = vec![Triangle {
522            v0: Vec3::new(-2.0, -2.0, 0.0),
523            v1: Vec3::new(2.0, -2.0, 0.0),
524            v2: Vec3::new(0.0, 2.0, 0.0),
525            normal: Vec3::Z,
526            color: Vec4::new(1.0, 0.0, 0.0, 1.0),
527            emission: Vec4::ZERO,
528        }];
529        cascaded.cascades[0].voxelize_scene(&triangles);
530
531        let gi = cascaded.sample_gi(Vec3::ZERO, Vec3::Y);
532        // Just verify it runs without panic
533        let _ = gi;
534    }
535
536    #[test]
537    fn test_update_rate_every_n_frames() {
538        let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(8.0));
539        let config = SvogiConfig {
540            resolution: 4,
541            max_depth: 2,
542            update_rate: UpdateRate::EveryNFrames(3),
543            bounce_count: 1,
544            ..Default::default()
545        };
546        let mut system = SvogiSystem::init(bounds, config);
547        let tris = make_test_triangles();
548
549        // Frame 1: should not update (counter = 1)
550        system.update(0.016, &tris, &[], &[]);
551        let count_1 = system.octree.node_count();
552
553        // Frame 2: should not update (counter = 2)
554        system.update(0.016, &tris, &[], &[]);
555
556        // Frame 3: should update (counter = 3, 3%3 == 0)
557        system.update(0.016, &tris, &[], &[]);
558        let count_3 = system.octree.node_count();
559
560        assert!(count_3 > count_1 || count_3 >= 1);
561    }
562}