|
1 | 1 | export const multiscatter_functions = /* glsl */` |
2 | 2 |
|
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 |
| 3 | +// GGX Multiscatter Energy Compensation |
| 4 | +// Implementation based on Blender Cycles' approach |
| 5 | +// |
| 6 | +// References: |
| 7 | +// - "Multiple-Scattering Microfacet BSDFs with the Smith Model" (Heitz et al. 2016) |
| 8 | +// - Blender Cycles: intern/cycles/kernel/closure/bsdf_microfacet_multi.h |
| 9 | +// |
| 10 | +// Single-scatter GGX loses energy due to rays bouncing multiple times within |
| 11 | +// the microfacet structure before escaping. This compensation adds back the |
| 12 | +// missing energy as a diffuse-like multiscatter lobe. |
| 13 | +// |
| 14 | +// The approach uses a fitted albedo approximation to estimate how much energy |
| 15 | +// single-scatter captures, then adds the remainder back. This is simpler and |
| 16 | +// faster than full random-walk multiscatter simulation while providing good |
| 17 | +// energy conservation for path tracers. |
| 18 | +
|
| 19 | +// Directional albedo approximation for single-scatter GGX |
| 20 | +// Returns the fraction of energy captured by single-scatter as a function of roughness |
| 21 | +// Fitted curve from Blender Cycles based on precomputed ground truth data |
| 22 | +float ggxAlbedo( float roughness ) { |
| 23 | + float r2 = roughness * roughness; |
| 24 | + return 0.806495 * exp( -1.98712 * r2 ) + 0.199531; |
| 25 | +} |
| 26 | +
|
| 27 | +// GGX multiscatter energy compensation term |
| 28 | +// wo: outgoing direction (view direction) |
| 29 | +// wi: incident direction (light direction) |
| 30 | +// roughness: surface roughness [0, 1] |
| 31 | +// F0: Fresnel reflectance at normal incidence |
| 32 | +// Returns: Additional BRDF contribution to compensate for multiscatter energy loss |
6 | 33 | vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness, vec3 F0 ) { |
7 | | - float NdotV = abs( wo.z ); |
8 | | - float NdotL = abs( wi.z ); |
| 34 | + // Estimate the fraction of energy captured by single-scatter GGX |
| 35 | + float singleScatterAlbedo = ggxAlbedo( roughness ); |
9 | 36 |
|
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 |
| 37 | + // The missing energy that needs compensation |
| 38 | + float missingEnergy = 1.0 - singleScatterAlbedo; |
15 | 39 |
|
16 | | - // Angular dependence - more energy lost at grazing angles |
17 | | - float angularLoss = ( 1.0 - NdotV * 0.9 ) * ( 1.0 - NdotL * 0.9 ); |
| 40 | + // Average Fresnel reflectance over all directions (spherical albedo) |
| 41 | + // Approximation: F_avg ≈ F0 + (1 - F0) / 21 |
| 42 | + vec3 Favg = F0 + ( 1.0 - F0 ) / 21.0; |
18 | 43 |
|
19 | | - // Combined energy compensation |
20 | | - vec3 compensation = F0 * energyFactor * angularLoss; |
| 44 | + // Multiscatter contribution: diffuse-like lobe scaled by average Fresnel |
| 45 | + // This represents energy that bounced multiple times before escaping |
| 46 | + vec3 Fms = Favg * missingEnergy; |
21 | 47 |
|
22 | | - // Conservative global scale to avoid over-brightening |
23 | | - return compensation * 0.25; |
| 48 | + // Return as a Lambertian BRDF (energy / π) |
| 49 | + // The π accounts for the hemispherical integral in the rendering equation |
| 50 | + return Fms / PI; |
24 | 51 | } |
25 | 52 |
|
26 | 53 |
|
|
0 commit comments