// Port of glsl/pathtracing/thin-film.glsl (Airy summation with complex arithmetic,
// evaluated at the hero wavelength).
fn cx(r: f32, i: f32) -> vec2<f32> { return vec2<f32>(r, i); }
fn cx_mul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
fn cx_conj(z: vec2<f32>) -> vec2<f32> { return vec2<f32>(z.x, -z.y); }
fn cx_abs2(z: vec2<f32>) -> f32 { return z.x * z.x + z.y * z.y; }
fn cx_div(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { return cx_mul(a, cx_conj(b)) / max(cx_abs2(b), 1e-30); }
fn cx_exp(z: vec2<f32>) -> vec2<f32> { return exp(z.x) * vec2<f32>(cos(z.y), sin(z.y)); }
fn cx_sqrt(z: vec2<f32>) -> vec2<f32> {
let r = length(z);
if (r < 1e-30) { return vec2<f32>(0.0); }
let half_angle = 0.5 * atan2(z.y, z.x);
return sqrt(r) * vec2<f32>(cos(half_angle), sin(half_angle));
}
struct NK {
n: vec3<f32>,
k: vec3<f32>,
};
// "Artist Friendly Metallic Fresnel", Gulbrandsen 2014
fn GulbrandsenMapping(F0: vec3<f32>, edgetint: vec3<f32>) -> NK {
let r = clamp(F0, vec3<f32>(0.0), vec3<f32>(0.99));
let g = edgetint;
let r_sqrt = sqrt(r);
let n_min = (vec3<f32>(1.0) - r) / (vec3<f32>(1.0) + r);
let n_max = (vec3<f32>(1.0) + r_sqrt) / (vec3<f32>(1.0) - r_sqrt);
var out: NK;
out.n = mix(n_max, n_min, g);
var k2 = (sqr3(out.n + vec3<f32>(1.0)) * r - sqr3(out.n - vec3<f32>(1.0))) / (vec3<f32>(1.0) - r);
k2 = max(vec3<f32>(0.0), k2);
out.k = sqrt(k2);
return out;
}
fn conductor_ior_at_wavelength(n_rgb: vec3<f32>, k_rgb: vec3<f32>, lambda: f32) -> vec2<f32> {
var w = max(vec3<f32>(0.0), xyzToRgb(xyzFit_1931(lambda)));
let ws = w.r + w.g + w.b;
if (ws < 1e-10) { return vec2<f32>(n_rgb.g, k_rgb.g); }
w /= ws;
return vec2<f32>(dot(n_rgb, w), dot(k_rgb, w));
}
fn cos_refracted(cos_i: vec2<f32>, n_i: vec2<f32>, n_t: vec2<f32>) -> vec2<f32> {
let sin_i_sq = cx(1.0, 0.0) - cx_mul(cos_i, cos_i);
let ratio = cx_div(n_i, n_t);
let sin_t_sq = cx_mul(cx_mul(ratio, ratio), sin_i_sq);
var cos_t = cx_sqrt(cx(1.0, 0.0) - sin_t_sq);
if (cx_mul(n_t, cos_t).y < 0.0) { cos_t = -cos_t; }
return cos_t;
}
struct FresnelCoeffs {
r_s: vec2<f32>,
r_p: vec2<f32>,
t_s: vec2<f32>,
t_p: vec2<f32>,
};
fn fresnel_coeffs(cos_i: vec2<f32>, cos_t: vec2<f32>, n_i: vec2<f32>, n_t: vec2<f32>) -> FresnelCoeffs {
let ni_ci = cx_mul(n_i, cos_i);
let nt_ct = cx_mul(n_t, cos_t);
let ni_ct = cx_mul(n_i, cos_t);
let nt_ci = cx_mul(n_t, cos_i);
var c: FresnelCoeffs;
c.r_s = cx_div(ni_ci - nt_ct, ni_ci + nt_ct);
c.t_s = cx_div(2.0 * ni_ci, ni_ci + nt_ct);
c.r_p = cx_div(nt_ci - ni_ct, nt_ci + ni_ct);
c.t_p = cx_div(2.0 * ni_ci, nt_ci + ni_ct);
return c;
}
fn thin_film_R(cos_1: f32, n_1: f32, n_2: f32, n_3: vec2<f32>, d: f32, lambda: f32) -> f32 {
let cn1 = cx(n_1, 0.0);
let cn2 = cx(n_2, 0.0);
let cos_2 = cos_refracted(cx(cos_1, 0.0), cn1, cn2);
let cos_3 = cos_refracted(cos_2, cn2, n_3);
let c12 = fresnel_coeffs(cx(cos_1, 0.0), cos_2, cn1, cn2);
let c23 = fresnel_coeffs(cos_2, cos_3, cn2, n_3);
let r21_s = -c12.r_s;
let r21_p = -c12.r_p;
let ratio = cx_div(cx_mul(cn2, cos_2), cx_mul(cn1, cx(cos_1, 0.0)));
let t21_s = cx_mul(c12.t_s, ratio);
let t21_p = cx_mul(c12.t_p, ratio);
let delta = (4.0 * PI * n_2 * d / lambda) * cos_2;
let phi = cx_exp(cx(-delta.y, delta.x));
let denom_s = cx(1.0, 0.0) - cx_mul(r21_s, cx_mul(c23.r_s, phi));
let numer_s = cx_mul(c12.t_s, cx_mul(c23.r_s, cx_mul(t21_s, phi)));
let r_s = c12.r_s + cx_div(numer_s, denom_s);
let denom_p = cx(1.0, 0.0) - cx_mul(r21_p, cx_mul(c23.r_p, phi));
let numer_p = cx_mul(c12.t_p, cx_mul(c23.r_p, cx_mul(t21_p, phi)));
let r_p = c12.r_p + cx_div(numer_p, denom_p);
let R = 0.5 * (cx_abs2(r_s) + cx_abs2(r_p));
return clamp(R, 0.0, 1.0);
}
// `internal`: the ray is inside the dielectric substrate hitting the film from below,
// so the media order is substrate -> film -> exterior (this is what makes TIR appear
// correctly in the film reflectance; the viewer always assumes exterior incidence).
fn FresnelThinFilmOverDielectric(mui: f32, eta_fe: f32, internal: bool) -> f32 {
let n_external = thin_film_ior / eta_fe;
// substrate IOR, scaled by the specular_weight IOR modulation
let n_substrate = specular_ior_dispersive() * eta_s() / max(eta_s_unmodulated(), DENOM_TOLERANCE);
if (internal) {
return thin_film_R(mui, n_substrate, thin_film_ior, cx(n_external, 0.0), thin_film_thickness, wavelength_nm);
}
return thin_film_R(mui, n_external, thin_film_ior, cx(n_substrate, 0.0), thin_film_thickness, wavelength_nm);
}
fn FresnelThinFilmOverConductor(mui: f32, eta_fe: f32) -> vec3<f32> {
let n_external = thin_film_ior / eta_fe;
let nk_rgb = GulbrandsenMapping(base_weight * base_color, specular_color);
let nk = conductor_ior_at_wavelength(nk_rgb.n, nk_rgb.k, wavelength_nm);
let R = thin_film_R(mui, n_external, thin_film_ior, cx(nk.x, nk.y), thin_film_thickness, wavelength_nm);
return vec3<f32>(R);
}