Skip to main content

pfx_materials/
brdf.rs

1use crate::material::Material;
2use crate::math::{add3, clamp01, dot, lerp3, normalize, scale3, sub3};
3
4pub const PI: f32 = std::f32::consts::PI;
5pub const RGB_WAVELENGTHS_NM: [f32; 3] = [640.0, 540.0, 455.0];
6pub const GLASS_IOR: f32 = 1.56;
7pub const GLASS_DISPERSION_RED: f32 = -0.016;
8pub const GLASS_DISPERSION_BLUE: f32 = 0.022;
9pub const WATER_IOR: f32 = 1.333;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct Shaded {
13    pub base: [f32; 3],
14    pub roughness: f32,
15    pub metalness: f32,
16    pub specular: f32,
17    pub clearcoat: f32,
18    pub clearcoat_roughness: f32,
19    pub sheen: f32,
20    pub transmission: f32,
21    pub ior: f32,
22    pub dispersion: f32,
23    pub thickness: f32,
24    pub subsurface: f32,
25    pub subsurface_tint: [f32; 3],
26    pub absorption: f32,
27    pub thin_film: f32,
28    pub thin_film_ior: f32,
29    pub thin_film_amount: f32,
30    pub emission: [f32; 3],
31    pub fresnel_power: f32,
32    pub normal: [f32; 3],
33}
34
35impl Shaded {
36    pub fn from_material(m: &Material) -> Self {
37        Self {
38            base: m.base,
39            roughness: m.roughness,
40            metalness: m.metalness,
41            specular: m.specular,
42            clearcoat: m.clearcoat,
43            clearcoat_roughness: m.clearcoat_roughness,
44            sheen: m.sheen,
45            transmission: m.transmission,
46            ior: m.ior,
47            dispersion: m.dispersion,
48            thickness: m.thickness,
49            subsurface: m.subsurface,
50            subsurface_tint: m.subsurface_tint,
51            absorption: m.absorption,
52            thin_film: m.thin_film,
53            thin_film_ior: m.thin_film_ior,
54            thin_film_amount: m.thin_film_amount,
55            emission: m.emission,
56            fresnel_power: m.fresnel_power,
57            normal: [0.0, 0.0, 1.0],
58        }
59    }
60}
61
62pub fn dielectric_f0(ior: f32) -> f32 {
63    let r = (ior - 1.0) / (ior + 1.0);
64    r * r
65}
66
67pub fn ior_from_f0(f0: f32) -> f32 {
68    let r = f0.clamp(0.0, 0.999).sqrt();
69    (1.0 + r) / (1.0 - r)
70}
71
72pub fn dispersed_ior(ior: f32, dispersion: f32) -> [f32; 3] {
73    [
74        ior + GLASS_DISPERSION_RED * dispersion,
75        ior,
76        ior + GLASS_DISPERSION_BLUE * dispersion,
77    ]
78}
79
80pub fn schlick(f0: [f32; 3], cos_theta: f32, power: f32) -> [f32; 3] {
81    let k = (1.0 - clamp01(cos_theta)).powf(power);
82    lerp3(f0, [1.0, 1.0, 1.0], k)
83}
84
85pub fn fresnel_dielectric(cos_theta: f32, eta: f32) -> f32 {
86    let c = clamp01(cos_theta);
87    let eta = if eta.abs() < 1e-4 { 1.0 } else { eta };
88    let s2 = 1.0 - c * c;
89    let t2 = 1.0 - s2 / (eta * eta);
90    if t2 <= 0.0 {
91        return 1.0;
92    }
93    let ct = t2.sqrt();
94    let rs = (c - eta * ct) / (c + eta * ct);
95    let rp = (eta * c - ct) / (eta * c + ct);
96    0.5 * (rs * rs + rp * rp)
97}
98
99pub fn fresnel_conductor(cos_theta: f32, eta: f32, k: f32) -> f32 {
100    let c = clamp01(cos_theta);
101    if c <= 1e-6 {
102        return 1.0;
103    }
104    let cos2 = c * c;
105    let sin2 = 1.0 - cos2;
106    let eta2 = eta * eta;
107    let k2 = k * k;
108    let t0 = eta2 - k2 - sin2;
109    let a2b2 = (t0 * t0 + 4.0 * eta2 * k2).sqrt();
110    let t1 = a2b2 + cos2;
111    let a = (0.5 * (a2b2 + t0)).max(0.0).sqrt();
112    let t2 = 2.0 * a * c;
113    let rs = (t1 - t2) / (t1 + t2);
114    let t3 = cos2 * a2b2 + sin2 * sin2;
115    let t4 = t2 * sin2;
116    let denom = t3 + t4;
117    let rp = if denom.abs() <= 1e-12 {
118        rs
119    } else {
120        rs * (t3 - t4) / denom
121    };
122    (0.5 * (rs + rp)).clamp(0.0, 1.0)
123}
124
125pub fn ggx_d(nh: f32, a2: f32) -> f32 {
126    let nh = nh.max(0.0);
127    let k = nh * nh * (a2 - 1.0) + 1.0;
128    a2 / (PI * k * k)
129}
130
131pub fn smith_g1(ndot: f32, a2: f32) -> f32 {
132    let n = ndot.max(0.0);
133    2.0 * n / (n + (a2 + (1.0 - a2) * n * n).sqrt())
134}
135
136pub fn smith_g(ndv: f32, ndl: f32, a2: f32) -> f32 {
137    smith_g1(ndv, a2) * smith_g1(ndl, a2)
138}
139
140pub fn smith_correlated(ndv: f32, ndl: f32, alpha: f32) -> f32 {
141    let a2 = alpha * alpha;
142    let gv = ndl * (ndv * ndv * (1.0 - a2) + a2).sqrt();
143    let gl = ndv * (ndl * ndl * (1.0 - a2) + a2).sqrt();
144    0.5 / (gv + gl).max(1e-6)
145}
146
147pub fn alpha(roughness: f32) -> f32 {
148    let rough = roughness.clamp(0.02, 1.0);
149    rough * rough
150}
151
152fn ggx_spec(ndv: f32, ndl: f32, ndh: f32, a2: f32, fresnel: [f32; 3]) -> [f32; 3] {
153    let d = ggx_d(ndh, a2);
154    let g = smith_g(ndv, ndl, a2);
155    scale3(fresnel, d * g / (4.0 * ndv * ndl))
156}
157
158pub fn lambert(albedo: [f32; 3]) -> [f32; 3] {
159    scale3(albedo, 1.0 / PI)
160}
161
162pub fn beer(distance: f32, tint: [f32; 3], absorption: f32) -> [f32; 3] {
163    if distance <= 0.0 || absorption <= 0.0 {
164        return [1.0, 1.0, 1.0];
165    }
166    [
167        (-distance * absorption * (1.0 - tint[0]).max(0.0)).exp(),
168        (-distance * absorption * (1.0 - tint[1]).max(0.0)).exp(),
169        (-distance * absorption * (1.0 - tint[2]).max(0.0)).exp(),
170    ]
171}
172
173fn film_phase(cosine: f32, thickness: f32, n_film: f32) -> (f32, f32, [f32; 3]) {
174    let sine = (1.0 - cosine * cosine).max(0.0).sqrt();
175    let nf = n_film.max(1.01);
176    let inner = (1.0 - (sine / nf) * (sine / nf)).max(0.0).sqrt();
177    let r12 = (cosine - nf * inner) / (cosine + nf * inner);
178    let delta = [
179        std::f32::consts::TAU * 2.0 * nf * thickness * inner / RGB_WAVELENGTHS_NM[0],
180        std::f32::consts::TAU * 2.0 * nf * thickness * inner / RGB_WAVELENGTHS_NM[1],
181        std::f32::consts::TAU * 2.0 * nf * thickness * inner / RGB_WAVELENGTHS_NM[2],
182    ];
183    (r12, inner, [delta[0].cos(), delta[1].cos(), delta[2].cos()])
184}
185
186pub fn film_rgb(cosine: f32, thickness: f32, n_film: f32, n_substrate: f32) -> [f32; 3] {
187    let cosine = clamp01(cosine);
188    if thickness <= 0.0 {
189        let f = fresnel_dielectric(cosine, n_substrate.max(1.01));
190        return [f, f, f];
191    }
192    let (r12, _, c) = film_phase(cosine, thickness, n_film);
193    let ns = n_substrate.max(1.01);
194    let nf = n_film.max(1.01);
195    let r23 = (nf - ns) / (nf + ns);
196    [
197        airy(r12, r23, c[0]),
198        airy(r12, r23, c[1]),
199        airy(r12, r23, c[2]),
200    ]
201}
202
203pub fn metal_film(cosine: f32, thickness: f32, n_film: f32, f0: [f32; 3]) -> [f32; 3] {
204    let cosine = clamp01(cosine);
205    if thickness <= 0.0 {
206        return f0;
207    }
208    let (r12, _, c) = film_phase(cosine, thickness, n_film);
209    [
210        airy(r12, -(f0[0].clamp(0.0, 0.999)).sqrt(), c[0]),
211        airy(r12, -(f0[1].clamp(0.0, 0.999)).sqrt(), c[1]),
212        airy(r12, -(f0[2].clamp(0.0, 0.999)).sqrt(), c[2]),
213    ]
214}
215
216fn airy(r12: f32, r23: f32, c: f32) -> f32 {
217    let top = r12 * r12 + r23 * r23 + 2.0 * r12 * r23 * c;
218    let bottom = 1.0 + r12 * r12 * r23 * r23 + 2.0 * r12 * r23 * c;
219    if bottom.abs() <= 1e-12 {
220        0.0
221    } else {
222        (top / bottom).clamp(0.0, 1.0)
223    }
224}
225
226pub fn liquid_dye(amount: f32, dye: [f32; 3]) -> [f32; 3] {
227    lerp3([1.0, 1.0, 1.0], dye, clamp01(amount))
228}
229
230pub fn refract(incident: [f32; 3], normal: [f32; 3], eta: f32) -> Option<[f32; 3]> {
231    let cos_i = dot(normal, incident);
232    let k = 1.0 - eta * eta * (1.0 - cos_i * cos_i);
233    if k < 0.0 {
234        return None;
235    }
236    let t = eta * cos_i + k.sqrt();
237    Some(sub3(scale3(incident, eta), scale3(normal, t)))
238}
239
240fn half_vector(wo: [f32; 3], wi: [f32; 3]) -> [f32; 3] {
241    normalize(add3(wo, wi))
242}
243
244fn reflection_colour(s: &Shaded, cos_theta: f32, ndh_cos: f32) -> [f32; 3] {
245    let power = if s.fresnel_power <= 0.0 {
246        5.0
247    } else {
248        s.fresnel_power
249    };
250    let f0 = lerp3([s.specular; 3], s.base, clamp01(s.metalness));
251    let mut plain = if s.transmission > 0.0 || s.dispersion != 0.0 {
252        let iors = dispersed_ior(s.ior, s.dispersion);
253        let metal = schlick(s.base, ndh_cos, power);
254        let diel = [
255            fresnel_dielectric(ndh_cos, iors[0]),
256            fresnel_dielectric(ndh_cos, iors[1]),
257            fresnel_dielectric(ndh_cos, iors[2]),
258        ];
259        lerp3(diel, metal, clamp01(s.metalness))
260    } else if f0[0] <= 0.0 && f0[1] <= 0.0 && f0[2] <= 0.0 {
261        [0.0, 0.0, 0.0]
262    } else {
263        schlick(f0, ndh_cos, power)
264    };
265    let amount = clamp01(s.thin_film_amount);
266    if s.thin_film > 0.0 && amount > 0.0 {
267        let filmed = if s.metalness >= 0.5 {
268            crate::film_conductor(cos_theta, s.thin_film, s.thin_film_ior, f0)
269        } else {
270            crate::film_dielectric(cos_theta, s.thin_film, s.thin_film_ior, s.ior)
271        };
272        plain = lerp3(plain, filmed, amount);
273    }
274    plain
275}
276
277pub fn eval(s: &Shaded, wo: [f32; 3], wi: [f32; 3]) -> [f32; 3] {
278    let ndv = wo[2];
279    if ndv <= 1e-5 {
280        return [0.0, 0.0, 0.0];
281    }
282    let ndl = wi[2];
283    if ndl.abs() <= 1e-8 {
284        return [0.0, 0.0, 0.0];
285    }
286    let metal = clamp01(s.metalness);
287    let trans = clamp01(s.transmission);
288    let sss = clamp01(s.subsurface) * (1.0 - trans);
289    let sheen = clamp01(s.sheen);
290    let sheen_cover = sheen * (1.0 - ndv).powi(4);
291    let view = clamp01(ndv);
292
293    if ndl < 0.0 {
294        let hcos = view;
295        let fresnel = reflection_colour(s, view, hcos);
296        let pass = sub3([1.0, 1.0, 1.0], fresnel);
297        let back = (1.0 - metal) * sss + trans * (1.0 - metal);
298        let tint = beer(s.thickness, s.subsurface_tint, s.absorption);
299        let colour = [
300            s.base[0] * tint[0] * pass[0],
301            s.base[1] * tint[1] * pass[1],
302            s.base[2] * tint[2] * pass[2],
303        ];
304        return scale3(colour, back / PI);
305    }
306
307    let h = half_vector(wo, wi);
308    let ndh = h[2].max(0.0);
309    let vdh = dot(wo, h).max(0.0);
310    let fresnel = reflection_colour(s, view, vdh);
311    let a2 = {
312        let a = alpha(s.roughness);
313        a * a
314    };
315    let spec = ggx_spec(ndv, ndl, ndh, a2, fresnel);
316    let pass = sub3([1.0, 1.0, 1.0], fresnel);
317    let body = (1.0 - metal) * (1.0 - trans) * (1.0 - sss);
318    let diffuse_w = body * (1.0 - sheen_cover);
319    let sheen_w = body * sheen_cover * (1.0 - ndh).powi(5);
320    let mut colour = [
321        spec[0] + s.base[0] * pass[0] * (diffuse_w + sheen_w) / PI,
322        spec[1] + s.base[1] * pass[1] * (diffuse_w + sheen_w) / PI,
323        spec[2] + s.base[2] * pass[2] * (diffuse_w + sheen_w) / PI,
324    ];
325    let coat = clamp01(s.clearcoat);
326    if coat > 0.0 {
327        let coat_a = alpha(s.clearcoat_roughness);
328        let coat_a2 = coat_a * coat_a;
329        let coat_f = schlick([0.04, 0.04, 0.04], vdh, 5.0);
330        let coat_spec = ggx_spec(ndv, ndl, ndh, coat_a2, coat_f);
331        let fc = schlick([0.04, 0.04, 0.04], view, 5.0)[0];
332        let atten = 1.0 - coat * fc;
333        colour = add3(scale3(coat_spec, coat), scale3(colour, atten));
334    }
335    colour
336}
337
338pub fn sample_vndf(wo: [f32; 3], roughness: f32, u1: f32, u2: f32) -> [f32; 3] {
339    let a = alpha(roughness);
340    let vh = normalize([a * wo[0], a * wo[1], wo[2].max(1e-6)]);
341    let lensq = vh[0] * vh[0] + vh[1] * vh[1];
342    let t1 = if lensq > 0.0 {
343        [-vh[1] / lensq.sqrt(), vh[0] / lensq.sqrt(), 0.0]
344    } else {
345        [1.0, 0.0, 0.0]
346    };
347    let t2 = [
348        vh[1] * t1[2] - vh[2] * t1[1],
349        vh[2] * t1[0] - vh[0] * t1[2],
350        vh[0] * t1[1] - vh[1] * t1[0],
351    ];
352    let r = u1.clamp(0.0, 1.0).sqrt();
353    let phi = std::f32::consts::TAU * u2.clamp(0.0, 1.0);
354    let p1 = r * phi.cos();
355    let s = 0.5 * (1.0 + vh[2]);
356    let p2 = (1.0 - s) * (1.0 - p1 * p1).max(0.0).sqrt() + s * r * phi.sin();
357    let nh_z = (1.0 - p1 * p1 - p2 * p2).max(0.0).sqrt();
358    let nh = add3(add3(scale3(t1, p1), scale3(t2, p2)), scale3(vh, nh_z));
359    normalize([a * nh[0], a * nh[1], nh[2].max(1e-6)])
360}