Skip to content

Commit 1594856

Browse files
committed
Implement explicit microsurface multiscattering for GGX BRDF
This implements true physically-based multiscattering by simulating a random walk on the microsurface structure, allowing rays to bounce multiple times within microfacets before escaping. Based on research from: - "Multiple-Scattering Microfacet BSDFs with the Smith Model" (Heitz et al. 2016) - "Position-Free Multiple-Bounce Computations for Smith Microfacet BSDFs" (Xie & Hanrahan 2018) Implementation details: - Added ggxMicrosurfaceScatter() function that performs random walk - For rough surfaces (roughness > 0.2), ray can bounce 2-4 times within microsurface - Each bounce samples a new microfacet normal using VNDF - Fresnel is accumulated at each bounce for proper colored metals - Russian roulette termination prevents infinite loops - Throughput is applied to the final scatter result This approach is correct for pathtracers (unlike rasterizer compensation methods) as it explicitly simulates the physical process of multiple scattering within the microfacet structure. The implementation only activates for rough surfaces where multiscatter has visible impact, falling back to single-scatter for smooth surfaces. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f7a3b44 commit 1594856

File tree

2 files changed

+130
-57
lines changed

2 files changed

+130
-57
lines changed

src/shader/bsdf/bsdf_functions.glsl.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,31 @@ 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+
8589
vec3 specularDirection( vec3 wo, SurfaceRecord surf ) {
8690
8791
// sample ggx vndf distribution which gives a new normal
8892
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
89110
vec3 halfVector = ggxDirection(
90111
wo,
91112
vec2( roughness ),
@@ -458,6 +479,12 @@ export const bsdf_functions = /* glsl */`
458479
result.pdf = bsdfEval( wo, clearcoatWo, wi, clearcoatWi, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight, result.specularPdf, result.color );
459480
result.direction = normalize( surf.normalBasis * wi );
460481
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+
461488
return result;
462489
463490
}

src/shader/bsdf/multiscatter_functions.glsl.js

Lines changed: 103 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,133 @@
11
export const multiscatter_functions = /* glsl */`
22
3-
// Multiscattering energy compensation for GGX microfacet BRDF
4-
// Based on Kulla & Conty 2017 - "Revisiting Physically Based Shading at Imageworks"
5-
// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
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)
66
//
7-
// Simplified analytical approximation using Turquin 2019 formulas
8-
// "Practical multiple scattering compensation for microfacet models"
9-
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
7+
// This simulates a random walk on the microsurface, allowing rays to bounce multiple times
8+
// within the microfacet structure before escaping.
109
11-
// Approximate directional albedo for GGX using simple fitted curve
12-
// More conservative than complex polynomial fits
13-
float ggxDirectionalAlbedoApprox( float NoV, float roughness ) {
10+
// Check if a direction is above the macrosurface
11+
bool isAboveSurface( vec3 w ) {
12+
return w.z > 0.0;
13+
}
1414
15-
// Clamp inputs
16-
NoV = clamp( NoV, 0.0, 1.0 );
17-
roughness = clamp( roughness, 0.04, 1.0 ); // Min roughness 0.04 for stability
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+
}
1821
19-
// Simple fit based on pre-integrated data
20-
// This is a conservative approximation
21-
float a = roughness * roughness;
22-
float s = a; // Simplified
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+
}
2328
24-
// Approximate using smooth curve
25-
float Ess = mix( 1.0, 0.0, a );
26-
Ess = mix( Ess, Ess * NoV, a );
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+
}
2753
28-
return clamp( Ess, 0.0, 1.0 );
54+
// Current ray direction (starts as view direction)
55+
vec3 w = wo;
56+
vec3 throughput = vec3( 1.0 );
2957
30-
}
58+
vec2 alpha = vec2( roughness );
59+
float f0 = ( f0Color.r + f0Color.g + f0Color.b ) / 3.0;
3160
32-
// Average directional albedo (integral over hemisphere)
33-
float ggxAverageAlbedoApprox( float roughness ) {
61+
// Maximum bounces within microsurface (typically 2-4 is enough)
62+
const int MAX_MICRO_BOUNCES = 3;
3463
35-
roughness = clamp( roughness, 0.04, 1.0 );
64+
for ( int bounce = 0; bounce < MAX_MICRO_BOUNCES; bounce++ ) {
3665
37-
float a = roughness * roughness;
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+
}
3874
39-
// Conservative fit - energy decreases with roughness
40-
return 1.0 - a * 0.5;
75+
// If going down on first bounce, reject (shouldn't happen with VNDF)
76+
if ( bounce == 0 && !isAboveSurface( w ) ) {
77+
return result;
78+
}
4179
42-
}
80+
// Sample a visible microfacet normal
81+
vec3 m = sampleGGXMicrofacet( w, roughness, alpha, rand2( 17 + bounce ) );
4382
44-
// Computes the multiscatter contribution color - DISABLED FOR NOW
45-
// wo = outgoing direction (view), wi = incoming direction (light)
46-
// roughness = linear roughness parameter
47-
// Returns the additional energy that should be added
48-
vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness, vec3 F0 ) {
83+
// Compute reflection direction
84+
vec3 wi = reflect( -w, m );
4985
50-
// DISABLED: Return zero contribution
51-
// The multiscatter compensation appears to be too aggressive for this pathtracer
52-
// The pathtracer already handles multiple bounces through recursive ray tracing
53-
// Additional testing is needed to validate the correct approach
86+
// Compute Fresnel for this bounce
87+
float cosTheta = dot( w, m );
88+
float F = fresnelSchlick( abs( cosTheta ), f0 );
5489
55-
return vec3( 0.0 );
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;
5694
57-
/* ORIGINAL FORMULA - DISABLED
58-
float mu_o = abs( wo.z );
59-
float mu_i = abs( wi.z );
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
6099
61-
// Only apply multiscatter for roughness > 0.3
62-
if ( roughness < 0.3 ) {
63-
return vec3( 0.0 );
64-
}
100+
if ( rand( 18 + bounce ) > q ) {
101+
// Path terminated
102+
return result;
103+
}
65104
66-
float Eo = ggxDirectionalAlbedoApprox( mu_o, roughness );
67-
float Ei = ggxDirectionalAlbedoApprox( mu_i, roughness );
68-
float Eavg = ggxAverageAlbedoApprox( roughness );
105+
// Adjust throughput for RR
106+
throughput /= q;
107+
}
69108
70-
// Kulla-Conty formula with conservative scaling
71-
float numerator = ( 1.0 - Eo ) * ( 1.0 - Ei );
72-
float denominator = PI * max( 1.0 - Eavg, 0.001 );
109+
// Update direction for next bounce
110+
w = wi;
73111
74-
float fms = numerator / denominator;
112+
}
75113
76-
// Very conservative Favg
77-
vec3 Favg = F0 * 0.5; // Much more conservative
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+
}
78120
79-
// Scale down the contribution significantly
80-
return fms * Favg * 0.1; // 10% strength for testing
81-
*/
121+
return result;
82122
83123
}
84124
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 );
129+
}
130+
85131
// Alternative: Single function that returns both single-scatter and multi-scatter
86132
// This can be more efficient as it reuses calculations
87133
void ggxEvalWithMultiScatter(

0 commit comments

Comments
 (0)