1use glam::{Vec2, Vec3};
17use std::f32::consts::{FRAC_1_PI, PI};
18
19pub mod vec3 {
25 use glam::Vec3;
26
27 #[inline]
29 pub fn dot_clamp(a: Vec3, b: Vec3) -> f32 {
30 a.dot(b).clamp(0.0, 1.0)
31 }
32
33 #[inline]
35 pub fn dot_clamp_eps(a: Vec3, b: Vec3) -> f32 {
36 a.dot(b).clamp(1e-5, 1.0)
37 }
38
39 #[inline]
41 pub fn reflect(v: Vec3, n: Vec3) -> Vec3 {
42 v - 2.0 * v.dot(n) * n
43 }
44
45 pub fn refract(v: Vec3, n: Vec3, eta: f32) -> Option<Vec3> {
48 let cos_i = (-v).dot(n);
49 let sin2_t = eta * eta * (1.0 - cos_i * cos_i);
50 if sin2_t >= 1.0 {
51 return None; }
53 let cos_t = (1.0 - sin2_t).sqrt();
54 Some(eta * v + (eta * cos_i - cos_t) * n)
55 }
56
57 #[inline]
59 pub fn schlick_scalar(cos_theta: f32, f0: f32) -> f32 {
60 f0 + (1.0 - f0) * (1.0 - cos_theta).powi(5)
61 }
62
63 pub fn orthonormal_basis(n: Vec3) -> (Vec3, Vec3) {
66 let sign = if n.z >= 0.0 { 1.0_f32 } else { -1.0_f32 };
68 let a = -1.0 / (sign + n.z);
69 let b = n.x * n.y * a;
70 let t = Vec3::new(1.0 + sign * n.x * n.x * a, sign * b, -sign * n.x);
71 let bi = Vec3::new(b, sign + n.y * n.y * a, -n.y);
72 (t, bi)
73 }
74
75 #[inline]
77 pub fn spherical_to_cartesian(sin_theta: f32, cos_theta: f32, phi: f32) -> Vec3 {
78 Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta)
79 }
80
81 #[inline]
83 pub fn cartesian_to_spherical(v: Vec3) -> (f32, f32) {
84 let theta = v.z.clamp(-1.0, 1.0).acos();
85 let phi = v.y.atan2(v.x);
86 (theta, phi)
87 }
88
89 #[inline]
91 pub fn saturate(v: Vec3) -> Vec3 {
92 v.clamp(Vec3::ZERO, Vec3::ONE)
93 }
94}
95
96pub mod distribution {
104 use std::f32::consts::PI;
105
106 pub fn ggx_d(n_dot_h: f32, roughness: f32) -> f32 {
111 let alpha = roughness * roughness;
112 let alpha2 = alpha * alpha;
113 let n_dot_h = n_dot_h.max(1e-5);
114 let d = (n_dot_h * n_dot_h) * (alpha2 - 1.0) + 1.0;
115 alpha2 / (PI * d * d)
116 }
117
118 pub fn beckmann_d(n_dot_h: f32, roughness: f32) -> f32 {
120 let alpha = roughness * roughness;
121 let alpha2 = alpha * alpha;
122 let n_dot_h = n_dot_h.max(1e-5);
123 let cos2 = n_dot_h * n_dot_h;
124 let tan2 = (1.0 - cos2) / (cos2 + 1e-10);
125 (-tan2 / alpha2).exp() / (PI * alpha2 * cos2 * cos2)
126 }
127
128 pub fn blinn_phong_d(n_dot_h: f32, shininess: f32) -> f32 {
130 let n_dot_h = n_dot_h.max(0.0);
131 (shininess + 2.0) / (2.0 * PI) * n_dot_h.powf(shininess)
132 }
133
134 pub fn anisotropic_ggx_d(
140 n_dot_h: f32,
141 h_dot_x: f32,
142 h_dot_y: f32,
143 ax: f32,
144 ay: f32,
145 ) -> f32 {
146 let ax = ax.max(1e-4);
147 let ay = ay.max(1e-4);
148 let hx = h_dot_x / ax;
149 let hy = h_dot_y / ay;
150 let n = n_dot_h.max(1e-5);
151 let denom = hx * hx + hy * hy + n * n;
152 1.0 / (PI * ax * ay * denom * denom)
153 }
154
155 pub fn phong_d(n_dot_h: f32, roughness: f32) -> f32 {
157 let shininess = 2.0 / (roughness * roughness).max(1e-5) - 2.0;
158 blinn_phong_d(n_dot_h, shininess.max(0.0))
159 }
160
161 pub fn ward_d(
163 n_dot_h: f32,
164 h_dot_x: f32,
165 h_dot_y: f32,
166 ax: f32,
167 ay: f32,
168 n_dot_l: f32,
169 n_dot_v: f32,
170 ) -> f32 {
171 let ax = ax.max(1e-4);
172 let ay = ay.max(1e-4);
173 let n = n_dot_h.max(1e-5);
174 let nl = n_dot_l.max(1e-5);
175 let nv = n_dot_v.max(1e-5);
176 let exponent = -((h_dot_x / ax).powi(2) + (h_dot_y / ay).powi(2)) / (n * n);
177 exponent.exp() / (4.0 * std::f32::consts::PI * ax * ay * (nl * nv).sqrt())
178 }
179}
180
181pub mod geometry {
187 #[inline]
192 pub fn schlick_ggx_g1(n_dot_v: f32, k: f32) -> f32 {
193 let n_dot_v = n_dot_v.max(0.0);
194 n_dot_v / (n_dot_v * (1.0 - k) + k + 1e-7)
195 }
196
197 pub fn smith_ggx(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
199 let k = (roughness + 1.0).powi(2) / 8.0;
200 schlick_ggx_g1(n_dot_v, k) * schlick_ggx_g1(n_dot_l, k)
201 }
202
203 pub fn smith_ggx_ibl(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
205 let k = (roughness * roughness) / 2.0;
206 schlick_ggx_g1(n_dot_v, k) * schlick_ggx_g1(n_dot_l, k)
207 }
208
209 pub fn smith_beckmann(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
211 let alpha = roughness * roughness;
212
213 fn g1(n_dot: f32, alpha: f32) -> f32 {
214 let c = n_dot / (alpha * (1.0 - n_dot * n_dot).max(0.0).sqrt());
215 if c >= 1.6 {
216 1.0
217 } else {
218 (3.535 * c + 2.181 * c * c) / (1.0 + 2.276 * c + 2.577 * c * c)
219 }
220 }
221
222 g1(n_dot_v.max(1e-5), alpha) * g1(n_dot_l.max(1e-5), alpha)
223 }
224
225 pub fn kelemen_szirmay_kalos_g(n_dot_v: f32, n_dot_l: f32) -> f32 {
227 let v = n_dot_v.max(1e-5);
228 let l = n_dot_l.max(1e-5);
229 (v * l) / (v + l - v * l)
230 }
231
232 #[inline]
234 pub fn implicit_g(n_dot_v: f32, n_dot_l: f32) -> f32 {
235 n_dot_v.max(0.0) * n_dot_l.max(0.0)
236 }
237
238 pub fn neumann_g(n_dot_v: f32, n_dot_l: f32) -> f32 {
240 let v = n_dot_v.max(1e-5);
241 let l = n_dot_l.max(1e-5);
242 (v * l) / v.max(l)
243 }
244
245 pub fn cook_torrance_g(n_dot_v: f32, n_dot_l: f32, n_dot_h: f32, v_dot_h: f32) -> f32 {
247 let v = n_dot_v.max(1e-5);
248 let l = n_dot_l.max(1e-5);
249 let h = n_dot_h.max(1e-5);
250 let vdh = v_dot_h.max(1e-5);
251 let t1 = 2.0 * h * v / vdh;
252 let t2 = 2.0 * h * l / vdh;
253 t1.min(t2).min(1.0)
254 }
255}
256
257pub mod fresnel {
263 use glam::Vec3;
264
265 #[inline]
267 pub fn schlick_f(cos_theta: f32, f0: Vec3) -> Vec3 {
268 let t = (1.0 - cos_theta.clamp(0.0, 1.0)).powi(5);
269 f0 + (Vec3::ONE - f0) * t
270 }
271
272 pub fn schlick_roughness_f(cos_theta: f32, f0: Vec3, roughness: f32) -> Vec3 {
274 let t = (1.0 - cos_theta.clamp(0.0, 1.0)).powi(5);
275 let max_comp = (1.0 - roughness).max(f0.x).max(f0.y).max(f0.z);
276 f0 + (Vec3::splat(max_comp) - f0) * t
277 }
278
279 pub fn conductor_fresnel(cos_theta: f32, ior: f32, k: f32) -> f32 {
283 let ct = cos_theta.clamp(0.0, 1.0);
284 let ct2 = ct * ct;
285 let n2 = ior * ior;
286 let k2 = k * k;
287 let n2k2 = n2 + k2;
288
289 let rs_num = n2k2 - 2.0 * ior * ct + ct2;
290 let rs_den = n2k2 + 2.0 * ior * ct + ct2;
291 let rs = rs_num / rs_den.max(1e-10);
292
293 let rp_num = n2k2 * ct2 - 2.0 * ior * ct + 1.0;
294 let rp_den = n2k2 * ct2 + 2.0 * ior * ct + 1.0;
295 let rp = rp_num / rp_den.max(1e-10);
296
297 (rs + rp) * 0.5
298 }
299
300 pub fn dielectric_fresnel(cos_theta_i: f32, ior: f32) -> f32 {
302 let ct_i = cos_theta_i.clamp(0.0, 1.0);
303 let sin2_t = (1.0 - ct_i * ct_i) / (ior * ior);
304 if sin2_t >= 1.0 {
305 return 1.0; }
307 let ct_t = (1.0 - sin2_t).sqrt();
308
309 let rs = (ct_i - ior * ct_t) / (ct_i + ior * ct_t);
310 let rp = (ior * ct_i - ct_t) / (ior * ct_i + ct_t);
311 (rs * rs + rp * rp) * 0.5
312 }
313
314 #[inline]
316 pub fn f0_from_ior(ior: f32) -> f32 {
317 let t = (ior - 1.0) / (ior + 1.0);
318 t * t
319 }
320
321 #[inline]
323 pub fn f0_vec3_from_ior(ior: f32) -> Vec3 {
324 Vec3::splat(f0_from_ior(ior))
325 }
326
327 #[inline]
329 pub fn schlick_scalar_f(cos_theta: f32, f0: f32) -> f32 {
330 f0 + (1.0 - f0) * (1.0 - cos_theta.clamp(0.0, 1.0)).powi(5)
331 }
332}
333
334pub struct SimpleRng {
341 state: u32,
342}
343
344impl SimpleRng {
345 pub fn new(seed: u32) -> Self {
346 Self {
347 state: if seed == 0 { 1 } else { seed },
348 }
349 }
350
351 pub fn next_u32(&mut self) -> u32 {
353 let mut x = self.state;
354 x ^= x << 13;
355 x ^= x >> 17;
356 x ^= x << 5;
357 self.state = x;
358 x
359 }
360
361 pub fn next_f32(&mut self) -> f32 {
363 (self.next_u32() as f32) / (u32::MAX as f32 + 1.0)
364 }
365
366 pub fn next_vec2(&mut self) -> Vec2 {
368 Vec2::new(self.next_f32(), self.next_f32())
369 }
370}
371
372pub struct CookTorranceBrdf;
380
381impl CookTorranceBrdf {
382 pub fn evaluate(
394 n: Vec3,
395 v: Vec3,
396 l: Vec3,
397 albedo: Vec3,
398 metallic: f32,
399 roughness: f32,
400 ) -> Vec3 {
401 let n_dot_l = n.dot(l).max(0.0);
402 let n_dot_v = n.dot(v).max(1e-5);
403 if n_dot_l < 1e-5 {
404 return Vec3::ZERO;
405 }
406
407 let h = (v + l).normalize();
408 let n_dot_h = n.dot(h).max(0.0);
409 let v_dot_h = v.dot(h).clamp(0.0, 1.0);
410
411 let rough = roughness.max(0.04);
413
414 let f0_dielectric = fresnel::f0_from_ior(1.5);
416 let f0 = Vec3::splat(f0_dielectric).lerp(albedo, metallic);
417
418 let d = distribution::ggx_d(n_dot_h, rough);
420
421 let g = geometry::smith_ggx(n_dot_v, n_dot_l, rough);
423
424 let f = fresnel::schlick_f(v_dot_h, f0);
426
427 let denom = (4.0 * n_dot_v * n_dot_l).max(1e-7);
429 let specular = d * g * f / denom;
430
431 let k_s = f;
433 let k_d = (Vec3::ONE - k_s) * (1.0 - metallic);
434 let diffuse = k_d * albedo * FRAC_1_PI;
435
436 (diffuse + specular) * n_dot_l
437 }
438
439 pub fn sample(n: Vec3, v: Vec3, roughness: f32, rng: &mut SimpleRng) -> (Vec3, f32) {
443 let (xi1, xi2) = (rng.next_f32(), rng.next_f32());
444
445 let alpha = roughness * roughness;
446 let alpha2 = alpha * alpha;
447
448 let cos_theta = ((1.0 - xi1) / (xi1 * (alpha2 - 1.0) + 1.0)).sqrt();
450 let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
451 let phi = 2.0 * PI * xi2;
452
453 let h_local = Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta);
454
455 let (t, b) = vec3::orthonormal_basis(n);
457 let h = (t * h_local.x + b * h_local.y + n * h_local.z).normalize();
458
459 let l = vec3::reflect(-v, h);
461 if l.dot(n) < 0.0 {
462 return (n, 1.0);
464 }
465
466 let n_dot_h = n.dot(h).max(1e-5);
468 let d = distribution::ggx_d(n_dot_h, roughness);
469 let pdf = (d * n_dot_h / (4.0 * v.dot(h).max(1e-5))).max(1e-7);
470
471 (l, pdf)
472 }
473
474 pub fn evaluate_specular(n: Vec3, v: Vec3, l: Vec3, roughness: f32, f0: Vec3) -> Vec3 {
476 let n_dot_l = n.dot(l).max(0.0);
477 let n_dot_v = n.dot(v).max(1e-5);
478 if n_dot_l < 1e-5 {
479 return Vec3::ZERO;
480 }
481
482 let h = (v + l).normalize();
483 let n_dot_h = n.dot(h).max(0.0);
484 let v_dot_h = v.dot(h).clamp(0.0, 1.0);
485 let rough = roughness.max(0.04);
486
487 let d = distribution::ggx_d(n_dot_h, rough);
488 let g = geometry::smith_ggx(n_dot_v, n_dot_l, rough);
489 let f = fresnel::schlick_f(v_dot_h, f0);
490
491 d * g * f / (4.0 * n_dot_v * n_dot_l).max(1e-7) * n_dot_l
492 }
493
494 pub fn multi_scatter_compensation(
497 n_dot_v: f32,
498 roughness: f32,
499 f0: Vec3,
500 brdf_lut: &ibl::BrdfLut,
501 ) -> Vec3 {
502 let lut = brdf_lut.integrate(n_dot_v, roughness);
503 let scale = lut.x;
504 let bias = lut.y;
505
506 let e_single = f0 * scale + Vec3::splat(bias);
507 let e_multi = Vec3::ONE - e_single;
508 let f_avg = f0 * (1.0 / 21.0) + Vec3::splat(20.0 / 21.0) * f0;
510 Vec3::ONE + f_avg * e_multi / (Vec3::ONE - f_avg * e_multi + Vec3::splat(1e-7))
511 }
512}
513
514pub struct LambertianBrdf;
516
517impl LambertianBrdf {
518 #[inline]
520 pub fn evaluate(albedo: Vec3, n_dot_l: f32) -> Vec3 {
521 albedo * FRAC_1_PI * n_dot_l.max(0.0)
522 }
523
524 pub fn sample(n: Vec3, rng: &mut SimpleRng) -> (Vec3, f32) {
526 let xi1 = rng.next_f32();
527 let xi2 = rng.next_f32();
528
529 let cos_theta = xi1.sqrt();
531 let sin_theta = (1.0 - xi1).sqrt();
532 let phi = 2.0 * PI * xi2;
533
534 let l_local = Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta);
535 let (t, b) = vec3::orthonormal_basis(n);
536 let l = (t * l_local.x + b * l_local.y + n * l_local.z).normalize();
537 let pdf = cos_theta * FRAC_1_PI;
538 (l, pdf.max(1e-7))
539 }
540}
541
542pub struct OrenNayarBrdf;
544
545impl OrenNayarBrdf {
546 pub fn evaluate(v: Vec3, l: Vec3, n: Vec3, albedo: Vec3, roughness: f32) -> Vec3 {
550 let sigma2 = roughness * roughness;
551 let a = 1.0 - 0.5 * sigma2 / (sigma2 + 0.33);
552 let b = 0.45 * sigma2 / (sigma2 + 0.09);
553
554 let n_dot_v = n.dot(v).max(0.0);
555 let n_dot_l = n.dot(l).max(0.0);
556
557 let v_perp = (v - n * n_dot_v).normalize();
559 let l_perp = (l - n * n_dot_l).normalize();
560 let cos_phi = v_perp.dot(l_perp).max(0.0);
561
562 let theta_i = n_dot_l.acos();
563 let theta_r = n_dot_v.acos();
564 let alpha = theta_i.max(theta_r);
565 let beta = theta_i.min(theta_r);
566
567 albedo * FRAC_1_PI * (a + b * cos_phi * alpha.sin() * (beta.tan() + 1e-5)) * n_dot_l
568 }
569}
570
571pub struct ClearcoatBrdf;
573
574impl ClearcoatBrdf {
575 pub fn evaluate(n: Vec3, v: Vec3, l: Vec3, strength: f32, roughness: f32) -> Vec3 {
580 if strength < 1e-5 {
581 return Vec3::ZERO;
582 }
583 let f0 = Vec3::splat(fresnel::f0_from_ior(1.5));
585 let spec = CookTorranceBrdf::evaluate_specular(n, v, l, roughness, f0);
586 spec * strength
587 }
588
589 pub fn attenuation(n: Vec3, v: Vec3, strength: f32) -> Vec3 {
591 if strength < 1e-5 {
592 return Vec3::ONE;
593 }
594 let n_dot_v = n.dot(v).clamp(0.0, 1.0);
595 let f = fresnel::schlick_scalar_f(n_dot_v, fresnel::f0_from_ior(1.5));
596 Vec3::ONE - Vec3::splat(strength * f)
597 }
598}
599
600pub struct AnisotropicBrdf;
602
603impl AnisotropicBrdf {
604 pub fn evaluate(
610 n: Vec3,
611 v: Vec3,
612 l: Vec3,
613 t: Vec3,
614 b: Vec3,
615 metallic: f32,
616 roughness_x: f32,
617 roughness_y: f32,
618 f0: Vec3,
619 ) -> Vec3 {
620 let n_dot_l = n.dot(l).max(0.0);
621 let n_dot_v = n.dot(v).max(1e-5);
622 if n_dot_l < 1e-5 {
623 return Vec3::ZERO;
624 }
625
626 let h = (v + l).normalize();
627 let n_dot_h = n.dot(h).max(0.0);
628 let v_dot_h = v.dot(h).clamp(0.0, 1.0);
629 let h_dot_t = h.dot(t);
630 let h_dot_b = h.dot(b);
631
632 let ax = roughness_x.max(0.04);
633 let ay = roughness_y.max(0.04);
634
635 let d = distribution::anisotropic_ggx_d(n_dot_h, h_dot_t, h_dot_b, ax, ay);
636
637 let rough_avg = (ax * ay).sqrt();
639 let g = geometry::smith_ggx(n_dot_v, n_dot_l, rough_avg);
640
641 let f = fresnel::schlick_f(v_dot_h, f0);
642
643 let specular = d * g * f / (4.0 * n_dot_v * n_dot_l).max(1e-7);
644
645 let k_d = (Vec3::ONE - f) * (1.0 - metallic);
647 let diffuse = k_d * FRAC_1_PI;
648
649 (diffuse + specular) * n_dot_l
650 }
651}
652
653pub mod ibl {
659 use super::{distribution, fresnel, geometry, SimpleRng, vec3, PI, FRAC_1_PI};
660 use glam::{Vec2, Vec3};
661
662 pub struct PrefilterEnv {
668 pub sky_color: Vec3,
670 pub sun_color: Vec3,
672 pub sun_dir: Vec3,
674 pub sun_radius: f32,
676 }
677
678 impl PrefilterEnv {
679 pub fn new(sky_color: Vec3, sun_color: Vec3, sun_dir: Vec3, sun_radius: f32) -> Self {
680 Self {
681 sky_color,
682 sun_color,
683 sun_dir: sun_dir.normalize(),
684 sun_radius,
685 }
686 }
687
688 pub fn sample_lod(&self, dir: Vec3, roughness: f32) -> Vec3 {
693 let dir = dir.normalize();
694 let cos_sun = dir.dot(self.sun_dir).max(0.0);
695 let blur_radius = self.sun_radius + roughness * (PI / 2.0);
697 let sun_falloff = ((cos_sun - (1.0 - blur_radius * blur_radius * 0.5))
698 / (blur_radius * blur_radius * 0.5 + 1e-5))
699 .clamp(0.0, 1.0);
700
701 let sky_lerp = (dir.y * 0.5 + 0.5).clamp(0.0, 1.0);
703 let ambient = self.sky_color * sky_lerp + Vec3::new(0.1, 0.1, 0.15) * (1.0 - sky_lerp);
704
705 ambient.lerp(self.sun_color, sun_falloff * (1.0 - roughness))
707 }
708
709 pub fn sample_irradiance(&self, normal: Vec3) -> Vec3 {
711 self.sample_lod(normal, 1.0)
712 }
713 }
714
715 impl Default for PrefilterEnv {
716 fn default() -> Self {
717 Self::new(
718 Vec3::new(0.5, 0.7, 1.0),
719 Vec3::new(10.0, 9.0, 7.0),
720 Vec3::new(0.3, 0.8, 0.5).normalize(),
721 0.01,
722 )
723 }
724 }
725
726 pub struct BrdfLut {
733 pub size: usize,
734 pub data: Vec<Vec2>,
737 }
738
739 impl BrdfLut {
740 pub fn generate(size: usize) -> Self {
744 let mut data = vec![Vec2::ZERO; size * size];
745 let n_samples = 1024usize;
746
747 for row in 0..size {
748 let roughness = (row as f32 + 0.5) / size as f32;
749 for col in 0..size {
750 let n_dot_v = (col as f32 + 0.5) / size as f32;
751 data[row * size + col] = integrate_brdf(n_dot_v, roughness, n_samples);
752 }
753 }
754
755 BrdfLut { size, data }
756 }
757
758 pub fn integrate(&self, n_dot_v: f32, roughness: f32) -> Vec2 {
760 let n_dot_v = n_dot_v.clamp(0.0, 1.0);
761 let roughness = roughness.clamp(0.0, 1.0);
762
763 let col_f = n_dot_v * (self.size as f32 - 1.0);
764 let row_f = roughness * (self.size as f32 - 1.0);
765
766 let col0 = (col_f as usize).min(self.size - 1);
767 let row0 = (row_f as usize).min(self.size - 1);
768 let col1 = (col0 + 1).min(self.size - 1);
769 let row1 = (row0 + 1).min(self.size - 1);
770
771 let tc = col_f - col0 as f32;
772 let tr = row_f - row0 as f32;
773
774 let s00 = self.data[row0 * self.size + col0];
775 let s10 = self.data[row0 * self.size + col1];
776 let s01 = self.data[row1 * self.size + col0];
777 let s11 = self.data[row1 * self.size + col1];
778
779 let s0 = s00.lerp(s10, tc);
780 let s1 = s01.lerp(s11, tc);
781 s0.lerp(s1, tr)
782 }
783
784 pub fn generate_lut(size: usize) -> Vec<Vec2> {
786 Self::generate(size).data
787 }
788 }
789
790 impl Default for BrdfLut {
791 fn default() -> Self {
792 Self::generate(64)
793 }
794 }
795
796 fn integrate_brdf(n_dot_v: f32, roughness: f32, n_samples: usize) -> Vec2 {
798 let v = Vec3::new(
800 (1.0 - n_dot_v * n_dot_v).max(0.0).sqrt(),
801 0.0,
802 n_dot_v,
803 );
804 let n = Vec3::Z;
805
806 let mut sum = Vec2::ZERO;
807 let mut rng = SimpleRng::new(0x12345678);
808
809 for _ in 0..n_samples {
810 let xi1 = rng.next_f32();
811 let xi2 = rng.next_f32();
812
813 let alpha = roughness * roughness;
815 let alpha2 = alpha * alpha;
816 let cos_theta_h = ((1.0 - xi1) / (xi1 * (alpha2 - 1.0) + 1.0)).sqrt();
817 let sin_theta_h = (1.0 - cos_theta_h * cos_theta_h).max(0.0).sqrt();
818 let phi = 2.0 * PI * xi2;
819
820 let h = Vec3::new(sin_theta_h * phi.cos(), sin_theta_h * phi.sin(), cos_theta_h);
821 let l = (2.0 * v.dot(h) * h - v).normalize();
822
823 if l.z > 0.0 {
824 let n_dot_l = l.z.max(0.0);
825 let n_dot_h = h.z.max(0.0);
826 let v_dot_h = v.dot(h).max(0.0);
827
828 let g = geometry::smith_ggx_ibl(n_dot_v, n_dot_l, roughness);
829 let g_vis = g * v_dot_h / (n_dot_h * n_dot_v).max(1e-7);
830 let fc = (1.0 - v_dot_h).powi(5);
831
832 sum.x += (1.0 - fc) * g_vis;
833 sum.y += fc * g_vis;
834 }
835 }
836
837 sum / n_samples as f32
838 }
839
840 pub struct AmbientOcclusion;
842
843 impl AmbientOcclusion {
844 pub fn ssao_kernel(n_samples: usize) -> Vec<Vec3> {
848 let mut rng = SimpleRng::new(0xDEADBEEF);
849 let mut kernel = Vec::with_capacity(n_samples);
850
851 for i in 0..n_samples {
852 let xi1 = rng.next_f32();
854 let xi2 = rng.next_f32();
855
856 let sin_theta = xi1.sqrt();
857 let cos_theta = (1.0 - xi1).sqrt();
858 let phi = 2.0 * PI * xi2;
859
860 let sample = Vec3::new(
861 sin_theta * phi.cos(),
862 sin_theta * phi.sin(),
863 cos_theta,
864 );
865
866 let scale = i as f32 / n_samples as f32;
868 let scale = 0.1_f32 + (1.0_f32 - 0.1_f32) * (scale * scale);
869
870 kernel.push(sample * scale);
871 }
872
873 kernel
874 }
875
876 pub fn bent_normal_ao(samples: &[Vec3], depth: &[f32], n: Vec3) -> (Vec3, f32) {
884 assert_eq!(
885 samples.len(),
886 depth.len(),
887 "samples and depth must have equal length"
888 );
889
890 let mut bent = Vec3::ZERO;
891 let mut unoccluded = 0u32;
892 let total = samples.len() as u32;
893
894 for (s, &vis) in samples.iter().zip(depth.iter()) {
895 if s.dot(n) > 0.0 {
896 if vis > 0.5 {
898 bent += *s;
899 unoccluded += 1;
900 }
901 }
902 }
903
904 let ao = unoccluded as f32 / total.max(1) as f32;
905 let bent_normal = if bent.length_squared() > 1e-10 {
906 bent.normalize()
907 } else {
908 n
909 };
910
911 (bent_normal, ao)
912 }
913 }
914}
915
916#[derive(Debug, Clone)]
922pub struct DirectionalLight {
923 pub direction: Vec3,
925 pub color: Vec3,
927 pub intensity: f32,
929}
930
931impl DirectionalLight {
932 pub fn new(direction: Vec3, color: Vec3, intensity: f32) -> Self {
933 Self {
934 direction: direction.normalize(),
935 color,
936 intensity,
937 }
938 }
939
940 pub fn irradiance(&self) -> Vec3 {
942 self.color * self.intensity
943 }
944}
945
946#[derive(Debug, Clone)]
948pub struct PointLight {
949 pub position: Vec3,
950 pub color: Vec3,
951 pub intensity: f32,
952 pub radius: f32,
954}
955
956impl PointLight {
957 pub fn new(position: Vec3, color: Vec3, intensity: f32, radius: f32) -> Self {
958 Self {
959 position,
960 color,
961 intensity,
962 radius: radius.max(0.001),
963 }
964 }
965
966 pub fn attenuation(&self, surface_pos: Vec3) -> f32 {
968 let dist = (self.position - surface_pos).length();
969 let r = self.radius;
970 let x = dist / r;
972 let window = (1.0 - x * x * x * x).max(0.0);
973 window * window / (dist * dist + 0.01)
974 }
975
976 pub fn direction_to(&self, surface_pos: Vec3) -> Vec3 {
978 (self.position - surface_pos).normalize()
979 }
980
981 pub fn irradiance_at(&self, surface_pos: Vec3) -> Vec3 {
983 self.color * self.intensity * self.attenuation(surface_pos)
984 }
985}
986
987#[derive(Debug, Clone)]
989pub struct SpotLight {
990 pub position: Vec3,
991 pub direction: Vec3,
993 pub color: Vec3,
994 pub intensity: f32,
995 pub inner_angle: f32,
997 pub outer_angle: f32,
999}
1000
1001impl SpotLight {
1002 pub fn new(
1003 position: Vec3,
1004 direction: Vec3,
1005 color: Vec3,
1006 intensity: f32,
1007 inner_angle: f32,
1008 outer_angle: f32,
1009 ) -> Self {
1010 Self {
1011 position,
1012 direction: direction.normalize(),
1013 color,
1014 intensity,
1015 inner_angle,
1016 outer_angle,
1017 }
1018 }
1019
1020 pub fn angular_attenuation(&self, surface_pos: Vec3) -> f32 {
1022 let l = (surface_pos - self.position).normalize();
1023 let cos_theta = l.dot(self.direction);
1024 let cos_inner = self.inner_angle.cos();
1025 let cos_outer = self.outer_angle.cos();
1026 let t = ((cos_theta - cos_outer) / (cos_inner - cos_outer + 1e-5)).clamp(0.0, 1.0);
1027 t * t }
1029
1030 pub fn radial_attenuation(&self, surface_pos: Vec3) -> f32 {
1032 let dist = (self.position - surface_pos).length();
1033 let falloff_radius = 10.0 * self.outer_angle; let x = dist / falloff_radius;
1035 (1.0 - x * x * x * x).max(0.0).powi(2) / (dist * dist + 0.01)
1036 }
1037
1038 pub fn irradiance_at(&self, surface_pos: Vec3) -> Vec3 {
1040 let angular = self.angular_attenuation(surface_pos);
1041 let radial = self.radial_attenuation(surface_pos);
1042 self.color * self.intensity * angular * radial
1043 }
1044
1045 pub fn direction_to(&self, surface_pos: Vec3) -> Vec3 {
1047 (self.position - surface_pos).normalize()
1048 }
1049}
1050
1051#[derive(Debug, Clone)]
1053pub struct AreaLight {
1054 pub position: Vec3,
1056 pub right: Vec3,
1058 pub up: Vec3,
1060 pub color: Vec3,
1061 pub intensity: f32,
1062}
1063
1064impl AreaLight {
1065 pub fn new(position: Vec3, right: Vec3, up: Vec3, color: Vec3, intensity: f32) -> Self {
1066 Self {
1067 position,
1068 right,
1069 up,
1070 color,
1071 intensity,
1072 }
1073 }
1074
1075 pub fn corners(&self) -> [Vec3; 4] {
1077 [
1078 self.position - self.right - self.up,
1079 self.position + self.right - self.up,
1080 self.position + self.right + self.up,
1081 self.position - self.right + self.up,
1082 ]
1083 }
1084
1085 pub fn area(&self) -> f32 {
1087 4.0 * self.right.length() * self.up.length()
1088 }
1089
1090 pub fn normal(&self) -> Vec3 {
1092 self.right.cross(self.up).normalize()
1093 }
1094
1095 pub fn solid_angle_at(&self, pos: Vec3) -> f32 {
1097 let to_center = self.position - pos;
1098 let dist2 = to_center.length_squared();
1099 let cos_angle = to_center
1100 .normalize()
1101 .dot(self.normal())
1102 .abs();
1103 (self.area() * cos_angle / dist2.max(1e-5)).min(2.0 * PI)
1104 }
1105
1106 pub fn irradiance_at(&self, pos: Vec3, n: Vec3) -> Vec3 {
1108 let l = (self.position - pos).normalize();
1109 let n_dot_l = n.dot(l).max(0.0);
1110 let omega = self.solid_angle_at(pos);
1111 self.color * self.intensity * omega * n_dot_l * FRAC_1_PI
1112 }
1113}
1114
1115#[derive(Debug, Clone)]
1117pub struct ShadeMaterial {
1118 pub albedo: Vec3,
1119 pub metallic: f32,
1120 pub roughness: f32,
1121 pub emission: Vec3,
1122 pub ao: f32,
1123 pub clearcoat: f32,
1124 pub clearcoat_roughness: f32,
1125 pub anisotropy: f32,
1126 pub anisotropy_tangent: Vec3,
1127 pub anisotropy_bitangent: Vec3,
1128 pub ior: f32,
1129}
1130
1131impl Default for ShadeMaterial {
1132 fn default() -> Self {
1133 Self {
1134 albedo: Vec3::new(0.8, 0.8, 0.8),
1135 metallic: 0.0,
1136 roughness: 0.5,
1137 emission: Vec3::ZERO,
1138 ao: 1.0,
1139 clearcoat: 0.0,
1140 clearcoat_roughness: 0.0,
1141 anisotropy: 0.0,
1142 anisotropy_tangent: Vec3::X,
1143 anisotropy_bitangent: Vec3::Y,
1144 ior: 1.5,
1145 }
1146 }
1147}
1148
1149#[derive(Debug, Clone)]
1151pub enum Light {
1152 Directional(DirectionalLight),
1153 Point(PointLight),
1154 Spot(SpotLight),
1155 Area(AreaLight),
1156}
1157
1158impl Light {
1159 pub fn contribution(&self, surface_pos: Vec3, n: Vec3) -> (Vec3, Vec3) {
1161 match self {
1162 Light::Directional(d) => (d.direction, d.irradiance()),
1163 Light::Point(p) => (p.direction_to(surface_pos), p.irradiance_at(surface_pos)),
1164 Light::Spot(s) => (s.direction_to(surface_pos), s.irradiance_at(surface_pos)),
1165 Light::Area(a) => {
1166 let dir = (a.position - surface_pos).normalize();
1167 (dir, a.irradiance_at(surface_pos, n))
1168 }
1169 }
1170 }
1171}
1172
1173pub fn shade_point(
1178 material: &ShadeMaterial,
1179 lights: &[Light],
1180 n: Vec3,
1181 v: Vec3,
1182 position: Vec3,
1183 env: Option<&ibl::PrefilterEnv>,
1184 brdf_lut: Option<&ibl::BrdfLut>,
1185) -> Vec3 {
1186 let n = n.normalize();
1187 let v = v.normalize();
1188 let n_dot_v = n.dot(v).max(1e-5);
1189
1190 let f0_d = fresnel::f0_from_ior(material.ior);
1192 let f0 = Vec3::splat(f0_d).lerp(material.albedo, material.metallic);
1193
1194 let mut color = Vec3::ZERO;
1195
1196 for light in lights {
1198 let (l, irradiance) = light.contribution(position, n);
1199 let l = l.normalize();
1200
1201 let brdf = if material.anisotropy > 1e-5 {
1202 AnisotropicBrdf::evaluate(
1203 n,
1204 v,
1205 l,
1206 material.anisotropy_tangent,
1207 material.anisotropy_bitangent,
1208 material.metallic,
1209 material.roughness * (1.0 + material.anisotropy),
1210 material.roughness * (1.0 - material.anisotropy),
1211 f0,
1212 )
1213 } else {
1214 CookTorranceBrdf::evaluate(n, v, l, material.albedo, material.metallic, material.roughness)
1215 };
1216
1217 let clearcoat = ClearcoatBrdf::evaluate(n, v, l, material.clearcoat, material.clearcoat_roughness);
1219 let cc_atten = ClearcoatBrdf::attenuation(n, v, material.clearcoat);
1220
1221 color += (brdf * cc_atten + clearcoat) * irradiance;
1222 }
1223
1224 if let Some(env) = env {
1226 let r = vec3::reflect(-v, n);
1227
1228 let spec_env = env.sample_lod(r, material.roughness);
1230 let lut_val = brdf_lut
1231 .map(|lut| lut.integrate(n_dot_v, material.roughness))
1232 .unwrap_or(Vec2::new(0.5, 0.1));
1233 let f_ibl = fresnel::schlick_roughness_f(n_dot_v, f0, material.roughness);
1234 let specular_ibl = spec_env * (f_ibl * lut_val.x + Vec3::splat(lut_val.y));
1235
1236 let diff_env = env.sample_irradiance(n);
1238 let k_s = f_ibl;
1239 let k_d = (Vec3::ONE - k_s) * (1.0 - material.metallic);
1240 let diffuse_ibl = diff_env * k_d * material.albedo;
1241
1242 color += (diffuse_ibl + specular_ibl) * material.ao;
1243 }
1244
1245 color += material.emission;
1247
1248 color
1249}
1250
1251pub mod tonemap {
1257 use glam::Vec3;
1258
1259 pub fn aces_filmic(x: Vec3) -> Vec3 {
1261 let a = 2.51_f32;
1263 let b = 0.03_f32;
1264 let c = 2.43_f32;
1265 let d = 0.59_f32;
1266 let e = 0.14_f32;
1267 ((x * (a * x + Vec3::splat(b))) / (x * (c * x + Vec3::splat(d)) + Vec3::splat(e)))
1268 .clamp(Vec3::ZERO, Vec3::ONE)
1269 }
1270
1271 pub fn reinhard_extended(x: Vec3, max_white: f32) -> Vec3 {
1273 let mw2 = max_white * max_white;
1274 x * (Vec3::ONE + x / mw2) / (Vec3::ONE + x)
1275 }
1276
1277 pub fn reinhard(x: Vec3) -> Vec3 {
1279 x / (Vec3::ONE + x)
1280 }
1281
1282 pub fn uncharted2(x: Vec3) -> Vec3 {
1284 fn hable(v: Vec3) -> Vec3 {
1285 let a = Vec3::splat(0.15_f32);
1286 let b = Vec3::splat(0.50_f32);
1287 let c = Vec3::splat(0.10_f32);
1288 let d = Vec3::splat(0.20_f32);
1289 let e = Vec3::splat(0.02_f32);
1290 let f = Vec3::splat(0.30_f32);
1291 ((v * (a * v + c * b) + d * e) / (v * (a * v + b) + d * f)) - e / f
1292 }
1293
1294 let white_scale = Vec3::ONE / hable(Vec3::splat(11.2));
1295 hable(x * 2.0) * white_scale
1296 }
1297
1298 pub fn lottes(x: Vec3) -> Vec3 {
1300 let a: f32 = 1.6;
1302 let d: f32 = 0.977;
1303 let hdr_max: f32 = 8.0;
1304 let mid_out: f32 = 0.267;
1305
1306 let b = (-mid_out.powf(a) + hdr_max.powf(a) * mid_out)
1308 / ((hdr_max.powf(a * d) - mid_out.powf(a * d)) * mid_out);
1309 let c_val = (hdr_max.powf(a * d) * mid_out.powf(a) - hdr_max.powf(a) * mid_out.powf(a * d))
1310 / ((hdr_max.powf(a * d) - mid_out.powf(a * d)) * mid_out);
1311
1312 let f = |v: f32| v.powf(a) / (v.powf(a * d) * b + c_val);
1313 Vec3::new(f(x.x), f(x.y), f(x.z))
1314 }
1315
1316 pub fn uchimura(x: Vec3) -> Vec3 {
1318 let max_brightness = 1.0_f32;
1320 let contrast = 1.0_f32;
1321 let linear_start = 0.22_f32;
1322 let linear_length = 0.4_f32;
1323 let black = 1.33_f32;
1324 let pedestal = 0.0_f32;
1325
1326 let l0 = (max_brightness - linear_start) * linear_length / contrast;
1327 let l = linear_start + l0;
1328
1329 fn gt_channel(x: f32, p: f32, a: f32, m: f32, l: f32, c: f32, b: f32) -> f32 {
1331 let l0 = (p - m) * l / a;
1332 let s0 = m + l0;
1333 let s1 = m + a * l0;
1334 let c2 = a * p / (p - s1);
1335 let cp = -c2 / p;
1336
1337 let w0 = 1.0 - (x / m).min(1.0).powf(b);
1338 let w1 = if x >= m && x < s0 { 1.0 } else { 0.0 };
1339 let w2 = if x >= s0 { 1.0 } else { 0.0 };
1340
1341 let t0 = m * (x / m).powf(c);
1342 let t1 = m + a * (x - m);
1343 let t2 = p - (p - s1) * (-c2 * (x - s0) / p).exp();
1344
1345 w0 * x.powf(b) + w1 * (m + a * (x - m)) + w2 * t2 * 0.0 + (1.0 - w2) * (w0 * t0 + w1 * t1)
1347 }
1348
1349 Vec3::new(
1350 gt_channel(x.x, max_brightness, contrast, linear_start, linear_length, black, pedestal),
1351 gt_channel(x.y, max_brightness, contrast, linear_start, linear_length, black, pedestal),
1352 gt_channel(x.z, max_brightness, contrast, linear_start, linear_length, black, pedestal),
1353 ).clamp(Vec3::ZERO, Vec3::ONE)
1354 }
1355
1356 pub fn agx(x: Vec3) -> Vec3 {
1358 fn agx_default_contrast(v: Vec3) -> Vec3 {
1361 let x_adj = v.clamp(Vec3::ZERO, Vec3::ONE);
1363 let p1 = x_adj * x_adj * (Vec3::splat(3.0) - Vec3::splat(2.0) * x_adj);
1365 p1
1366 }
1367
1368 let agx_mat = [
1370 Vec3::new(0.842479062253094, 0.0423282422610123, 0.0423756549057051),
1371 Vec3::new(0.0784335999999992, 0.878468636469772, 0.0784336),
1372 Vec3::new(0.0792237451477643, 0.0791661274605434, 0.879142973793104),
1373 ];
1374
1375 let agx_in = Vec3::new(
1376 agx_mat[0].dot(x),
1377 agx_mat[1].dot(x),
1378 agx_mat[2].dot(x),
1379 ).max(Vec3::ZERO);
1380
1381 let log_min = -12.47393_f32;
1383 let log_max = 4.026069_f32;
1384 let clamped = agx_in.max(Vec3::splat(1e-10));
1385 let log_vec = Vec3::new(clamped.x.log2(), clamped.y.log2(), clamped.z.log2());
1386 let encoded = (log_vec - Vec3::splat(log_min)) / (log_max - log_min);
1387 let encoded = encoded.clamp(Vec3::ZERO, Vec3::ONE);
1388
1389 let curved = agx_default_contrast(encoded);
1391
1392 let inv_mat = [
1394 Vec3::new(1.19687900512017, -0.0528968517574562, -0.0529716355144438),
1395 Vec3::new(-0.0980208811401368, 1.15190312990417, -0.0980434501171241),
1396 Vec3::new(-0.0990297440797205, -0.0989611768448433, 1.15107367264116),
1397 ];
1398
1399 Vec3::new(
1400 inv_mat[0].dot(curved),
1401 inv_mat[1].dot(curved),
1402 inv_mat[2].dot(curved),
1403 ).max(Vec3::ZERO)
1404 }
1405
1406 #[inline]
1408 pub fn exposure(x: Vec3, ev: f32) -> Vec3 {
1409 x * 2.0_f32.powf(ev)
1410 }
1411
1412 #[inline]
1414 pub fn gamma_correct(x: Vec3, gamma: f32) -> Vec3 {
1415 x.max(Vec3::ZERO).powf(1.0 / gamma)
1416 }
1417
1418 pub fn linear_to_srgb(x: Vec3) -> Vec3 {
1420 let f = |c: f32| {
1421 if c <= 0.003_130_8 {
1422 c * 12.92
1423 } else {
1424 1.055 * c.powf(1.0 / 2.4) - 0.055
1425 }
1426 };
1427 Vec3::new(f(x.x), f(x.y), f(x.z))
1428 }
1429
1430 pub fn srgb_to_linear(x: Vec3) -> Vec3 {
1432 let f = |c: f32| {
1433 if c <= 0.04045 {
1434 c / 12.92
1435 } else {
1436 ((c + 0.055) / 1.055).powf(2.4)
1437 }
1438 };
1439 Vec3::new(f(x.x), f(x.y), f(x.z))
1440 }
1441
1442 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1444 pub enum ToneMapOp {
1445 AcesFilmic,
1446 ReinhardExtended,
1447 Uncharted2,
1448 Lottes,
1449 Uchimura,
1450 AgX,
1451 }
1452}
1453
1454pub use tonemap::ToneMapOp;
1456
1457pub struct BrdfGlsl;
1463
1464impl BrdfGlsl {
1465 pub fn cook_torrance_source() -> &'static str {
1467 r#"
1468// ── Cook-Torrance PBR BRDF (auto-generated) ───────────────────────────────
1469// GGX normal distribution
1470float ggxD(float NdotH, float roughness) {
1471 float alpha = roughness * roughness;
1472 float alpha2 = alpha * alpha;
1473 float d = (NdotH * NdotH) * (alpha2 - 1.0) + 1.0;
1474 return alpha2 / (PI * d * d);
1475}
1476
1477// Smith-GGX masking-shadowing
1478float schlickG1(float NdotX, float k) {
1479 return NdotX / (NdotX * (1.0 - k) + k + 1e-7);
1480}
1481float smithGGX(float NdotV, float NdotL, float roughness) {
1482 float k = pow(roughness + 1.0, 2.0) / 8.0;
1483 return schlickG1(NdotV, k) * schlickG1(NdotL, k);
1484}
1485
1486// Schlick Fresnel
1487vec3 schlickF(float cosTheta, vec3 F0) {
1488 float t = pow(1.0 - clamp(cosTheta, 0.0, 1.0), 5.0);
1489 return F0 + (vec3(1.0) - F0) * t;
1490}
1491
1492// Cook-Torrance BRDF evaluation — returns radiance (already multiplied by NdotL)
1493vec3 cookTorranceBrdf(
1494 vec3 N, vec3 V, vec3 L,
1495 vec3 albedo, float metallic, float roughness,
1496 float ior)
1497{
1498 float NdotL = max(dot(N, L), 0.0);
1499 float NdotV = max(dot(N, V), 1e-5);
1500 if (NdotL < 1e-5) return vec3(0.0);
1501
1502 vec3 H = normalize(V + L);
1503 float NdotH = max(dot(N, H), 0.0);
1504 float VdotH = clamp(dot(V, H), 0.0, 1.0);
1505
1506 float rough = max(roughness, 0.04);
1507 float f0d = pow((ior - 1.0) / (ior + 1.0), 2.0);
1508 vec3 F0 = mix(vec3(f0d), albedo, metallic);
1509
1510 float D = ggxD(NdotH, rough);
1511 float G = smithGGX(NdotV, NdotL, rough);
1512 vec3 F = schlickF(VdotH, F0);
1513
1514 vec3 specular = D * G * F / max(4.0 * NdotV * NdotL, 1e-7);
1515 vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
1516 vec3 diffuse = kD * albedo / PI;
1517
1518 return (diffuse + specular) * NdotL;
1519}
1520"#
1521 }
1522
1523 pub fn ibl_source() -> &'static str {
1525 r#"
1526// ── IBL ambient (auto-generated) ─────────────────────────────────────────────
1527// Env and BRDF LUT samplers must be declared by the caller:
1528// uniform samplerCube u_PrefilteredEnv;
1529// uniform sampler2D u_BrdfLut;
1530// uniform samplerCube u_IrradianceMap;
1531
1532vec3 evaluateIbl(
1533 vec3 N, vec3 V,
1534 vec3 albedo, float metallic, float roughness,
1535 float ao, float ior,
1536 float maxReflectionLod)
1537{
1538 float NdotV = max(dot(N, V), 1e-5);
1539 vec3 R = reflect(-V, N);
1540
1541 float f0d = pow((ior - 1.0) / (ior + 1.0), 2.0);
1542 vec3 F0 = mix(vec3(f0d), albedo, metallic);
1543
1544 // Fresnel with roughness for ambient
1545 float t = pow(1.0 - NdotV, 5.0);
1546 float maxF = max(max(1.0 - roughness, F0.r), max(F0.g, F0.b));
1547 vec3 Fibl = F0 + (vec3(maxF) - F0) * t;
1548
1549 // Pre-filtered specular
1550 vec3 prefilt = textureLod(u_PrefilteredEnv, R, roughness * maxReflectionLod).rgb;
1551 vec2 brdf = texture(u_BrdfLut, vec2(NdotV, roughness)).rg;
1552 vec3 specIbl = prefilt * (Fibl * brdf.x + vec3(brdf.y));
1553
1554 // Diffuse irradiance
1555 vec3 irrad = texture(u_IrradianceMap, N).rgb;
1556 vec3 kD = (vec3(1.0) - Fibl) * (1.0 - metallic);
1557 vec3 diffIbl = irrad * kD * albedo;
1558
1559 return (diffIbl + specIbl) * ao;
1560}
1561"#
1562 }
1563
1564 pub fn tonemap_source(op: ToneMapOp) -> &'static str {
1566 match op {
1567 ToneMapOp::AcesFilmic => r#"
1568vec3 toneMap(vec3 x) {
1569 // ACES filmic (Stephen Hill fit)
1570 return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
1571}
1572"#,
1573 ToneMapOp::ReinhardExtended => r#"
1574uniform float u_MaxWhite;
1575vec3 toneMap(vec3 x) {
1576 return x * (1.0 + x / (u_MaxWhite * u_MaxWhite)) / (1.0 + x);
1577}
1578"#,
1579 ToneMapOp::Uncharted2 => r#"
1580vec3 hable(vec3 x) {
1581 return ((x * (0.15 * x + 0.05) + 0.004) / (x * (0.15 * x + 0.5) + 0.06)) - 1.0/15.0;
1582}
1583vec3 toneMap(vec3 x) {
1584 vec3 ws = vec3(1.0) / hable(vec3(11.2));
1585 return hable(x * 2.0) * ws;
1586}
1587"#,
1588 ToneMapOp::Lottes => r#"
1589vec3 toneMap(vec3 x) {
1590 const vec3 a = vec3(1.6);
1591 const vec3 d = vec3(0.977);
1592 const vec3 hdrM = vec3(8.0);
1593 const vec3 midIn = vec3(0.18);
1594 const vec3 midO = vec3(0.267);
1595 vec3 b = (-pow(midO, a) + pow(hdrM, a) * midO)
1596 / ((pow(hdrM, a * d) - pow(midO, a * d)) * midO);
1597 vec3 c = (pow(hdrM, a * d) * pow(midO, a) - pow(hdrM, a) * pow(midO, a * d))
1598 / ((pow(hdrM, a * d) - pow(midO, a * d)) * midO);
1599 return pow(x, a) / (pow(x, a * d) * b + c);
1600}
1601"#,
1602 ToneMapOp::Uchimura => r#"
1603vec3 toneMap(vec3 x) {
1604 // GT Uchimura
1605 const float P = 1.0, a = 1.0, m = 0.22, l = 0.4, c = 1.33, b = 0.0;
1606 float l0 = (P - m) * l / a;
1607 float L = m + l0;
1608 float S0 = m + l0;
1609 float S1 = m + a * l0;
1610 float C2 = a * P / (P - S1);
1611 float CP = -C2 / P;
1612 vec3 w0 = vec3(1.0 - smoothstep(vec3(0.0), vec3(m), x));
1613 vec3 w2 = step(vec3(S0), x);
1614 vec3 w1 = vec3(1.0) - w0 - w2;
1615 vec3 T = vec3(m) * pow(x / vec3(m), vec3(c));
1616 vec3 S = vec3(P) - (vec3(P) - vec3(S1)) * exp(CP * (x - vec3(S0)));
1617 vec3 Lin = vec3(m + a) * (x - vec3(m));
1618 return T * w0 + Lin * w1 + S * w2;
1619}
1620"#,
1621 ToneMapOp::AgX => r#"
1622vec3 toneMap(vec3 x) {
1623 // Simplified AgX (Blender 3.x default)
1624 mat3 agxMat = mat3(
1625 0.842479, 0.042328, 0.042376,
1626 0.078434, 0.878469, 0.078434,
1627 0.079224, 0.079166, 0.879143
1628 );
1629 vec3 enc = agxMat * max(x, vec3(0.0));
1630 float logMin = -12.47393, logMax = 4.026069;
1631 enc = clamp((log2(max(enc, vec3(1e-10))) - logMin) / (logMax - logMin), 0.0, 1.0);
1632 // Sigmoid contrast
1633 enc = enc * enc * (3.0 - 2.0 * enc);
1634 // Inverse
1635 mat3 invMat = mat3(
1636 1.19688, -0.05290, -0.05297,
1637 -0.09802, 1.15190, -0.09804,
1638 -0.09903, -0.09896, 1.15107
1639 );
1640 return max(invMat * enc, vec3(0.0));
1641}
1642"#,
1643 }
1644 }
1645
1646 pub fn full_brdf_glsl(op: ToneMapOp) -> String {
1649 const HEADER: &str = "#ifndef BRDF_GLSL\n#define BRDF_GLSL\n\n#define PI 3.14159265358979\n\n";
1650 const FOOTER: &str = "\n#endif // BRDF_GLSL\n";
1651 format!(
1652 "{}{}{}{}{}",
1653 HEADER,
1654 Self::cook_torrance_source(),
1655 Self::ibl_source(),
1656 Self::tonemap_source(op),
1657 FOOTER
1658 )
1659 }
1660}
1661
1662#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use glam::Vec3;
1670
1671 #[test]
1674 fn ggx_d_is_positive() {
1675 let d = distribution::ggx_d(0.8, 0.3);
1676 assert!(d > 0.0, "GGX D must be positive, got {d}");
1677 }
1678
1679 #[test]
1680 fn ggx_d_at_n_dot_h_one_is_finite() {
1681 let d = distribution::ggx_d(1.0, 0.5);
1683 assert!(d.is_finite() && d > 0.0);
1684 }
1685
1686 #[test]
1687 fn beckmann_d_is_positive() {
1688 let d = distribution::beckmann_d(0.7, 0.4);
1689 assert!(d > 0.0);
1690 }
1691
1692 #[test]
1693 fn blinn_phong_d_is_positive() {
1694 let d = distribution::blinn_phong_d(0.9, 64.0);
1695 assert!(d > 0.0);
1696 }
1697
1698 #[test]
1699 fn anisotropic_ggx_d_is_positive() {
1700 let d = distribution::anisotropic_ggx_d(0.8, 0.1, 0.2, 0.2, 0.5);
1701 assert!(d > 0.0);
1702 }
1703
1704 #[test]
1707 fn smith_ggx_is_in_unit_range() {
1708 let g = geometry::smith_ggx(0.9, 0.8, 0.3);
1709 assert!((0.0..=1.0).contains(&g), "G={g} must be in [0,1]");
1710 }
1711
1712 #[test]
1713 fn implicit_g_is_product() {
1714 let g = geometry::implicit_g(0.7, 0.5);
1715 assert!((g - 0.35).abs() < 1e-6);
1716 }
1717
1718 #[test]
1719 fn kelemen_g_is_positive() {
1720 let g = geometry::kelemen_szirmay_kalos_g(0.8, 0.9);
1721 assert!(g > 0.0);
1722 }
1723
1724 #[test]
1727 fn f0_from_ior_glass() {
1728 let f0 = fresnel::f0_from_ior(1.5);
1729 assert!((f0 - 0.04).abs() < 0.001, "Glass F0 ~0.04, got {f0}");
1730 }
1731
1732 #[test]
1733 fn schlick_f_at_zero_angle_equals_f0() {
1734 let f0 = Vec3::splat(0.04);
1735 let f = fresnel::schlick_f(1.0, f0);
1736 assert!((f - f0).length() < 1e-5);
1737 }
1738
1739 #[test]
1740 fn schlick_f_at_grazing_is_white() {
1741 let f0 = Vec3::splat(0.04);
1742 let f = fresnel::schlick_f(0.0, f0);
1743 assert!((f - Vec3::ONE).length() < 1e-5);
1744 }
1745
1746 #[test]
1747 fn dielectric_fresnel_tir() {
1748 let f = fresnel::dielectric_fresnel(0.0, 1.5);
1750 assert_eq!(f, 1.0, "Should be total internal reflection at cos=0");
1751 }
1752
1753 #[test]
1756 fn cook_torrance_returns_vec3_non_negative() {
1757 let n = Vec3::Y;
1758 let v = Vec3::new(0.0, 1.0, 0.0);
1759 let l = Vec3::new(0.5, 0.866, 0.0).normalize();
1760 let result = CookTorranceBrdf::evaluate(n, v, l, Vec3::splat(0.8), 0.0, 0.5);
1761 assert!(result.x >= 0.0 && result.y >= 0.0 && result.z >= 0.0);
1762 }
1763
1764 #[test]
1765 fn cook_torrance_below_horizon_is_zero() {
1766 let n = Vec3::Y;
1767 let v = Vec3::Y;
1768 let l = -Vec3::Y; let result = CookTorranceBrdf::evaluate(n, v, l, Vec3::ONE, 0.0, 0.5);
1770 assert_eq!(result, Vec3::ZERO);
1771 }
1772
1773 #[test]
1774 fn cook_torrance_metal_has_coloured_specular() {
1775 let n = Vec3::Y;
1776 let v = Vec3::new(0.0, 1.0, 0.0);
1777 let l = Vec3::new(0.5, 0.866, 0.0).normalize();
1778 let gold = Vec3::new(1.0, 0.766, 0.336);
1779 let result = CookTorranceBrdf::evaluate(n, v, l, gold, 1.0, 0.1);
1780 assert!(result.x > result.z, "Gold should be redder than blue");
1782 }
1783
1784 #[test]
1787 fn lambertian_energy_conservation() {
1788 let albedo = Vec3::ONE;
1790 let mut sum = Vec3::ZERO;
1792 let n_theta = 100;
1793 let n_phi = 200;
1794 for i in 0..n_theta {
1795 let theta = (i as f32 + 0.5) / n_theta as f32 * std::f32::consts::FRAC_PI_2;
1796 for j in 0..n_phi {
1797 let phi = (j as f32 + 0.5) / n_phi as f32 * 2.0 * PI;
1798 let sin_t = theta.sin();
1799 let cos_t = theta.cos();
1800 let l = Vec3::new(sin_t * phi.cos(), cos_t, sin_t * phi.sin());
1801 let n_dot_l = cos_t;
1802 let solid_angle = sin_t
1803 * (std::f32::consts::FRAC_PI_2 / n_theta as f32)
1804 * (2.0 * PI / n_phi as f32);
1805 sum += LambertianBrdf::evaluate(albedo, n_dot_l) * solid_angle;
1806 }
1807 }
1808 assert!(
1810 (sum.x - 1.0).abs() < 0.02,
1811 "Lambertian integral = {:.4}",
1812 sum.x
1813 );
1814 }
1815
1816 #[test]
1819 fn oren_nayar_at_zero_roughness_matches_lambertian() {
1820 let n = Vec3::Y;
1821 let v = Vec3::new(0.3, 0.95, 0.0).normalize();
1822 let l = Vec3::new(-0.3, 0.95, 0.0).normalize();
1823 let albedo = Vec3::ONE;
1824 let on = OrenNayarBrdf::evaluate(v, l, n, albedo, 0.0);
1825 let lam = LambertianBrdf::evaluate(albedo, n.dot(l).max(0.0));
1826 assert!(
1827 (on - lam).length() < 0.05,
1828 "ON with roughness=0 should ~match Lambertian: on={on:?} lam={lam:?}"
1829 );
1830 }
1831
1832 #[test]
1835 fn clearcoat_zero_strength_returns_zero() {
1836 let n = Vec3::Y;
1837 let v = Vec3::new(0.0, 1.0, 0.0);
1838 let l = Vec3::new(0.5, 0.866, 0.0).normalize();
1839 let result = ClearcoatBrdf::evaluate(n, v, l, 0.0, 0.3);
1840 assert_eq!(result, Vec3::ZERO);
1841 }
1842
1843 #[test]
1846 fn brdf_lut_values_in_unit_range() {
1847 let lut = ibl::BrdfLut::generate(32);
1848 for &v in &lut.data {
1849 assert!(
1850 (0.0..=1.0).contains(&v.x) && (0.0..=1.0).contains(&v.y),
1851 "LUT value out of range: {v:?}"
1852 );
1853 }
1854 }
1855
1856 #[test]
1857 fn ssao_kernel_has_correct_count() {
1858 let k = ibl::AmbientOcclusion::ssao_kernel(32);
1859 assert_eq!(k.len(), 32);
1860 }
1861
1862 #[test]
1865 fn aces_filmic_maps_zero_to_zero() {
1866 let out = tonemap::aces_filmic(Vec3::ZERO);
1867 assert!(out.length() < 1e-4);
1868 }
1869
1870 #[test]
1871 fn aces_filmic_clamps_to_one() {
1872 let out = tonemap::aces_filmic(Vec3::splat(1000.0));
1873 assert!(out.x <= 1.0 && out.y <= 1.0 && out.z <= 1.0);
1874 }
1875
1876 #[test]
1877 fn srgb_round_trip() {
1878 let linear = Vec3::new(0.5, 0.2, 0.8);
1879 let srgb = tonemap::linear_to_srgb(linear);
1880 let back = tonemap::srgb_to_linear(srgb);
1881 assert!((back - linear).length() < 1e-4, "sRGB round-trip error: {back:?}");
1882 }
1883
1884 #[test]
1885 fn gamma_correct_identity_at_gamma_one() {
1886 let v = Vec3::new(0.4, 0.7, 0.1);
1887 let out = tonemap::gamma_correct(v, 1.0);
1888 assert!((out - v).length() < 1e-5);
1889 }
1890
1891 #[test]
1894 fn glsl_cook_torrance_contains_ggx() {
1895 let src = BrdfGlsl::cook_torrance_source();
1896 assert!(src.contains("ggxD"), "Expected ggxD function");
1897 assert!(src.contains("smithGGX"), "Expected smithGGX function");
1898 assert!(src.contains("schlickF"), "Expected schlickF function");
1899 }
1900
1901 #[test]
1902 fn glsl_ibl_source_contains_expected_samplers() {
1903 let src = BrdfGlsl::ibl_source();
1904 assert!(src.contains("u_PrefilteredEnv"));
1905 assert!(src.contains("u_BrdfLut"));
1906 }
1907
1908 #[test]
1909 fn glsl_full_brdf_compiles_to_large_string() {
1910 let src = BrdfGlsl::full_brdf_glsl(ToneMapOp::AcesFilmic);
1911 assert!(src.len() > 1000);
1912 assert!(src.contains("#define PI"));
1913 }
1914
1915 #[test]
1918 fn simple_rng_produces_values_in_range() {
1919 let mut rng = SimpleRng::new(42);
1920 for _ in 0..1000 {
1921 let v = rng.next_f32();
1922 assert!((0.0..1.0).contains(&v), "RNG value {v} out of [0,1)");
1923 }
1924 }
1925}