Skip to content

Commit b3d05dc

Browse files
committed
Fix GGX multiscatter energy conservation with analytical compensation.
1 parent e67d3bf commit b3d05dc

File tree

2 files changed

+19
-151
lines changed

2 files changed

+19
-151
lines changed

src/shader/bsdf/bsdf_functions.glsl.js

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -82,38 +82,17 @@ export const bsdf_functions = /* glsl */`
8282
8383
}
8484
85-
// Global variable to store microsurface scatter throughput
86-
// This is set by specularDirection and used by bsdfSample
87-
vec3 g_microsurfaceThroughput = vec3( 1.0 );
88-
8985
vec3 specularDirection( vec3 wo, SurfaceRecord surf ) {
9086
91-
// sample ggx vndf distribution which gives a new normal
87+
// Sample GGX VNDF distribution to get a microfacet normal
9288
float roughness = surf.filteredRoughness;
93-
94-
// Reset microsurface throughput
95-
g_microsurfaceThroughput = vec3( 1.0 );
96-
97-
// For rough surfaces, optionally use microsurface multiscatter
98-
// This simulates multiple bounces within the microfacet structure
99-
vec3 f0Color = mix( surf.f0 * surf.specularColor * surf.specularIntensity, surf.color, surf.metalness );
100-
101-
MicrosurfaceScatterResult microResult = ggxMicrosurfaceScatter( wo, roughness, f0Color );
102-
103-
if ( microResult.valid ) {
104-
// Use the microsurface scattered direction
105-
g_microsurfaceThroughput = microResult.throughput;
106-
return microResult.direction;
107-
}
108-
109-
// Fall back to standard single-scatter sampling
11089
vec3 halfVector = ggxDirection(
11190
wo,
11291
vec2( roughness ),
11392
rand2( 12 )
11493
);
11594
116-
// apply to new ray by reflecting off the new normal
95+
// Reflect view direction off the sampled microfacet normal
11796
return - reflect( wo, halfVector );
11897
11998
}
@@ -479,12 +458,6 @@ export const bsdf_functions = /* glsl */`
479458
result.pdf = bsdfEval( wo, clearcoatWo, wi, clearcoatWi, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight, result.specularPdf, result.color );
480459
result.direction = normalize( surf.normalBasis * wi );
481460
482-
// Apply microsurface scattering throughput if we sampled the specular lobe
483-
if ( r > cdf[0] && r <= cdf[1] ) {
484-
// Specular lobe was sampled - apply microsurface throughput
485-
result.color *= g_microsurfaceThroughput;
486-
}
487-
488461
return result;
489462
490463
}

src/shader/bsdf/multiscatter_functions.glsl.js

Lines changed: 17 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,26 @@
11
export const multiscatter_functions = /* glsl */`
22
3-
// Explicit Microsurface Multiscattering for GGX
4-
// Based on "Multiple-Scattering Microfacet BSDFs with the Smith Model" (Heitz et al. 2016)
5-
// and "Position-Free Multiple-Bounce Computations for Smith Microfacet BSDFs" (Xie & Hanrahan 2018)
6-
//
7-
// This simulates a random walk on the microsurface, allowing rays to bounce multiple times
8-
// within the microfacet structure before escaping.
9-
10-
// Check if a direction is above the macrosurface
11-
bool isAboveSurface( vec3 w ) {
12-
return w.z > 0.0;
13-
}
14-
15-
// Sample a microfacet normal visible from direction v
16-
// Returns the microsurface normal in tangent space
17-
vec3 sampleGGXMicrofacet( vec3 v, float roughness, vec2 alpha, vec2 rand ) {
18-
// Use VNDF sampling (already implemented in ggx_functions)
19-
return ggxDirection( v, alpha, rand );
20-
}
21-
22-
// Compute Fresnel reflectance for a given cosine
23-
float fresnelSchlick( float cosTheta, float f0 ) {
24-
float c = 1.0 - cosTheta;
25-
float c2 = c * c;
26-
return f0 + ( 1.0 - f0 ) * c2 * c2 * c;
27-
}
28-
29-
// Perform a random walk on the microsurface for multiscatter GGX
30-
// This function traces the path of a ray bouncing within the microfacet structure
31-
// wo: outgoing direction (view direction) in tangent space
32-
// roughness: surface roughness
33-
// f0Color: Fresnel at normal incidence
34-
// Returns: throughput color after microsurface bounces and final exit direction
35-
struct MicrosurfaceScatterResult {
36-
vec3 direction; // Final exit direction in tangent space
37-
vec3 throughput; // Accumulated throughput/color
38-
bool valid; // Whether the scatter was successful
39-
};
40-
41-
MicrosurfaceScatterResult ggxMicrosurfaceScatter( vec3 wo, float roughness, vec3 f0Color ) {
42-
43-
MicrosurfaceScatterResult result;
44-
result.throughput = vec3( 1.0 );
45-
result.valid = false;
46-
47-
// Only enable multiscatter for rough surfaces (roughness > 0.2)
48-
// For smooth surfaces, single-scatter is sufficient
49-
if ( roughness < 0.2 ) {
50-
// Return invalid - use regular single-scatter path
51-
return result;
52-
}
53-
54-
// Current ray direction (starts as view direction)
55-
vec3 w = wo;
56-
vec3 throughput = vec3( 1.0 );
57-
58-
vec2 alpha = vec2( roughness );
59-
float f0 = ( f0Color.r + f0Color.g + f0Color.b ) / 3.0;
60-
61-
// Maximum bounces within microsurface (typically 2-4 is enough)
62-
const int MAX_MICRO_BOUNCES = 3;
63-
64-
for ( int bounce = 0; bounce < MAX_MICRO_BOUNCES; bounce++ ) {
65-
66-
// Check if ray escaped the microsurface
67-
if ( isAboveSurface( w ) && bounce > 0 ) {
68-
// Ray escaped! Return the result
69-
result.direction = w;
70-
result.throughput = throughput;
71-
result.valid = true;
72-
return result;
73-
}
74-
75-
// If going down on first bounce, reject (shouldn't happen with VNDF)
76-
if ( bounce == 0 && !isAboveSurface( w ) ) {
77-
return result;
78-
}
79-
80-
// Sample a visible microfacet normal
81-
vec3 m = sampleGGXMicrofacet( w, roughness, alpha, rand2( 17 + bounce ) );
82-
83-
// Compute reflection direction
84-
vec3 wi = reflect( -w, m );
85-
86-
// Compute Fresnel for this bounce
87-
float cosTheta = dot( w, m );
88-
float F = fresnelSchlick( abs( cosTheta ), f0 );
89-
90-
// Apply Fresnel to throughput
91-
// For metals, use colored Fresnel
92-
vec3 fresnelColor = f0Color + ( vec3( 1.0 ) - f0Color ) * pow( 1.0 - abs( cosTheta ), 5.0 );
93-
throughput *= fresnelColor;
94-
95-
// Russian roulette for path termination
96-
if ( bounce > 0 ) {
97-
float q = max( throughput.r, max( throughput.g, throughput.b ) );
98-
q = min( q, 0.95 ); // Cap at 95% to ensure termination
99-
100-
if ( rand( 18 + bounce ) > q ) {
101-
// Path terminated
102-
return result;
103-
}
104-
105-
// Adjust throughput for RR
106-
throughput /= q;
107-
}
108-
109-
// Update direction for next bounce
110-
w = wi;
111-
112-
}
3+
// Analytical multiscatter energy compensation for GGX BRDF
4+
// Compensates for energy loss due to multiple bounces within the microfacet structure
5+
// Based on observations that rough surfaces at grazing angles lose the most energy
6+
vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness, vec3 F0 ) {
7+
float NdotV = abs( wo.z );
8+
float NdotL = abs( wi.z );
1139
114-
// If we hit max bounces, check if we're above surface
115-
if ( isAboveSurface( w ) ) {
116-
result.direction = w;
117-
result.throughput = throughput;
118-
result.valid = true;
119-
}
10+
// Energy compensation increases with roughness
11+
// At roughness=0, no compensation needed (perfect mirror)
12+
// At roughness=1, significant compensation needed (very rough)
13+
float a = roughness * roughness;
14+
float energyFactor = a * sqrt( a ); // Scales as roughness^1.5
12015
121-
return result;
16+
// Angular dependence - more energy lost at grazing angles
17+
float angularLoss = ( 1.0 - NdotV * 0.9 ) * ( 1.0 - NdotL * 0.9 );
12218
123-
}
19+
// Combined energy compensation
20+
vec3 compensation = F0 * energyFactor * angularLoss;
12421
125-
// Stub function for compatibility - not used in explicit multiscatter approach
126-
vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness, vec3 F0 ) {
127-
// Not used when explicit microsurface scattering is enabled
128-
return vec3( 0.0 );
22+
// Conservative global scale to avoid over-brightening
23+
return compensation * 0.25;
12924
}
13025
13126

0 commit comments

Comments
 (0)