Skip to content

Commit ce2398c

Browse files
Improve Fresnel weighting in GGX energy compensation (AcademySoftwareFoundation#2958)
This changelist improves the multiple-scattering energy compensation of the GGX microfacet model in hardware shading languages, tinting the recovered energy by the average Fresnel reflectance over the hemisphere rather than a single-direction Fresnel value. The following specific changes are included: - Update `mx_ggx_energy_compensation` to take a `FresnelData` argument and compute the cosine-weighted average of the Fresnel reflectance over the hemisphere internally, replacing the prior overloads that accepted a precomputed single-angle value. - Update the conductor, dielectric, generalized Schlick, and Chiang hair closures to pass `FresnelData` directly, removing the redundant Fresnel evaluations that existed only to feed energy compensation. As measured by our render test suite in GitHub CI, this change consistently improves the visual parity between MSL and OSL renders, with a few typical examples below (per-pixel RMS error, before -> after): - `standard_surface_brick_procedural`: 0.00925 -> 0.00916 - `standard_surface_marble_procedural`: 0.00571 -> 0.00558 - `open_pbr_carpaint`: 0.00734 -> 0.00720 - `open_pbr_default`: 0.00627 -> 0.00619
1 parent acc7f9d commit ce2398c

5 files changed

Lines changed: 31 additions & 32 deletions

File tree

libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,19 +173,6 @@ float mx_ggx_dir_albedo(float NdotV, float alpha, float F0, float F90)
173173
return mx_ggx_dir_albedo(NdotV, alpha, vec3(F0), vec3(F90)).x;
174174
}
175175

176-
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
177-
// Equations 14 and 16
178-
vec3 mx_ggx_energy_compensation(float NdotV, float alpha, vec3 Fss)
179-
{
180-
float Ess = mx_ggx_dir_albedo(NdotV, alpha, 1.0, 1.0);
181-
return 1.0 + Fss * (1.0 - Ess) / Ess;
182-
}
183-
184-
float mx_ggx_energy_compensation(float NdotV, float alpha, float Fss)
185-
{
186-
return mx_ggx_energy_compensation(NdotV, alpha, vec3(Fss)).x;
187-
}
188-
189176
// Compute the average of an anisotropic alpha pair.
190177
float mx_average_alpha(vec2 alpha)
191178
{
@@ -511,6 +498,28 @@ vec3 mx_ggx_dir_albedo(float NdotV, float alpha, FresnelData fd)
511498
}
512499
}
513500

501+
// Compute the cosine-weighted average of the Fresnel reflectance over the hemisphere.
502+
// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
503+
vec3 mx_fresnel_average(FresnelData fd)
504+
{
505+
vec3 F0 = mx_compute_fresnel(1.0, fd);
506+
vec3 F90 = (fd.model == FRESNEL_MODEL_SCHLICK && !fd.airy) ? fd.F90 : vec3(1.0);
507+
508+
// The constant 1/21 is exact for a Schlick term with an exponent of 5, while for
509+
// a generalized Schlick exponent n it would be 2 / ((n + 1) * (n + 2)).
510+
return F0 + (F90 - F0) * (1.0 / 21.0);
511+
}
512+
513+
// Multiple-scattering energy compensation for the GGX microfacet model.
514+
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
515+
// Equations 14 and 16
516+
vec3 mx_ggx_energy_compensation(float NdotV, float alpha, FresnelData fd)
517+
{
518+
vec3 Fss = mx_fresnel_average(fd);
519+
float Ess = mx_ggx_dir_albedo(NdotV, alpha, 1.0, 1.0);
520+
return 1.0 + Fss * (1.0 - Ess) / Ess;
521+
}
522+
514523
// Compute the refraction of a ray through a solid sphere.
515524
vec3 mx_refraction_solid_sphere(vec3 R, vec3 N, float ior)
516525
{

libraries/pbrlib/genglsl/mx_chiang_hair_bsdf.glsl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,14 @@ void mx_chiang_hair_bsdf(ClosureData closureData, vec3 tint_R, vec3 tint_TT, vec
272272

273273
float NdotV = clamp(dot(N, V), M_FLOAT_EPS, 1.0);
274274
FresnelData fd = mx_init_fresnel_dielectric(ior, 0.0, 1.0);
275-
vec3 F = mx_compute_fresnel(NdotV, fd);
276275

277276
vec2 roughness = (roughness_R + roughness_TT + roughness_TRT) / vec2(3.0); // ?
278277
vec2 safeAlpha = clamp(roughness, M_FLOAT_EPS, 1.0);
279278
float avgAlpha = mx_average_alpha(safeAlpha);
280279

281280
// Use GGX to match the behavior of mx_environment_radiance.
282281
float F0 = mx_ior_to_f0(ior);
283-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
282+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
284283
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp;
285284

286285
vec3 Li = mx_environment_radiance(N, V, X, safeAlpha, 0, fd);

libraries/pbrlib/genglsl/mx_conductor_bsdf.glsl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,14 @@ void mx_conductor_bsdf(ClosureData closureData, float weight, vec3 ior_n, vec3 i
3737
float D = mx_ggx_NDF(Ht, safeAlpha);
3838
float G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha);
3939

40-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
40+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
4141

4242
// Note: NdotL is cancelled out
4343
bsdf.response = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV);
4444
}
4545
else if (closureData.closureType == CLOSURE_TYPE_INDIRECT)
4646
{
47-
vec3 F = mx_compute_fresnel(NdotV, fd);
48-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
47+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
4948
vec3 Li = mx_environment_radiance(N, V, X, safeAlpha, distribution, fd);
5049
bsdf.response = Li * comp * weight;
5150
}

libraries/pbrlib/genglsl/mx_dielectric_bsdf.glsl

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,15 @@ void mx_dielectric_bsdf(ClosureData closureData, float weight, vec3 tint, float
4444
float D = mx_ggx_NDF(Ht, safeAlpha);
4545
float G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha);
4646

47-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
47+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
4848
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp;
4949
bsdf.throughput = 1.0 - dirAlbedo * weight;
5050

5151
bsdf.response = D * F * G * comp * safeTint * closureData.occlusion * weight / (4.0 * NdotV);
5252
}
5353
else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION)
5454
{
55-
vec3 F = mx_compute_fresnel(NdotV, fd);
56-
57-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
55+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
5856
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp;
5957
bsdf.throughput = 1.0 - dirAlbedo * weight;
6058

@@ -65,9 +63,7 @@ void mx_dielectric_bsdf(ClosureData closureData, float weight, vec3 tint, float
6563
}
6664
else if (closureData.closureType == CLOSURE_TYPE_INDIRECT)
6765
{
68-
vec3 F = mx_compute_fresnel(NdotV, fd);
69-
70-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
66+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
7167
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp;
7268
bsdf.throughput = 1.0 - dirAlbedo * weight;
7369

libraries/pbrlib/genglsl/mx_generalized_schlick_bsdf.glsl

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void mx_generalized_schlick_bsdf(ClosureData closureData, float weight, vec3 col
4545
float D = mx_ggx_NDF(Ht, safeAlpha);
4646
float G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha);
4747

48-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
48+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
4949
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp;
5050
float avgDirAlbedo = dot(dirAlbedo, vec3(1.0 / 3.0));
5151
bsdf.throughput = vec3(1.0 - avgDirAlbedo * weight);
@@ -55,9 +55,7 @@ void mx_generalized_schlick_bsdf(ClosureData closureData, float weight, vec3 col
5555
}
5656
else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION)
5757
{
58-
vec3 F = mx_compute_fresnel(NdotV, fd);
59-
60-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
58+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
6159
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp;
6260
float avgDirAlbedo = dot(dirAlbedo, vec3(1.0 / 3.0));
6361
bsdf.throughput = vec3(1.0 - avgDirAlbedo * weight);
@@ -71,9 +69,7 @@ void mx_generalized_schlick_bsdf(ClosureData closureData, float weight, vec3 col
7169
}
7270
else if (closureData.closureType == CLOSURE_TYPE_INDIRECT)
7371
{
74-
vec3 F = mx_compute_fresnel(NdotV, fd);
75-
76-
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
72+
vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, fd);
7773
vec3 dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp;
7874
float avgDirAlbedo = dot(dirAlbedo, vec3(1.0 / 3.0));
7975
bsdf.throughput = vec3(1.0 - avgDirAlbedo * weight);

0 commit comments

Comments
 (0)