// Port of glsl/pathtracing/coat_brdf.glsl
struct Alphas {
x: f32,
y: f32,
};
fn coat_ndf_roughnesses() -> Alphas {
let rsqr = sqr(coat_roughness);
var a: Alphas;
a.x = rsqr * sqrt(2.0 / (1.0 + sqr(1.0 - coat_anisotropy)));
a.y = (1.0 - coat_anisotropy) * a.x;
let min_alpha = 1.0e-4;
a.x = max(min_alpha, a.x);
a.y = max(min_alpha, a.y);
return a;
}
fn coat_brdf_evaluate(winputL: vec3<f32>, woutputL: vec3<f32>) -> BsdfEval {
var out: BsdfEval;
out.f = vec3<f32>(0.0);
out.pdf = 0.0;
let transmitted = woutputL.z * winputL.z < 0.0;
if (transmitted) { return out; }
let external_reflection = winputL.z > 0.0;
let n_exterior = 1.0;
let n_interior = coat_ior;
let eta_ti_refl = select(n_exterior / n_interior, n_interior / n_exterior, external_reflection);
if (abs(eta_ti_refl - 1.0) < IOR_EPSILON) { return out; }
let a = coat_ndf_roughnesses();
let mR = normalize(woutputL + winputL);
let D = ggx_ndf_eval(mR, a.x, a.y);
let DV = D * ggx_G1(winputL, a.x, a.y) * abs(dot(winputL, mR)) / max(DENOM_TOLERANCE, abs(winputL.z));
let F = FresnelDielectricReflectance(abs(dot(winputL, mR)), eta_ti_refl);
let dwh_dwo = 1.0 / max(abs(4.0 * dot(winputL, mR)), DENOM_TOLERANCE);
out.pdf = DV * dwh_dwo;
out.f = vec3<f32>(F) * D * ggx_G2(winputL, woutputL, a.x, a.y) / max(4.0 * abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
return out;
}
fn coat_brdf_sample(winputL: vec3<f32>) -> BsdfSample {
var out: BsdfSample;
out.f = vec3<f32>(0.0);
out.woutputL = vec3<f32>(0.0, 0.0, 1.0);
out.pdf = 0.0;
let external_reflection = winputL.z > 0.0;
let n_exterior = 1.0;
let n_interior = coat_ior;
let eta_ti_refl = select(n_exterior / n_interior, n_interior / n_exterior, external_reflection);
if (abs(eta_ti_refl - 1.0) < IOR_EPSILON) { return out; }
let a = coat_ndf_roughnesses();
if (winputL.z <= 0.0) { return out; }
let mR = ggx_ndf_sample(winputL, a.x, a.y);
let woutputL = -winputL + 2.0 * dot(winputL, mR) * mR;
out.woutputL = woutputL;
if (winputL.z * woutputL.z < FLT_EPSILON) { return out; }
let D = ggx_ndf_eval(mR, a.x, a.y);
let DV = D * ggx_G1(winputL, a.x, a.y) * abs(dot(winputL, mR)) / max(DENOM_TOLERANCE, abs(winputL.z));
let G2 = ggx_G2(winputL, woutputL, a.x, a.y);
let dwh_dwo = 1.0 / max(abs(4.0 * dot(winputL, mR)), DENOM_TOLERANCE);
out.pdf = DV * dwh_dwo;
let F = FresnelDielectricReflectance(abs(dot(winputL, mR)), eta_ti_refl);
out.f = vec3<f32>(F) * D * G2 / max(4.0 * abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
return out;
}
// Directional albedo of the coat's reflection lobe.
//
// Tabulated on the CPU (`energy.rs`, table `ENERGY_COAT_R`) rather than
// estimated with the viewer's one-sample Monte-Carlo draw. The estimate was
// wrong in a way that does not look like noise in the image: this albedo sets
// the coat layering weight, the lobe sampling probabilities *and* -- at the
// visible surface -- the denoiser's specular and diffuse demodulation guides.
// A guide that flickers frame to frame makes a denoiser dividing by it shake
// the whole image, which is exactly the failure 5fb5428 chased down for the
// props' wireframe. It also cost a GGX sample and two `rand()` draws at every
// vertex of every path.
//
// `coat_brdf_sample` returns nothing for a direction below the surface, so the
// albedo seen from inside is zero and only the outside table is needed.
fn coat_brdf_albedo(winputL: vec3<f32>) -> vec3<f32> {
if (abs(coat_ior - 1.0) < IOR_EPSILON) { return vec3<f32>(0.0); }
if (winputL.z <= 0.0) { return vec3<f32>(0.0); }
return vec3<f32>(energy_table_lookup(ENERGY_COAT_R, winputL.z));
}