@@ -4,111 +4,91 @@ export const multiscatter_functions = /* glsl */`
44// Based on Kulla & Conty 2017 - "Revisiting Physically Based Shading at Imageworks"
55// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
66
7- // Computes the directional albedo E(mu, alpha ) for GGX
7+ // Computes the directional albedo E(mu, roughness ) for GGX
88// This represents the total energy reflected for a given view angle and roughness
9- // mu = cos(theta), alpha = roughness^2
10- vec3 ggxDirectionalAlbedo( float mu, float alpha, vec3 F0 ) {
11-
12- // Analytical approximation from Lazanyi & Szirmay-Kalos 2005
13- // Refined by Fdez-Aguera 2018
14- // This approximates the integral of the GGX BRDF over the hemisphere
15-
16- float a = alpha * alpha;
17- float a2 = a * a;
18-
19- // Compute the directional albedo for dielectric (F0=0.04) case
20- // This is an analytical fit to pre-integrated tables
21- float cosTheta = clamp( mu, 0.0, 1.0 );
22- vec4 X = vec4( 1.0, cosTheta, cosTheta * cosTheta, cosTheta * cosTheta * cosTheta );
23- vec4 Y = vec4( 1.0, a, a2, a2 * a );
24-
25- // Polynomial approximation
26- vec2 AB = vec2(
27- dot( X, vec4( 0.0, 0.0, 0.2121, 1.0 - 0.2121 ) * Y.x +
28- vec4( 0.0, 1.0, -1.0, 0.0 ) * Y.y +
29- vec4( 1.0, -1.0, 0.0, 0.0 ) * Y.z ),
30- dot( X, vec4( 0.0, 0.0, 0.0, 0.0 ) * Y.x +
31- vec4( 0.0, 0.0, 0.0, 0.0 ) * Y.y +
32- vec4( 0.0, -1.0, 1.0, 0.0 ) * Y.z )
33- );
34-
35- // Better analytical fit from Kulla & Conty supplemental material
36- // E(mu) ≈ r0 + (r90 - r0) * (1 - mu)^5 for each roughness
37- // We use a simpler direct computation here
9+ // Based on fitted polynomial from Fdez-Aguera 2019
10+ // "A Multiple-Scattering Microfacet Model for Real-Time Image-based Lighting"
11+ vec3 ggxDirectionalAlbedo( float cosTheta, float roughness, vec3 F0 ) {
3812
13+ // Clamp inputs
14+ cosTheta = clamp( cosTheta, 0.0, 1.0 );
15+ roughness = clamp( roughness, 0.0, 1.0 );
16+
17+ // Polynomial fit for the directional albedo
18+ // This is derived from pre-integrated lookup tables
3919 float c = 1.0 - cosTheta;
40- float c3 = c * c * c;
41- float c5 = c3 * c * c;
20+ float c2 = c * c;
21+ float c3 = c2 * c;
22+ float c4 = c3 * c;
23+ float c5 = c4 * c;
24+
25+ // Roughness term
26+ float r = roughness;
27+ float r2 = r * r;
4228
43- // Approximate E for metallic/ dielectric blending
44- float Ess = 1.0 - c5;
45- Ess = Ess - a * ( Ess - ( 1.0 - c3 ) );
46- Ess = Ess - a2 * 0.5 * ( Ess - ( 1.0 - c ) );
29+ // Fitted polynomial approximation for dielectric base (F0 = 0)
30+ // Returns the scale and bias for the Fresnel term
31+ float bias = -0.0408 + r * ( 0.6192 + r * ( -0.8164 + r * 0.4268 ) );
32+ float scale = 1.0398 + r * ( -1.3982 + r * ( 1.8305 + r * -0.9869 ) );
4733
48- // Fresnel adjustment - interpolate between dielectric and perfect mirror
49- vec3 Eavg = F0 + ( vec3( 1.0 ) - F0 ) * Ess ;
34+ // Apply Fresnel using fitted approximation
35+ float fresnel = clamp( scale * c5 + bias, 0.0, 1.0 );
5036
51- return Eavg;
37+ // Directional albedo with Fresnel
38+ vec3 E = F0 + ( vec3( 1.0 ) - F0 ) * fresnel;
39+
40+ return E;
5241
5342}
5443
55- // Computes the average albedo E_avg(alpha ) for GGX
44+ // Computes the average albedo E_avg(roughness ) for GGX with F0=0
5645// This is the hemispherical-hemispherical reflectance
57- float ggxAverageAlbedo( float alpha ) {
46+ // Fitted approximation from Fdez-Aguera 2019
47+ float ggxAverageAlbedo( float roughness ) {
5848
59- // Analytical approximation of the average directional albedo
60- // This represents the average energy reflected across all view angles
49+ // Clamp roughness
50+ roughness = clamp( roughness, 0.0, 1.0 );
6151
62- float a = alpha * alpha;
63-
64- // Simple analytical fit from multiple importance sampling integration
65- // For GGX, this decreases with roughness
66- float Eavg = 1.0 - 0.0275 * a / ( 1.0 + 0.4265 * a );
67- Eavg = 1.0 - 0.3607 * a / ( 1.0 + 0.6487 * a );
52+ // Polynomial fit for average albedo
53+ // This represents the average energy reflected across all view angles
54+ float r = roughness;
6855
69- // Alternative: more accurate fit from Kulla & Conty
70- // Uses polynomial approximation
71- float a2 = a * a;
72- Eavg = 1.0 + a * ( -0.0428 + a * ( -0.2952 + a * 0.3375 ) );
73- Eavg = max( 0.0, Eavg );
56+ // Fitted polynomial (for F0 = 0, dielectric case)
57+ float Eavg = 1.0 + r * ( -0.1104 + r * ( -0.3879 + r * 0.4958 ) );
7458
75- return Eavg;
59+ return clamp( Eavg, 0.0, 1.0 ) ;
7660
7761}
7862
7963// Computes the multiscatter contribution color
8064// wo = outgoing direction (view), wi = incoming direction (light)
65+ // roughness = linear roughness parameter (NOT alpha = roughness^2)
8166// Returns the additional energy that should be added to compensate for multiple scattering
82- vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float alpha , vec3 F0 ) {
67+ vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness , vec3 F0 ) {
8368
8469 float mu_o = abs( wo.z );
8570 float mu_i = abs( wi.z );
8671
8772 // Compute directional albedos for both directions
88- vec3 Eo = ggxDirectionalAlbedo( mu_o, alpha , F0 );
89- vec3 Ei = ggxDirectionalAlbedo( mu_i, alpha , F0 );
73+ vec3 Eo = ggxDirectionalAlbedo( mu_o, roughness , F0 );
74+ vec3 Ei = ggxDirectionalAlbedo( mu_i, roughness , F0 );
9075
91- // Compute average albedo
92- // For colored F0 (metals), we use the average of the color
93- float avgF0 = ( F0.r + F0.g + F0.b ) / 3.0;
94- float Eavg = ggxAverageAlbedo( alpha );
76+ // Compute average albedo for dielectric base (F0=0)
77+ float Eavg = ggxAverageAlbedo( roughness );
9578
9679 // Kulla-Conty multiscatter formula:
97- // f_ms = (1 - Eo) * (1 - Ei) / (PI * (1 - Eavg))
80+ // f_ms = (1 - Eo) * (1 - Ei) / (PI * (1 - Eavg)) * Favg
9881 // This redistributes the missing energy as a diffuse-like lobe
9982
10083 vec3 numerator = ( vec3( 1.0 ) - Eo ) * ( vec3( 1.0 ) - Ei );
10184 float denominator = PI * max( 1.0 - Eavg, 0.001 ); // Prevent division by zero
10285
10386 vec3 fms = numerator / denominator;
10487
105- // The multiscatter compensation uses the base reflectance color
106- // For metals, this preserves the colored reflection
107- // For dielectrics, this is nearly white (F0 ≈ 0.04)
108-
10988 // The average Fresnel for the multiscatter term
110- // This accounts for the colored reflection of metals
111- vec3 Favg = F0 + ( vec3( 1.0 ) - F0 ) / 21.0; // Analytical average for Fresnel
89+ // For metals, this is approximately F0
90+ // For dielectrics, this is slightly higher than F0
91+ vec3 Favg = F0 + ( vec3( 1.0 ) - F0 ) / 21.0;
11292
11393 return fms * Favg;
11494
@@ -119,7 +99,7 @@ vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float alpha, vec3 F0 ) {
11999void ggxEvalWithMultiScatter(
120100 vec3 wo,
121101 vec3 wi,
122- float alpha ,
102+ float roughness ,
123103 vec3 F0,
124104 float D,
125105 float G,
@@ -133,22 +113,22 @@ void ggxEvalWithMultiScatter(
133113 singleScatter = F * G * D / max( denom, 0.001 );
134114
135115 // Multi scatter compensation
136- multiScatter = ggxMultiScatterCompensation( wo, wi, alpha , F0 );
116+ multiScatter = ggxMultiScatterCompensation( wo, wi, roughness , F0 );
137117
138118}
139119
140120// Simplified version: Returns just the energy compensation factor
141121// Can be used to scale existing single-scatter results
142- float ggxEnergyCompensation( float mu, float alpha ) {
122+ float ggxEnergyCompensation( float mu, float roughness ) {
143123
144124 // Returns a factor >= 1.0 that compensates for energy loss
145125 // Multiply your existing BRDF by this factor
146126
147127 vec3 F0 = vec3( 0.04 ); // Assume dielectric for this calculation
148- vec3 E = ggxDirectionalAlbedo( mu, alpha , F0 );
128+ vec3 E = ggxDirectionalAlbedo( mu, roughness , F0 );
149129 float Eavg_scalar = ( E.r + E.g + E.b ) / 3.0;
150130
151- float Eavg = ggxAverageAlbedo( alpha );
131+ float Eavg = ggxAverageAlbedo( roughness );
152132
153133 // The compensation factor accounts for missing energy
154134 // At grazing angles and high roughness, this can be significant
0 commit comments