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}