1use glam::{Mat4, Vec2, Vec3, Vec4};
9use std::f32::consts::{FRAC_1_PI, PI};
10
11#[derive(Debug, Clone)]
21pub struct RayleighScattering {
22 pub beta: Vec3,
23 pub scale_height: f32,
24}
25
26impl RayleighScattering {
27 pub fn earth() -> Self {
29 Self {
30 beta: Vec3::new(5.8e-6, 1.35e-5, 3.31e-5),
31 scale_height: 8000.0,
32 }
33 }
34
35 #[inline]
37 pub fn density(&self, h: f32) -> f32 {
38 (-h / self.scale_height).exp()
39 }
40
41 #[inline]
43 pub fn beta_at(&self, h: f32) -> Vec3 {
44 self.beta * self.density(h)
45 }
46}
47
48impl Default for RayleighScattering {
49 fn default() -> Self {
50 Self::earth()
51 }
52}
53
54#[derive(Debug, Clone)]
60pub struct MieScattering {
61 pub beta: f32,
62 pub scale_height: f32,
63 pub g: f32,
65}
66
67impl MieScattering {
68 pub fn earth() -> Self {
70 Self {
71 beta: 21e-6,
72 scale_height: 1200.0,
73 g: 0.758,
74 }
75 }
76
77 #[inline]
79 pub fn density(&self, h: f32) -> f32 {
80 (-h / self.scale_height).exp()
81 }
82
83 #[inline]
85 pub fn beta_at(&self, h: f32) -> f32 {
86 self.beta * self.density(h)
87 }
88}
89
90impl Default for MieScattering {
91 fn default() -> Self {
92 Self::earth()
93 }
94}
95
96#[inline]
104pub fn rayleigh_phase(cos_theta: f32) -> f32 {
105 let ct2 = cos_theta * cos_theta;
106 3.0 / (16.0 * PI) * (1.0 + ct2)
107}
108
109#[inline]
113pub fn mie_phase(cos_theta: f32, g: f32) -> f32 {
114 let g2 = g * g;
115 let t = 1.0 + g2 - 2.0 * g * cos_theta;
116 (1.0 - g2) / (4.0 * PI * t.max(1e-10).powf(1.5))
117}
118
119#[inline]
121pub fn cornette_shanks_phase(cos_theta: f32, g: f32) -> f32 {
122 let g2 = g * g;
123 let t = 1.0 + g2 - 2.0 * g * cos_theta;
124 3.0 * (1.0 - g2) * (1.0 + cos_theta * cos_theta)
125 / (8.0 * PI * (2.0 + g2) * t.max(1e-10).powf(1.5))
126}
127
128pub fn optical_depth(
143 start: Vec3,
144 dir: Vec3,
145 atm: &AtmosphereParams,
146 n_steps: usize,
147) -> Vec2 {
148 let ray_len = ray_sphere_intersect(start, dir, atm.radius_atm)
149 .unwrap_or(0.0)
150 .max(0.0);
151
152 if ray_len < 1e-3 {
153 return Vec2::ZERO;
154 }
155
156 let step_len = ray_len / n_steps as f32;
157 let mut rayleigh = 0.0f32;
158 let mut mie = 0.0f32;
159
160 for i in 0..n_steps {
161 let t = (i as f32 + 0.5) * step_len;
162 let pos = start + dir * t;
163 let h = (pos.length() - atm.radius_planet).max(0.0);
164 rayleigh += atm.rayleigh.density(h) * step_len;
165 mie += atm.mie.density(h) * step_len;
166 }
167
168 Vec2::new(rayleigh, mie)
169}
170
171fn ray_sphere_intersect(origin: Vec3, dir: Vec3, radius: f32) -> Option<f32> {
174 let b = origin.dot(dir);
175 let c = origin.dot(origin) - radius * radius;
176 let disc = b * b - c;
177 if disc < 0.0 {
178 return None;
179 }
180 let sq = disc.sqrt();
181 let t1 = -b - sq;
182 let t2 = -b + sq;
183 if t2 < 0.0 {
184 None
185 } else {
186 Some(t2.max(0.0))
187 }
188}
189
190#[derive(Debug, Clone)]
196pub struct AtmosphereParams {
197 pub radius_planet: f32,
199 pub radius_atm: f32,
201 pub rayleigh: RayleighScattering,
202 pub mie: MieScattering,
203 pub sun_intensity: f32,
205 pub sun_dir: Vec3,
207}
208
209impl AtmosphereParams {
210 pub fn earth_default(sun_dir: Vec3) -> Self {
211 Self {
212 radius_planet: 6_371_000.0,
213 radius_atm: 6_471_000.0,
214 rayleigh: RayleighScattering::earth(),
215 mie: MieScattering::earth(),
216 sun_intensity: 20.0,
217 sun_dir: sun_dir.normalize(),
218 }
219 }
220}
221
222impl Default for AtmosphereParams {
223 fn default() -> Self {
224 Self::earth_default(Vec3::new(0.0, 1.0, 0.0))
225 }
226}
227
228pub struct SkyModel {
230 pub params: AtmosphereParams,
231}
232
233impl SkyModel {
234 pub fn new(params: AtmosphereParams) -> Self {
235 Self { params }
236 }
237
238 pub fn sky_color(&self, ray_dir: Vec3, n_steps: usize) -> Vec3 {
243 let p = &self.params;
244 let origin = Vec3::new(0.0, p.radius_planet + 1.0, 0.0);
245 let ray_dir = ray_dir.normalize();
246
247 let ray_len = match ray_sphere_intersect(origin, ray_dir, p.radius_atm) {
248 Some(t) => t,
249 None => return Vec3::ZERO,
250 };
251
252 let ray_len = if let Some(t_ground) = ray_sphere_intersect(origin, ray_dir, p.radius_planet) {
254 ray_len.min(t_ground)
255 } else {
256 ray_len
257 };
258
259 let step_len = ray_len / n_steps as f32;
260 let mut rayleigh_sum = Vec3::ZERO;
261 let mut mie_sum = Vec3::ZERO;
262
263 let mut od_view_r = 0.0f32;
265 let mut od_view_m = 0.0f32;
266
267 let n_light = 8;
268
269 for i in 0..n_steps {
270 let t = (i as f32 + 0.5) * step_len;
271 let pos = origin + ray_dir * t;
272 let h = (pos.length() - p.radius_planet).max(0.0);
273
274 let rho_r = p.rayleigh.density(h);
275 let rho_m = p.mie.density(h);
276
277 od_view_r += rho_r * step_len;
278 od_view_m += rho_m * step_len;
279
280 let od_light = optical_depth(pos, p.sun_dir, p, n_light);
282
283 let tau_r = p.rayleigh.beta * (od_view_r + od_light.x);
285 let tau_m = Vec3::splat(p.mie.beta * 1.1) * (od_view_m + od_light.y);
286 let attenuation = (-(tau_r + tau_m)).exp();
287
288 rayleigh_sum += attenuation * rho_r * step_len;
289 mie_sum += attenuation * Vec3::splat(rho_m * step_len);
290 }
291
292 let cos_theta = ray_dir.dot(p.sun_dir);
293 let phase_r = rayleigh_phase(cos_theta);
294 let phase_m = mie_phase(cos_theta, p.mie.g);
295
296 p.sun_intensity
297 * (p.rayleigh.beta * phase_r * rayleigh_sum
298 + Vec3::splat(p.mie.beta * phase_m) * mie_sum)
299 }
300
301 pub fn sun_transmittance(&self, ray_dir: Vec3) -> Vec3 {
303 let p = &self.params;
304 let origin = Vec3::new(0.0, p.radius_planet + 1.0, 0.0);
305 let od = optical_depth(origin, ray_dir.normalize(), p, 32);
306 let tau_r = p.rayleigh.beta * od.x;
307 let tau_m = Vec3::splat(p.mie.beta * 1.1 * od.y);
308 (-(tau_r + tau_m)).exp()
309 }
310
311 pub fn sky_irradiance(&self, normal: Vec3) -> Vec3 {
316 let n_theta = 8;
317 let n_phi = 16;
318 let mut irradiance = Vec3::ZERO;
319 let n = normal.normalize();
320
321 for it in 0..n_theta {
322 let theta = (it as f32 + 0.5) / n_theta as f32 * std::f32::consts::FRAC_PI_2;
323 let sin_t = theta.sin();
324 let cos_t = theta.cos();
325 for ip in 0..n_phi {
326 let phi = (ip as f32 + 0.5) / n_phi as f32 * 2.0 * PI;
327 let dir = Vec3::new(sin_t * phi.cos(), cos_t, sin_t * phi.sin());
328 let n_dot_l = n.dot(dir).max(0.0);
329 let d_omega = sin_t
330 * (std::f32::consts::FRAC_PI_2 / n_theta as f32)
331 * (2.0 * PI / n_phi as f32);
332 irradiance += self.sky_color(dir, 6) * n_dot_l * d_omega;
333 }
334 }
335 irradiance
336 }
337}
338
339pub struct Preetham;
345
346impl Preetham {
347 pub fn zenith_luminance(turbidity: f32, theta_s: f32) -> f32 {
349 let chi = (4.0 / 9.0 - turbidity / 120.0) * (PI - 2.0 * theta_s);
350 (4.0453 * turbidity - 4.9710) * chi.tan() - 0.2155 * turbidity + 2.4192
351 }
352
353 fn perez(theta: f32, gamma: f32, a: f32, b: f32, c: f32, d: f32, e: f32) -> f32 {
355 (1.0 + a * (-b / theta.cos().max(1e-3)).exp())
356 * (1.0 + c * (-d * gamma).exp() + e * gamma.cos() * gamma.cos())
357 }
358
359 pub fn sky_luminance(view_dir: Vec3, sun_dir: Vec3, turbidity: f32) -> Vec3 {
365 let view_dir = view_dir.normalize();
366 let sun_dir = sun_dir.normalize();
367
368 let theta_s = sun_dir.y.clamp(-1.0, 1.0).acos();
370
371 let t = turbidity;
373 let ay = 0.1787 * t - 1.4630;
374 let by = -0.3554 * t + 0.4275;
375 let cy = -0.0227 * t + 5.3251;
376 let dy = 0.1206 * t - 2.5771;
377 let ey = -0.0670 * t + 0.3703;
378
379 let ax = -0.0193 * t - 0.2592;
380 let bx = -0.0665 * t + 0.0008;
381 let cx = -0.0004 * t + 0.2125;
382 let dx = -0.0641 * t - 0.8989;
383 let ex = -0.0033 * t + 0.0452;
384
385 let az = -0.0167 * t - 0.2608;
386 let bz = -0.0950 * t + 0.0092;
387 let cz = -0.0079 * t + 0.2102;
388 let dz = -0.0441 * t - 1.6537;
389 let ez = -0.0109 * t + 0.0529;
390
391 let theta = view_dir.y.clamp(-1.0, 1.0).acos();
393 let cos_gamma = view_dir.dot(sun_dir).clamp(-1.0, 1.0);
395 let gamma = cos_gamma.acos();
396
397 let yz = Self::zenith_luminance(t, theta_s);
398 let xz = t * (0.0026 + 0.00008 * t - 0.000021 * theta_s * theta_s)
399 + (-0.0669 + 0.00209 * t + 0.000028 * theta_s * theta_s);
400 let zz = t * (-0.0065 + 0.0 + 0.000001 * theta_s * theta_s)
401 + (0.0659 - 0.0015 * t + 0.000047 * theta_s * theta_s);
402
403 let y_val = yz
404 * Self::perez(theta, gamma, ay, by, cy, dy, ey)
405 / Self::perez(0.0, theta_s, ay, by, cy, dy, ey);
406 let x_val = xz
407 * Self::perez(theta, gamma, ax, bx, cx, dx, ex)
408 / Self::perez(0.0, theta_s, ax, bx, cx, dx, ex);
409 let z_val = zz
410 * Self::perez(theta, gamma, az, bz, cz, dz, ez)
411 / Self::perez(0.0, theta_s, az, bz, cz, dz, ez);
412
413 Vec3::new(y_val, x_val, z_val)
414 }
415
416 pub fn yxy_to_rgb(yxy: Vec3) -> Vec3 {
418 let y = yxy.x;
419 let x_chrom = yxy.y;
420 let y_chrom = yxy.z;
421
422 if y < 1e-5 {
423 return Vec3::ZERO;
424 }
425
426 let xyz_x = x_chrom / y_chrom * y;
428 let xyz_y = y;
429 let xyz_z = (1.0 - x_chrom - y_chrom) / y_chrom * y;
430
431 let r = 3.2406 * xyz_x - 1.5372 * xyz_y - 0.4986 * xyz_z;
433 let g = -0.9689 * xyz_x + 1.8758 * xyz_y + 0.0415 * xyz_z;
434 let b = 0.0557 * xyz_x - 0.2040 * xyz_y + 1.0570 * xyz_z;
435
436 Vec3::new(r, g, b).max(Vec3::ZERO)
437 }
438}
439
440pub struct TransmittanceLut {
450 pub width: usize,
451 pub height: usize,
452 pub data: Vec<Vec3>,
453 pub params: AtmosphereParams,
454}
455
456impl TransmittanceLut {
457 pub fn compute(params: &AtmosphereParams) -> Self {
459 let width = 256;
460 let height = 64;
461 let mut data = vec![Vec3::ONE; width * height];
462
463 for row in 0..height {
464 let altitude = row as f32 / (height - 1) as f32
465 * (params.radius_atm - params.radius_planet);
466 let h = params.radius_planet + altitude;
467
468 for col in 0..width {
469 let cos_z = col as f32 / (width - 1) as f32 * 2.0 - 1.0;
470 let sin_z = (1.0 - cos_z * cos_z).max(0.0).sqrt();
471 let dir = Vec3::new(sin_z, cos_z, 0.0);
472 let origin = Vec3::new(0.0, h, 0.0);
473
474 let od = optical_depth(origin, dir, params, 32);
475 let tau_r = params.rayleigh.beta * od.x;
476 let tau_m = Vec3::splat(params.mie.beta * 1.1 * od.y);
477 let transmittance = (-(tau_r + tau_m)).exp();
478
479 data[row * width + col] = transmittance;
480 }
481 }
482
483 Self {
484 width,
485 height,
486 data,
487 params: params.clone(),
488 }
489 }
490
491 pub fn sample(&self, cos_zenith: f32, altitude: f32) -> Vec3 {
495 let atm_thickness = self.params.radius_atm - self.params.radius_planet;
496 let alt_norm = (altitude / atm_thickness).clamp(0.0, 1.0);
497 let cz_norm = (cos_zenith * 0.5 + 0.5).clamp(0.0, 1.0);
498
499 let row_f = alt_norm * (self.height - 1) as f32;
500 let col_f = cz_norm * (self.width - 1) as f32;
501
502 let row0 = (row_f as usize).min(self.height - 1);
503 let col0 = (col_f as usize).min(self.width - 1);
504 let row1 = (row0 + 1).min(self.height - 1);
505 let col1 = (col0 + 1).min(self.width - 1);
506
507 let tr = row_f - row0 as f32;
508 let tc = col_f - col0 as f32;
509
510 let s00 = self.data[row0 * self.width + col0];
511 let s10 = self.data[row0 * self.width + col1];
512 let s01 = self.data[row1 * self.width + col0];
513 let s11 = self.data[row1 * self.width + col1];
514
515 s00.lerp(s10, tc).lerp(s01.lerp(s11, tc), tr)
516 }
517}
518
519#[derive(Debug, Clone)]
525pub struct VolumetricFog {
526 pub density: f32,
528 pub absorption: Vec3,
530 pub scattering: Vec3,
532 pub anisotropy: f32,
534}
535
536impl VolumetricFog {
537 pub fn new(density: f32, absorption: Vec3, scattering: Vec3, anisotropy: f32) -> Self {
538 Self {
539 density,
540 absorption,
541 scattering,
542 anisotropy,
543 }
544 }
545
546 pub fn uniform_white(density: f32) -> Self {
548 Self::new(density, Vec3::ZERO, Vec3::ONE, 0.0)
549 }
550
551 pub fn extinction(&self) -> Vec3 {
553 (self.absorption + self.scattering) * self.density
554 }
555}
556
557impl Default for VolumetricFog {
558 fn default() -> Self {
559 Self::uniform_white(0.02)
560 }
561}
562
563pub fn fog_transmittance(distance: f32, fog: &VolumetricFog) -> f32 {
565 let ext = fog.extinction().length(); (-ext * distance).exp()
567}
568
569pub fn fog_inscattering(
574 start: Vec3,
575 end: Vec3,
576 light_dir: Vec3,
577 light_color: Vec3,
578 fog: &VolumetricFog,
579 n_steps: usize,
580) -> Vec3 {
581 let seg = end - start;
582 let dist = seg.length();
583 if dist < 1e-5 {
584 return Vec3::ZERO;
585 }
586 let dir = seg / dist;
587 let step_len = dist / n_steps as f32;
588
589 let cos_theta = dir.dot(light_dir.normalize());
590 let phase = mie_phase(cos_theta, fog.anisotropy);
591 let ext = fog.extinction();
592
593 let mut inscattered = Vec3::ZERO;
594 for i in 0..n_steps {
595 let t = (i as f32 + 0.5) * step_len;
596 let t_view = (-ext * t).exp();
598 let t_light = (-ext * step_len).exp();
600 let sample_inscatter = fog.scattering * fog.density * phase * light_color * t_view * t_light * step_len;
601 inscattered += sample_inscatter;
602 }
603 inscattered
604}
605
606pub fn exponential_height_fog(pos: Vec3, density: f32, falloff: f32, fog_height: f32) -> f32 {
612 let h = pos.y - fog_height;
613 density * (-falloff * h).exp()
614}
615
616#[derive(Debug, Clone)]
622pub struct CloudLayer {
623 pub altitude: f32,
625 pub thickness: f32,
627 pub coverage: f32,
629 pub density: f32,
631 pub wind: Vec3,
633}
634
635impl CloudLayer {
636 pub fn new(altitude: f32, thickness: f32, coverage: f32, density: f32, wind: Vec3) -> Self {
637 Self {
638 altitude,
639 thickness,
640 coverage,
641 density,
642 wind,
643 }
644 }
645
646 pub fn cumulus() -> Self {
648 Self::new(2000.0, 1500.0, 0.5, 0.8, Vec3::new(10.0, 0.0, 5.0))
649 }
650
651 pub fn cirrus() -> Self {
653 Self::new(8000.0, 500.0, 0.3, 0.4, Vec3::new(30.0, 0.0, 15.0))
654 }
655}
656
657pub fn sample_cloud_density(pos: Vec3, time: f32, layer: &CloudLayer) -> f32 {
661 let rel_h = (pos.y - layer.altitude) / layer.thickness.max(1.0);
663 if rel_h < 0.0 || rel_h > 1.0 {
664 return 0.0;
665 }
666 let h_weight = {
668 let x = rel_h * 2.0 - 1.0;
669 (-x * x * 4.0).exp()
670 };
671
672 let wind_offset = layer.wind * time;
674 let sample_pos = pos + wind_offset;
675
676 let density_noise = fbm_noise(sample_pos, 5);
678
679 let raw = (density_noise - (1.0 - layer.coverage)).max(0.0) / layer.coverage.max(1e-4);
681 raw * h_weight * layer.density
682}
683
684fn fbm_noise(p: Vec3, octaves: usize) -> f32 {
686 let mut value = 0.0f32;
687 let mut amplitude = 0.5f32;
688 let mut frequency = 1.0f32;
689
690 for _ in 0..octaves {
691 value += amplitude * hash31(p * frequency);
692 amplitude *= 0.5;
693 frequency *= 2.0;
694 }
695 value
696}
697
698fn hash31(p: Vec3) -> f32 {
700 let mut h = p.x * 127.1 + p.y * 311.7 + p.z * 74.7;
701 h = (h.sin() * 43758.5453123).fract();
702 h.abs()
703}
704
705pub fn raymarch_cloud(
713 ray_origin: Vec3,
714 ray_dir: Vec3,
715 layers: &[CloudLayer],
716 sun_dir: Vec3,
717 n_steps: usize,
718) -> Vec4 {
719 if layers.is_empty() {
720 return Vec4::ZERO;
721 }
722 let ray_dir = ray_dir.normalize();
723
724 let min_alt = layers.iter().map(|l| l.altitude).fold(f32::INFINITY, f32::min);
726 let max_alt = layers.iter().map(|l| l.altitude + l.thickness).fold(0.0f32, f32::max);
727
728 if ray_dir.y.abs() < 1e-5 {
730 return Vec4::ZERO; }
732 let t_min = (min_alt - ray_origin.y) / ray_dir.y;
733 let t_max = (max_alt - ray_origin.y) / ray_dir.y;
734 let (t_enter, t_exit) = if t_min < t_max {
735 (t_min.max(0.0), t_max.max(0.0))
736 } else {
737 (t_max.max(0.0), t_min.max(0.0))
738 };
739
740 if t_enter >= t_exit {
741 return Vec4::ZERO;
742 }
743
744 let step_len = (t_exit - t_enter) / n_steps as f32;
745 let mut accumulated_density = 0.0f32;
746 let mut light_energy = Vec3::ZERO;
747
748 for i in 0..n_steps {
749 let t = t_enter + (i as f32 + 0.5) * step_len;
750 let pos = ray_origin + ray_dir * t;
751
752 let mut total_density = 0.0f32;
753 for layer in layers {
754 total_density += sample_cloud_density(pos, 0.0, layer);
755 }
756 if total_density < 1e-4 {
757 continue;
758 }
759
760 let transmittance_view = (-accumulated_density * 0.1).exp();
762
763 let mut sun_density = 0.0f32;
765 for ls in 0..4 {
766 let sun_pos = pos + sun_dir * (ls as f32 * 200.0);
767 for layer in layers {
768 sun_density += sample_cloud_density(sun_pos, 0.0, layer);
769 }
770 }
771 let transmittance_sun = (-sun_density * 0.1 * 200.0).exp();
772
773 let cos_theta = ray_dir.dot(sun_dir.normalize());
775 let phase = mie_phase(cos_theta, 0.3);
776
777 let sun_col = Vec3::new(1.0, 0.95, 0.8) * 3.0;
779
780 light_energy += sun_col * transmittance_sun * transmittance_view * phase * total_density * step_len;
781 accumulated_density += total_density * step_len;
782 }
783
784 let alpha = 1.0 - (-accumulated_density * 0.1).exp();
785 let color = light_energy + Vec3::new(0.8, 0.85, 0.95) * (1.0 - alpha); Vec4::new(color.x, color.y, color.z, alpha.clamp(0.0, 1.0))
788}
789
790#[derive(Debug, Clone)]
796pub struct OceanWave {
797 pub amplitude: f32,
799 pub wavelength: f32,
801 pub direction: Vec2,
803 pub phase_speed: f32,
805}
806
807impl OceanWave {
808 pub fn new(amplitude: f32, wavelength: f32, direction: Vec2, phase_speed: f32) -> Self {
809 Self {
810 amplitude,
811 wavelength,
812 direction: direction.normalize(),
813 phase_speed,
814 }
815 }
816
817 pub fn deep_water(amplitude: f32, wavelength: f32, direction: Vec2) -> Self {
819 let g = 9.81_f32;
820 let phase_speed = (g * wavelength / (2.0 * PI)).sqrt();
821 Self::new(amplitude, wavelength, direction, phase_speed)
822 }
823
824 #[inline]
826 pub fn wave_number(&self) -> f32 {
827 2.0 * PI / self.wavelength.max(1e-5)
828 }
829
830 #[inline]
832 pub fn omega(&self) -> f32 {
833 self.wave_number() * self.phase_speed
834 }
835}
836
837pub fn gerstner_wave(pos: Vec2, time: f32, waves: &[OceanWave]) -> Vec3 {
842 let mut displacement = Vec3::ZERO;
843
844 for wave in waves {
845 let k = wave.wave_number();
846 let omega = wave.omega();
847 let phase = wave.direction.dot(pos) * k - omega * time;
848 let sin_phase = phase.sin();
849 let cos_phase = phase.cos();
850
851 let q = (wave.amplitude * k).min(0.9);
853
854 displacement.x += q * wave.amplitude * wave.direction.x * cos_phase;
855 displacement.y += wave.amplitude * sin_phase;
856 displacement.z += q * wave.amplitude * wave.direction.y * cos_phase;
857 }
858
859 displacement
860}
861
862pub fn ocean_normal(pos: Vec2, time: f32, waves: &[OceanWave]) -> Vec3 {
866 let mut b = Vec3::ZERO; let mut t = Vec3::ZERO; for wave in waves {
870 let k = wave.wave_number();
871 let omega = wave.omega();
872 let phase = wave.direction.dot(pos) * k - omega * time;
873 let sin_phase = phase.sin();
874 let cos_phase = phase.cos();
875
876 let q = (wave.amplitude * k).min(0.9);
877 let wa = wave.amplitude * k;
878
879 b.x += -q * wa * wave.direction.x * wave.direction.x * sin_phase;
881 b.y += wa * wave.direction.x * cos_phase;
882 b.z += -q * wa * wave.direction.x * wave.direction.y * sin_phase;
883
884 t.x += -q * wa * wave.direction.x * wave.direction.y * sin_phase;
885 t.y += wa * wave.direction.y * cos_phase;
886 t.z += -q * wa * wave.direction.y * wave.direction.y * sin_phase;
887 }
888
889 let tx = Vec3::new(1.0 + b.x, b.y, b.z);
891 let tz = Vec3::new(t.x, t.y, 1.0 + t.z);
892 tx.cross(tz).normalize()
893}
894
895pub fn water_fresnel(view_angle: f32) -> f32 {
899 let cos_theta = view_angle.cos().clamp(0.0, 1.0);
900 let ior = 1.333_f32;
901 let f0_val = {
902 let t = (ior - 1.0) / (ior + 1.0);
903 t * t
904 };
905 f0_val + (1.0 - f0_val) * (1.0 - cos_theta).powi(5)
906}
907
908pub fn water_color(depth: f32, sun_pos: Vec3, view_dir: Vec3) -> Vec3 {
912 let view_dir = view_dir.normalize();
913 let sun_dir = sun_pos.normalize();
914
915 let absorption = Vec3::new(0.45, 0.12, 0.05);
917 let transmitted = (-absorption * depth.max(0.0)).exp();
918
919 let deep_color = Vec3::new(0.01, 0.12, 0.25);
921 let subsurface = deep_color * transmitted;
922
923 let n = Vec3::Y; let h = (sun_dir - view_dir).normalize();
926 let n_dot_h = n.dot(h).max(0.0);
927 let sun_spec = Vec3::splat(200.0 * n_dot_h.powf(256.0));
928
929 let cos_theta = (-view_dir).dot(n).clamp(0.0, 1.0);
931 let fresnel = water_fresnel(cos_theta.acos());
932
933 subsurface * (1.0 - fresnel) + sun_spec * fresnel
934}
935
936pub fn cascade_shadow_splits(near: f32, far: f32, n_cascades: usize, lambda: f32) -> Vec<f32> {
950 let n = n_cascades as f32;
951 let mut splits = Vec::with_capacity(n_cascades + 1);
952 splits.push(near);
953
954 for i in 1..n_cascades {
955 let p = i as f32 / n;
956 let log_split = near * (far / near).powf(p);
957 let uni_split = near + (far - near) * p;
958 splits.push(lambda * log_split + (1.0 - lambda) * uni_split);
959 }
960 splits.push(far);
961 splits
962}
963
964pub fn light_space_matrix(
969 light_dir: Vec3,
970 scene_aabb: (Vec3, Vec3),
971) -> (Mat4, Mat4) {
972 let (aabb_min, aabb_max) = scene_aabb;
973 let center = (aabb_min + aabb_max) * 0.5;
974 let extent = aabb_max - aabb_min;
975 let radius = extent.length() * 0.5;
976
977 let light_dir = light_dir.normalize();
978 let up = if light_dir.y.abs() < 0.99 {
980 Vec3::Y
981 } else {
982 Vec3::Z
983 };
984
985 let eye = center - light_dir * radius;
986 let view = Mat4::look_at_rh(eye, center, up);
987
988 let half = radius * 1.05; let proj = Mat4::orthographic_rh(-half, half, -half, half, -half * 2.0, half * 2.0);
991
992 (view, proj)
993}
994
995pub fn bias_matrix() -> Mat4 {
997 Mat4::from_cols_array(&[
998 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0, ])
1003}
1004
1005pub fn pcf_kernel(size: usize) -> Vec<Vec2> {
1009 let mut samples = Vec::with_capacity(size);
1011 let sqrt_size = (size as f32).sqrt().ceil() as usize;
1012
1013 for i in 0..sqrt_size {
1014 for j in 0..sqrt_size {
1015 if samples.len() >= size {
1016 break;
1017 }
1018 let u = (i as f32 + 0.5) / sqrt_size as f32;
1019 let v = (j as f32 + 0.5) / sqrt_size as f32;
1020 let jx = hash31(Vec3::new(i as f32, j as f32, 0.3)) * 0.5 / sqrt_size as f32;
1022 let jy = hash31(Vec3::new(j as f32, i as f32, 0.7)) * 0.5 / sqrt_size as f32;
1023 samples.push(Vec2::new(u + jx, v + jy) * 2.0 - Vec2::ONE);
1024 }
1025 if samples.len() >= size {
1026 break;
1027 }
1028 }
1029
1030 samples.truncate(size);
1031 samples
1032}
1033
1034pub fn pcss_blocker_search_uv(light_size_uv: f32, depth: f32) -> f32 {
1039 light_size_uv * depth / (1.0 - depth).max(0.001)
1041}
1042
1043#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use glam::Vec3;
1051
1052 #[test]
1053 fn rayleigh_phase_integrates_to_one() {
1054 let n = 1000;
1057 let mut sum = 0.0f32;
1058 for i in 0..n {
1059 let theta = (i as f32 + 0.5) / n as f32 * PI;
1060 let cos_t = theta.cos();
1061 let sin_t = theta.sin();
1062 sum += rayleigh_phase(cos_t) * sin_t * (PI / n as f32) * 2.0 * PI;
1063 }
1064 assert!((sum - 1.0).abs() < 0.01, "Rayleigh phase integral = {sum}");
1065 }
1066
1067 #[test]
1068 fn mie_phase_integrates_to_one() {
1069 let n = 1000;
1070 let g = 0.7;
1071 let mut sum = 0.0f32;
1072 for i in 0..n {
1073 let theta = (i as f32 + 0.5) / n as f32 * PI;
1074 let cos_t = theta.cos();
1075 let sin_t = theta.sin();
1076 sum += mie_phase(cos_t, g) * sin_t * (PI / n as f32) * 2.0 * PI;
1077 }
1078 assert!((sum - 1.0).abs() < 0.02, "Mie phase integral = {sum}");
1079 }
1080
1081 #[test]
1082 fn sky_color_is_non_negative() {
1083 let sky = SkyModel::new(AtmosphereParams::default());
1084 let color = sky.sky_color(Vec3::Y, 16);
1085 assert!(color.x >= 0.0 && color.y >= 0.0 && color.z >= 0.0);
1086 }
1087
1088 #[test]
1089 fn sky_color_is_blue_biased() {
1090 let sky = SkyModel::new(AtmosphereParams::default());
1091 let color = sky.sky_color(Vec3::new(0.2, 0.8, 0.0).normalize(), 16);
1092 assert!(color.z >= color.x, "Sky should be blue-biased: {color:?}");
1094 }
1095
1096 #[test]
1097 fn transmittance_lut_values_in_range() {
1098 let params = AtmosphereParams::default();
1099 let lut = TransmittanceLut::compute(¶ms);
1100 for &t in &lut.data {
1101 assert!(t.x >= 0.0 && t.x <= 1.0, "Transmittance out of [0,1]: {t:?}");
1102 }
1103 }
1104
1105 #[test]
1106 fn fog_transmittance_zero_distance_is_one() {
1107 let fog = VolumetricFog::default();
1108 let t = fog_transmittance(0.0, &fog);
1109 assert!((t - 1.0).abs() < 1e-5);
1110 }
1111
1112 #[test]
1113 fn fog_transmittance_decreases_with_distance() {
1114 let fog = VolumetricFog::default();
1115 let t1 = fog_transmittance(10.0, &fog);
1116 let t2 = fog_transmittance(100.0, &fog);
1117 assert!(t1 > t2, "Transmittance should decrease with distance");
1118 }
1119
1120 #[test]
1121 fn gerstner_wave_zero_time_is_finite() {
1122 let waves = vec![OceanWave::deep_water(0.5, 10.0, Vec2::X)];
1123 let d = gerstner_wave(Vec2::new(5.0, 5.0), 0.0, &waves);
1124 assert!(d.is_finite(), "Gerstner displacement should be finite: {d:?}");
1125 }
1126
1127 #[test]
1128 fn ocean_normal_is_unit_length() {
1129 let waves = vec![
1130 OceanWave::deep_water(0.3, 8.0, Vec2::X),
1131 OceanWave::deep_water(0.2, 5.0, Vec2::Y),
1132 ];
1133 let n = ocean_normal(Vec2::new(3.0, 7.0), 2.5, &waves);
1134 assert!((n.length() - 1.0).abs() < 1e-4, "Normal should be unit: {n:?}");
1135 }
1136
1137 #[test]
1138 fn water_fresnel_at_grazing_is_one() {
1139 let f = water_fresnel(std::f32::consts::FRAC_PI_2);
1140 assert!((f - 1.0).abs() < 0.01, "Grazing fresnel ~1, got {f}");
1141 }
1142
1143 #[test]
1144 fn cascade_splits_monotone() {
1145 let splits = cascade_shadow_splits(0.1, 1000.0, 4, 0.5);
1146 assert_eq!(splits.len(), 5);
1147 for w in splits.windows(2) {
1148 assert!(w[1] > w[0], "Splits should be monotone increasing");
1149 }
1150 }
1151
1152 #[test]
1153 fn bias_matrix_maps_neg1_to_0() {
1154 let bm = bias_matrix();
1155 let p = bm.transform_point3(Vec3::new(-1.0, -1.0, -1.0));
1156 assert!((p - Vec3::ZERO).length() < 1e-4, "Bias matrix: {p:?}");
1157 }
1158
1159 #[test]
1160 fn pcf_kernel_correct_count() {
1161 let k = pcf_kernel(16);
1162 assert_eq!(k.len(), 16);
1163 }
1164
1165 #[test]
1166 fn exponential_height_fog_decreases_with_height() {
1167 let f0 = exponential_height_fog(Vec3::new(0.0, 0.0, 0.0), 0.1, 0.5, 0.0);
1168 let f1 = exponential_height_fog(Vec3::new(0.0, 100.0, 0.0), 0.1, 0.5, 0.0);
1169 assert!(f0 > f1, "Fog should be denser near the ground");
1170 }
1171
1172 #[test]
1173 fn cloud_density_outside_layer_is_zero() {
1174 let layer = CloudLayer::cumulus();
1175 let pos_below = Vec3::new(100.0, layer.altitude - 100.0, 100.0);
1176 let d = sample_cloud_density(pos_below, 0.0, &layer);
1177 assert_eq!(d, 0.0, "Cloud density below layer should be 0");
1178 }
1179
1180 #[test]
1181 fn pcss_blocker_search_grows_with_depth() {
1182 let uv0 = pcss_blocker_search_uv(0.01, 0.3);
1183 let uv1 = pcss_blocker_search_uv(0.01, 0.7);
1184 assert!(uv1 > uv0, "PCSS blocker radius should grow with depth");
1185 }
1186
1187 #[test]
1188 fn preetham_sky_luminance_is_finite() {
1189 let view_dir = Vec3::new(0.3, 0.8, 0.1).normalize();
1190 let sun_dir = Vec3::new(0.5, 0.6, 0.3).normalize();
1191 let yxy = Preetham::sky_luminance(view_dir, sun_dir, 2.5);
1192 assert!(
1193 yxy.x.is_finite() && yxy.y.is_finite() && yxy.z.is_finite(),
1194 "Preetham luminance should be finite: {yxy:?}"
1195 );
1196 }
1197}