Skip to content
Closed
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
Binary file modified examples/screenshots/webgl_materials_physical_clearcoat.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_clearcoat.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 10 additions & 29 deletions src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,29 @@ import BRDF_GGX from './BRDF_GGX.js';
import DFGApprox from './DFGApprox.js';
import { normalView } from '../../accessors/Normal.js';
import { positionViewDirection } from '../../accessors/Position.js';
import { EPSILON } from '../../math/MathNode.js';
import { Fn, float } from '../../tsl/TSLBase.js';

// GGX BRDF with multi-scattering energy compensation for direct lighting
// This provides more accurate energy conservation, especially for rough materials
// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
// Based on "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
// by Fdez-Agüera: http://www.jcgt.org/published/0008/01/03/
const BRDF_GGX_Multiscatter = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness: _roughness, f, USE_IRIDESCENCE, USE_ANISOTROPY } ) => {

// Single-scattering BRDF (standard GGX)
const singleScatter = BRDF_GGX( { lightDirection, f0, f90, roughness: _roughness, f, USE_IRIDESCENCE, USE_ANISOTROPY } );

// Multi-scattering compensation
const dotNL = normalView.dot( lightDirection ).clamp();
// Multi-scattering compensation using Fdez-Agüera's approximation
const dotNV = normalView.dot( positionViewDirection ).clamp();
const fab = DFGApprox( { roughness: _roughness, dotNV } );

// Precomputed DFG values for view and light directions
const dfgV = DFGApprox( { roughness: _roughness, dotNV } );
const dfgL = DFGApprox( { roughness: _roughness, dotNV: dotNL } );
const FssEss = f0.mul( fab.x ).add( f90.mul( fab.y ) );
const Ess = fab.x.add( fab.y );
const Ems = float( 1.0 ).sub( Ess );

// Single-scattering energy for view and light
const FssEss_V = f0.mul( dfgV.x ).add( f90.mul( dfgV.y ) );
const FssEss_L = f0.mul( dfgL.x ).add( f90.mul( dfgL.y ) );

const Ess_V = dfgV.x.add( dfgV.y );
const Ess_L = dfgL.x.add( dfgL.y );

// Energy lost to multiple scattering
const Ems_V = float( 1.0 ).sub( Ess_V );
const Ems_L = float( 1.0 ).sub( Ess_L );

// Average Fresnel reflectance
const Favg = f0.add( f0.oneMinus().mul( 0.047619 ) ); // 1/21
const Fms = FssEss.mul( Favg ).div( float( 1.0 ).sub( Ems.mul( Favg ) ) );

// Multiple scattering contribution
// Uses geometric mean of view and light contributions for better energy distribution
const Fms = FssEss_V.mul( FssEss_L ).mul( Favg ).div( float( 1.0 ).sub( Ems_V.mul( Ems_L ).mul( Favg ).mul( Favg ) ).add( EPSILON ) );

// Energy compensation factor
const compensationFactor = Ems_V.mul( Ems_L );

const multiScatter = Fms.mul( compensationFactor );
// Add multi-scattering contribution
const multiScatter = Fms.mul( Ems );

return singleScatter.add( multiScatter );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,35 +434,19 @@ vec3 BRDF_GGX_Multiscatter( const in vec3 lightDir, const in vec3 viewDir, const
// Single-scattering BRDF (standard GGX)
vec3 singleScatter = BRDF_GGX( lightDir, viewDir, normal, material );

// Multi-scattering compensation
float dotNL = saturate( dot( normal, lightDir ) );
// Multi-scattering compensation using Fdez-Agüera's approximation
float dotNV = saturate( dot( normal, viewDir ) );
vec2 fab = DFGApprox( normal, viewDir, material.roughness );

// Precomputed DFG values for view and light directions
vec2 dfgV = DFGApprox( vec3(0.0, 0.0, 1.0), vec3(sqrt(1.0 - dotNV * dotNV), 0.0, dotNV), material.roughness );
vec2 dfgL = DFGApprox( vec3(0.0, 0.0, 1.0), vec3(sqrt(1.0 - dotNL * dotNL), 0.0, dotNL), material.roughness );

// Single-scattering energy for view and light
vec3 FssEss_V = material.specularColor * dfgV.x + material.specularF90 * dfgV.y;
vec3 FssEss_L = material.specularColor * dfgL.x + material.specularF90 * dfgL.y;

float Ess_V = dfgV.x + dfgV.y;
float Ess_L = dfgL.x + dfgL.y;

// Energy lost to multiple scattering
float Ems_V = 1.0 - Ess_V;
float Ems_L = 1.0 - Ess_L;
vec3 FssEss = material.specularColor * fab.x + material.specularF90 * fab.y;
float Ess = fab.x + fab.y;
float Ems = 1.0 - Ess;

// Average Fresnel reflectance
vec3 Favg = material.specularColor + ( 1.0 - material.specularColor ) * 0.047619; // 1/21
vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );

// Multiple scattering contribution
vec3 Fms = FssEss_V * FssEss_L * Favg / ( 1.0 - Ems_V * Ems_L * Favg * Favg + EPSILON );

// Energy compensation factor
float compensationFactor = Ems_V * Ems_L;

vec3 multiScatter = Fms * compensationFactor;
// Add multi-scattering contribution
vec3 multiScatter = Fms * Ems;

return singleScatter + multiScatter;

Expand Down