From e1069a5379ab4aaf9fe4fe8ad6d5ee402da0c652 Mon Sep 17 00:00:00 2001 From: Asif Date: Mon, 2 Jan 2023 22:44:16 +0530 Subject: [PATCH] Refactor BSDF into separate models For sampling/evaluation, the interior is now treated the same as exterior for Dielectric/Metallic BRDF. Specular BSDF takes care of the interface and TIR. This is in line with how the 2015 paper describes the mixing of the 3 models. Also fixes #78 --- src/shaders/common/disney.glsl | 221 +++++++++++++++++------------- src/shaders/common/pathtrace.glsl | 2 +- 2 files changed, 128 insertions(+), 95 deletions(-) diff --git a/src/shaders/common/disney.glsl b/src/shaders/common/disney.glsl index eb43419..053a014 100644 --- a/src/shaders/common/disney.glsl +++ b/src/shaders/common/disney.glsl @@ -31,8 +31,11 @@ * [6] [Microfacet Models for Refraction through Rough Surfaces] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf * [7] [Sampling the GGX Distribution of Visible Normals] https://jcgt.org/published/0007/04/01/paper.pdf * [8] [Pixar's Foundation for Materials] https://graphics.pixar.com/library/PxrMaterialsCourse2017/paper.pdf + * [9] [Mitsuba 3] https://github.com/mitsuba-renderer/mitsuba3 */ +vec3 DisneyEval(State state, vec3 V, vec3 N, vec3 L, out float pdf); + vec3 ToWorld(vec3 X, vec3 Y, vec3 Z, vec3 V) { return V.x * X + V.y * Y + V.z * Z; @@ -43,13 +46,16 @@ vec3 ToLocal(vec3 X, vec3 Y, vec3 Z, vec3 V) return vec3(dot(V, X), dot(V, Y), dot(V, Z)); } -void SpecAndSheenColor(Material mat, float eta, out vec3 specCol, out vec3 sheenCol) +void TintColors(Material mat, float eta, out float F0, out vec3 Csheen, out vec3 Cspec0) { float lum = Luminance(mat.baseColor); - vec3 ctint = lum > 0.0 ? mat.baseColor / lum : vec3(1.0f); - float F0 = (1.0 - eta) / (1.0 + eta); - specCol = mix(F0 * F0 * mix(vec3(1.0), ctint, mat.specularTint), mat.baseColor, mat.metallic); - sheenCol = mix(vec3(1.0), ctint, mat.sheenTint); + vec3 ctint = lum > 0.0 ? mat.baseColor / lum : vec3(1.0); + + F0 = (1.0 - eta) / (1.0 + eta); + F0 *= F0; + + Cspec0 = F0 * mix(vec3(1.0), ctint, mat.specularTint); + Csheen = mix(vec3(1.0), ctint, mat.sheenTint); } vec3 EvalDisneyDiffuse(Material mat, vec3 Csheen, vec3 V, vec3 L, vec3 H, out float pdf) @@ -60,23 +66,25 @@ vec3 EvalDisneyDiffuse(Material mat, vec3 Csheen, vec3 V, vec3 L, vec3 H, out fl float LDotH = dot(L, H); + float Rr = 2.0 * mat.roughness * LDotH * LDotH; + // Diffuse float FL = SchlickWeight(L.z); float FV = SchlickWeight(V.z); - float FH = SchlickWeight(LDotH); - float Fd90 = 0.5 + 2.0 * LDotH * LDotH * mat.roughness; - float Fd = mix(1.0, Fd90, FL) * mix(1.0, Fd90, FV); + float Fretro = Rr * (FL + FV + FL * FV * (Rr - 1.0)); + float Fd = (1.0 - 0.5 * FL) * (1.0 - 0.5 * FV); // Fake subsurface - float Fss90 = LDotH * LDotH * mat.roughness; + float Fss90 = 0.5 * Rr; float Fss = mix(1.0, Fss90, FL) * mix(1.0, Fss90, FV); float ss = 1.25 * (Fss * (1.0 / (L.z + V.z) - 0.5) + 0.5); // Sheen + float FH = SchlickWeight(LDotH); vec3 Fsheen = FH * mat.sheen * Csheen; pdf = L.z * INV_PI; - return INV_PI * mix(Fd, ss, mat.subsurface) * mat.baseColor + Fsheen; + return INV_PI * mat.baseColor * mix(Fd + Fretro, ss, mat.subsurface) + Fsheen; } vec3 EvalMicrofacetReflection(Material mat, vec3 V, vec3 L, vec3 H, vec3 F, out float pdf) @@ -93,7 +101,7 @@ vec3 EvalMicrofacetReflection(Material mat, vec3 V, vec3 L, vec3 H, vec3 F, out return F * D * G2 / (4.0 * L.z * V.z); } -vec3 EvalMicrofacetRefraction(Material mat, float eta, vec3 V, vec3 L, vec3 H, float F, out float pdf) +vec3 EvalMicrofacetRefraction(Material mat, float eta, vec3 V, vec3 L, vec3 H, vec3 F, out float pdf) { pdf = 0.0; if (L.z >= 0.0) @@ -122,20 +130,18 @@ vec3 EvalClearcoat(Material mat, vec3 V, vec3 L, vec3 H, out float pdf) float VDotH = dot(V, H); - float FH = DielectricFresnel(VDotH, 1.0 / 1.5); - float F = mix(0.04, 1.0, FH); + float F = mix(0.04, 1.0, SchlickWeight(VDotH)); float D = GTR1(H.z, mat.clearcoatRoughness); float G = SmithG(L.z, 0.25) * SmithG(V.z, 0.25); float jacobian = 1.0 / (4.0 * VDotH); pdf = D * H.z * jacobian; - return vec3(1.0) * F * D * G / (4.0 * L.z * V.z); + return vec3(F) * D * G; } vec3 DisneySample(State state, vec3 V, vec3 N, out vec3 L, out float pdf) { pdf = 0.0; - vec3 f = vec3(0.0); float r1 = rand(); float r2 = rand(); @@ -147,89 +153,92 @@ vec3 DisneySample(State state, vec3 V, vec3 N, out vec3 L, out float pdf) // Transform to shading space to simplify operations (NDotL = L.z; NDotV = V.z; NDotH = H.z) V = ToLocal(T, B, N, V); - // Sample microfacet normal - vec3 H = SampleGGXVNDF(V, state.mat.ax, state.mat.ay, r1, r2); + // Tint colors + vec3 Csheen, Cspec0; + float F0; + TintColors(state.mat, state.eta, F0, Csheen, Cspec0); - if (H.z < 0.0) - H = -H; + // Model weights + float dielectricWt = (1.0 - state.mat.metallic) * (1.0 - state.mat.specTrans); + float metalWt = state.mat.metallic; + float glassWt = (1.0 - state.mat.metallic) * state.mat.specTrans; - // Specular and sheen color - vec3 specCol, sheenCol; - SpecAndSheenColor(state.mat, state.eta, specCol, sheenCol); + // Lobe probabilities + float schlickWt = SchlickWeight(V.z); - // Dielectric fresnel - float dielectricFr = DielectricFresnel(abs(dot(V, H)), state.eta); - // Disney's 2015 paper doesn't specify how fresnel is handled between the BRDF and BSDF - vec3 disneyFr = mix(specCol, vec3(1.0), mix(dielectricFr, SchlickWeight(dot(V, H)), state.mat.metallic)); - - // Lobe weights - float diffWt = (1.0 - state.mat.metallic) * (1.0 - state.mat.specTrans); - float transWt = (1.0 - state.mat.metallic) * state.mat.specTrans; - - // Lobe sampling probabilities - float lumBaseCol = Luminance(state.mat.baseColor); - float diffPr = diffWt * lumBaseCol; - float reflectPr = Luminance(disneyFr); - float refractPr = transWt * (1.0 - dielectricFr) * lumBaseCol; + float diffPr = dielectricWt * Luminance(state.mat.baseColor); + float dielectricPr = dielectricWt * Luminance(mix(Cspec0, vec3(1.0), schlickWt)); + float metalPr = metalWt * Luminance(mix(state.mat.baseColor, vec3(1.0), schlickWt)); + float glassPr = glassWt; float clearCtPr = 0.25 * state.mat.clearcoat; // Normalize probabilities - float invTotalWt = 1.0 / (diffPr + reflectPr + refractPr + clearCtPr); + float invTotalWt = 1.0 / (diffPr + dielectricPr + metalPr + glassPr + clearCtPr); diffPr *= invTotalWt; - reflectPr *= invTotalWt; - refractPr *= invTotalWt; + dielectricPr *= invTotalWt; + metalPr *= invTotalWt; + glassPr *= invTotalWt; clearCtPr *= invTotalWt; // CDF of the sampling probabilities - float cdf[4]; + float cdf[5]; cdf[0] = diffPr; - cdf[1] = cdf[0] + reflectPr; - cdf[2] = cdf[1] + refractPr; - cdf[3] = cdf[2] + clearCtPr; - - // Sample and evaluate a single lobe based on its importance - // To keep the implementation simple, one-sample MIS is not used (See 3.1 of [8]) + cdf[1] = cdf[0] + dielectricPr; + cdf[2] = cdf[1] + metalPr; + cdf[3] = cdf[2] + glassPr; + cdf[4] = cdf[3] + clearCtPr; + // Sample a lobe based on its importance float r3 = rand(); if (r3 < cdf[0]) // Diffuse { L = CosineSampleHemisphere(r1, r2); - - H = normalize(L + V); - - f = EvalDisneyDiffuse(state.mat, sheenCol, V, L, H, pdf) * diffWt; - pdf *= diffPr; } - else if (r3 < cdf[1]) // Reflection + else if (r3 < cdf[2]) // Dielectric + Metallic reflection { - L = normalize(reflect(-V, H)); + vec3 H = SampleGGXVNDF(V, state.mat.ax, state.mat.ay, r1, r2); - f = EvalMicrofacetReflection(state.mat, V, L, H, disneyFr, pdf); - pdf *= reflectPr; + if (H.z < 0.0) + H = -H; + + L = normalize(reflect(-V, H)); } - else if (r3 < cdf[2]) // Refraction + else if (r3 < cdf[3]) // Glass { - L = normalize(refract(-V, H, state.eta)); + vec3 H = SampleGGXVNDF(V, state.mat.ax, state.mat.ay, r1, r2); + float F = DielectricFresnel(abs(dot(V, H)), state.eta); - f = EvalMicrofacetRefraction(state.mat, state.eta, V, L, H, dielectricFr, pdf) * transWt; - pdf *= refractPr; + if (H.z < 0.0) + H = -H; + + // Rescale random number for reuse + r3 = (r3 - cdf[2]) / (cdf[3] - cdf[2]); + + // Reflection + if (r3 < F) + { + L = normalize(reflect(-V, H)); + } + else // Transmission + { + L = normalize(refract(-V, H, state.eta)); + } } else // Clearcoat { - H = SampleGTR1(state.mat.clearcoatRoughness, r1, r2); + vec3 H = SampleGTR1(state.mat.clearcoatRoughness, r1, r2); if (H.z < 0.0) H = -H; L = normalize(reflect(-V, H)); - - f = EvalClearcoat(state.mat, V, L, H, pdf) * clearCtPr; - pdf *= clearCtPr; } L = ToWorld(T, B, N, L); - return f * abs(dot(N, L)); + V = ToWorld(T, B, N, V); + + return DisneyEval(state, V, N, L, pdf); } vec3 DisneyEval(State state, vec3 V, vec3 N, vec3 L, out float pdf) @@ -242,7 +251,7 @@ vec3 DisneyEval(State state, vec3 V, vec3 N, vec3 L, out float pdf) Onb(N, T, B); // Transform to shading space to simplify operations (NDotL = L.z; NDotV = V.z; NDotH = H.z) - V = ToLocal(T, B, N, V); + V = ToLocal(T, B, N, V); L = ToLocal(T, B, N, L); vec3 H; @@ -254,63 +263,87 @@ vec3 DisneyEval(State state, vec3 V, vec3 N, vec3 L, out float pdf) if (H.z < 0.0) H = -H; - // Specular and sheen color - vec3 specCol, sheenCol; - SpecAndSheenColor(state.mat, state.eta, specCol, sheenCol); + // Tint colors + vec3 Csheen, Cspec0; + float F0; + TintColors(state.mat, state.eta, F0, Csheen, Cspec0); - // Dielectric fresnel - float dielectricFr = DielectricFresnel(abs(dot(V, H)), state.eta); - // Disney's 2015 paper doesn't specify how fresnel is handled between the BRDF and BSDF - vec3 disneyFr = mix(specCol, vec3(1.0), mix(dielectricFr, SchlickWeight(dot(V, H)), state.mat.metallic)); + // Model weights + float dielectricWt = (1.0 - state.mat.metallic) * (1.0 - state.mat.specTrans); + float metalWt = state.mat.metallic; + float glassWt = (1.0 - state.mat.metallic) * state.mat.specTrans; - // Lobe weights - float diffWt = (1.0 - state.mat.metallic) * (1.0 - state.mat.specTrans); - float transWt = (1.0 - state.mat.metallic) * state.mat.specTrans; + // Lobe probabilities + float schlickWt = SchlickWeight(V.z); - // Lobe sampling probabilities - float lumBaseCol = Luminance(state.mat.baseColor); - float diffPr = diffWt * lumBaseCol; - float reflectPr = Luminance(disneyFr); - float refractPr = transWt * (1.0 - dielectricFr) * lumBaseCol; + float diffPr = dielectricWt * Luminance(state.mat.baseColor); + float dielectricPr = dielectricWt * Luminance(mix(Cspec0, vec3(1.0), schlickWt)); + float metalPr = metalWt * Luminance(mix(state.mat.baseColor, vec3(1.0), schlickWt)); + float glassPr = glassWt; float clearCtPr = 0.25 * state.mat.clearcoat; // Normalize probabilities - float invTotalWt = 1.0 / (diffPr + reflectPr + refractPr + clearCtPr); + float invTotalWt = 1.0 / (diffPr + dielectricPr + metalPr + glassPr + clearCtPr); diffPr *= invTotalWt; - reflectPr *= invTotalWt; - refractPr *= invTotalWt; + dielectricPr *= invTotalWt; + metalPr *= invTotalWt; + glassPr *= invTotalWt; clearCtPr *= invTotalWt; bool reflect = L.z * V.z > 0; - bool refract = L.z * V.z < 0; float tmpPdf = 0.0; + float VDotH = abs(dot(V, H)); // Diffuse if (diffPr > 0.0 && reflect) { - f += EvalDisneyDiffuse(state.mat, sheenCol, V, L, H, tmpPdf) * diffWt; + f += EvalDisneyDiffuse(state.mat, Csheen, V, L, H, tmpPdf) * dielectricWt; pdf += tmpPdf * diffPr; } - // Reflection - if (reflectPr > 0.0 && reflect) + // Dielectric Reflection + if (dielectricPr > 0.0 && reflect) { - f += EvalMicrofacetReflection(state.mat, V, L, H, disneyFr, tmpPdf); - pdf += tmpPdf * reflectPr; + // Normalize for interpolating based on Cspec0 + float F = (DielectricFresnel(VDotH, 1.0 / state.mat.ior) - F0) / (1.0 - F0); + + f += EvalMicrofacetReflection(state.mat, V, L, H, mix(Cspec0, vec3(1.0), F), tmpPdf) * dielectricWt; + pdf += tmpPdf * dielectricPr; + } + + // Metallic Reflection + if (metalPr > 0.0 && reflect) + { + // Tinted to base color + vec3 F = mix(state.mat.baseColor, vec3(1.0), SchlickWeight(VDotH)); + + f += EvalMicrofacetReflection(state.mat, V, L, H, F, tmpPdf) * metalWt; + pdf += tmpPdf * metalPr; } - // Refraction - if (refractPr > 0 && refract) + // Glass/Specular BSDF + if (glassPr > 0.0) { - f += EvalMicrofacetRefraction(state.mat, state.eta, V, L, H, dielectricFr, tmpPdf) * transWt; - pdf += tmpPdf * refractPr; + // Dielectric fresnel (achromatic) + float F = DielectricFresnel(VDotH, state.eta); + + if (reflect) + { + f += EvalMicrofacetReflection(state.mat, V, L, H, vec3(F), tmpPdf) * glassWt; + pdf += tmpPdf * glassPr * F; + } + else + { + f += EvalMicrofacetRefraction(state.mat, state.eta, V, L, H, vec3(F), tmpPdf) * glassWt; + pdf += tmpPdf * glassPr * (1.0 - F); + } } // Clearcoat if (clearCtPr > 0.0 && reflect) { - f += EvalClearcoat(state.mat, V, L, H, tmpPdf) * clearCtPr; + f += EvalClearcoat(state.mat, V, L, H, tmpPdf) * 0.25 * state.mat.clearcoat; pdf += tmpPdf * clearCtPr; } diff --git a/src/shaders/common/pathtrace.glsl b/src/shaders/common/pathtrace.glsl index 11b3cd6..81b1e4e 100644 --- a/src/shaders/common/pathtrace.glsl +++ b/src/shaders/common/pathtrace.glsl @@ -159,7 +159,7 @@ vec3 DirectLight(in Ray r, in State state, bool isSurface) { vec3 Ld = vec3(0.0); vec3 Li = vec3(0.0); - vec3 scatterPos = state.fhp + state.ffnormal * EPS; + vec3 scatterPos = state.fhp + state.normal * EPS; ScatterSampleRec scatterSample;