1use glam::{Vec3, Vec4};
2use super::octree::{SparseVoxelOctree, VoxelData};
3
4#[derive(Debug, Clone)]
6pub struct ConeTraceConfig {
7 pub max_distance: f32,
8 pub step_multiplier: f32,
9 pub ao_weight: f32,
10 pub gi_weight: f32,
11}
12
13impl Default for ConeTraceConfig {
14 fn default() -> Self {
15 Self {
16 max_distance: 100.0,
17 step_multiplier: 1.0,
18 ao_weight: 1.0,
19 gi_weight: 1.0,
20 }
21 }
22}
23
24#[derive(Debug, Clone, Copy)]
26pub struct ConeTraceResult {
27 pub color: Vec3,
28 pub occlusion: f32,
29 pub hit_distance: f32,
30}
31
32impl Default for ConeTraceResult {
33 fn default() -> Self {
34 Self {
35 color: Vec3::ZERO,
36 occlusion: 0.0,
37 hit_distance: f32::MAX,
38 }
39 }
40}
41
42pub fn trace_cone(
47 octree: &SparseVoxelOctree,
48 origin: Vec3,
49 direction: Vec3,
50 cone_angle: f32,
51 config: &ConeTraceConfig,
52) -> ConeTraceResult {
53 let dir = direction.normalize_or_zero();
54 if dir.length_squared() < 0.5 {
55 return ConeTraceResult::default();
56 }
57
58 let voxel_size = octree.world_bounds.size().x / (1u32 << octree.max_depth) as f32;
59 let tan_half = cone_angle.tan();
60
61 let mut accumulated_color = Vec3::ZERO;
62 let mut accumulated_opacity = 0.0f32;
63 let mut hit_distance = f32::MAX;
64
65 let mut t = voxel_size * 1.0;
67
68 while t < config.max_distance && accumulated_opacity < 0.95 {
69 let sample_pos = origin + dir * t;
70
71 let diameter = 2.0 * t * tan_half;
73 let diameter = diameter.max(voxel_size);
74
75 let lod = (diameter / voxel_size).log2().max(0.0).min(octree.max_depth as f32);
77 let lod_level = lod.round() as u8;
78
79 if let Some(data) = octree.lookup(sample_pos, lod_level) {
81 if !data.is_empty() {
82 let sample_color = Vec3::new(data.radiance.x, data.radiance.y, data.radiance.z);
83 let sample_opacity = data.opacity;
84
85 let alpha = (1.0 - accumulated_opacity) * sample_opacity;
87 accumulated_color += sample_color * alpha * config.gi_weight;
88 accumulated_opacity += alpha;
89
90 if hit_distance == f32::MAX {
91 hit_distance = t;
92 }
93 }
94 }
95
96 let step = diameter * config.step_multiplier;
98 t += step.max(voxel_size * 0.5);
99 }
100
101 ConeTraceResult {
102 color: accumulated_color,
103 occlusion: accumulated_opacity.min(1.0) * config.ao_weight,
104 hit_distance,
105 }
106}
107
108pub fn diffuse_gi(
110 octree: &SparseVoxelOctree,
111 position: Vec3,
112 normal: Vec3,
113 config: &ConeTraceConfig,
114) -> Vec3 {
115 let cones = hemisphere_cones(normal, 6);
116 let mut total_color = Vec3::ZERO;
117 let mut total_weight = 0.0f32;
118
119 for (dir, aperture) in &cones {
120 let result = trace_cone(octree, position, *dir, *aperture, config);
121 let weight = normal.dot(*dir).max(0.0);
123 total_color += result.color * weight;
124 total_weight += weight;
125 }
126
127 if total_weight > 0.0 {
128 total_color / total_weight
129 } else {
130 Vec3::ZERO
131 }
132}
133
134pub fn specular_gi(
136 octree: &SparseVoxelOctree,
137 position: Vec3,
138 normal: Vec3,
139 view_dir: Vec3,
140 roughness: f32,
141 config: &ConeTraceConfig,
142) -> Vec3 {
143 let reflect_dir = view_dir - 2.0 * normal.dot(view_dir) * normal;
144 let reflect_dir = reflect_dir.normalize_or_zero();
145
146 let aperture = (roughness * std::f32::consts::FRAC_PI_4).max(0.01);
148
149 let result = trace_cone(octree, position, reflect_dir, aperture, config);
150 result.color
151}
152
153pub fn ambient_occlusion(
155 octree: &SparseVoxelOctree,
156 position: Vec3,
157 normal: Vec3,
158 config: &ConeTraceConfig,
159) -> f32 {
160 let cones = hemisphere_cones(normal, 4);
161 let mut total_occlusion = 0.0f32;
162
163 let ao_config = ConeTraceConfig {
164 max_distance: config.max_distance * 0.3, ao_weight: 1.0,
166 gi_weight: 0.0, ..*config
168 };
169
170 for (dir, aperture) in &cones {
171 let wide_aperture = aperture * 2.0; let result = trace_cone(octree, position, *dir, wide_aperture, &ao_config);
173 total_occlusion += result.occlusion;
174 }
175
176 let avg = total_occlusion / cones.len() as f32;
177 1.0 - avg.min(1.0)
178}
179
180pub fn soft_shadows(
182 octree: &SparseVoxelOctree,
183 position: Vec3,
184 light_dir: Vec3,
185 light_angle: f32,
186 config: &ConeTraceConfig,
187) -> f32 {
188 let result = trace_cone(octree, position, light_dir, light_angle, config);
189 1.0 - result.occlusion.min(1.0)
190}
191
192pub fn hemisphere_cones(normal: Vec3, count: usize) -> Vec<(Vec3, f32)> {
194 let n = normal.normalize_or_zero();
195 if n.length_squared() < 0.5 {
196 return vec![(Vec3::Y, 0.5); count];
197 }
198
199 let up = if n.y.abs() < 0.99 { Vec3::Y } else { Vec3::X };
201 let tangent = n.cross(up).normalize();
202 let bitangent = n.cross(tangent).normalize();
203
204 let mut cones = Vec::with_capacity(count);
205 let aperture = std::f32::consts::PI / (count as f32 * 1.5);
206
207 match count {
208 1 => {
209 cones.push((n, aperture));
210 }
211 c => {
212 cones.push((n, aperture));
214
215 let ring_count = c - 1;
217 let ring_angle = std::f32::consts::FRAC_PI_3;
218 let cos_ring = ring_angle.cos();
219 let sin_ring = ring_angle.sin();
220
221 for i in 0..ring_count {
222 let phi = 2.0 * std::f32::consts::PI * i as f32 / ring_count as f32;
223 let dir = n * cos_ring
224 + tangent * sin_ring * phi.cos()
225 + bitangent * sin_ring * phi.sin();
226 cones.push((dir.normalize(), aperture * 1.2));
227 }
228 }
229 }
230
231 cones
232}
233
234pub struct ConeDistribution;
236
237impl ConeDistribution {
238 pub fn diffuse_6_cones(normal: Vec3) -> Vec<(Vec3, f32)> {
240 hemisphere_cones(normal, 6)
241 }
242
243 pub fn diffuse_16_cones(normal: Vec3) -> Vec<(Vec3, f32)> {
245 let n = normal.normalize_or_zero();
246 let up = if n.y.abs() < 0.99 { Vec3::Y } else { Vec3::X };
247 let tangent = n.cross(up).normalize();
248 let bitangent = n.cross(tangent).normalize();
249
250 let mut cones = Vec::with_capacity(16);
251 let aperture = std::f32::consts::PI / 24.0;
252
253 cones.push((n, aperture));
255
256 let inner_count = 5;
258 let inner_angle = std::f32::consts::FRAC_PI_6;
259 for i in 0..inner_count {
260 let phi = 2.0 * std::f32::consts::PI * i as f32 / inner_count as f32;
261 let dir = n * inner_angle.cos()
262 + tangent * inner_angle.sin() * phi.cos()
263 + bitangent * inner_angle.sin() * phi.sin();
264 cones.push((dir.normalize(), aperture));
265 }
266
267 let outer_count = 10;
269 let outer_angle = std::f32::consts::FRAC_PI_3;
270 for i in 0..outer_count {
271 let phi = 2.0 * std::f32::consts::PI * i as f32 / outer_count as f32
272 + std::f32::consts::FRAC_PI_6 / outer_count as f32;
273 let dir = n * outer_angle.cos()
274 + tangent * outer_angle.sin() * phi.cos()
275 + bitangent * outer_angle.sin() * phi.sin();
276 cones.push((dir.normalize(), aperture * 1.5));
277 }
278
279 cones
280 }
281
282 pub fn ao_4_cones(normal: Vec3) -> Vec<(Vec3, f32)> {
284 hemisphere_cones(normal, 4)
285 }
286}
287
288pub const CONE_TRACE_FRAG_SRC: &str = r#"
290#version 450
291
292in vec2 vTexCoord;
293
294layout(location = 0) out vec4 fragColor;
295
296uniform sampler2D gPosition;
297uniform sampler2D gNormal;
298uniform sampler2D gAlbedo;
299uniform sampler3D voxelTexture;
300
301uniform mat4 voxelWorldToUVW; // Transform from world to [0,1] voxel UVW
302uniform float maxDistance;
303uniform float stepMultiplier;
304uniform float giIntensity;
305uniform float aoIntensity;
306uniform int maxDepth;
307
308// Cone trace through 3D texture
309vec4 traceCone(vec3 origin, vec3 dir, float aperture) {
310 float voxelSize = 1.0 / float(textureSize(voxelTexture, 0).x);
311 float t = voxelSize * 2.0;
312 vec3 color = vec3(0.0);
313 float alpha = 0.0;
314
315 while (t < maxDistance && alpha < 0.95) {
316 vec3 pos = origin + dir * t;
317 vec3 uvw = (voxelWorldToUVW * vec4(pos, 1.0)).xyz;
318
319 if (any(lessThan(uvw, vec3(0.0))) || any(greaterThan(uvw, vec3(1.0)))) break;
320
321 float diameter = 2.0 * t * tan(aperture);
322 float lod = log2(max(diameter / voxelSize, 1.0));
323
324 vec4 sample_val = textureLod(voxelTexture, uvw, lod);
325
326 float a = (1.0 - alpha) * sample_val.a;
327 color += sample_val.rgb * a;
328 alpha += a;
329
330 t += max(diameter * stepMultiplier, voxelSize * 0.5);
331 }
332
333 return vec4(color, alpha);
334}
335
336void main() {
337 vec3 worldPos = texture(gPosition, vTexCoord).xyz;
338 vec3 normal = normalize(texture(gNormal, vTexCoord).xyz);
339 vec3 albedo = texture(gAlbedo, vTexCoord).rgb;
340
341 if (length(normal) < 0.1) {
342 fragColor = vec4(0.0);
343 return;
344 }
345
346 // Build tangent frame
347 vec3 up = abs(normal.y) < 0.99 ? vec3(0,1,0) : vec3(1,0,0);
348 vec3 T = normalize(cross(normal, up));
349 vec3 B = normalize(cross(normal, T));
350
351 // Diffuse GI: 6 cones
352 vec3 diffuseGI = vec3(0.0);
353 float ao = 0.0;
354 float coneAperture = 0.5236; // ~30 degrees
355
356 // Center cone
357 vec4 c0 = traceCone(worldPos, normal, coneAperture);
358 diffuseGI += c0.rgb;
359 ao += c0.a;
360
361 // 5 ring cones at 60 degrees
362 float ringAngle = 1.0472; // 60 degrees
363 for (int i = 0; i < 5; i++) {
364 float phi = float(i) * 1.2566;
365 vec3 dir = normal * cos(ringAngle)
366 + T * sin(ringAngle) * cos(phi)
367 + B * sin(ringAngle) * sin(phi);
368 vec4 ci = traceCone(worldPos, normalize(dir), coneAperture * 1.2);
369 float weight = max(dot(normalize(dir), normal), 0.0);
370 diffuseGI += ci.rgb * weight;
371 ao += ci.a;
372 }
373
374 diffuseGI /= 6.0;
375 ao = 1.0 - min(ao / 6.0, 1.0);
376
377 vec3 finalColor = albedo * (diffuseGI * giIntensity + vec3(ao * aoIntensity * 0.1));
378 fragColor = vec4(finalColor, 1.0);
379}
380"#;
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::svogi::octree::{SparseVoxelOctree, VoxelData, Aabb};
386
387 fn make_empty_octree() -> SparseVoxelOctree {
388 SparseVoxelOctree::new(
389 Aabb::new(Vec3::ZERO, Vec3::splat(16.0)),
390 4,
391 )
392 }
393
394 fn make_filled_octree() -> SparseVoxelOctree {
395 let mut octree = make_empty_octree();
396 for x in 4..8 {
398 for y in 4..8 {
399 for z in 4..8 {
400 octree.insert(
401 Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5),
402 VoxelData {
403 radiance: Vec4::new(1.0, 0.5, 0.25, 1.0),
404 normal: Vec3::Y,
405 opacity: 1.0,
406 sh_coeffs: [0.0; 9],
407 },
408 );
409 }
410 }
411 }
412 octree.build_mipmaps();
413 octree
414 }
415
416 #[test]
417 fn test_empty_octree_no_occlusion() {
418 let octree = make_empty_octree();
419 let config = ConeTraceConfig::default();
420 let result = trace_cone(
421 &octree,
422 Vec3::new(8.0, 8.0, 0.0),
423 Vec3::Z,
424 0.5,
425 &config,
426 );
427 assert!(result.occlusion < 0.01, "Empty octree should produce no occlusion, got {}", result.occlusion);
428 }
429
430 #[test]
431 fn test_filled_octree_occlusion() {
432 let octree = make_filled_octree();
433 let config = ConeTraceConfig {
434 max_distance: 20.0,
435 ..Default::default()
436 };
437
438 let result = trace_cone(
440 &octree,
441 Vec3::new(6.0, 6.0, 0.0),
442 Vec3::Z,
443 0.1,
444 &config,
445 );
446 assert!(result.occlusion > 0.0, "Should have some occlusion from filled block, got {}", result.occlusion);
447 }
448
449 #[test]
450 fn test_diffuse_gi_returns_color() {
451 let octree = make_filled_octree();
452 let config = ConeTraceConfig {
453 max_distance: 20.0,
454 ..Default::default()
455 };
456
457 let gi = diffuse_gi(&octree, Vec3::new(6.0, 9.0, 6.0), Vec3::Y, &config);
459 let _ = gi;
461 }
462
463 #[test]
464 fn test_specular_gi() {
465 let octree = make_filled_octree();
466 let config = ConeTraceConfig::default();
467
468 let spec = specular_gi(
469 &octree,
470 Vec3::new(6.0, 10.0, 6.0),
471 Vec3::Y,
472 Vec3::new(0.0, -1.0, -0.5).normalize(),
473 0.5,
474 &config,
475 );
476 let _ = spec;
477 }
478
479 #[test]
480 fn test_ambient_occlusion_value_range() {
481 let octree = make_empty_octree();
482 let config = ConeTraceConfig::default();
483
484 let ao = ambient_occlusion(&octree, Vec3::splat(8.0), Vec3::Y, &config);
485 assert!(ao >= 0.0 && ao <= 1.0, "AO should be in [0,1], got {ao}");
486 assert!(ao > 0.5, "Empty scene should have high AO (low occlusion), got {ao}");
488 }
489
490 #[test]
491 fn test_soft_shadows() {
492 let octree = make_filled_octree();
493 let config = ConeTraceConfig::default();
494
495 let shadow = soft_shadows(
496 &octree,
497 Vec3::new(6.0, 0.0, 6.0),
498 Vec3::Y,
499 0.05,
500 &config,
501 );
502 assert!(shadow >= 0.0 && shadow <= 1.0);
503 }
504
505 #[test]
506 fn test_hemisphere_cones_count() {
507 let cones = hemisphere_cones(Vec3::Y, 6);
508 assert_eq!(cones.len(), 6);
509
510 let cones2 = hemisphere_cones(Vec3::Y, 1);
511 assert_eq!(cones2.len(), 1);
512 }
513
514 #[test]
515 fn test_hemisphere_cones_directions() {
516 let normal = Vec3::Y;
517 let cones = hemisphere_cones(normal, 6);
518
519 for (dir, _) in &cones {
521 let dot = dir.dot(normal);
522 assert!(dot > 0.0, "Cone direction should be in hemisphere, dot={dot}");
523 }
524 }
525
526 #[test]
527 fn test_cone_distribution_presets() {
528 let n = Vec3::Z;
529 let d6 = ConeDistribution::diffuse_6_cones(n);
530 assert_eq!(d6.len(), 6);
531
532 let d16 = ConeDistribution::diffuse_16_cones(n);
533 assert_eq!(d16.len(), 16);
534
535 let ao4 = ConeDistribution::ao_4_cones(n);
536 assert_eq!(ao4.len(), 4);
537 }
538
539 #[test]
540 fn test_gi_color_matches_injected() {
541 let mut octree = SparseVoxelOctree::new(
542 Aabb::new(Vec3::ZERO, Vec3::splat(16.0)),
543 4,
544 );
545
546 for x in 6..10 {
548 for y in 6..10 {
549 for z in 6..10 {
550 octree.insert(
551 Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5),
552 VoxelData {
553 radiance: Vec4::new(5.0, 0.0, 0.0, 1.0),
554 normal: Vec3::Y,
555 opacity: 1.0,
556 sh_coeffs: [0.0; 9],
557 },
558 );
559 }
560 }
561 }
562 octree.build_mipmaps();
563
564 let config = ConeTraceConfig {
565 max_distance: 20.0,
566 step_multiplier: 1.0,
567 ao_weight: 1.0,
568 gi_weight: 1.0,
569 };
570
571 let result = trace_cone(
573 &octree,
574 Vec3::new(8.0, 8.0, 0.0),
575 Vec3::Z,
576 0.1,
577 &config,
578 );
579
580 if result.color.length() > 0.0 {
581 assert!(result.color.x >= result.color.y, "Color should be red-dominant");
583 assert!(result.color.x >= result.color.z, "Color should be red-dominant");
584 }
585 }
586}