1use glam::{Vec3, Vec4, Mat4};
2use super::octree::{VoxelData, VoxelGrid};
3
4#[derive(Debug, Clone, Copy)]
6pub struct DirectionalLight {
7 pub direction: Vec3,
8 pub color: Vec3,
9 pub intensity: f32,
10}
11
12#[derive(Debug, Clone, Copy)]
14pub struct PointLight {
15 pub position: Vec3,
16 pub color: Vec3,
17 pub intensity: f32,
18 pub radius: f32,
19}
20
21#[derive(Debug, Clone, Copy)]
23pub struct SpotLight {
24 pub position: Vec3,
25 pub direction: Vec3,
26 pub color: Vec3,
27 pub intensity: f32,
28 pub angle: f32,
29 pub falloff: f32,
30}
31
32#[derive(Debug, Clone, Copy)]
34pub enum LightSource {
35 Directional(DirectionalLight),
36 Point(PointLight),
37 Spot(SpotLight),
38}
39
40#[derive(Debug, Clone)]
42pub struct ShadowMap {
43 pub depth_data: Vec<f32>,
44 pub resolution: u32,
45 pub view_proj: Mat4,
46}
47
48impl ShadowMap {
49 pub fn new(resolution: u32, view_proj: Mat4) -> Self {
50 let count = (resolution * resolution) as usize;
51 Self {
52 depth_data: vec![1.0; count],
53 resolution,
54 view_proj,
55 }
56 }
57
58 fn sample_depth(&self, u: f32, v: f32) -> f32 {
60 let x = (u * self.resolution as f32) as i32;
61 let y = (v * self.resolution as f32) as i32;
62 if x < 0 || y < 0 || x >= self.resolution as i32 || y >= self.resolution as i32 {
63 return 1.0;
64 }
65 let idx = (y as u32 * self.resolution + x as u32) as usize;
66 if idx < self.depth_data.len() {
67 self.depth_data[idx]
68 } else {
69 1.0
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct LightInjectionConfig {
77 pub bounce_emissive: bool,
78 pub shadow_bias: f32,
79 pub pcf_radius: f32,
80}
81
82impl Default for LightInjectionConfig {
83 fn default() -> Self {
84 Self {
85 bounce_emissive: true,
86 shadow_bias: 0.005,
87 pcf_radius: 1.0,
88 }
89 }
90}
91
92pub fn shadow_test(world_pos: Vec3, shadow_map: &ShadowMap) -> f32 {
95 let clip = shadow_map.view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
96 if clip.w <= 0.0 {
97 return 1.0; }
99 let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
100
101 let u = ndc.x * 0.5 + 0.5;
103 let v = ndc.y * 0.5 + 0.5;
104 let depth = ndc.z * 0.5 + 0.5;
105
106 if u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 {
107 return 1.0; }
109
110 let shadow_depth = shadow_map.sample_depth(u, v);
111 let bias = 0.005;
112 if depth - bias > shadow_depth {
113 0.0 } else {
115 1.0 }
117}
118
119pub fn pcf_sample(shadow_map: &ShadowMap, uv: (f32, f32), depth: f32, kernel_size: u32) -> f32 {
121 let texel_size = 1.0 / shadow_map.resolution as f32;
122 let half_k = kernel_size as i32 / 2;
123 let bias = 0.005;
124 let mut lit_count = 0.0;
125 let mut total = 0.0;
126
127 for dy in -half_k..=half_k {
128 for dx in -half_k..=half_k {
129 let su = uv.0 + dx as f32 * texel_size;
130 let sv = uv.1 + dy as f32 * texel_size;
131 let shadow_depth = shadow_map.sample_depth(su, sv);
132 if depth - bias <= shadow_depth {
133 lit_count += 1.0;
134 }
135 total += 1.0;
136 }
137 }
138
139 if total > 0.0 { lit_count / total } else { 1.0 }
140}
141
142fn shadow_test_pcf(world_pos: Vec3, shadow_map: &ShadowMap, config: &LightInjectionConfig) -> f32 {
144 let clip = shadow_map.view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
145 if clip.w <= 0.0 {
146 return 1.0;
147 }
148 let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
149 let u = ndc.x * 0.5 + 0.5;
150 let v = ndc.y * 0.5 + 0.5;
151 let depth = ndc.z * 0.5 + 0.5;
152
153 if u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 {
154 return 1.0;
155 }
156
157 let kernel = (config.pcf_radius * 2.0 + 1.0) as u32;
158 pcf_sample(shadow_map, (u, v), depth, kernel.max(1))
159}
160
161fn compute_light_contribution(
163 light: &LightSource,
164 world_pos: Vec3,
165 normal: Vec3,
166) -> Vec3 {
167 match light {
168 LightSource::Directional(dl) => {
169 let n_dot_l = normal.dot(-dl.direction.normalize()).max(0.0);
170 dl.color * dl.intensity * n_dot_l
171 }
172 LightSource::Point(pl) => {
173 let to_light = pl.position - world_pos;
174 let dist = to_light.length();
175 if dist > pl.radius || dist < 1e-6 {
176 return Vec3::ZERO;
177 }
178 let dir = to_light / dist;
179 let n_dot_l = normal.dot(dir).max(0.0);
180 let attenuation = 1.0 / (1.0 + dist * dist);
181 let range_falloff = (1.0 - (dist / pl.radius).powi(2)).max(0.0);
182 pl.color * pl.intensity * n_dot_l * attenuation * range_falloff
183 }
184 LightSource::Spot(sl) => {
185 let to_light = sl.position - world_pos;
186 let dist = to_light.length();
187 if dist < 1e-6 {
188 return Vec3::ZERO;
189 }
190 let dir = to_light / dist;
191 let n_dot_l = normal.dot(dir).max(0.0);
192 let spot_cos = (-dir).dot(sl.direction.normalize());
193 let cone_cos = sl.angle.cos();
194 if spot_cos < cone_cos {
195 return Vec3::ZERO;
196 }
197 let spot_factor = ((spot_cos - cone_cos) / (1.0 - cone_cos)).powf(sl.falloff);
198 let attenuation = 1.0 / (1.0 + dist * dist);
199 sl.color * sl.intensity * n_dot_l * attenuation * spot_factor
200 }
201 }
202}
203
204fn voxel_world_pos(grid: &VoxelGrid, x: u32, y: u32, z: u32, world_min: Vec3, voxel_size: Vec3) -> Vec3 {
206 world_min + Vec3::new(
207 (x as f32 + 0.5) * voxel_size.x,
208 (y as f32 + 0.5) * voxel_size.y,
209 (z as f32 + 0.5) * voxel_size.z,
210 )
211}
212
213pub fn inject_direct_light(
215 grid: &mut VoxelGrid,
216 lights: &[LightSource],
217 shadow_maps: &[ShadowMap],
218 world_min: Vec3,
219 world_size: Vec3,
220) {
221 inject_direct_light_with_config(grid, lights, shadow_maps, world_min, world_size, &LightInjectionConfig::default());
222}
223
224pub fn inject_direct_light_with_config(
226 grid: &mut VoxelGrid,
227 lights: &[LightSource],
228 shadow_maps: &[ShadowMap],
229 world_min: Vec3,
230 world_size: Vec3,
231 config: &LightInjectionConfig,
232) {
233 let res = grid.resolution;
234 let voxel_size = world_size / Vec3::new(res.x as f32, res.y as f32, res.z as f32);
235
236 for z in 0..res.z {
237 for y in 0..res.y {
238 for x in 0..res.x {
239 let vd = grid.get(x, y, z);
240 if vd.is_empty() {
241 continue;
242 }
243 let normal = vd.normal;
244 let existing_color = Vec3::new(vd.radiance.x, vd.radiance.y, vd.radiance.z);
245 let world_pos = voxel_world_pos(grid, x, y, z, world_min, voxel_size);
246
247 let mut total_light = Vec3::ZERO;
248 for (light_idx, light) in lights.iter().enumerate() {
249 let contribution = compute_light_contribution(light, world_pos, normal);
250
251 let shadow_factor = if light_idx < shadow_maps.len() {
253 shadow_test_pcf(world_pos, &shadow_maps[light_idx], config)
254 } else {
255 1.0
256 };
257
258 total_light += contribution * shadow_factor;
259 }
260
261 let vd = grid.get_mut(x, y, z);
262 let lit_color = Vec3::new(
264 existing_color.x * total_light.x,
265 existing_color.y * total_light.y,
266 existing_color.z * total_light.z,
267 ) + total_light * 0.1; vd.radiance = Vec4::new(lit_color.x, lit_color.y, lit_color.z, vd.radiance.w);
269 }
270 }
271 }
272}
273
274pub fn inject_emissive(grid: &mut VoxelGrid) {
276 let res = grid.resolution;
277 for z in 0..res.z {
278 for y in 0..res.y {
279 for x in 0..res.x {
280 let vd = grid.get(x, y, z);
281 if vd.is_empty() {
282 continue;
283 }
284 let emission_strength = vd.radiance.w;
286 if emission_strength > 1.0 {
287 let boost = emission_strength - 1.0;
288 let idx = grid.index(x, y, z);
289 let vd = &mut grid.data[idx];
290 vd.radiance.x += boost;
291 vd.radiance.y += boost;
292 vd.radiance.z += boost;
293 }
294 }
295 }
296 }
297}
298
299pub fn inject_emissive_from_sh(grid: &mut VoxelGrid) {
301 let res = grid.resolution;
302 for z in 0..res.z {
303 for y in 0..res.y {
304 for x in 0..res.x {
305 let idx = grid.index(x, y, z);
306 let emission = grid.data[idx].sh_coeffs[0];
307 if emission > 0.0 {
308 grid.data[idx].radiance.x += emission;
309 grid.data[idx].radiance.y += emission;
310 grid.data[idx].radiance.z += emission;
311 }
312 }
313 }
314 }
315}
316
317pub const INJECT_COMP_SRC: &str = r#"
319#version 450
320layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
321
322layout(rgba16f, binding = 0) uniform image3D voxelRadiance;
323layout(r32f, binding = 1) uniform readonly image3D voxelOpacity;
324
325struct DirectionalLight {
326 vec4 direction;
327 vec4 color; // xyz = color, w = intensity
328};
329
330layout(std140, binding = 2) uniform LightBlock {
331 DirectionalLight lights[8];
332 int lightCount;
333};
334
335layout(binding = 3) uniform sampler2D shadowMap;
336uniform mat4 shadowViewProj;
337uniform vec3 worldMin;
338uniform vec3 worldSize;
339uniform uint resolution;
340
341float pcfShadow(vec3 worldPos) {
342 vec4 clip = shadowViewProj * vec4(worldPos, 1.0);
343 vec3 ndc = clip.xyz / clip.w;
344 vec2 uv = ndc.xy * 0.5 + 0.5;
345 float depth = ndc.z * 0.5 + 0.5;
346
347 if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 1.0;
348
349 float shadow = 0.0;
350 float texelSize = 1.0 / float(textureSize(shadowMap, 0).x);
351 for (int dy = -1; dy <= 1; dy++) {
352 for (int dx = -1; dx <= 1; dx++) {
353 float d = texture(shadowMap, uv + vec2(dx, dy) * texelSize).r;
354 shadow += (depth - 0.005 > d) ? 0.0 : 1.0;
355 }
356 }
357 return shadow / 9.0;
358}
359
360void main() {
361 ivec3 coord = ivec3(gl_GlobalInvocationID.xyz);
362 if (any(greaterThanEqual(coord, ivec3(resolution)))) return;
363
364 float opacity = imageLoad(voxelOpacity, coord).r;
365 if (opacity <= 0.0) return;
366
367 vec3 voxelSize = worldSize / float(resolution);
368 vec3 worldPos = worldMin + (vec3(coord) + 0.5) * voxelSize;
369
370 vec4 currentRadiance = imageLoad(voxelRadiance, coord);
371 vec3 normal = normalize(currentRadiance.xyz * 2.0 - 1.0); // packed normal approximation
372
373 vec3 totalLight = vec3(0.0);
374 for (int i = 0; i < lightCount; i++) {
375 vec3 lightDir = -normalize(lights[i].direction.xyz);
376 float NdotL = max(dot(normal, lightDir), 0.0);
377 float shadow = pcfShadow(worldPos);
378 totalLight += lights[i].color.xyz * lights[i].color.w * NdotL * shadow;
379 }
380
381 imageStore(voxelRadiance, coord, vec4(totalLight, opacity));
382}
383"#;
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use glam::UVec3;
389
390 fn make_test_grid() -> VoxelGrid {
391 let mut grid = VoxelGrid::new(UVec3::new(4, 4, 4));
392 grid.set(1, 1, 1, VoxelData {
394 radiance: Vec4::new(1.0, 1.0, 1.0, 1.0),
395 normal: Vec3::Y,
396 opacity: 1.0,
397 sh_coeffs: [0.0; 9],
398 });
399 grid.set(2, 2, 2, VoxelData {
401 radiance: Vec4::new(0.5, 0.5, 0.5, 1.0),
402 normal: Vec3::Y,
403 opacity: 1.0,
404 sh_coeffs: [0.0; 9],
405 });
406 grid
407 }
408
409 #[test]
410 fn test_inject_directional_light() {
411 let mut grid = make_test_grid();
412 let light = LightSource::Directional(DirectionalLight {
413 direction: Vec3::new(0.0, -1.0, 0.0),
414 color: Vec3::new(1.0, 1.0, 1.0),
415 intensity: 2.0,
416 });
417
418 inject_direct_light(
419 &mut grid,
420 &[light],
421 &[],
422 Vec3::ZERO,
423 Vec3::splat(4.0),
424 );
425
426 let vd = grid.get(1, 1, 1);
427 assert!(vd.radiance.x > 0.0, "Lit voxel should have positive radiance");
429 }
430
431 #[test]
432 fn test_inject_point_light() {
433 let mut grid = make_test_grid();
434 let light = LightSource::Point(PointLight {
435 position: Vec3::new(1.5, 3.0, 1.5),
436 color: Vec3::ONE,
437 intensity: 5.0,
438 radius: 10.0,
439 });
440
441 inject_direct_light(&mut grid, &[light], &[], Vec3::ZERO, Vec3::splat(4.0));
442 let vd = grid.get(1, 1, 1);
443 assert!(vd.radiance.x > 0.0);
444 }
445
446 #[test]
447 fn test_inject_spot_light() {
448 let mut grid = make_test_grid();
449 let light = LightSource::Spot(SpotLight {
450 position: Vec3::new(1.5, 4.0, 1.5),
451 direction: Vec3::new(0.0, -1.0, 0.0),
452 color: Vec3::ONE,
453 intensity: 5.0,
454 angle: std::f32::consts::FRAC_PI_4,
455 falloff: 1.0,
456 });
457
458 inject_direct_light(&mut grid, &[light], &[], Vec3::ZERO, Vec3::splat(4.0));
459 let vd = grid.get(1, 1, 1);
460 assert!(vd.radiance.x > 0.0);
461 }
462
463 #[test]
464 fn test_shadow_test_fully_shadowed() {
465 let mut sm = ShadowMap::new(4, Mat4::IDENTITY);
467 for d in &mut sm.depth_data {
468 *d = 0.0;
469 }
470
471 let result = shadow_test(Vec3::new(0.0, 0.0, 0.5), &sm);
472 assert!(result < 0.5, "Should be in shadow, got {result}");
474 }
475
476 #[test]
477 fn test_shadow_test_fully_lit() {
478 let sm = ShadowMap::new(4, Mat4::IDENTITY);
480
481 let result = shadow_test(Vec3::new(0.0, 0.0, 0.0), &sm);
482 assert!((result - 1.0).abs() < 0.01, "Should be fully lit, got {result}");
483 }
484
485 #[test]
486 fn test_pcf_sample() {
487 let sm = ShadowMap::new(8, Mat4::IDENTITY);
488 let result = pcf_sample(&sm, (0.5, 0.5), 0.4, 3);
489 assert!((result - 1.0).abs() < 0.01, "All texels at depth 1.0, should be fully lit");
490 }
491
492 #[test]
493 fn test_inject_emissive() {
494 let mut grid = VoxelGrid::new(UVec3::new(4, 4, 4));
495 grid.set(1, 1, 1, VoxelData {
496 radiance: Vec4::new(0.5, 0.5, 0.5, 2.0), normal: Vec3::Y,
498 opacity: 1.0,
499 sh_coeffs: [0.0; 9],
500 });
501
502 inject_emissive(&mut grid);
503 let vd = grid.get(1, 1, 1);
504 assert!(vd.radiance.x > 0.5, "Emissive voxel should be brighter");
505 }
506
507 #[test]
508 fn test_inject_with_shadow_map() {
509 let mut grid = make_test_grid();
510 let light = LightSource::Directional(DirectionalLight {
511 direction: Vec3::new(0.0, -1.0, 0.0),
512 color: Vec3::ONE,
513 intensity: 2.0,
514 });
515
516 let mut sm = ShadowMap::new(64, Mat4::IDENTITY);
518 for d in &mut sm.depth_data {
519 *d = 0.0;
520 }
521
522 inject_direct_light(&mut grid, &[light], &[sm], Vec3::ZERO, Vec3::splat(4.0));
523
524 let vd = grid.get(1, 1, 1);
526 assert!(vd.radiance.x < 1.0, "Shadowed voxel should have reduced radiance");
528 }
529
530 #[test]
531 fn test_no_lights_no_change() {
532 let mut grid = make_test_grid();
533 let original_radiance = grid.get(1, 1, 1).radiance;
534 inject_direct_light(&mut grid, &[], &[], Vec3::ZERO, Vec3::splat(4.0));
535 let vd = grid.get(1, 1, 1);
537 assert!(vd.radiance.x <= original_radiance.x);
539 }
540}