Skip to content

Commit

Permalink
Refactor BSDF into separate models
Browse files Browse the repository at this point in the history
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
  • Loading branch information
knightcrawler25 committed Jan 2, 2023
1 parent a35af14 commit e1069a5
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 95 deletions.
221 changes: 127 additions & 94 deletions src/shaders/common/disney.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/shaders/common/pathtrace.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit e1069a5

Please sign in to comment.