// Port of glsl/pathtracing/specular_btdf.glsl
// Delta (straight-through) lobe: it cannot be evaluated for an arbitrary direction, so
// it contributes nothing (and no pdf) to MIS. (The viewer returned pdf = 1 here, which
// inflated the combined pdf of other lobes and lost energy.)
fn thin_walled_transmission_evaluate(winputL: vec3<f32>, woutputL: vec3<f32>) -> BsdfEval {
var out: BsdfEval;
out.pdf = 0.0;
out.f = vec3<f32>(0.0);
out.fresnel = 0.0;
return out;
}
// Given the direction (wt) of a light beam transmitted through a plane dielectric interface
// with the given normal (n) in any orientation, and eta_ti_photon = nt/ni,
// compute the direction of the incident light beam (wi). Returns w = 0 on TIR.
struct RefractResult {
ok: bool,
wi: vec3<f32>,
};
fn refraction_given_transmitted_beam(n: vec3<f32>, eta_ti_photon: f32, wt: vec3<f32>) -> RefractResult {
var r: RefractResult;
let wtn = dot(wt, n);
let disciminant = 1.0 - sqr(eta_ti_photon) * (1.0 - sqr(wtn));
if (disciminant < 0.0) {
r.ok = false;
r.wi = vec3<f32>(0.0);
return r;
}
r.ok = true;
r.wi = eta_ti_photon * wt - n * sign(wtn) * (eta_ti_photon * abs(wtn) - sqrt(disciminant));
return r;
}
// Evaluate the rough dielectric BTDF for an arbitrary (transmitted) direction
// pair. (The reference GLSL leaves this branch unimplemented; it is only
// reached when both transmission and bulk subsurface are active, so we use a
// standard generalized-half-vector evaluation consistent with the sampler.)
fn specular_btdf_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;
if (geometry_thin_walled) {
return thin_walled_transmission_evaluate(winputL, woutputL);
}
let reflected = woutputL.z * winputL.z > 0.0;
if (reflected) {
out.pdf = 0.0; // (viewer: 1.0, which inflates the MIS pdf of reflection samples)
return out;
}
let external_transmission = winputL.z > 0.0;
let eta_ie = eta_s();
let eta_ti_photon = select(eta_ie, 1.0 / eta_ie, external_transmission);
if (abs(eta_ti_photon - 1.0) < IOR_EPSILON) {
return out;
}
let a = specular_ndf_roughnesses();
// Generalized half vector. With directions pointing away from the surface,
// the photon arrives along woutputL (IOR n_i) and leaves along winputL
// (IOR n_t): m ∝ -(n_i*woutputL + n_t*winputL), i.e. -(woutputL + eta_ti_photon*winputL).
var mR = safe_normalize(-(woutputL + eta_ti_photon * winputL));
// The sampler draws micronormals in winputL's hemisphere; match that.
if (mR.z * winputL.z < 0.0) { mR = -mR; }
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 im = dot(woutputL, mR);
let om = dot(winputL, mR);
let dwh_dwo = sqr(eta_ti_photon) * abs(om) / max(sqr(im + eta_ti_photon * om), DENOM_TOLERANCE);
let G2 = ggx_G2(winputL, woutputL, a.x, a.y);
let DG2 = D * G2;
out.pdf = DV * dwh_dwo;
let eta_ti_refl = 1.0 / eta_ti_photon;
let F = specular_fresnel(abs(dot(winputL, mR)), eta_ti_refl);
out.fresnel = F;
let T = clamp(1.0 - F, 0.0, 1.0);
let f = T * abs(dot(winputL, mR)) * dwh_dwo * DG2 / max(abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
let tint = select(vec3<f32>(1.0), transmission_color, transmission_depth == 0.0);
out.f = f * tint * dielectric_energy_scale(winputL);
return out;
}
// Refraction through a given micronormal mR (sampled from the visible NDF).
fn specular_btdf_sample_m(winputL: vec3<f32>, mR: vec3<f32>) -> BsdfSample {
var out: BsdfSample;
out.f = vec3<f32>(0.0);
out.woutputL = -winputL;
out.pdf = 0.0;
out.fresnel = 0.0;
if (geometry_thin_walled) {
// Straight-through delta transmission (OpenPBR "thin-walled"). The path tracer
// multiplies by |cos|/pdf, so divide by |cos| here so the sheet transmits
// exactly transmission_color regardless of angle. (The reference viewer omits
// this and darkens thin sheets at grazing angles.)
out.woutputL = -winputL;
out.pdf = 1.0;
out.f = transmission_color / max(abs(winputL.z), DENOM_TOLERANCE);
return out;
}
let external_transmission = winputL.z > 0.0;
let eta_ie = eta_s();
let eta_ti_photon = select(eta_ie, 1.0 / eta_ie, external_transmission);
if (abs(eta_ti_photon - 1.0) < IOR_EPSILON) {
out.woutputL = -winputL;
out.pdf = 1.0 / PDF_EPSILON;
let tint0 = select(vec3<f32>(1.0), transmission_color, transmission_depth == 0.0);
out.f = tint0 * out.pdf / max(DENOM_TOLERANCE, abs(out.woutputL.z));
return out;
}
let a = specular_ndf_roughnesses();
let beamOutgoingR = winputL;
let refr = refraction_given_transmitted_beam(mR, eta_ti_photon, beamOutgoingR);
if (!refr.ok) {
out.pdf = PDF_EPSILON;
return out;
}
let beamIncidentR = refr.wi;
let woutputL = -safe_normalize(beamIncidentR);
out.woutputL = woutputL;
// A refraction through a strongly tilted microfacet can land on the incident side of
// the macro surface; like pbrt-v4 (SameHemisphere check) treat it as invalid.
// (The viewer keeps these samples, which continue with inconsistent bookkeeping and
// produce a divergent tail of fireflies for rough dielectrics.)
if (woutputL.z * winputL.z >= 0.0) {
out.pdf = PDF_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 im = dot(-beamIncidentR, mR);
let om = dot(beamOutgoingR, mR);
let dwh_dwo = sqr(eta_ti_photon) * abs(om) / max(sqr(im + eta_ti_photon * om), DENOM_TOLERANCE);
let G2 = ggx_G2(winputL, woutputL, a.x, a.y);
let DG2 = D * G2;
out.pdf = DV * dwh_dwo;
let eta_ti_refl = 1.0 / eta_ti_photon;
let F = specular_fresnel(abs(dot(winputL, mR)), eta_ti_refl);
out.fresnel = F;
let T = clamp(1.0 - F, 0.0, 1.0);
let f = T * abs(dot(winputL, mR)) * dwh_dwo * DG2 / max(abs(woutputL.z) * abs(winputL.z), DENOM_TOLERANCE);
let tint = select(vec3<f32>(1.0), transmission_color, transmission_depth == 0.0);
out.f = f * tint * dielectric_energy_scale(winputL);
return out;
}
fn specular_btdf_sample(winputL: vec3<f32>) -> BsdfSample {
return specular_btdf_sample_m(winputL, sample_specular_micronormal(winputL));
}
fn specular_btdf_albedo(winputL: vec3<f32>) -> vec3<f32> {
let external_transmission = winputL.z > 0.0;
let eta_ie = eta_s();
let eta_ti_photon = select(eta_ie, 1.0 / eta_ie, external_transmission);
if (abs(eta_ti_photon - 1.0) < IOR_EPSILON) {
return select(vec3<f32>(1.0), transmission_color, transmission_depth == 0.0);
}
// Tabulated single-scatter transmission albedo (see specular_brdf_albedo), with a small
// floor since rough surfaces still transmit a little beyond the macro critical angle.
let eta_ti_refl = 1.0 / eta_ti_photon;
let tint = select(vec3<f32>(1.0), transmission_color, transmission_depth == 0.0);
var E_T = energy_table_lookup(select(6u, 5u, external_transmission), abs(winputL.z));
if (thin_film_weight > 0.0) {
let F_film = specular_fresnel(clamp(abs(winputL.z), 0.0, 1.0), eta_ti_refl);
let E_ndf = energy_table_lookup(0u, abs(winputL.z));
E_T = mix(E_T, (1.0 - F_film) * E_ndf, thin_film_weight);
}
var scale = 1.0;
if (energy_compensation) { scale = dielectric_energy_scale(winputL); }
return tint * clamp(max(E_T, 0.005) * scale, 0.0, 1.0);
}