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#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum UpdateRate {
12 EveryFrame,
13 EveryNFrames(u32),
14 OnDemand,
15}
16
17#[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#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum SvogiDebugView {
44 Voxels,
45 Radiance,
46 Normals,
47 AO,
48 SHBands,
49 ConeDirections,
50}
51
52#[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
61pub 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 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 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 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 self.octree = SparseVoxelOctree::build_from_voxel_grid(grid, self.world_bounds);
115 }
116 }
117
118 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 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 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 if self.dirty_regions.is_empty() {
189 self.voxelize_scene(scene_triangles);
190 } else {
191 self.voxelize_scene(scene_triangles);
194 self.dirty_regions.clear();
195 }
196
197 self.inject_lights(lights, shadow_maps);
199
200 self.propagate(self.config.bounce_count);
202 }
203
204 pub fn mark_dirty(&mut self, region: Aabb) {
206 self.dirty_regions.push(region);
207 }
208
209 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 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 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 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 pub fn stats(&self) -> SvogiStats {
265 SvogiStats {
266 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
277pub struct CascadedSvogi {
279 pub cascades: Vec<SvogiSystem>,
280}
281
282impl CascadedSvogi {
283 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, 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 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 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 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); }
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 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 let leaves: Vec<_> = system.octree.iter_leaves().collect();
434 assert!(!leaves.is_empty(), "Should have voxels after pipeline");
435
436 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 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); }
520
521 #[test]
522 fn test_cascaded_sample_gi() {
523 let mut cascaded = CascadedSvogi::new(2, 8, 16.0);
524
525 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 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 system.update(0.016, &tris, &[], &[]);
556 let count_1 = system.octree.node_count();
557
558 system.update(0.016, &tris, &[], &[]);
560
561 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}