1use glam::{Vec3, Vec4, UVec3};
2use super::octree::{VoxelData, VoxelGrid, SparseVoxelOctree, Aabb};
3
4#[derive(Debug, Clone)]
6pub struct PropagationConfig {
7 pub iterations: u32,
8 pub damping: f32,
9 pub flux_weight: f32,
10}
11
12impl Default for PropagationConfig {
13 fn default() -> Self {
14 Self {
15 iterations: 4,
16 damping: 0.8,
17 flux_weight: 1.0,
18 }
19 }
20}
21
22#[derive(Debug, Clone, Copy)]
24pub struct SHCoeffs {
25 pub coeffs: [f32; 9],
26}
27
28impl Default for SHCoeffs {
29 fn default() -> Self {
30 Self { coeffs: [0.0; 9] }
31 }
32}
33
34impl SHCoeffs {
35 pub fn evaluate(&self, direction: Vec3) -> f32 {
37 let d = direction.normalize_or_zero();
38 let x = d.x;
39 let y = d.y;
40 let z = d.z;
41
42 let y00 = 0.282095; let y1n1 = 0.488603 * y; let y10 = 0.488603 * z; let y1p1 = 0.488603 * x; let y2n2 = 1.092548 * x * y; let y2n1 = 1.092548 * y * z; let y20 = 0.315392 * (3.0 * z * z - 1.0); let y2p1 = 1.092548 * x * z; let y2p2 = 0.546274 * (x * x - y * y); self.coeffs[0] * y00
54 + self.coeffs[1] * y1n1
55 + self.coeffs[2] * y10
56 + self.coeffs[3] * y1p1
57 + self.coeffs[4] * y2n2
58 + self.coeffs[5] * y2n1
59 + self.coeffs[6] * y20
60 + self.coeffs[7] * y2p1
61 + self.coeffs[8] * y2p2
62 }
63
64 pub fn project(&mut self, direction: Vec3, value: f32) {
66 let d = direction.normalize_or_zero();
67 let x = d.x;
68 let y = d.y;
69 let z = d.z;
70
71 self.coeffs[0] += value * 0.282095;
72 self.coeffs[1] += value * 0.488603 * y;
73 self.coeffs[2] += value * 0.488603 * z;
74 self.coeffs[3] += value * 0.488603 * x;
75 self.coeffs[4] += value * 1.092548 * x * y;
76 self.coeffs[5] += value * 1.092548 * y * z;
77 self.coeffs[6] += value * 0.315392 * (3.0 * z * z - 1.0);
78 self.coeffs[7] += value * 1.092548 * x * z;
79 self.coeffs[8] += value * 0.546274 * (x * x - y * y);
80 }
81
82 pub fn add(&self, other: &SHCoeffs) -> SHCoeffs {
83 let mut result = SHCoeffs::default();
84 for i in 0..9 {
85 result.coeffs[i] = self.coeffs[i] + other.coeffs[i];
86 }
87 result
88 }
89
90 pub fn scale(&self, s: f32) -> SHCoeffs {
91 let mut result = SHCoeffs::default();
92 for i in 0..9 {
93 result.coeffs[i] = self.coeffs[i] * s;
94 }
95 result
96 }
97
98 pub fn convolve_cosine(&self) -> SHCoeffs {
100 let a0 = std::f32::consts::PI;
102 let a1 = 2.0 * std::f32::consts::PI / 3.0;
103 let a2 = std::f32::consts::PI / 4.0;
104
105 let mut result = *self;
106 result.coeffs[0] *= a0;
107 result.coeffs[1] *= a1;
108 result.coeffs[2] *= a1;
109 result.coeffs[3] *= a1;
110 result.coeffs[4] *= a2;
111 result.coeffs[5] *= a2;
112 result.coeffs[6] *= a2;
113 result.coeffs[7] *= a2;
114 result.coeffs[8] *= a2;
115 result
116 }
117
118 pub fn from_direction(dir: Vec3) -> SHCoeffs {
120 let mut sh = SHCoeffs::default();
121 sh.project(dir, 1.0);
122 sh
123 }
124
125 pub fn energy(&self) -> f32 {
127 self.coeffs.iter().map(|c| c * c).sum()
128 }
129}
130
131const FACE_DIRECTIONS: [Vec3; 6] = [
133 Vec3::new(1.0, 0.0, 0.0),
134 Vec3::new(-1.0, 0.0, 0.0),
135 Vec3::new(0.0, 1.0, 0.0),
136 Vec3::new(0.0, -1.0, 0.0),
137 Vec3::new(0.0, 0.0, 1.0),
138 Vec3::new(0.0, 0.0, -1.0),
139];
140
141const FACE_OFFSETS: [(i32, i32, i32); 6] = [
143 (1, 0, 0), (-1, 0, 0),
144 (0, 1, 0), (0, -1, 0),
145 (0, 0, 1), (0, 0, -1),
146];
147
148pub fn flux_through_face(voxel: &VoxelData, face_normal: Vec3) -> Vec3 {
150 let sh = SHCoeffs { coeffs: voxel.sh_coeffs };
151 let flux_weight = sh.evaluate(face_normal).max(0.0);
152 let radiance = Vec3::new(voxel.radiance.x, voxel.radiance.y, voxel.radiance.z);
153 radiance * flux_weight
154}
155
156pub fn gather_from_neighbors(
158 grid: &VoxelGrid,
159 x: u32,
160 y: u32,
161 z: u32,
162) -> Vec3 {
163 let mut incoming = Vec3::ZERO;
164
165 for (i, &(dx, dy, dz)) in FACE_OFFSETS.iter().enumerate() {
166 let nx = x as i32 + dx;
167 let ny = y as i32 + dy;
168 let nz = z as i32 + dz;
169
170 if grid.in_bounds(nx, ny, nz) {
171 let neighbor = grid.get(nx as u32, ny as u32, nz as u32);
172 if !neighbor.is_empty() {
173 let outgoing_face = -FACE_DIRECTIONS[i];
175 let flux = flux_through_face(neighbor, outgoing_face);
176 incoming += flux;
177 }
178 }
179 }
180
181 incoming
182}
183
184pub fn propagate_step(
186 source: &VoxelGrid,
187 dest: &mut VoxelGrid,
188 config: &PropagationConfig,
189) {
190 let res = source.resolution;
191
192 for z in 0..res.z {
193 for y in 0..res.y {
194 for x in 0..res.x {
195 let src = source.get(x, y, z);
196 let idx = dest.index(x, y, z);
197
198 if src.is_empty() {
199 dest.data[idx] = *src;
200 continue;
201 }
202
203 let incoming = gather_from_neighbors(source, x, y, z);
204 let damped = incoming * config.damping * config.flux_weight;
205
206 let existing = Vec3::new(src.radiance.x, src.radiance.y, src.radiance.z);
207 let new_radiance = existing + damped;
208
209 dest.data[idx] = VoxelData {
210 radiance: Vec4::new(new_radiance.x, new_radiance.y, new_radiance.z, src.radiance.w),
211 normal: src.normal,
212 opacity: src.opacity,
213 sh_coeffs: src.sh_coeffs,
214 };
215
216 let mut sh = SHCoeffs { coeffs: dest.data[idx].sh_coeffs };
218 for (i, &(dx, dy, dz)) in FACE_OFFSETS.iter().enumerate() {
219 let nx = x as i32 + dx;
220 let ny = y as i32 + dy;
221 let nz = z as i32 + dz;
222 if source.in_bounds(nx, ny, nz) {
223 let neighbor = source.get(nx as u32, ny as u32, nz as u32);
224 if !neighbor.is_empty() {
225 let luminance = neighbor.radiance.x * 0.299
226 + neighbor.radiance.y * 0.587
227 + neighbor.radiance.z * 0.114;
228 let dir = -FACE_DIRECTIONS[i];
229 sh.project(dir, luminance * config.damping * 0.1);
230 }
231 }
232 }
233 dest.data[idx].sh_coeffs = sh.coeffs;
234 }
235 }
236 }
237}
238
239pub fn propagate_light(grid: &mut VoxelGrid, config: &PropagationConfig) {
241 let mut temp = VoxelGrid::new(grid.resolution);
242
243 for _ in 0..config.iterations {
244 propagate_step(grid, &mut temp, config);
245 std::mem::swap(grid, &mut temp);
246 }
247}
248
249pub fn propagate_hierarchical(octree: &mut SparseVoxelOctree, iterations: u32) {
251 let max_depth = octree.max_depth;
253 let bounds = octree.world_bounds;
254
255 for level in (1..=max_depth).rev() {
256 let res = 1u32 << level;
257 let mut grid = VoxelGrid::new(UVec3::splat(res));
258 let voxel_size = bounds.size() / Vec3::splat(res as f32);
259
260 for (pos, data) in octree.iter_leaves() {
262 let vx = ((pos.x - bounds.min.x) / voxel_size.x).floor() as u32;
263 let vy = ((pos.y - bounds.min.y) / voxel_size.y).floor() as u32;
264 let vz = ((pos.z - bounds.min.z) / voxel_size.z).floor() as u32;
265 if vx < res && vy < res && vz < res {
266 grid.set(vx, vy, vz, *data);
267 }
268 }
269
270 let config = PropagationConfig {
271 iterations,
272 damping: 0.8,
273 flux_weight: 1.0,
274 };
275 propagate_light(&mut grid, &config);
276
277 for z in 0..res {
279 for y in 0..res {
280 for x in 0..res {
281 let vd = grid.get(x, y, z);
282 if !vd.is_empty() {
283 let pos = bounds.min + Vec3::new(
284 (x as f32 + 0.5) * voxel_size.x,
285 (y as f32 + 0.5) * voxel_size.y,
286 (z as f32 + 0.5) * voxel_size.z,
287 );
288 octree.insert(pos, *vd);
289 }
290 }
291 }
292 }
293 }
294
295 octree.build_mipmaps();
296}
297
298pub const PROPAGATE_COMP_SRC: &str = r#"
300#version 450
301layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
302
303layout(rgba16f, binding = 0) uniform readonly image3D srcRadiance;
304layout(rgba16f, binding = 1) uniform writeonly image3D dstRadiance;
305
306uniform float damping;
307uniform float fluxWeight;
308uniform uint resolution;
309
310// SH evaluation for directional weighting
311float evaluateSH(vec4 shCoeffs, vec3 dir) {
312 return shCoeffs.x * 0.282095
313 + shCoeffs.y * 0.488603 * dir.y
314 + shCoeffs.z * 0.488603 * dir.z
315 + shCoeffs.w * 0.488603 * dir.x;
316}
317
318void main() {
319 ivec3 coord = ivec3(gl_GlobalInvocationID.xyz);
320 if (any(greaterThanEqual(coord, ivec3(resolution)))) return;
321
322 vec4 current = imageLoad(srcRadiance, coord);
323 if (current.a <= 0.0) {
324 imageStore(dstRadiance, coord, current);
325 return;
326 }
327
328 // 6-directional gather
329 vec3 incoming = vec3(0.0);
330 ivec3 offsets[6] = ivec3[6](
331 ivec3(1,0,0), ivec3(-1,0,0),
332 ivec3(0,1,0), ivec3(0,-1,0),
333 ivec3(0,0,1), ivec3(0,0,-1)
334 );
335
336 for (int i = 0; i < 6; i++) {
337 ivec3 ncoord = coord + offsets[i];
338 if (all(greaterThanEqual(ncoord, ivec3(0))) && all(lessThan(ncoord, ivec3(resolution)))) {
339 vec4 neighbor = imageLoad(srcRadiance, ncoord);
340 if (neighbor.a > 0.0) {
341 incoming += neighbor.rgb * fluxWeight;
342 }
343 }
344 }
345
346 vec3 propagated = current.rgb + incoming * damping / 6.0;
347 imageStore(dstRadiance, coord, vec4(propagated, current.a));
348}
349"#;
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use glam::UVec3;
355
356 #[test]
357 fn test_sh_coeffs_evaluate_project() {
358 let mut sh = SHCoeffs::default();
359 let dir = Vec3::Y;
360 sh.project(dir, 1.0);
361 let val = sh.evaluate(dir);
362 assert!(val > 0.0, "SH evaluation in projected direction should be positive");
363 }
364
365 #[test]
366 fn test_sh_coeffs_energy() {
367 let sh = SHCoeffs::from_direction(Vec3::X);
368 assert!(sh.energy() > 0.0);
369 }
370
371 #[test]
372 fn test_propagation_energy_decreases_with_damping() {
373 let mut grid = VoxelGrid::new(UVec3::new(8, 8, 8));
374 grid.set(4, 4, 4, VoxelData {
376 radiance: Vec4::new(10.0, 10.0, 10.0, 1.0),
377 normal: Vec3::Y,
378 opacity: 1.0,
379 sh_coeffs: [0.282095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
380 });
381 for &(dx, dy, dz) in &[(1,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)] {
383 let x = (4 + dx) as u32;
384 let y = (4 + dy) as u32;
385 let z = (4 + dz) as u32;
386 grid.set(x, y, z, VoxelData {
387 radiance: Vec4::new(0.0, 0.0, 0.0, 0.01),
388 normal: Vec3::Y,
389 opacity: 0.5,
390 sh_coeffs: [0.282095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
391 });
392 }
393
394 let initial_energy: f32 = grid.data.iter()
395 .map(|v| v.radiance.x + v.radiance.y + v.radiance.z)
396 .sum();
397
398 let config = PropagationConfig {
399 iterations: 1,
400 damping: 0.5,
401 flux_weight: 0.5,
402 };
403 propagate_light(&mut grid, &config);
404
405 let neighbor_radiance = grid.get(5, 4, 4).radiance.x;
407 assert!(neighbor_radiance > 0.0, "Neighbor should receive light, got {neighbor_radiance}");
408 }
409
410 #[test]
411 fn test_single_emitter_reaches_neighbors() {
412 let mut grid = VoxelGrid::new(UVec3::new(8, 8, 8));
413
414 grid.set(4, 4, 4, VoxelData {
416 radiance: Vec4::new(5.0, 5.0, 5.0, 1.0),
417 normal: Vec3::Y,
418 opacity: 1.0,
419 sh_coeffs: [0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
420 });
421
422 for &(dx, dy, dz) in &[(1i32,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)] {
424 grid.set((4+dx) as u32, (4+dy) as u32, (4+dz) as u32, VoxelData {
425 radiance: Vec4::new(0.0, 0.0, 0.0, 0.01),
426 normal: Vec3::Y,
427 opacity: 0.5,
428 sh_coeffs: [0.282095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
429 });
430 }
431
432 let config = PropagationConfig {
433 iterations: 2,
434 damping: 0.9,
435 flux_weight: 1.0,
436 };
437 propagate_light(&mut grid, &config);
438
439 for &(dx, dy, dz) in &[(1i32,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)] {
441 let vd = grid.get((4+dx) as u32, (4+dy) as u32, (4+dz) as u32);
442 let luminance = vd.radiance.x + vd.radiance.y + vd.radiance.z;
443 assert!(luminance > 0.0, "Neighbor at ({dx},{dy},{dz}) should have light: {luminance}");
444 }
445 }
446
447 #[test]
448 fn test_flux_through_face() {
449 let vd = VoxelData {
450 radiance: Vec4::new(1.0, 0.5, 0.25, 1.0),
451 normal: Vec3::Y,
452 opacity: 1.0,
453 sh_coeffs: [0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
454 };
455
456 let flux = flux_through_face(&vd, Vec3::X);
457 assert!(flux.x > 0.0);
459 }
460
461 #[test]
462 fn test_propagation_config_default() {
463 let config = PropagationConfig::default();
464 assert_eq!(config.iterations, 4);
465 assert!(config.damping < 1.0);
466 }
467
468 #[test]
469 fn test_sh_convolve_cosine() {
470 let sh = SHCoeffs::from_direction(Vec3::Y);
471 let convolved = sh.convolve_cosine();
472 let val_y = convolved.evaluate(Vec3::Y);
474 let val_neg_y = convolved.evaluate(-Vec3::Y);
475 assert!(val_y > val_neg_y);
476 }
477}