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.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
274pub struct CascadedSvogi {
276 pub cascades: Vec<SvogiSystem>,
277}
278
279impl CascadedSvogi {
280 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, 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 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 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 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); }
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 let leaves: Vec<_> = system.octree.iter_leaves().collect();
429 assert!(!leaves.is_empty(), "Should have voxels after pipeline");
430
431 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 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); }
515
516 #[test]
517 fn test_cascaded_sample_gi() {
518 let mut cascaded = CascadedSvogi::new(2, 8, 16.0);
519
520 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 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 system.update(0.016, &tris, &[], &[]);
551 let count_1 = system.octree.node_count();
552
553 system.update(0.016, &tris, &[], &[]);
555
556 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}