// Port of glsl/pathtracing/openpbr_surface.glsl: lobe weights/albedos/probabilities,
// evaluation and sampling of the layered OpenPBR BSDF.
const ID_FUZZ_BRDF: i32 = 0;
const ID_COAT_BRDF: i32 = 1;
const ID_META_BRDF: i32 = 2;
const ID_SPEC_BRDF: i32 = 3;
const ID_SPEC_BTDF: i32 = 4;
const ID_DIFF_BRDF: i32 = 5;
const ID_DIFF_BTDF: i32 = 6;
const ID_SSSC_BTDF: i32 = 7;
const NUM_LOBES: i32 = 8;
// Precomputed per-vertex lobe data
var<private> lobe_weights: array<vec3<f32>, 8>;
var<private> lobe_albedos: array<vec3<f32>, 8>;
var<private> lobe_probs: array<f32, 8>;
// Scratch: per-lobe PDFs of the most recent evaluation
var<private> lobe_pdfs: array<f32, 8>;
// The diffuse lobe is a sum of an EON lobe with rho = base_color (glossy-diffuse, spec:
// rho enters the multi-scatter term non-linearly) and an albedo-1 EON lobe (thin-walled
// subsurface reflection). These are their weights.
var<private> diffuse_w_color: vec3<f32>;
var<private> diffuse_w_white: vec3<f32>;
fn openpbr_lobe_weights(winputL: vec3<f32>) {
let F = fuzz_weight;
let C = coat_weight;
let M = base_metalness;
let T = transmission_weight;
let S = subsurface_weight;
let fuzzed = F > 0.0;
let coated = C > 0.0;
let metallic = M > 0.0;
let fully_metallic = M == 1.0;
let transmissive = T > 0.0;
let fully_transmissive = T == 1.0;
let subsurfaced = S > 0.0;
let fully_subsurfaced = S == 1.0;
let has_opaque_dielectric = !fully_metallic && !fully_transmissive;
lobe_albedos[ID_FUZZ_BRDF] = select(vec3<f32>(0.0), fuzz_brdf_albedo(winputL), fuzzed);
lobe_albedos[ID_COAT_BRDF] = vec3<f32>(0.0);
if (coated) { lobe_albedos[ID_COAT_BRDF] = coat_brdf_albedo(winputL); }
lobe_albedos[ID_META_BRDF] = vec3<f32>(0.0);
if (metallic) { lobe_albedos[ID_META_BRDF] = metal_brdf_albedo(winputL); }
lobe_albedos[ID_SPEC_BRDF] = vec3<f32>(0.0);
if (!fully_metallic) { lobe_albedos[ID_SPEC_BRDF] = specular_brdf_albedo(winputL); }
lobe_albedos[ID_SPEC_BTDF] = vec3<f32>(0.0);
if (!fully_metallic && transmissive) { lobe_albedos[ID_SPEC_BTDF] = specular_btdf_albedo(winputL); }
lobe_albedos[ID_DIFF_BRDF] = vec3<f32>(0.0); // (set below once its weights are known)
lobe_albedos[ID_DIFF_BTDF] = vec3<f32>(0.0);
if (has_opaque_dielectric && subsurfaced && geometry_thin_walled) { lobe_albedos[ID_DIFF_BTDF] = diffuse_btdf_albedo(winputL); }
lobe_albedos[ID_SSSC_BTDF] = vec3<f32>(0.0);
if (has_opaque_dielectric && subsurfaced && !geometry_thin_walled) { lobe_albedos[ID_SSSC_BTDF] = subsurface_color; }
// Fuzz BRDF
lobe_weights[ID_FUZZ_BRDF] = vec3<f32>(F);
// Coated base
let w_coated_base = mix(vec3<f32>(1.0), vec3<f32>(1.0) - lobe_albedos[ID_FUZZ_BRDF], F);
// Coat BRDF
lobe_weights[ID_COAT_BRDF] = w_coated_base * C;
// Base substrate: coat layering lerp(1, T_coat (1 - E_coat), C) times the modulated
// darkening factor lerp(1, Delta, C * coat_darkening) (spec "Coat" / "Darkening").
var w_base_substrate: vec3<f32>;
if (coated) {
let EF = DielectricFresnelAvg(coat_ior);
let Kr = 1.0 - (1.0 - EF) / sqr(coat_ior); // rough base
let Ks = FresnelDielectricReflectance(abs(winputL.z), coat_ior); // smooth base
let F0 = FresnelDielectricReflectance(1.0, eta_s_unmodulated());
let Fs = clamp(specular_weight * F0, 0.0, 1.0);
let rd = mix(1.0, specular_roughness, Fs); // dielectric roughness estimate
let rb = mix(rd, specular_roughness, M); // base roughness estimate
let K = mix(Ks, Kr, rb); // internal diffuse reflection coeff.
let base_col = base_weight * base_color;
let E_dielec = mix(mix(base_col, subsurface_color, S), vec3<f32>(1.0 - F0), T);
let E_metal = clamp(base_col * specular_weight, vec3<f32>(0.0), vec3<f32>(1.0));
let E_base = mix(E_dielec, E_metal, M); // base albedo estimate
let Delta = max(1.0 - K, 0.0) / max(vec3<f32>(1.0) - E_base * K, vec3<f32>(DENOM_TOLERANCE));
let modulated_darkening = mix(vec3<f32>(1.0), Delta, C * coat_darkening);
let coat_transmittance = coat_color; // T_coat^2 at normal incidence
let coat_layering = mix(vec3<f32>(1.0), coat_transmittance * (vec3<f32>(1.0) - lobe_albedos[ID_COAT_BRDF]), C);
w_base_substrate = w_coated_base * coat_layering * modulated_darkening;
} else {
w_base_substrate = w_coated_base;
}
// Metal BRDF
lobe_weights[ID_META_BRDF] = w_base_substrate * M;
// Dielectric base
let w_dielectric_base = w_base_substrate * vec3<f32>(max(0.0, 1.0 - M));
// Specular BRDF. specular_weight acts through the modulated IOR inside the lobe (spec),
// and specular_color tints only the reflection of light incident from above.
let spec_tint = select(vec3<f32>(1.0), specular_color, winputL.z > 0.0);
lobe_weights[ID_SPEC_BRDF] = spec_tint * w_dielectric_base;
// Specular BTDF. The physical rough BTDF carries its own Fresnel transmission
// factor (1 - F), so no albedo scaling is needed; the thin-walled delta transmission
// (f = transmission_color) does not, so it gets the spec's (1 - E[f^R]) factor.
if (geometry_thin_walled) {
lobe_weights[ID_SPEC_BTDF] = w_dielectric_base * T * (vec3<f32>(1.0) - lobe_albedos[ID_SPEC_BRDF]);
} else {
lobe_weights[ID_SPEC_BTDF] = w_dielectric_base * T;
}
// Opaque dielectric base
let w_opaque_dielectric_base = w_dielectric_base * (1.0 - T);
// Diffuse BRDF: EON with rho = base_color, weight w_d = base_weight, under the
// (untinted) specular reflection: (1 - E[f^R]) f_diffuse.
let specular_throughput = vec3<f32>(1.0) - lobe_albedos[ID_SPEC_BRDF];
diffuse_w_color = w_opaque_dielectric_base * (1.0 - S) * base_weight * specular_throughput;
diffuse_w_white = vec3<f32>(0.0);
// Subsurface
let w_subsurface = w_opaque_dielectric_base * S;
if (!geometry_thin_walled) {
lobe_weights[ID_SSSC_BTDF] = w_subsurface;
lobe_weights[ID_DIFF_BTDF] = vec3<f32>(0.0);
} else {
// thin-walled SSS: f^R = 1/2 S (1-g) f+, f^T = 1/2 S (1+g) f- with albedo-1 lobes
diffuse_w_white = w_subsurface * 0.5 * subsurface_color * (1.0 - subsurface_anisotropy) * specular_throughput;
lobe_weights[ID_DIFF_BTDF] = w_subsurface * 0.5 * subsurface_color * (1.0 + subsurface_anisotropy) * specular_throughput;
lobe_weights[ID_SSSC_BTDF] = vec3<f32>(0.0);
}
lobe_weights[ID_DIFF_BRDF] = vec3<f32>(1.0);
if (has_opaque_dielectric) {
lobe_albedos[ID_DIFF_BRDF] = diffuse_brdf_albedo(winputL);
}
}
fn openpbr_lobe_probabilities() {
var W_total = 0.0;
for (var lobe_id = 0; lobe_id < NUM_LOBES; lobe_id++) {
lobe_probs[lobe_id] = length(lobe_weights[lobe_id] * lobe_albedos[lobe_id]);
// A probability floor is not required for unbiasedness, but a lobe carrying weight whose
// albedo *estimate* came out near zero must still be sampleable, or its energy is lost.
// The metal lobe is still a Monte-Carlo estimate that can land on exactly 0; the coat is
// tabulated now (see coat_brdf_albedo) but keeps the floor, because a grazing-incidence
// coat is legitimately faint and starving it of samples is the same failure.
let needs_floor = lobe_id == ID_COAT_BRDF || lobe_id == ID_META_BRDF;
if (needs_floor && maxComponent(lobe_weights[lobe_id]) > 0.0) {
lobe_probs[lobe_id] = max(lobe_probs[lobe_id], 0.02);
}
W_total += lobe_probs[lobe_id];
}
W_total = max(DENOM_TOLERANCE, W_total);
for (var lobe_id = 0; lobe_id < NUM_LOBES; lobe_id++) {
lobe_probs[lobe_id] /= W_total;
}
// The dielectric interface lobes (specular reflection, specular transmission and the
// bulk-subsurface entry, which shares the transmission BSDF) are sampled *jointly*:
// one micronormal is drawn and reflection vs. refraction is chosen with that
// microfacet's Fresnel factor (as in pbrt-v4 / Mitsuba). Their probability mass is
// pooled on ID_SPEC_BRDF; ID_SPEC_BTDF / ID_SSSC_BTDF carry no separate probability.
// (Independent lobe selection, as in the viewer, gives 1/p-weighted samples with an
// exploding second moment for TIR-trapped paths inside rough dielectrics.)
lobe_probs[ID_SPEC_BRDF] += lobe_probs[ID_SPEC_BTDF] + lobe_probs[ID_SSSC_BTDF];
lobe_probs[ID_SPEC_BTDF] = 0.0;
lobe_probs[ID_SSSC_BTDF] = 0.0;
}
fn dielectric_group_active() -> bool {
return lobe_probs[ID_SPEC_BRDF] > 0.0;
}
// Combined weight of the two transmission lobes (they share the same BSDF)
fn dielectric_transmission_weight() -> vec3<f32> {
return lobe_weights[ID_SPEC_BTDF] + lobe_weights[ID_SSSC_BTDF];
}
// Probability of choosing reflection at a microfacet with Fresnel reflectance F: F itself,
// unless the material has no transmission lobe at all (opaque), where reflection is certain.
fn dielectric_reflect_prob(F: f32) -> f32 {
if (maxComponent(dielectric_transmission_weight()) <= 0.0) { return 1.0; }
return F;
}
fn openpbr_prepare(winputL: vec3<f32>) {
openpbr_lobe_weights(winputL);
openpbr_lobe_probabilities();
}
/// Roughness of a lobe, as the world cache's cone-spread test needs it. The
/// diffuse and subsurface lobes scatter over the whole hemisphere, which is
/// what a roughness of one stands for here.
fn openpbr_lobe_roughness(lobe_id: i32) -> f32 {
if (lobe_id == ID_COAT_BRDF) { return coat_roughness; }
if (lobe_id == ID_META_BRDF || lobe_id == ID_SPEC_BRDF || lobe_id == ID_SPEC_BTDF) {
return specular_roughness;
}
if (lobe_id == ID_FUZZ_BRDF) { return fuzz_roughness; }
return 1.0;
}
fn openpbr_is_opaque() -> bool {
if (transmission_weight > 0.0) { return false; }
if (subsurface_weight > 0.0) { return false; }
if (geometry_opacity < 1.0) { return false; }
return true;
}
fn openpbr_is_thinwalled() -> bool {
return geometry_thin_walled;
}
// Evaluate all lobes except `skip_lobe_id`; per-lobe PDFs are written to lobe_pdfs.
fn openpbr_bsdf_evaluate_lobes(winputL: vec3<f32>, woutputL: vec3<f32>, skip_lobe_id: i32) -> vec3<f32> {
var f = vec3<f32>(0.0);
for (var i = 0; i < NUM_LOBES; i++) { lobe_pdfs[i] = 0.0; }
if (skip_lobe_id != ID_FUZZ_BRDF && lobe_probs[ID_FUZZ_BRDF] > 0.0) {
let e = fuzz_brdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_FUZZ_BRDF] * e.f; lobe_pdfs[ID_FUZZ_BRDF] = e.pdf;
}
if (skip_lobe_id != ID_COAT_BRDF && lobe_probs[ID_COAT_BRDF] > 0.0) {
let e = coat_brdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_COAT_BRDF] * e.f; lobe_pdfs[ID_COAT_BRDF] = e.pdf;
}
if (skip_lobe_id != ID_META_BRDF && lobe_probs[ID_META_BRDF] > 0.0) {
let e = metal_brdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_META_BRDF] * e.f; lobe_pdfs[ID_META_BRDF] = e.pdf;
}
if (skip_lobe_id != ID_SPEC_BRDF && dielectric_group_active()) {
// reflection and transmission live in opposite hemispheres, so exactly one is non-zero
if (woutputL.z * winputL.z > 0.0) {
let e = specular_brdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_SPEC_BRDF] * e.f;
lobe_pdfs[ID_SPEC_BRDF] = dielectric_reflect_prob(e.fresnel) * e.pdf;
} else {
let e = specular_btdf_evaluate(winputL, woutputL);
f += dielectric_transmission_weight() * e.f;
lobe_pdfs[ID_SPEC_BRDF] = (1.0 - dielectric_reflect_prob(e.fresnel)) * e.pdf;
}
}
if (skip_lobe_id != ID_DIFF_BRDF && lobe_probs[ID_DIFF_BRDF] > 0.0) {
let e = diffuse_brdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_DIFF_BRDF] * e.f; lobe_pdfs[ID_DIFF_BRDF] = e.pdf;
}
if (skip_lobe_id != ID_DIFF_BTDF && lobe_probs[ID_DIFF_BTDF] > 0.0) {
let e = diffuse_btdf_evaluate(winputL, woutputL);
f += lobe_weights[ID_DIFF_BTDF] * e.f; lobe_pdfs[ID_DIFF_BTDF] = e.pdf;
}
return f;
}
fn openpbr_bsdf_total_pdf() -> f32 {
var pdf_woutputL = 0.0;
for (var lobe_id = 0; lobe_id < NUM_LOBES; lobe_id++) {
pdf_woutputL += lobe_probs[lobe_id] * lobe_pdfs[lobe_id];
}
return pdf_woutputL;
}
fn openpbr_bsdf_evaluate(winputL: vec3<f32>, woutputL: vec3<f32>) -> BsdfEval {
var out: BsdfEval;
out.f = openpbr_bsdf_evaluate_lobes(winputL, woutputL, -1);
out.pdf = openpbr_bsdf_total_pdf();
return out;
}
// ---------------------------------------------------------------------------
// Sampling
// ---------------------------------------------------------------------------
fn fill_transmission_medium() -> Volume {
var m: Volume;
if (transmission_depth > 0.0) {
// Spec "Translucent base": mu_t = -ln(T)/depth, mu_s = S/depth,
// mu_a = mu_t - mu_s shifted by gray if any component is negative.
let mu_t = -log(max(vec3<f32>(1e-6), transmission_color)) / transmission_depth;
let mu_s = transmission_scatter / transmission_depth;
var mu_a = mu_t - mu_s;
let mn = minComponent(mu_a);
if (mn < 0.0) { mu_a -= vec3<f32>(mn); }
let mu_t_final = mu_a + mu_s;
m.extinction = mu_t_final;
m.albedo = mu_s / max(mu_t_final, vec3<f32>(DENOM_TOLERANCE));
m.anisotropy = transmission_scatter_anisotropy;
} else {
m.extinction = vec3<f32>(0.0);
m.albedo = vec3<f32>(0.0);
m.anisotropy = 0.0;
}
return m;
}
fn fill_subsurface_medium() -> Volume {
var m: Volume;
let g = clamp(subsurface_anisotropy, -0.95, 0.95);
let A = subsurface_color;
let A2 = A * A;
let A3 = A * A2;
let r = subsurface_radius * subsurface_radius_scale;
let s2 = exp(-11.43 * A + 15.38 * A2 - 13.91 * A3);
m.extinction = 1.0 / max(vec3<f32>(3.0 * RAY_OFFSET), r);
m.albedo = (1.0 - s2) / (1.0 - g * s2);
m.anisotropy = g;
return m;
}
struct OpenPbrSample {
f: vec3<f32>,
woutputL: vec3<f32>,
pdf: f32,
medium: Volume,
/// true if a transmission lobe into/out of the interior was sampled
transmission: bool,
/// Which of the `ID_*` lobes was drawn, so the path tracer can tell a
/// view-dependent scatter from a view-independent one.
lobe_id: i32,
};
/// Whether a lobe's outgoing radiance moves when the camera does.
///
/// The mirror-like lobes -- coat, metal, dielectric specular and the
/// transmissions -- reflect the surroundings, so what they show depends on
/// where they are seen from. Fuzz is grouped with them: it is a grazing-angle
/// sheen and shifts with view angle for the same reason. The diffuse and
/// subsurface lobes scatter roughly the same energy whichever way they are
/// looked at, which is what makes them worth filtering spatially and worth
/// caching.
fn openpbr_lobe_is_view_dependent(lobe_id: i32) -> bool {
return lobe_id == ID_FUZZ_BRDF
|| lobe_id == ID_COAT_BRDF
|| lobe_id == ID_META_BRDF
|| lobe_id == ID_SPEC_BRDF
|| lobe_id == ID_SPEC_BTDF;
}
fn openpbr_bsdf_sample(winputL: vec3<f32>) -> OpenPbrSample {
var out: OpenPbrSample;
out.medium.extinction = vec3<f32>(0.0);
out.medium.albedo = vec3<f32>(0.0);
out.medium.anisotropy = 0.0;
out.transmission = false;
out.lobe_id = -1;
let X = rand();
var CDF = 0.0;
for (var lobe_id = 0; lobe_id < NUM_LOBES; lobe_id++) {
CDF += lobe_probs[lobe_id];
if (X < CDF) {
out.lobe_id = lobe_id;
var s: BsdfSample;
var f_lobe = vec3<f32>(0.0);
var pdf_lobe = 0.0;
if (lobe_id == ID_SPEC_BRDF) {
// Joint dielectric sampling: draw a micronormal, then reflect with probability F.
let mR = sample_specular_micronormal(winputL);
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);
let F = dielectric_reflect_prob(specular_fresnel(abs(dot(winputL, mR)), eta_ti_refl));
if (rand() < F) {
s = specular_brdf_sample_m(winputL, mR);
f_lobe = lobe_weights[ID_SPEC_BRDF] * s.f;
pdf_lobe = F * s.pdf;
} else {
s = specular_btdf_sample_m(winputL, mR);
pdf_lobe = (1.0 - F) * s.pdf;
// The two transmission lobes share the BSDF but not the interior medium:
// pick one in proportion to its weight and reweight.
let wT = maxComponent(lobe_weights[ID_SPEC_BTDF]);
let wS = maxComponent(lobe_weights[ID_SSSC_BTDF]);
let pS = wS / max(wT + wS, DENOM_TOLERANCE);
if (rand() < pS) {
f_lobe = lobe_weights[ID_SSSC_BTDF] / pS * s.f;
out.medium = fill_subsurface_medium();
} else {
f_lobe = lobe_weights[ID_SPEC_BTDF] / max(1.0 - pS, DENOM_TOLERANCE) * s.f;
out.medium = fill_transmission_medium();
}
out.transmission = true;
}
} else {
if (lobe_id == ID_FUZZ_BRDF) { s = fuzz_brdf_sample(winputL); }
else if (lobe_id == ID_COAT_BRDF) { s = coat_brdf_sample(winputL); }
else if (lobe_id == ID_META_BRDF) { s = metal_brdf_sample(winputL); }
else if (lobe_id == ID_DIFF_BRDF) { s = diffuse_brdf_sample(winputL); }
else { s = diffuse_btdf_sample(winputL); }
f_lobe = lobe_weights[lobe_id] * s.f;
pdf_lobe = s.pdf;
}
let woutputL = s.woutputL;
var f = openpbr_bsdf_evaluate_lobes(winputL, woutputL, lobe_id);
f += f_lobe;
lobe_pdfs[lobe_id] = pdf_lobe;
out.pdf = openpbr_bsdf_total_pdf();
out.f = f;
out.woutputL = woutputL;
return out;
}
}
out.pdf = 1.0;
out.f = vec3<f32>(0.0);
out.woutputL = vec3<f32>(0.0, 0.0, 1.0);
return out;
}