// Port of glsl/pathtracing/specular_brdf.glsl (haze / retroreflection lobes
// omitted: not part of the OpenPBR 1.1 nodedef, and the viewer defaults them
// to zero which makes them no-ops).
// Effective base roughness including the roughening due to a rough coat
// (spec "Roughening": r' = lerp(r, min(1, r^4 + 2 r_c^4)^(1/4), C)).
fn effective_specular_roughness() -> f32 {
let r = specular_roughness;
let rc = coat_roughness;
let r4 = r * r * r * r;
let rc4 = rc * rc * rc * rc;
let roughened = pow(min(1.0, r4 + 2.0 * rc4), 0.25);
return mix(r, roughened, coat_weight);
}
fn specular_ndf_roughnesses() -> Alphas {
let rsqr = sqr(effective_specular_roughness());
var a: Alphas;
a.x = rsqr * sqrt(2.0 / (1.0 + sqr(1.0 - specular_anisotropy)));
a.y = (1.0 - specular_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;
}
// Ratio of the (dispersive) dielectric IOR to the surrounding IOR, blending ambient and
// coat according to coat_weight and inverting the coat/base ratio to avoid spurious TIR
// (spec eq. [specular_ior_ratio] + [specular_ior_ratio_with_tir_fix]).
fn eta_s_unmodulated() -> f32 {
let n_s = specular_ior_dispersive();
var eta_sc = n_s / coat_ior;
if (eta_sc < 1.0) { eta_sc = 1.0 / eta_sc; }
return mix(n_s / ambient_ior, eta_sc, coat_weight);
}
// specular_weight modulates the IOR ratio so that the normal-incidence reflectance is
// scaled by specular_weight (spec eq. [modulated_ior]); the same modulated ratio is used
// for reflection *and* refraction, as the spec requires.
fn modulate_eta(eta: f32) -> f32 {
if (specular_weight == 1.0) { return eta; }
let etam1 = eta - 1.0;
if (abs(etam1) < FLT_EPSILON) { return eta; }
let F0 = sqr(etam1) / sqr(1.0 + eta);
let epsilon = sign(etam1) * sqrt(clamp(specular_weight * F0, 0.0, 0.99999));
return (1.0 + epsilon) / max(1.0 - epsilon, DENOM_TOLERANCE);
}
fn eta_s() -> f32 {
return modulate_eta(eta_s_unmodulated());
}
// Dielectric Fresnel factor including the (optional) thin film.
fn specular_fresnel(mu: f32, eta_ti_refl: f32) -> f32 {
if (thin_film_weight > 0.0) {
let eta_fe = mix(thin_film_ior / ambient_ior, thin_film_ior / coat_ior, coat_weight);
let F_film = FresnelThinFilmOverDielectric(mu, eta_fe, eta_ti_refl < 1.0);
let F_nofilm = FresnelDielectricReflectance(mu, eta_ti_refl);
return mix(F_nofilm, F_film, thin_film_weight);
}
return FresnelDielectricReflectance(mu, eta_ti_refl);
}
// Sample a micronormal of the base specular NDF from the hemisphere of winputL.
fn sample_specular_micronormal(winputL: vec3<f32>) -> vec3<f32> {
let a = specular_ndf_roughnesses();
if (winputL.z > 0.0) {
return ggx_ndf_sample(winputL, a.x, a.y);
}
var winputL_reflected = winputL;
winputL_reflected.z *= -1.0;
var mR = ggx_ndf_sample(winputL_reflected, a.x, a.y);
mR.z *= -1.0;
return mR;
}
fn specular_brdf_evaluate(winputL: vec3<f32>, woutputL: vec3<f32>) -> BsdfEval {
var out: BsdfEval;
out.f = vec3<f32>(0.0);
out.pdf = 0.0;
out.fresnel = 0.0;
let transmitted = woutputL.z * winputL.z < 0.0;
if (transmitted) { return out; }
let external_reflection = winputL.z > 0.0;
let eta_ie = eta_s();
let eta_ti_refl = select(1.0 / eta_ie, eta_ie, external_reflection);
// Index-matched interface: no dielectric reflection, unless a thin film is present
// (the film itself reflects; the reference viewer drops this case entirely).
if (abs(eta_ti_refl - 1.0) < IOR_EPSILON && thin_film_weight <= 0.0) { return out; }
let a = specular_ndf_roughnesses();
let mR = normalize(woutputL + winputL);
if (dot(mR, winputL) * winputL.z < 0.0 || dot(mR, woutputL) * woutputL.z < 0.0) { return out; }
let D = ggx_ndf_eval(mR, a.x, a.y);
let DV = D * ggx_G1(winputL, a.x, a.y) * max(0.0, dot(winputL, mR)) / max(DENOM_TOLERANCE, winputL.z);
let dwh_dwo = 1.0 / max(abs(4.0 * dot(winputL, mR)), DENOM_TOLERANCE);
let G2 = ggx_G2(winputL, woutputL, a.x, a.y);
let DG2 = D * G2;
out.pdf = DV * dwh_dwo;
let F = specular_fresnel(abs(dot(winputL, mR)), eta_ti_refl);
out.fresnel = F;
let f = F * DG2 / max(4.0 * abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
out.f = vec3<f32>(f * dielectric_energy_scale(winputL));
return out;
}
// Reflection about a given micronormal mR (sampled from the visible NDF).
fn specular_brdf_sample_m(winputL: vec3<f32>, mR: 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;
out.fresnel = 0.0;
let external_reflection = winputL.z > 0.0;
let eta_ie = eta_s();
let eta_ti_refl = select(1.0 / eta_ie, eta_ie, external_reflection);
if (abs(eta_ti_refl - 1.0) < IOR_EPSILON && thin_film_weight <= 0.0) { return out; }
let a = specular_ndf_roughnesses();
let woutputL = -winputL + 2.0 * dot(winputL, mR) * mR;
out.woutputL = woutputL;
if (winputL.z * woutputL.z < 0.0) {
out.pdf = 0.0;
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 dwh_dwo = 1.0 / max(abs(4.0 * dot(winputL, mR)), DENOM_TOLERANCE);
let G2 = ggx_G2(winputL, woutputL, a.x, a.y);
let DG2 = D * G2;
out.pdf = DV * dwh_dwo;
let F = specular_fresnel(abs(dot(winputL, mR)), eta_ti_refl);
out.fresnel = F;
let f = F * DG2 / max(4.0 * abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
out.f = vec3<f32>(f * dielectric_energy_scale(winputL));
return out;
}
fn specular_brdf_sample(winputL: vec3<f32>) -> BsdfSample {
return specular_brdf_sample_m(winputL, sample_specular_micronormal(winputL));
}
// Directional albedo estimate of the specular reflection lobe, used both for the
// (1 - E[f^R]) layering factor and the lobe sampling probability. We use the
// macro-surface Fresnel factor (film-aware, times the multiple-scatter energy
// scale) rather than the viewer's 4-sample Monte-Carlo estimate: the latter is
// noisy and frequently exactly zero from inside a dielectric (TIR'd microfacets),
// which starves the lobe of samples and produces heavy-tailed fireflies.
fn specular_brdf_albedo(winputL: vec3<f32>) -> vec3<f32> {
let eta_ie = eta_s();
if (abs(eta_ie - 1.0) < IOR_EPSILON && thin_film_weight <= 0.0) { return vec3<f32>(0.0); }
let is_external = winputL.z > 0.0;
let eta_ti_refl = select(1.0 / eta_ie, eta_ie, is_external);
// Tabulated single-scatter reflection albedo (Fresnel-weighted, per material) times the
// energy compensation. With a thin film the Fresnel differs; blend towards the film's
// macro-normal reflectance in that case.
var E_R = energy_table_lookup(select(4u, 3u, is_external), abs(winputL.z));
if (thin_film_weight > 0.0) {
let E_ndf = energy_table_lookup(0u, abs(winputL.z));
let F_film = specular_fresnel(clamp(abs(winputL.z), 0.0, 1.0), eta_ti_refl);
E_R = mix(E_R, F_film * E_ndf, thin_film_weight);
}
var scale = 1.0;
if (energy_compensation) { scale = dielectric_energy_scale(winputL); }
return vec3<f32>(clamp(E_R * scale, 0.0, 1.0));
}