Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ bower_components
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Build output (generated by rollup)
build/

# Dependency directories
node_modules/
jspm_packages/
Expand Down
1 change: 1 addition & 0 deletions src/materials/pathtracing/PhysicalPathTracingMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export class PhysicalPathTracingMaterial extends MaterialBase {
${ BSDFGLSL.ggx_functions }
${ BSDFGLSL.sheen_functions }
${ BSDFGLSL.iridescence_functions }
${ BSDFGLSL.multiscatter_functions }
${ BSDFGLSL.fog_functions }
${ BSDFGLSL.bsdf_functions }

Expand Down
26 changes: 22 additions & 4 deletions src/shader/bsdf/bsdf_functions.glsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,31 @@ export const bsdf_functions = /* glsl */`
float G1 = ggxShadowMaskG1( incidentTheta, roughness );
float ggxPdf = D * G1 * max( 0.0, abs( dot( wo, wh ) ) ) / abs ( wo.z );

color = wi.z * F * G * D / ( 4.0 * abs( wi.z * wo.z ) );
// Single-scatter term (standard Cook-Torrance microfacet BRDF)
vec3 singleScatter = wi.z * F * G * D / ( 4.0 * abs( wi.z * wo.z ) );

// Multiscatter energy compensation
// Adds back energy lost to multiple bounces within the microfacet structure
// Returns a diffuse-like lobe scaled by the missing energy and average Fresnel
vec3 multiScatter = ggxMultiScatterCompensation( wo, wi, roughness, f0Color ) * wi.z;

// Total specular reflection = single-scatter + multiscatter
color = singleScatter + multiScatter;
return ggxPdf / ( 4.0 * dot( wo, wh ) );

}

vec3 specularDirection( vec3 wo, SurfaceRecord surf ) {

// sample ggx vndf distribution which gives a new normal
// Sample GGX VNDF distribution to get a microfacet normal
float roughness = surf.filteredRoughness;
vec3 halfVector = ggxDirection(
wo,
vec2( roughness ),
rand2( 12 )
);

// apply to new ray by reflecting off the new normal
// Reflect view direction off the sampled microfacet normal
return - reflect( wo, halfVector );

}
Expand Down Expand Up @@ -196,7 +205,16 @@ export const bsdf_functions = /* glsl */`
float D = ggxDistribution( wh, roughness );
float F = schlickFresnel( dot( wi, wh ), f0 );

float fClearcoat = F * D * G / ( 4.0 * abs( wi.z * wo.z ) );
// Single-scatter clearcoat term
float fClearcoatSingle = F * D * G / ( 4.0 * abs( wi.z * wo.z ) );

// Multiscatter energy compensation for clearcoat layer
// Clearcoat is a dielectric layer (IOR 1.5), so we use its F0 for compensation
vec3 f0ColorClearcoat = vec3( f0 );
vec3 clearcoatMultiScatter = ggxMultiScatterCompensation( wo, wi, roughness, f0ColorClearcoat );

// Total clearcoat reflection = single-scatter + multiscatter
float fClearcoat = fClearcoatSingle + clearcoatMultiScatter.r;
color = color * ( 1.0 - surf.clearcoat * F ) + fClearcoat * surf.clearcoat * wi.z;

// PDF
Expand Down
1 change: 1 addition & 0 deletions src/shader/bsdf/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './fog_functions.glsl.js';
export * from './ggx_functions.glsl.js';
export * from './iridescence_functions.glsl.js';
export * from './sheen_functions.glsl.js';
export * from './multiscatter_functions.glsl.js';
54 changes: 54 additions & 0 deletions src/shader/bsdf/multiscatter_functions.glsl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export const multiscatter_functions = /* glsl */`

// GGX Multiscatter Energy Compensation
// Implementation based on Blender Cycles' approach
//
// References:
// - "Multiple-Scattering Microfacet BSDFs with the Smith Model" (Heitz et al. 2016)
// - Blender Cycles: intern/cycles/kernel/closure/bsdf_microfacet_multi.h
//
// Single-scatter GGX loses energy due to rays bouncing multiple times within
// the microfacet structure before escaping. This compensation adds back the
// missing energy as a diffuse-like multiscatter lobe.
//
// The approach uses a fitted albedo approximation to estimate how much energy
// single-scatter captures, then adds the remainder back. This is simpler and
// faster than full random-walk multiscatter simulation while providing good
// energy conservation for path tracers.

// Directional albedo approximation for single-scatter GGX
// Returns the fraction of energy captured by single-scatter as a function of roughness
// Fitted curve from Blender Cycles based on precomputed ground truth data
float ggxAlbedo( float roughness ) {
float r2 = roughness * roughness;
return 0.806495 * exp( -1.98712 * r2 ) + 0.199531;
}

// GGX multiscatter energy compensation term
// wo: outgoing direction (view direction)
// wi: incident direction (light direction)
// roughness: surface roughness [0, 1]
// F0: Fresnel reflectance at normal incidence
// Returns: Additional BRDF contribution to compensate for multiscatter energy loss
vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float roughness, vec3 F0 ) {
// Estimate the fraction of energy captured by single-scatter GGX
float singleScatterAlbedo = ggxAlbedo( roughness );

// The missing energy that needs compensation
float missingEnergy = 1.0 - singleScatterAlbedo;

// Average Fresnel reflectance over all directions (spherical albedo)
// Approximation: F_avg ≈ F0 + (1 - F0) / 21
vec3 Favg = F0 + ( 1.0 - F0 ) / 21.0;

// Multiscatter contribution: diffuse-like lobe scaled by average Fresnel
// This represents energy that bounced multiple times before escaping
vec3 Fms = Favg * missingEnergy;

// Return as a Lambertian BRDF (energy / π)
// The π accounts for the hemispherical integral in the rendering equation
return Fms / PI;
}


`;