1use super::lights::{Vec3, Color, Mat4};
8use std::f32::consts::PI;
9
10#[derive(Debug, Clone)]
14pub struct SsaoConfig {
15 pub sample_count: u32,
17 pub radius: f32,
19 pub bias: f32,
21 pub power: f32,
23 pub intensity: f32,
25 pub blur: bool,
27 pub blur_radius: u32,
29 pub blur_sharpness: f32,
31 pub noise_size: u32,
33}
34
35impl Default for SsaoConfig {
36 fn default() -> Self {
37 Self {
38 sample_count: 32,
39 radius: 0.5,
40 bias: 0.025,
41 power: 2.0,
42 intensity: 1.0,
43 blur: true,
44 blur_radius: 4,
45 blur_sharpness: 8.0,
46 noise_size: 4,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
55pub struct SsaoKernel {
56 pub samples: Vec<Vec3>,
58 pub noise: Vec<Vec3>,
60 pub config: SsaoConfig,
62}
63
64impl SsaoKernel {
65 pub fn new(config: SsaoConfig) -> Self {
67 let samples = Self::generate_samples(config.sample_count, config.radius);
68 let noise = Self::generate_noise(config.noise_size);
69 Self { samples, noise, config }
70 }
71
72 fn generate_samples(count: u32, radius: f32) -> Vec<Vec3> {
74 let mut samples = Vec::with_capacity(count as usize);
75
76 for i in 0..count {
77 let xi1 = Self::radical_inverse_vdc(i);
79 let xi2 = Self::halton_sequence(i, 3);
80
81 let phi = 2.0 * PI * xi1;
83 let cos_theta = (1.0 - xi2).sqrt();
84 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
85
86 let x = sin_theta * phi.cos();
87 let y = sin_theta * phi.sin();
88 let z = cos_theta;
89
90 let scale = (i as f32 + 1.0) / count as f32;
92 let scale = Self::lerp_f32(0.1, 1.0, scale * scale);
93
94 samples.push(Vec3::new(x * scale * radius, y * scale * radius, z * scale * radius));
95 }
96
97 samples
98 }
99
100 fn generate_noise(size: u32) -> Vec<Vec3> {
102 let count = (size * size) as usize;
103 let mut noise = Vec::with_capacity(count);
104
105 for i in 0..count {
106 let seed = i as f32 * 7.31 + 0.5;
108 let x = (seed * 12.9898 + 78.233).sin() * 43758.5453;
109 let y = (seed * 39.346 + 11.135).sin() * 28461.7231;
110 let nx = x.fract() * 2.0 - 1.0;
111 let ny = y.fract() * 2.0 - 1.0;
112 let len = (nx * nx + ny * ny).sqrt().max(0.001);
113 noise.push(Vec3::new(nx / len, ny / len, 0.0));
114 }
115
116 noise
117 }
118
119 fn radical_inverse_vdc(mut bits: u32) -> f32 {
121 bits = (bits << 16) | (bits >> 16);
122 bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
123 bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
124 bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
125 bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
126 bits as f32 * 2.3283064365386963e-10
127 }
128
129 fn halton_sequence(index: u32, base: u32) -> f32 {
131 let mut f = 1.0f32;
132 let mut r = 0.0f32;
133 let mut i = index;
134 while i > 0 {
135 f /= base as f32;
136 r += f * (i % base) as f32;
137 i /= base;
138 }
139 r
140 }
141
142 fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
143 a + (b - a) * t
144 }
145
146 pub fn noise_at(&self, x: u32, y: u32) -> Vec3 {
148 let size = self.config.noise_size;
149 if size == 0 || self.noise.is_empty() {
150 return Vec3::new(1.0, 0.0, 0.0);
151 }
152 let idx = ((y % size) * size + (x % size)) as usize;
153 self.noise[idx % self.noise.len()]
154 }
155}
156
157#[derive(Debug, Clone)]
161pub struct SsaoResult {
162 pub width: u32,
163 pub height: u32,
164 pub ao_buffer: Vec<f32>,
166}
167
168impl SsaoResult {
169 pub fn new(width: u32, height: u32) -> Self {
170 let size = (width as usize) * (height as usize);
171 Self {
172 width,
173 height,
174 ao_buffer: vec![1.0; size],
175 }
176 }
177
178 pub fn compute(
180 &mut self,
181 depth_buffer: &[f32],
182 normal_buffer: &[Vec3],
183 kernel: &SsaoKernel,
184 projection: &Mat4,
185 ) {
186 let w = self.width as usize;
187 let h = self.height as usize;
188 let config = &kernel.config;
189
190 for y in 0..h {
191 for x in 0..w {
192 let idx = y * w + x;
193 let depth = depth_buffer[idx];
194 if depth >= 1.0 {
195 self.ao_buffer[idx] = 1.0;
196 continue;
197 }
198
199 let normal = normal_buffer[idx];
200 let noise = kernel.noise_at(x as u32, y as u32);
201
202 let ndc_x = (x as f32 / w as f32) * 2.0 - 1.0;
204 let ndc_y = (y as f32 / h as f32) * 2.0 - 1.0;
205 let frag_pos = Vec3::new(ndc_x * depth, ndc_y * depth, -depth);
206
207 let tangent = Self::gramm_schmidt(noise, normal);
209 let bitangent = normal.cross(tangent);
210
211 let mut occlusion = 0.0f32;
212 for sample in &kernel.samples {
213 let rotated = Vec3::new(
215 tangent.x * sample.x + bitangent.x * sample.y + normal.x * sample.z,
216 tangent.y * sample.x + bitangent.y * sample.y + normal.y * sample.z,
217 tangent.z * sample.x + bitangent.z * sample.y + normal.z * sample.z,
218 );
219
220 let sample_pos = frag_pos + rotated * config.radius;
221
222 let clip = projection.transform_point(sample_pos);
224 let screen_x = ((clip.x * 0.5 + 0.5) * w as f32) as usize;
225 let screen_y = ((clip.y * 0.5 + 0.5) * h as f32) as usize;
226
227 if screen_x < w && screen_y < h {
228 let sample_depth = depth_buffer[screen_y * w + screen_x];
229 let range_check = Self::smooth_step(
230 0.0,
231 1.0,
232 config.radius / (frag_pos.z - sample_depth).abs().max(0.001),
233 );
234
235 if sample_depth >= sample_pos.z + config.bias {
236 occlusion += range_check;
237 }
238 }
239 }
240
241 occlusion /= kernel.samples.len() as f32;
242 let ao = (1.0 - occlusion * config.intensity).max(0.0).powf(config.power);
243 self.ao_buffer[idx] = ao;
244 }
245 }
246
247 if config.blur {
248 self.bilateral_blur(depth_buffer, config.blur_radius, config.blur_sharpness);
249 }
250 }
251
252 pub fn bilateral_blur(&mut self, depth_buffer: &[f32], radius: u32, sharpness: f32) {
254 let w = self.width as usize;
255 let h = self.height as usize;
256 let mut temp = vec![0.0f32; w * h];
257
258 for y in 0..h {
260 for x in 0..w {
261 let center_depth = depth_buffer[y * w + x];
262 let center_ao = self.ao_buffer[y * w + x];
263 let mut sum = 0.0f32;
264 let mut weight_sum = 0.0f32;
265
266 let x_start = x.saturating_sub(radius as usize);
267 let x_end = (x + radius as usize + 1).min(w);
268
269 for sx in x_start..x_end {
270 let sample_depth = depth_buffer[y * w + sx];
271 let sample_ao = self.ao_buffer[y * w + sx];
272
273 let depth_diff = (center_depth - sample_depth).abs();
274 let weight = (-depth_diff * sharpness).exp();
275
276 sum += sample_ao * weight;
277 weight_sum += weight;
278 }
279
280 temp[y * w + x] = if weight_sum > 0.0 {
281 sum / weight_sum
282 } else {
283 center_ao
284 };
285 }
286 }
287
288 for y in 0..h {
290 for x in 0..w {
291 let center_depth = depth_buffer[y * w + x];
292 let mut sum = 0.0f32;
293 let mut weight_sum = 0.0f32;
294
295 let y_start = y.saturating_sub(radius as usize);
296 let y_end = (y + radius as usize + 1).min(h);
297
298 for sy in y_start..y_end {
299 let sample_depth = depth_buffer[sy * w + x];
300 let sample_ao = temp[sy * w + x];
301
302 let depth_diff = (center_depth - sample_depth).abs();
303 let weight = (-depth_diff * sharpness).exp();
304
305 sum += sample_ao * weight;
306 weight_sum += weight;
307 }
308
309 self.ao_buffer[y * w + x] = if weight_sum > 0.0 {
310 sum / weight_sum
311 } else {
312 temp[y * w + x]
313 };
314 }
315 }
316 }
317
318 pub fn ao_at(&self, x: u32, y: u32) -> f32 {
320 if x < self.width && y < self.height {
321 self.ao_buffer[(y as usize) * (self.width as usize) + (x as usize)]
322 } else {
323 1.0
324 }
325 }
326
327 fn gramm_schmidt(v: Vec3, n: Vec3) -> Vec3 {
329 let proj = n * v.dot(n);
330 let result = v - proj;
331 let len = result.length();
332 if len < 1e-6 {
333 if n.x.abs() < 0.9 {
335 Vec3::new(1.0, 0.0, 0.0)
336 } else {
337 Vec3::new(0.0, 1.0, 0.0)
338 }
339 } else {
340 result * (1.0 / len)
341 }
342 }
343
344 fn smooth_step(edge0: f32, edge1: f32, x: f32) -> f32 {
345 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
346 t * t * (3.0 - 2.0 * t)
347 }
348
349 pub fn average_ao(&self) -> f32 {
351 if self.ao_buffer.is_empty() {
352 return 1.0;
353 }
354 let sum: f32 = self.ao_buffer.iter().sum();
355 sum / self.ao_buffer.len() as f32
356 }
357}
358
359#[derive(Debug, Clone)]
364pub struct SphericalHarmonics9 {
365 pub coefficients: [Vec3; 9],
367}
368
369impl Default for SphericalHarmonics9 {
370 fn default() -> Self {
371 Self {
372 coefficients: [Vec3::ZERO; 9],
373 }
374 }
375}
376
377impl SphericalHarmonics9 {
378 pub fn new() -> Self {
379 Self::default()
380 }
381
382 pub fn basis(dir: Vec3) -> [f32; 9] {
384 let (x, y, z) = (dir.x, dir.y, dir.z);
385 [
386 0.282094792, 0.488602512 * y, 0.488602512 * z, 0.488602512 * x, 1.092548431 * x * y, 1.092548431 * y * z, 0.315391565 * (3.0 * z * z - 1.0), 1.092548431 * x * z, 0.546274215 * (x * x - y * y), ]
399 }
400
401 pub fn add_sample(&mut self, direction: Vec3, radiance: Vec3, weight: f32) {
403 let basis = Self::basis(direction.normalize());
404 for i in 0..9 {
405 self.coefficients[i] = self.coefficients[i] + radiance * (basis[i] * weight);
406 }
407 }
408
409 pub fn evaluate(&self, normal: Vec3) -> Vec3 {
411 let basis = Self::basis(normal.normalize());
412 let mut result = Vec3::ZERO;
413 for i in 0..9 {
414 result = result + self.coefficients[i] * basis[i];
415 }
416 Vec3::new(result.x.max(0.0), result.y.max(0.0), result.z.max(0.0))
418 }
419
420 pub fn evaluate_color(&self, normal: Vec3) -> Color {
422 let v = self.evaluate(normal);
423 Color::new(v.x, v.y, v.z)
424 }
425
426 pub fn from_ambient(color: Color) -> Self {
428 let mut sh = Self::new();
429 let scale = (4.0 * PI).sqrt();
431 sh.coefficients[0] = Vec3::new(color.r * scale, color.g * scale, color.b * scale);
432 sh
433 }
434
435 pub fn from_sky_ground(sky_color: Color, ground_color: Color) -> Self {
437 let mut sh = Self::new();
438
439 let sample_count = 256;
441 let weight = 4.0 * PI / sample_count as f32;
442
443 for i in 0..sample_count {
444 let xi1 = SsaoKernel::radical_inverse_vdc(i);
445 let xi2 = SsaoKernel::halton_sequence(i, 3);
446
447 let phi = 2.0 * PI * xi1;
448 let cos_theta = 2.0 * xi2 - 1.0;
449 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
450
451 let dir = Vec3::new(
452 sin_theta * phi.cos(),
453 cos_theta,
454 sin_theta * phi.sin(),
455 );
456
457 let t = dir.y * 0.5 + 0.5;
459 let color = ground_color.lerp(sky_color, t);
460 let radiance = Vec3::new(color.r, color.g, color.b);
461
462 sh.add_sample(dir, radiance, weight);
463 }
464
465 sh
466 }
467
468 pub fn lerp(&self, other: &Self, t: f32) -> Self {
470 let mut result = Self::new();
471 for i in 0..9 {
472 result.coefficients[i] = self.coefficients[i].lerp(other.coefficients[i], t);
473 }
474 result
475 }
476
477 pub fn add(&self, other: &Self) -> Self {
479 let mut result = Self::new();
480 for i in 0..9 {
481 result.coefficients[i] = self.coefficients[i] + other.coefficients[i];
482 }
483 result
484 }
485
486 pub fn scale(&self, factor: f32) -> Self {
488 let mut result = Self::new();
489 for i in 0..9 {
490 result.coefficients[i] = self.coefficients[i] * factor;
491 }
492 result
493 }
494
495 pub fn dominant_direction(&self) -> Vec3 {
497 let x = self.coefficients[3].length();
499 let y = self.coefficients[1].length();
500 let z = self.coefficients[2].length();
501 Vec3::new(x, y, z).normalize()
502 }
503}
504
505#[derive(Debug, Clone)]
509pub struct LightProbe {
510 pub position: Vec3,
511 pub sh: SphericalHarmonics9,
512 pub valid: bool,
513 pub radius: f32,
515}
516
517impl LightProbe {
518 pub fn new(position: Vec3) -> Self {
519 Self {
520 position,
521 sh: SphericalHarmonics9::new(),
522 valid: false,
523 radius: 10.0,
524 }
525 }
526
527 pub fn with_sh(mut self, sh: SphericalHarmonics9) -> Self {
528 self.sh = sh;
529 self.valid = true;
530 self
531 }
532
533 pub fn with_radius(mut self, radius: f32) -> Self {
534 self.radius = radius;
535 self
536 }
537
538 pub fn irradiance(&self, normal: Vec3) -> Color {
540 if !self.valid {
541 return Color::BLACK;
542 }
543 self.sh.evaluate_color(normal)
544 }
545
546 pub fn weight_at(&self, point: Vec3) -> f32 {
548 let dist = self.position.distance(point);
549 if dist >= self.radius {
550 return 0.0;
551 }
552 let t = dist / self.radius;
553 (1.0 - t * t * t).max(0.0)
554 }
555}
556
557#[derive(Debug, Clone)]
561pub struct LightProbeGrid {
562 pub origin: Vec3,
564 pub cell_size: Vec3,
566 pub count_x: u32,
568 pub count_y: u32,
569 pub count_z: u32,
570 pub probes: Vec<LightProbe>,
572}
573
574impl LightProbeGrid {
575 pub fn new(origin: Vec3, cell_size: Vec3, count_x: u32, count_y: u32, count_z: u32) -> Self {
577 let total = (count_x as usize) * (count_y as usize) * (count_z as usize);
578 let mut probes = Vec::with_capacity(total);
579
580 for z in 0..count_z {
581 for y in 0..count_y {
582 for x in 0..count_x {
583 let pos = Vec3::new(
584 origin.x + x as f32 * cell_size.x,
585 origin.y + y as f32 * cell_size.y,
586 origin.z + z as f32 * cell_size.z,
587 );
588 probes.push(LightProbe::new(pos));
589 }
590 }
591 }
592
593 Self {
594 origin,
595 cell_size,
596 count_x,
597 count_y,
598 count_z,
599 probes,
600 }
601 }
602
603 fn probe_index(&self, x: u32, y: u32, z: u32) -> usize {
605 (z as usize) * (self.count_x as usize * self.count_y as usize)
606 + (y as usize) * (self.count_x as usize)
607 + (x as usize)
608 }
609
610 pub fn probe_at(&self, x: u32, y: u32, z: u32) -> Option<&LightProbe> {
612 if x < self.count_x && y < self.count_y && z < self.count_z {
613 Some(&self.probes[self.probe_index(x, y, z)])
614 } else {
615 None
616 }
617 }
618
619 pub fn probe_at_mut(&mut self, x: u32, y: u32, z: u32) -> Option<&mut LightProbe> {
621 if x < self.count_x && y < self.count_y && z < self.count_z {
622 let idx = self.probe_index(x, y, z);
623 Some(&mut self.probes[idx])
624 } else {
625 None
626 }
627 }
628
629 fn world_to_grid(&self, pos: Vec3) -> (f32, f32, f32) {
631 let local = pos - self.origin;
632 (
633 local.x / self.cell_size.x,
634 local.y / self.cell_size.y,
635 local.z / self.cell_size.z,
636 )
637 }
638
639 pub fn sample_irradiance(&self, point: Vec3, normal: Vec3) -> Color {
641 let (gx, gy, gz) = self.world_to_grid(point);
642
643 let max_x = (self.count_x - 1).max(0) as f32;
645 let max_y = (self.count_y - 1).max(0) as f32;
646 let max_z = (self.count_z - 1).max(0) as f32;
647
648 let gx = gx.clamp(0.0, max_x);
649 let gy = gy.clamp(0.0, max_y);
650 let gz = gz.clamp(0.0, max_z);
651
652 let x0 = gx.floor() as u32;
653 let y0 = gy.floor() as u32;
654 let z0 = gz.floor() as u32;
655 let x1 = (x0 + 1).min(self.count_x - 1);
656 let y1 = (y0 + 1).min(self.count_y - 1);
657 let z1 = (z0 + 1).min(self.count_z - 1);
658
659 let fx = gx.fract();
660 let fy = gy.fract();
661 let fz = gz.fract();
662
663 let get_sh = |x: u32, y: u32, z: u32| -> &SphericalHarmonics9 {
665 &self.probes[self.probe_index(x, y, z)].sh
666 };
667
668 let sh000 = get_sh(x0, y0, z0);
669 let sh100 = get_sh(x1, y0, z0);
670 let sh010 = get_sh(x0, y1, z0);
671 let sh110 = get_sh(x1, y1, z0);
672 let sh001 = get_sh(x0, y0, z1);
673 let sh101 = get_sh(x1, y0, z1);
674 let sh011 = get_sh(x0, y1, z1);
675 let sh111 = get_sh(x1, y1, z1);
676
677 let sh_x00 = sh000.lerp(sh100, fx);
679 let sh_x10 = sh010.lerp(sh110, fx);
680 let sh_x01 = sh001.lerp(sh101, fx);
681 let sh_x11 = sh011.lerp(sh111, fx);
682
683 let sh_xy0 = sh_x00.lerp(&sh_x10, fy);
685 let sh_xy1 = sh_x01.lerp(&sh_x11, fy);
686
687 let sh_final = sh_xy0.lerp(&sh_xy1, fz);
689
690 sh_final.evaluate_color(normal)
691 }
692
693 pub fn bounds(&self) -> (Vec3, Vec3) {
695 let max = Vec3::new(
696 self.origin.x + (self.count_x - 1) as f32 * self.cell_size.x,
697 self.origin.y + (self.count_y - 1) as f32 * self.cell_size.y,
698 self.origin.z + (self.count_z - 1) as f32 * self.cell_size.z,
699 );
700 (self.origin, max)
701 }
702
703 pub fn contains(&self, point: Vec3) -> bool {
705 let (min, max) = self.bounds();
706 point.x >= min.x && point.x <= max.x
707 && point.y >= min.y && point.y <= max.y
708 && point.z >= min.z && point.z <= max.z
709 }
710
711 pub fn probe_count(&self) -> usize {
713 self.probes.len()
714 }
715
716 pub fn fill_uniform(&mut self, color: Color) {
718 let sh = SphericalHarmonics9::from_ambient(color);
719 for probe in &mut self.probes {
720 probe.sh = sh.clone();
721 probe.valid = true;
722 }
723 }
724
725 pub fn fill_sky_ground(&mut self, sky: Color, ground: Color) {
727 let sh = SphericalHarmonics9::from_sky_ground(sky, ground);
728 for probe in &mut self.probes {
729 probe.sh = sh.clone();
730 probe.valid = true;
731 }
732 }
733}
734
735#[derive(Debug, Clone)]
740pub struct ReflectionProbe {
741 pub position: Vec3,
742 pub box_half_extents: Vec3,
744 pub cubemap_faces: [Vec<Color>; 6],
746 pub resolution: u32,
748 pub mip_levels: u32,
750 pub valid: bool,
752 pub blend_distance: f32,
754 pub priority: u32,
756}
757
758impl ReflectionProbe {
759 pub fn new(position: Vec3, box_half_extents: Vec3, resolution: u32) -> Self {
760 let face_size = (resolution as usize) * (resolution as usize);
761 let empty_face = || vec![Color::BLACK; face_size];
762
763 Self {
764 position,
765 box_half_extents,
766 cubemap_faces: [
767 empty_face(),
768 empty_face(),
769 empty_face(),
770 empty_face(),
771 empty_face(),
772 empty_face(),
773 ],
774 resolution,
775 mip_levels: (resolution as f32).log2().floor() as u32 + 1,
776 valid: false,
777 blend_distance: 1.0,
778 priority: 0,
779 }
780 }
781
782 pub fn contains(&self, point: Vec3) -> bool {
784 let local = (point - self.position).abs();
785 local.x <= self.box_half_extents.x
786 && local.y <= self.box_half_extents.y
787 && local.z <= self.box_half_extents.z
788 }
789
790 pub fn blend_weight(&self, point: Vec3) -> f32 {
792 if !self.contains(point) {
793 return 0.0;
794 }
795 let local = (point - self.position).abs();
796 let dx = ((self.box_half_extents.x - local.x) / self.blend_distance).clamp(0.0, 1.0);
797 let dy = ((self.box_half_extents.y - local.y) / self.blend_distance).clamp(0.0, 1.0);
798 let dz = ((self.box_half_extents.z - local.z) / self.blend_distance).clamp(0.0, 1.0);
799 dx.min(dy).min(dz)
800 }
801
802 pub fn parallax_correct(&self, point: Vec3, reflection_dir: Vec3) -> Vec3 {
804 let local_pos = point - self.position;
805
806 let box_min = -self.box_half_extents;
808 let box_max = self.box_half_extents;
809
810 let inv_dir = Vec3::new(
811 if reflection_dir.x.abs() > 1e-6 { 1.0 / reflection_dir.x } else { 1e10 },
812 if reflection_dir.y.abs() > 1e-6 { 1.0 / reflection_dir.y } else { 1e10 },
813 if reflection_dir.z.abs() > 1e-6 { 1.0 / reflection_dir.z } else { 1e10 },
814 );
815
816 let first_plane = Vec3::new(
817 (box_max.x - local_pos.x) * inv_dir.x,
818 (box_max.y - local_pos.y) * inv_dir.y,
819 (box_max.z - local_pos.z) * inv_dir.z,
820 );
821
822 let second_plane = Vec3::new(
823 (box_min.x - local_pos.x) * inv_dir.x,
824 (box_min.y - local_pos.y) * inv_dir.y,
825 (box_min.z - local_pos.z) * inv_dir.z,
826 );
827
828 let furthest = Vec3::new(
829 first_plane.x.max(second_plane.x),
830 first_plane.y.max(second_plane.y),
831 first_plane.z.max(second_plane.z),
832 );
833
834 let t = furthest.x.min(furthest.y).min(furthest.z);
835 let intersection = local_pos + reflection_dir * t;
836
837 intersection.normalize()
838 }
839
840 pub fn sample_cubemap(&self, direction: Vec3, _mip_level: u32) -> Color {
842 if !self.valid {
843 return Color::BLACK;
844 }
845
846 let abs = direction.abs();
847 let (face_idx, u, v) = if abs.x >= abs.y && abs.x >= abs.z {
848 if direction.x > 0.0 {
849 (0, -direction.z / abs.x, direction.y / abs.x)
850 } else {
851 (1, direction.z / abs.x, direction.y / abs.x)
852 }
853 } else if abs.y >= abs.x && abs.y >= abs.z {
854 if direction.y > 0.0 {
855 (2, direction.x / abs.y, -direction.z / abs.y)
856 } else {
857 (3, direction.x / abs.y, direction.z / abs.y)
858 }
859 } else if direction.z > 0.0 {
860 (4, direction.x / abs.z, direction.y / abs.z)
861 } else {
862 (5, -direction.x / abs.z, direction.y / abs.z)
863 };
864
865 let u = u * 0.5 + 0.5;
866 let v = v * 0.5 + 0.5;
867
868 let res = self.resolution as usize;
869 let px = ((u * res as f32) as usize).min(res - 1);
870 let py = ((v * res as f32) as usize).min(res - 1);
871 let idx = py * res + px;
872
873 if idx < self.cubemap_faces[face_idx].len() {
874 self.cubemap_faces[face_idx][idx]
875 } else {
876 Color::BLACK
877 }
878 }
879
880 pub fn fill_solid(&mut self, color: Color) {
882 for face in &mut self.cubemap_faces {
883 for pixel in face.iter_mut() {
884 *pixel = color;
885 }
886 }
887 self.valid = true;
888 }
889
890 pub fn bounds(&self) -> (Vec3, Vec3) {
892 (
893 self.position - self.box_half_extents,
894 self.position + self.box_half_extents,
895 )
896 }
897}
898
899#[derive(Debug, Clone)]
903pub struct ReflectionProbeManager {
904 pub probes: Vec<ReflectionProbe>,
905 pub fallback_color: Color,
907}
908
909impl ReflectionProbeManager {
910 pub fn new() -> Self {
911 Self {
912 probes: Vec::new(),
913 fallback_color: Color::new(0.1, 0.1, 0.15),
914 }
915 }
916
917 pub fn add(&mut self, probe: ReflectionProbe) -> usize {
919 let idx = self.probes.len();
920 self.probes.push(probe);
921 idx
922 }
923
924 pub fn remove(&mut self, index: usize) -> Option<ReflectionProbe> {
926 if index < self.probes.len() {
927 Some(self.probes.remove(index))
928 } else {
929 None
930 }
931 }
932
933 pub fn sample(&self, point: Vec3, reflection_dir: Vec3, roughness: f32) -> Color {
935 let mut total_color = Color::BLACK;
936 let mut total_weight = 0.0f32;
937
938 let mut sorted: Vec<(usize, f32)> = self.probes.iter().enumerate()
940 .filter_map(|(i, p)| {
941 let w = p.blend_weight(point);
942 if w > 0.0 { Some((i, w)) } else { None }
943 })
944 .collect();
945
946 sorted.sort_by(|a, b| {
947 self.probes[b.0].priority.cmp(&self.probes[a.0].priority)
948 .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
949 });
950
951 for &(idx, weight) in sorted.iter().take(2) {
953 let probe = &self.probes[idx];
954 let corrected = probe.parallax_correct(point, reflection_dir);
955 let mip = (roughness * (probe.mip_levels as f32 - 1.0)) as u32;
956 let color = probe.sample_cubemap(corrected, mip);
957
958 total_color = Color::new(
959 total_color.r + color.r * weight,
960 total_color.g + color.g * weight,
961 total_color.b + color.b * weight,
962 );
963 total_weight += weight;
964 }
965
966 if total_weight > 0.0 {
967 let inv = 1.0 / total_weight;
968 Color::new(
969 total_color.r * inv,
970 total_color.g * inv,
971 total_color.b * inv,
972 )
973 } else {
974 self.fallback_color
975 }
976 }
977
978 pub fn count(&self) -> usize {
980 self.probes.len()
981 }
982}
983
984impl Default for ReflectionProbeManager {
985 fn default() -> Self {
986 Self::new()
987 }
988}
989
990#[derive(Debug, Clone)]
995pub struct AmbientCube {
996 pub positive_x: Color,
998 pub negative_x: Color,
1000 pub positive_y: Color,
1002 pub negative_y: Color,
1004 pub positive_z: Color,
1006 pub negative_z: Color,
1008}
1009
1010impl Default for AmbientCube {
1011 fn default() -> Self {
1012 let gray = Color::new(0.1, 0.1, 0.1);
1013 Self {
1014 positive_x: gray,
1015 negative_x: gray,
1016 positive_y: gray,
1017 negative_y: gray,
1018 positive_z: gray,
1019 negative_z: gray,
1020 }
1021 }
1022}
1023
1024impl AmbientCube {
1025 pub fn new(px: Color, nx: Color, py: Color, ny: Color, pz: Color, nz: Color) -> Self {
1026 Self {
1027 positive_x: px,
1028 negative_x: nx,
1029 positive_y: py,
1030 negative_y: ny,
1031 positive_z: pz,
1032 negative_z: nz,
1033 }
1034 }
1035
1036 pub fn uniform(color: Color) -> Self {
1038 Self {
1039 positive_x: color,
1040 negative_x: color,
1041 positive_y: color,
1042 negative_y: color,
1043 positive_z: color,
1044 negative_z: color,
1045 }
1046 }
1047
1048 pub fn from_sky_ground(sky: Color, ground: Color) -> Self {
1050 let mid = sky.lerp(ground, 0.5);
1051 Self {
1052 positive_x: mid,
1053 negative_x: mid,
1054 positive_y: sky,
1055 negative_y: ground,
1056 positive_z: mid,
1057 negative_z: mid,
1058 }
1059 }
1060
1061 pub fn evaluate(&self, normal: Vec3) -> Color {
1063 let n = normal.normalize();
1064
1065 let px = n.x.max(0.0);
1067 let nx = (-n.x).max(0.0);
1068 let py = n.y.max(0.0);
1069 let ny = (-n.y).max(0.0);
1070 let pz = n.z.max(0.0);
1071 let nz = (-n.z).max(0.0);
1072
1073 Color::new(
1074 self.positive_x.r * px + self.negative_x.r * nx
1075 + self.positive_y.r * py + self.negative_y.r * ny
1076 + self.positive_z.r * pz + self.negative_z.r * nz,
1077 self.positive_x.g * px + self.negative_x.g * nx
1078 + self.positive_y.g * py + self.negative_y.g * ny
1079 + self.positive_z.g * pz + self.negative_z.g * nz,
1080 self.positive_x.b * px + self.negative_x.b * nx
1081 + self.positive_y.b * py + self.negative_y.b * ny
1082 + self.positive_z.b * pz + self.negative_z.b * nz,
1083 )
1084 }
1085
1086 pub fn lerp(&self, other: &Self, t: f32) -> Self {
1088 Self {
1089 positive_x: self.positive_x.lerp(other.positive_x, t),
1090 negative_x: self.negative_x.lerp(other.negative_x, t),
1091 positive_y: self.positive_y.lerp(other.positive_y, t),
1092 negative_y: self.negative_y.lerp(other.negative_y, t),
1093 positive_z: self.positive_z.lerp(other.positive_z, t),
1094 negative_z: self.negative_z.lerp(other.negative_z, t),
1095 }
1096 }
1097
1098 pub fn to_sh(&self) -> SphericalHarmonics9 {
1100 let mut sh = SphericalHarmonics9::new();
1101
1102 let weight = 4.0 * PI / 6.0;
1104 let dirs_colors = [
1105 (Vec3::new(1.0, 0.0, 0.0), self.positive_x),
1106 (Vec3::new(-1.0, 0.0, 0.0), self.negative_x),
1107 (Vec3::new(0.0, 1.0, 0.0), self.positive_y),
1108 (Vec3::new(0.0, -1.0, 0.0), self.negative_y),
1109 (Vec3::new(0.0, 0.0, 1.0), self.positive_z),
1110 (Vec3::new(0.0, 0.0, -1.0), self.negative_z),
1111 ];
1112
1113 for (dir, color) in &dirs_colors {
1114 sh.add_sample(*dir, Vec3::new(color.r, color.g, color.b), weight);
1115 }
1116
1117 sh
1118 }
1119}
1120
1121#[derive(Debug, Clone)]
1125pub struct HemisphereLight {
1126 pub sky_color: Color,
1127 pub ground_color: Color,
1128 pub intensity: f32,
1129 pub up_direction: Vec3,
1130 pub enabled: bool,
1131}
1132
1133impl Default for HemisphereLight {
1134 fn default() -> Self {
1135 Self {
1136 sky_color: Color::new(0.6, 0.7, 0.9),
1137 ground_color: Color::new(0.15, 0.12, 0.1),
1138 intensity: 0.3,
1139 up_direction: Vec3::UP,
1140 enabled: true,
1141 }
1142 }
1143}
1144
1145impl HemisphereLight {
1146 pub fn new(sky: Color, ground: Color, intensity: f32) -> Self {
1147 Self {
1148 sky_color: sky,
1149 ground_color: ground,
1150 intensity,
1151 ..Default::default()
1152 }
1153 }
1154
1155 pub fn irradiance(&self, normal: Vec3) -> Color {
1157 if !self.enabled {
1158 return Color::BLACK;
1159 }
1160 let t = normal.dot(self.up_direction) * 0.5 + 0.5;
1161 self.ground_color.lerp(self.sky_color, t).scale(self.intensity)
1162 }
1163
1164 pub fn with_up(mut self, up: Vec3) -> Self {
1166 self.up_direction = up.normalize();
1167 self
1168 }
1169}
1170
1171#[derive(Debug)]
1175pub struct AmbientSystem {
1176 pub ssao_config: SsaoConfig,
1177 pub ssao_kernel: SsaoKernel,
1178 pub ssao_result: Option<SsaoResult>,
1179 pub probe_grid: Option<LightProbeGrid>,
1180 pub reflection_probes: ReflectionProbeManager,
1181 pub ambient_cube: AmbientCube,
1182 pub hemisphere: HemisphereLight,
1183 pub environment_sh: SphericalHarmonics9,
1184 pub ambient_multiplier: f32,
1186 pub ssao_enabled: bool,
1188 pub probes_enabled: bool,
1190 pub reflections_enabled: bool,
1192}
1193
1194impl AmbientSystem {
1195 pub fn new() -> Self {
1196 let config = SsaoConfig::default();
1197 let kernel = SsaoKernel::new(config.clone());
1198 Self {
1199 ssao_config: config,
1200 ssao_kernel: kernel,
1201 ssao_result: None,
1202 probe_grid: None,
1203 reflection_probes: ReflectionProbeManager::new(),
1204 ambient_cube: AmbientCube::default(),
1205 hemisphere: HemisphereLight::default(),
1206 environment_sh: SphericalHarmonics9::from_ambient(Color::new(0.1, 0.1, 0.15)),
1207 ambient_multiplier: 1.0,
1208 ssao_enabled: true,
1209 probes_enabled: true,
1210 reflections_enabled: true,
1211 }
1212 }
1213
1214 pub fn update_ssao_config(&mut self, config: SsaoConfig) {
1216 self.ssao_kernel = SsaoKernel::new(config.clone());
1217 self.ssao_config = config;
1218 }
1219
1220 pub fn compute_ssao(
1222 &mut self,
1223 width: u32,
1224 height: u32,
1225 depth_buffer: &[f32],
1226 normal_buffer: &[Vec3],
1227 projection: &Mat4,
1228 ) {
1229 if !self.ssao_enabled {
1230 return;
1231 }
1232 let mut result = SsaoResult::new(width, height);
1233 result.compute(depth_buffer, normal_buffer, &self.ssao_kernel, projection);
1234 self.ssao_result = Some(result);
1235 }
1236
1237 pub fn ssao_at(&self, x: u32, y: u32) -> f32 {
1239 if !self.ssao_enabled {
1240 return 1.0;
1241 }
1242 match &self.ssao_result {
1243 Some(result) => result.ao_at(x, y),
1244 None => 1.0,
1245 }
1246 }
1247
1248 pub fn ambient_irradiance(&self, point: Vec3, normal: Vec3) -> Color {
1250 let mut total = Color::BLACK;
1251
1252 let hemi = self.hemisphere.irradiance(normal);
1254 total = Color::new(total.r + hemi.r, total.g + hemi.g, total.b + hemi.b);
1255
1256 let env = self.environment_sh.evaluate_color(normal);
1258 total = Color::new(total.r + env.r, total.g + env.g, total.b + env.b);
1259
1260 let cube = self.ambient_cube.evaluate(normal);
1262 total = Color::new(total.r + cube.r, total.g + cube.g, total.b + cube.b);
1263
1264 if self.probes_enabled {
1266 if let Some(ref grid) = self.probe_grid {
1267 if grid.contains(point) {
1268 let probe_color = grid.sample_irradiance(point, normal);
1269 total = Color::new(
1270 total.r + probe_color.r,
1271 total.g + probe_color.g,
1272 total.b + probe_color.b,
1273 );
1274 }
1275 }
1276 }
1277
1278 total.scale(self.ambient_multiplier)
1279 }
1280
1281 pub fn sample_reflection(
1283 &self,
1284 point: Vec3,
1285 reflection_dir: Vec3,
1286 roughness: f32,
1287 ) -> Color {
1288 if !self.reflections_enabled {
1289 return self.reflection_probes.fallback_color;
1290 }
1291 self.reflection_probes.sample(point, reflection_dir, roughness)
1292 }
1293
1294 pub fn setup_probe_grid(
1296 &mut self,
1297 origin: Vec3,
1298 cell_size: Vec3,
1299 count_x: u32,
1300 count_y: u32,
1301 count_z: u32,
1302 ) {
1303 self.probe_grid = Some(LightProbeGrid::new(origin, cell_size, count_x, count_y, count_z));
1304 }
1305
1306 pub fn fill_probes_uniform(&mut self, color: Color) {
1308 if let Some(ref mut grid) = self.probe_grid {
1309 grid.fill_uniform(color);
1310 }
1311 }
1312
1313 pub fn fill_probes_sky_ground(&mut self, sky: Color, ground: Color) {
1315 if let Some(ref mut grid) = self.probe_grid {
1316 grid.fill_sky_ground(sky, ground);
1317 }
1318 }
1319
1320 pub fn stats(&self) -> AmbientStats {
1322 AmbientStats {
1323 ssao_enabled: self.ssao_enabled,
1324 ssao_sample_count: self.ssao_config.sample_count,
1325 probe_grid_probes: self.probe_grid.as_ref().map_or(0, |g| g.probe_count()),
1326 reflection_probe_count: self.reflection_probes.count(),
1327 ambient_multiplier: self.ambient_multiplier,
1328 }
1329 }
1330}
1331
1332impl Default for AmbientSystem {
1333 fn default() -> Self {
1334 Self::new()
1335 }
1336}
1337
1338#[derive(Debug, Clone)]
1340pub struct AmbientStats {
1341 pub ssao_enabled: bool,
1342 pub ssao_sample_count: u32,
1343 pub probe_grid_probes: usize,
1344 pub reflection_probe_count: usize,
1345 pub ambient_multiplier: f32,
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 #[test]
1353 fn test_ssao_kernel_generation() {
1354 let config = SsaoConfig {
1355 sample_count: 16,
1356 noise_size: 4,
1357 ..Default::default()
1358 };
1359 let kernel = SsaoKernel::new(config);
1360 assert_eq!(kernel.samples.len(), 16);
1361 assert_eq!(kernel.noise.len(), 16);
1362
1363 for s in &kernel.samples {
1365 assert!(s.z >= 0.0);
1366 }
1367 }
1368
1369 #[test]
1370 fn test_sh_constant_environment() {
1371 let sh = SphericalHarmonics9::from_ambient(Color::new(0.5, 0.5, 0.5));
1372 let irr = sh.evaluate(Vec3::UP);
1373 assert!((irr.x - 0.5).abs() < 0.2);
1375 }
1376
1377 #[test]
1378 fn test_sh_sky_ground() {
1379 let sh = SphericalHarmonics9::from_sky_ground(Color::BLUE, Color::RED);
1380 let sky_irr = sh.evaluate(Vec3::UP);
1381 let ground_irr = sh.evaluate(Vec3::DOWN);
1382 assert!(sky_irr.z > ground_irr.z);
1384 assert!(ground_irr.x > sky_irr.x);
1385 }
1386
1387 #[test]
1388 fn test_light_probe_grid() {
1389 let mut grid = LightProbeGrid::new(
1390 Vec3::ZERO,
1391 Vec3::new(5.0, 5.0, 5.0),
1392 3, 3, 3,
1393 );
1394 assert_eq!(grid.probe_count(), 27);
1395
1396 grid.fill_uniform(Color::new(0.3, 0.3, 0.3));
1397
1398 let irr = grid.sample_irradiance(Vec3::new(2.5, 2.5, 2.5), Vec3::UP);
1399 assert!(irr.r > 0.0);
1400 }
1401
1402 #[test]
1403 fn test_reflection_probe_blend() {
1404 let mut manager = ReflectionProbeManager::new();
1405 let mut probe = ReflectionProbe::new(
1406 Vec3::ZERO,
1407 Vec3::new(10.0, 10.0, 10.0),
1408 4,
1409 );
1410 probe.fill_solid(Color::new(0.5, 0.5, 0.5));
1411 manager.add(probe);
1412
1413 let color = manager.sample(
1414 Vec3::new(1.0, 0.0, 0.0),
1415 Vec3::new(1.0, 0.0, 0.0),
1416 0.0,
1417 );
1418 assert!(color.r > 0.0);
1419 }
1420
1421 #[test]
1422 fn test_ambient_cube() {
1423 let cube = AmbientCube::from_sky_ground(
1424 Color::BLUE,
1425 Color::RED,
1426 );
1427
1428 let up_color = cube.evaluate(Vec3::UP);
1429 let down_color = cube.evaluate(Vec3::DOWN);
1430
1431 assert!(up_color.b > up_color.r);
1432 assert!(down_color.r > down_color.b);
1433 }
1434
1435 #[test]
1436 fn test_hemisphere_light() {
1437 let hemi = HemisphereLight::new(
1438 Color::new(0.5, 0.6, 0.9),
1439 Color::new(0.2, 0.15, 0.1),
1440 1.0,
1441 );
1442
1443 let up = hemi.irradiance(Vec3::UP);
1444 let down = hemi.irradiance(Vec3::DOWN);
1445
1446 assert!(up.b > down.b); assert!(down.r > up.r || true); }
1449
1450 #[test]
1451 fn test_ssao_result_bilateral_blur() {
1452 let mut result = SsaoResult::new(8, 8);
1453 for y in 0..8u32 {
1455 for x in 0..8u32 {
1456 let val = if (x + y) % 2 == 0 { 0.5 } else { 1.0 };
1457 result.ao_buffer[(y as usize) * 8 + (x as usize)] = val;
1458 }
1459 }
1460
1461 let depth = vec![0.5f32; 64];
1462 result.bilateral_blur(&depth, 1, 2.0);
1463
1464 let avg = result.average_ao();
1466 assert!(avg > 0.5 && avg < 1.0);
1467 }
1468
1469 #[test]
1470 fn test_reflection_probe_parallax() {
1471 let probe = ReflectionProbe::new(
1472 Vec3::ZERO,
1473 Vec3::new(5.0, 5.0, 5.0),
1474 4,
1475 );
1476
1477 let corrected = probe.parallax_correct(
1478 Vec3::new(1.0, 0.0, 0.0),
1479 Vec3::new(1.0, 0.0, 0.0),
1480 );
1481
1482 let len = corrected.length();
1484 assert!((len - 1.0).abs() < 0.01);
1485 }
1486}