Fix: EEVEE BXDF LUT generation

Fixes:
1. Mixed use of `sample_reflection` and `sample_refraction`.
`sample_reflection` is intended for when only reflection is required.
For refraction, `sample_vndf` or `sample_refraction` should be used.
2. Wrong weight when accumulating the contribution. Previously
`brdf * NV / (fresnel * pdf)` always evaluates to `GL`, but with the new
technique of bounded VNDF sampling this is not true anymore. Fixed by
adding a field `weight` in the struct `BsdfEval`.
3. Schlick's approximation of the fresnel factor is
`F + (1 - F) * (1 - cos(theta))^5`, but BSDF LUT was using
`cos^2(theta)`, which was incorrect.
This commit is contained in:
Weizhen Huang
2024-10-10 00:07:32 +02:00
committed by Clément Foucault
parent 0cf7484817
commit 313a7d6236
5 changed files with 89 additions and 51 deletions

View File

@@ -20,6 +20,8 @@ struct BsdfSample {
struct BsdfEval {
float throughput;
float pdf;
/* `throughput / pdf`. */
float weight;
};
struct ClosureLight {

View File

@@ -38,12 +38,13 @@ float bxdf_ggx_smith_G1(float NX, float a2)
* The Z component can be biased towards 1.
* \param Vt: View vector in tangent space.
* \param alpha: roughness parameter.
* \param do_clamp: clamp for numerical stability during ray tracing.
*
* \return: the sampled direction and the pdf of sampling the direction.
*/
BsdfSample bxdf_ggx_sample_reflection(vec3 rand, vec3 Vt, float alpha)
BsdfSample bxdf_ggx_sample_reflection(vec3 rand, vec3 Vt, float alpha, const bool do_clamp)
{
if (alpha < square(BSDF_ROUGHNESS_THRESHOLD)) {
if (do_clamp && alpha < square(BSDF_ROUGHNESS_THRESHOLD)) {
BsdfSample samp;
samp.pdf = 1e6;
samp.direction = reflect(-Vt, vec3(0.0, 0.0, 1.0));
@@ -92,7 +93,7 @@ BsdfSample bxdf_ggx_sample_reflection(vec3 rand, vec3 Vt, float alpha)
/* Evaluate the GGX BRDF without the Fresnel term, multiplied by the cosine foreshortening term.
* Also evaluate the probability of sampling the reflection direction. */
BsdfEval bxdf_ggx_eval_reflection(vec3 N, vec3 L, vec3 V, float alpha)
BsdfEval bxdf_ggx_eval_reflection(vec3 N, vec3 L, vec3 V, float alpha, const bool do_clamp)
{
float NV = dot(N, V);
if (NV <= 0.0) {
@@ -101,6 +102,7 @@ BsdfEval bxdf_ggx_eval_reflection(vec3 N, vec3 L, vec3 V, float alpha)
BsdfEval eval;
eval.throughput = 0.0;
eval.pdf = 0.0;
eval.weight = 0.0;
return eval;
#endif
}
@@ -110,11 +112,11 @@ BsdfEval bxdf_ggx_eval_reflection(vec3 N, vec3 L, vec3 V, float alpha)
/* This threshold was computed based on precision of NVidia compiler (see #118997).
* These drivers tend to produce NaNs in the computation of the NDF (`D`) if alpha is close to 0.
*/
alpha = max(1e-3, alpha);
if (do_clamp) {
alpha = max(1e-3, alpha);
}
float a2 = square(alpha);
float NH = max(dot(N, H), 1e-8);
float D = bxdf_ggx_D(NH, a2);
BsdfEval eval;
@@ -124,14 +126,29 @@ BsdfEval bxdf_ggx_eval_reflection(vec3 N, vec3 L, vec3 V, float alpha)
float t = sqrt(len_ai_sqr + NV2);
if (NV >= 0.0) {
float k = (1.0 - a2) * s2 / (s2 + a2 * NV2);
eval.pdf = D / (2.0 * (k * NV + t));
eval.pdf = 1.0 / (2.0 * (k * NV + t));
}
else {
eval.pdf = D * (t - NV) / (2.0 * len_ai_sqr);
eval.pdf = (t - NV) / (2.0 * len_ai_sqr);
}
/* TODO: But also unused for now. */
eval.throughput = 1.0;
float NH = dot(N, H);
float NL = dot(N, L);
if (do_clamp) {
NH = max(NH, 1e-8);
NL = max(NL, 1e-8);
}
float D = bxdf_ggx_D(NH, a2);
float G_V = bxdf_ggx_smith_G1(NV, a2);
float G_L = bxdf_ggx_smith_G1(NL, a2);
eval.throughput = (0.25f * G_V * G_L) / NV;
eval.weight = eval.throughput / eval.pdf;
/* Multiply `D` after computing the weighted throughput for numerical stability. */
eval.throughput *= D;
eval.pdf *= D;
return eval;
}
@@ -171,17 +188,19 @@ vec3 bxdf_ggx_sample_vndf(vec3 rand, vec3 Vt, float alpha, out float G_V)
* \param Vt: View vector in tangent space.
* \param alpha: roughness parameter
* \param ior: refractive index of the material.
* \param do_clamp: clamp for numerical stability during ray tracing.
*
* \return: the sampled direction and the pdf of sampling the direction.
*/
BsdfSample bxdf_ggx_sample_refraction(vec3 rand, vec3 Vt, float alpha, float ior, float thickness)
BsdfSample bxdf_ggx_sample_refraction(
vec3 rand, vec3 Vt, float alpha, float ior, float thickness, const bool do_clamp)
{
if (thickness != 0.0) {
/* The incoming ray is inside the material for the second refraction event. */
ior = 1.0 / ior;
}
if (alpha < square(BSDF_ROUGHNESS_THRESHOLD)) {
if (do_clamp && alpha < square(BSDF_ROUGHNESS_THRESHOLD)) {
BsdfSample samp;
samp.pdf = 1e6;
samp.direction = refract(-Vt, vec3(0.0, 0.0, 1.0), 1.0 / ior);
@@ -209,18 +228,20 @@ BsdfSample bxdf_ggx_sample_refraction(vec3 rand, vec3 Vt, float alpha, float ior
/* Evaluate the GGX BTDF without the Fresnel term, multiplied by the cosine foreshortening term.
* Also evaluate the probability of sampling the refraction direction. */
BsdfEval bxdf_ggx_eval_refraction(vec3 N, vec3 L, vec3 V, float alpha, float ior, float thickness)
BsdfEval bxdf_ggx_eval_refraction(
vec3 N, vec3 L, vec3 V, float alpha, float ior, float thickness, const bool do_clamp)
{
if (thickness != 0.0) {
ior = 1.0 / ior;
}
float LV = dot(L, V);
if (is_equal(ior, 1.0, 1e-4)) {
if (do_clamp && is_equal(ior, 1.0, 1e-4)) {
/* Only valid when `L` and `V` point in the opposite directions. */
BsdfEval eval;
eval.throughput = float(is_equal(LV, -1.0, 1e-3));
eval.pdf = 1e6;
eval.weight = eval.throughput;
return eval;
}
@@ -230,6 +251,7 @@ BsdfEval bxdf_ggx_eval_refraction(vec3 N, vec3 L, vec3 V, float alpha, float ior
BsdfEval eval;
eval.throughput = 0.0;
eval.pdf = 0.0;
eval.weight = 0.0;
return eval;
}
@@ -240,14 +262,18 @@ BsdfEval bxdf_ggx_eval_refraction(vec3 N, vec3 L, vec3 V, float alpha, float ior
H *= inv_len_H;
/* For transmission, `L` lies in the opposite hemisphere as `H`, therefore negate `L`. */
float NL = max(dot(N, -L), 1e-8);
float NH = max(dot(N, H), 1e-8);
float NL = dot(N, -L);
float NH = dot(N, H);
float NV = dot(N, V);
/* This threshold was computed based on precision of NVidia compiler (see #118997).
* These drivers tend to produce NaNs in the computation of the NDF (`D`) if alpha is close to 0.
*/
alpha = max(1e-3, alpha);
if (do_clamp) {
NL = max(NL, 1e-8);
NH = max(NH, 1e-8);
/* This threshold was computed based on precision of NVidia compiler (see #118997).
* These drivers tend to produce NaNs in the computation of the NDF (`D`) if alpha is close to
* 0. */
alpha = max(1e-3, alpha);
}
float a2 = square(alpha);
float G_V = bxdf_ggx_smith_G1(NV, a2);
@@ -260,6 +286,7 @@ BsdfEval bxdf_ggx_eval_refraction(vec3 N, vec3 L, vec3 V, float alpha, float ior
BsdfEval eval;
eval.pdf = (D * G_V * VH * LH * square(ior * inv_len_H)) / NV;
eval.throughput = eval.pdf * G_L;
eval.weight = G_L;
return eval;
}

View File

@@ -39,11 +39,13 @@ float closure_evaluate_pdf(ClosureUndetermined cl, vec3 L, vec3 V, float thickne
return bxdf_diffuse_eval(cl.N, L).pdf;
case CLOSURE_BSDF_MICROFACET_GGX_REFLECTION_ID: {
ClosureReflection cl_ = to_closure_reflection(cl);
return bxdf_ggx_eval_reflection(cl.N, L, V, square(cl_.roughness)).pdf;
float roughness_sq = square(cl_.roughness);
return bxdf_ggx_eval_reflection(cl.N, L, V, roughness_sq, true).pdf;
}
case CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID: {
ClosureRefraction cl_ = to_closure_refraction(cl);
return bxdf_ggx_eval_refraction(cl.N, L, V, square(cl_.roughness), cl_.ior, thickness).pdf;
float roughness_sq = square(cl_.roughness);
return bxdf_ggx_eval_refraction(cl.N, L, V, roughness_sq, cl_.ior, thickness, true).pdf;
}
}
/* TODO(fclem): Assert. */

View File

@@ -27,6 +27,7 @@ vec4 ggx_brdf_split_sum(vec3 lut_coord)
float NV = clamp(1.0 - square(lut_coord.y), 1e-4, 0.9999);
vec3 V = vec3(sqrt(1.0 - square(NV)), 0.0, NV);
vec3 N = vec3(0.0, 0.0, 1.0);
/* Integrating BRDF. */
float scale = 0.0;
@@ -38,15 +39,13 @@ vec4 ggx_brdf_split_sum(vec3 lut_coord)
vec3 Xi = sample_cylinder(rand);
/* Microfacet normal. */
vec3 R = bxdf_ggx_sample_reflection(Xi, V, roughness).direction;
vec3 H = normalize(V + R);
vec3 L = -reflect(V, H);
vec3 L = bxdf_ggx_sample_reflection(Xi, V, roughness, false).direction;
vec3 H = normalize(V + L);
float NL = L.z;
if (NL > 0.0) {
float VH = saturate(dot(V, H));
/* Assuming sample visible normals, `weight = brdf * NV / (pdf * fresnel).` */
float weight = bxdf_ggx_smith_G1(NL, roughness_sq);
float weight = bxdf_ggx_eval_reflection(N, L, V, roughness, false).weight;
/* Schlick's Fresnel. */
float s = saturate(pow5f(1.0 - VH));
scale += (1.0 - s) * weight;
@@ -71,7 +70,7 @@ vec4 ggx_brdf_split_sum(vec3 lut_coord)
* `transmittance = (1 - F0) * transmission_factor`. */
vec4 ggx_bsdf_split_sum(vec3 lut_coord)
{
float ior = sqrt(lut_coord.x);
float ior = clamp(sqrt(lut_coord.x), 1e-4, 0.9999);
/* ior is sin of critical angle. */
float critical_cos = sqrt(1.0 - saturate(square(ior)));
@@ -90,6 +89,7 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
float roughness_sq = square(roughness);
vec3 V = vec3(sqrt(1.0 - square(NV)), 0.0, NV);
vec3 N = vec3(0.0, 0.0, 1.0);
/* Integrating BSDF */
float scale = 0.0;
@@ -100,28 +100,33 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
vec2 rand = hammersley_2d(i, sample_count);
vec3 Xi = sample_cylinder(rand);
/* Microfacet normal. */
vec3 R = bxdf_ggx_sample_reflection(Xi, V, roughness).direction;
vec3 H = normalize(V + R);
float HL = 1.0 - (1.0 - square(dot(V, H))) / square(ior);
float s = saturate(pow5f(1.0 - saturate(HL)));
/* Reflection. */
vec3 R = bxdf_ggx_sample_reflection(Xi, V, roughness, false).direction;
float NR = R.z;
if (NR > 0.0) {
/* Assuming sample visible normals, `weight = brdf * NV / (pdf * fresnel).` */
float weight = bxdf_ggx_smith_G1(NR, roughness_sq);
vec3 H = normalize(V + R);
vec3 L = refract(-V, H, 1.0 / ior);
float HL = abs(dot(H, L));
/* Schlick's Fresnel. */
float s = saturate(pow5f(1.0 - saturate(HL)));
float weight = bxdf_ggx_eval_reflection(N, R, V, roughness, false).weight;
scale += (1.0 - s) * weight;
bias += s * weight;
}
/* Refraction. */
vec3 T = refract(-V, H, 1.0 / ior);
vec3 T = bxdf_ggx_sample_refraction(Xi, V, roughness, ior, 0.0, false).direction;
float NT = T.z;
/* In the case of TIR, `T == vec3(0)`. */
if (NT < 0.0) {
/* Assuming sample visible normals, accumulating `btdf * NV / (pdf * (1 - F0)).` */
transmission_factor += (1.0 - s) * bxdf_ggx_smith_G1(NT, roughness_sq);
vec3 H = normalize(ior * T + V);
float HL = abs(dot(H, T));
/* Schlick's Fresnel. */
float s = saturate(pow5f(1.0 - saturate(HL)));
float weight = bxdf_ggx_eval_refraction(N, T, V, roughness, ior, 0.0, false).weight;
transmission_factor += (1.0 - s) * weight;
}
}
transmission_factor /= float(sample_count);
@@ -141,11 +146,12 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
* `transmittance = (1 - F0) * transmission_factor`. */
vec4 ggx_btdf_gt_one(vec3 lut_coord)
{
float f0 = square(lut_coord.x);
float inv_ior = (1.0 - f0) / (1.0 + f0);
float f0 = clamp(square(lut_coord.x), 1e-4, 0.9999);
float ior = (1.0 + f0) / (1.0 - f0);
float NV = clamp(1.0 - square(lut_coord.y), 1e-4, 0.9999);
vec3 V = vec3(sqrt(1.0 - square(NV)), 0.0, NV);
vec3 N = vec3(0.0, 0.0, 1.0);
/* Squaring for perceptually linear roughness, see [Physically Based Shading at Disney]
* (https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
@@ -160,19 +166,18 @@ vec4 ggx_btdf_gt_one(vec3 lut_coord)
vec2 rand = hammersley_2d(i, sample_count);
vec3 Xi = sample_cylinder(rand);
/* Microfacet normal. */
vec3 R = bxdf_ggx_sample_reflection(Xi, V, roughness).direction;
vec3 H = normalize(V + R);
/* Refraction. */
vec3 L = refract(-V, H, inv_ior);
vec3 L = bxdf_ggx_sample_refraction(Xi, V, roughness, ior, 0.0, false).direction;
float NL = L.z;
if (NL < 0.0) {
vec3 H = normalize(ior * L + V);
float HV = abs(dot(H, V));
/* Schlick's Fresnel. */
float s = saturate(pow5f(1.0 - saturate(dot(V, H))));
/* Assuming sample visible normals, accumulating `btdf * NV / (pdf * (1 - F0)).` */
transmission_factor += (1.0 - s) * bxdf_ggx_smith_G1(NL, roughness_sq);
float s = saturate(pow5f(1.0 - saturate(HV)));
float weight = bxdf_ggx_eval_refraction(N, L, V, roughness, ior, 0.0, false).weight;
transmission_factor += (1.0 - s) * weight;
}
}
transmission_factor /= float(sample_count);

View File

@@ -51,7 +51,8 @@ BsdfSample ray_generate_direction(vec2 noise, ClosureUndetermined cl, vec3 V, fl
case CLOSURE_BSDF_MICROFACET_GGX_REFLECTION_ID: {
samp = bxdf_ggx_sample_reflection(random_point_on_cylinder,
V * tangent_to_world,
square(to_closure_reflection(cl).roughness));
square(to_closure_reflection(cl).roughness),
true);
break;
}
case CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID: {
@@ -59,7 +60,8 @@ BsdfSample ray_generate_direction(vec2 noise, ClosureUndetermined cl, vec3 V, fl
V * tangent_to_world,
square(to_closure_refraction(cl).roughness),
to_closure_refraction(cl).ior,
thickness);
thickness,
true);
break;
}
}