diff --git a/.gitignore b/.gitignore index 440231c0c..f4e5bc1f3 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/src/materials/pathtracing/PhysicalPathTracingMaterial.js b/src/materials/pathtracing/PhysicalPathTracingMaterial.js index 1794015ee..cf09b31fa 100644 --- a/src/materials/pathtracing/PhysicalPathTracingMaterial.js +++ b/src/materials/pathtracing/PhysicalPathTracingMaterial.js @@ -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 } diff --git a/src/shader/bsdf/bsdf_functions.glsl.js b/src/shader/bsdf/bsdf_functions.glsl.js index 99aaa2c42..eaaf6ad35 100644 --- a/src/shader/bsdf/bsdf_functions.glsl.js +++ b/src/shader/bsdf/bsdf_functions.glsl.js @@ -69,14 +69,23 @@ 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, @@ -84,7 +93,7 @@ export const bsdf_functions = /* glsl */` rand2( 12 ) ); - // apply to new ray by reflecting off the new normal + // Reflect view direction off the sampled microfacet normal return - reflect( wo, halfVector ); } @@ -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 diff --git a/src/shader/bsdf/index.js b/src/shader/bsdf/index.js index f61c08924..9f45153cb 100644 --- a/src/shader/bsdf/index.js +++ b/src/shader/bsdf/index.js @@ -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'; diff --git a/src/shader/bsdf/multiscatter_functions.glsl.js b/src/shader/bsdf/multiscatter_functions.glsl.js new file mode 100644 index 000000000..7bdd52553 --- /dev/null +++ b/src/shader/bsdf/multiscatter_functions.glsl.js @@ -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; +} + + +`;