Skip to content

Commit

Permalink
Restore energy loss in indirect specular lighting by approximating
Browse files Browse the repository at this point in the history
multiscattering via Fdez-Agüera's "A Multiple-Scattering Microfacet
Model for Real-Time Image-based Lighting".
  • Loading branch information
jsantell committed Jan 29, 2019
1 parent 77adaa3 commit 4e8486f
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 13 deletions.
1 change: 1 addition & 0 deletions examples/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var files = {
"webgl_effects_peppersghost",
"webgl_effects_stereo",
"webgl_framebuffer_texture",
"webgl_furnace_test",
"webgl_geometries",
"webgl_geometries_parametric",
"webgl_geometry_colors",
Expand Down
179 changes: 179 additions & 0 deletions examples/webgl_furnace_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js white furnace test</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
background:#777;
padding:0;
margin:0;
font-weight: bold;
overflow:hidden;
}

#info {
position: absolute;
top: 0px;
min-width: 60%;
margin: 0 20%;
color: #ffffff;
padding: 15px;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: black;
color: white;
}

a {
color: #ffffff;
}
</style>

<script src="../build/three.js"></script>
<script src="js/WebGL.js"></script>
<script src="js/loaders/EquirectangularToCubeGenerator.js"></script>
<script src="js/pmrem/PMREMGenerator.js"></script>
<script src="js/pmrem/PMREMCubeUVPacker.js"></script>

</head>
<body>

<div id="container">
<div id="info">
<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> -
<a href="https://google.github.io/filament/Filament.md.html#toc4.7.2" target="_blank" rel="noopener">White Furnace</a> energy conservation test.<br />
There are 11 fully metal spheres with full white specular color and increasing roughness values rendered here. A metal object, no matter how rough, fully reflects all energy. If uniformly lit, the spheres should be indistinguishable from the environment. If spheres are visible, then some energy has been lost or gained after reflection.<br />
<a href="https://jsantell.com/" target="_blank" rel="noopener">by Jordan Santell</a><br/><br/></div>
</div>

<script>

if ( WEBGL.isWebGLAvailable() === false ) {

document.body.appendChild( WEBGL.getWebGLErrorMessage() );

}

var scene, camera, renderer, envMap, radianceMap, gui;
var right = 6;
var config = {
preserveEnergy: true,
};

function init() {

var width = window.innerWidth;
var height = window.innerHeight;
var aspect = width / height;

// renderer

renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height );
renderer.setPixelRatio( window.devicePixelRatio );
document.body.appendChild( renderer.domElement );

window.addEventListener( 'resize', onResize, false );

// scene

scene = new THREE.Scene();

// camera

camera = new THREE.OrthographicCamera( - right, right, right / aspect, - right / aspect, 1, 30 );
camera.position.set( 0, 0, 9 );

}

function createObjects() {

var geo = new THREE.SphereBufferGeometry( 0.4, 32, 32 );
var count = 10;
for ( var x = 0; x <= count; x ++ ) {

var mesh = new THREE.Mesh( geo, new THREE.MeshPhysicalMaterial( {
roughness: x / count,
metalness: 1,
color: 0xffffff,
envMap: radianceMap,
envMapIntensity: 1,
reflectivity: 1,
} ) );
mesh.position.x = x - ( Math.floor( count / 2 ) );
scene.add( mesh );

}

}

function createEnvironment() {

var color = new THREE.Color( 0xcccccc );
var sky = new THREE.Mesh( new THREE.SphereBufferGeometry( 1, 32, 32 ), new THREE.MeshBasicMaterial( {
color: color,
side: THREE.DoubleSide,
} ) );
sky.scale.setScalar( 100 );

var envScene = new THREE.Scene();
envScene.add( sky );
envScene.background = color;
var cubeCamera = new THREE.CubeCamera( 1, 100, 256, 256 );
cubeCamera.update( renderer, envScene );

envMap = cubeCamera.renderTarget.texture;

scene.background = color;

}

function getRadiance() {

return new Promise( function ( resolve, reject ) {

var pmremGenerator = new THREE.PMREMGenerator( envMap );
pmremGenerator.update( renderer );
var pmremCubeUVPacker = new THREE.PMREMCubeUVPacker( pmremGenerator.cubeLods );
pmremCubeUVPacker.update( renderer );
var cubeRenderTarget = pmremCubeUVPacker.CubeUVRenderTarget;

pmremGenerator.dispose();
pmremCubeUVPacker.dispose();
radianceMap = cubeRenderTarget.texture;
resolve();

} );

}

function onResize() {

var aspect = window.innerWidth / window.innerHeight;
camera.top = right / aspect;
camera.bottom = - camera.top;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();

}

function render() {

renderer.render( scene, camera );

}

Promise.resolve()
.then( init )
.then( createEnvironment )
.then( getRadiance )
.then( createObjects )
.then( render );

</script>
</body>
</html>
2 changes: 2 additions & 0 deletions src/materials/MeshPhysicalMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { MeshStandardMaterial } from './MeshStandardMaterial.js';
*
* parameters = {
* reflectivity: <float>
* clearCoat: <float>
* clearCoatRoughness: <float>
* }
*/

Expand Down
49 changes: 41 additions & 8 deletions src/renderers/shaders/ShaderChunk/bsdfs.glsl.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
export default /* glsl */`
export default /* glsl */ `
// Analytical approximation of the DFG LUT, one half of the
// split-sum approximation used in indirect specular lighting.
// via 'environmentBRDF' from "Physically Based Shading on Mobile"
// https://www.unrealengine.com/blog/physically-based-shading-on-mobile - environmentBRDF for GGX on mobile
vec2 integrateSpecularBRDF( const in float dotNV, const in float roughness ) {
const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );
const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );
vec4 r = roughness * c0 + c1;
float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;
return vec2( -1.04, 1.04 ) * a004 + r.zw;
}
float punctualLightIntensityToIrradianceFactor( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {
#if defined ( PHYSICALLY_CORRECT_LIGHTS )
Expand Down Expand Up @@ -239,20 +257,35 @@ vec3 BRDF_Specular_GGX_Environment( const in GeometricContext geometry, const in
float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );
const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );
vec2 brdf = integrateSpecularBRDF( dotNV, roughness );
const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );
return specularColor * brdf.x + brdf.y;
vec4 r = roughness * c0 + c1;
} // validated
float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;
// Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
// Approximates multiscattering in order to preserve energy.
// http://www.jcgt.org/published/0008/01/03/
void BRDF_Specular_Multiscattering_Environment( const in GeometricContext geometry, const in vec3 specularColor, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {
vec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw;
float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );
return specularColor * AB.x + AB.y;
vec3 F = F_Schlick( specularColor, dotNV );
vec2 brdf = integrateSpecularBRDF( dotNV, roughness );
vec3 FssEss = F * brdf.x + brdf.y;
} // validated
float Ess = brdf.x + brdf.y;
float Ems = 1.0 - Ess;
// Paper incorrect indicates coefficient is PI/21, and will
// be corrected to 1/21 in future updates.
vec3 Favg = specularColor + ( 1.0 - specularColor ) * 0.047619; // 1/21
vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );
singleScatter += FssEss;
multiScatter += Fms * Ems;
}
float G_BlinnPhong_Implicit( /* const in float dotNL, const in float dotNV */ ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default /* glsl */`
#if defined( RE_IndirectSpecular )
RE_IndirectSpecular( radiance, clearCoatRadiance, geometry, material, reflectedLight );
RE_IndirectSpecular( radiance, irradiance, clearCoatRadiance, geometry, material, reflectedLight );
#endif
`;
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,17 @@ void RE_Direct_Physical( const in IncidentLight directLight, const in GeometricC
void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {
reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
// Defer to the IndirectSpecular function to compute
// the indirectDiffuse if energy preservation is enabled.
#ifndef ENVMAP_TYPE_CUBE_UV
reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
#endif
}
void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 clearCoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {
void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearCoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {
#ifndef STANDARD
float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );
Expand All @@ -110,14 +116,35 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 clearCo
float clearCoatDHR = 0.0;
#endif
reflectedLight.indirectSpecular += ( 1.0 - clearCoatDHR ) * radiance * BRDF_Specular_GGX_Environment( geometry, material.specularColor, material.specularRoughness );
float clearCoatInv = 1.0 - clearCoatDHR;
// Both indirect specular and diffuse light accumulate here
// if energy preservation enabled, and PMREM provided.
#if defined( ENVMAP_TYPE_CUBE_UV )
vec3 singleScattering = vec3( 0.0 );
vec3 multiScattering = vec3( 0.0 );
vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;
BRDF_Specular_Multiscattering_Environment( geometry, material.specularColor, material.specularRoughness, singleScattering, multiScattering );
vec3 diffuse = material.diffuseColor * ( 1.0 - ( singleScattering + multiScattering ) );
reflectedLight.indirectSpecular += clearCoatInv * radiance * singleScattering;
reflectedLight.indirectDiffuse += multiScattering * cosineWeightedIrradiance;
reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;
#else
reflectedLight.indirectSpecular += clearCoatInv * radiance * BRDF_Specular_GGX_Environment( geometry, material.specularColor, material.specularRoughness );
#endif
#ifndef STANDARD
reflectedLight.indirectSpecular += clearCoatRadiance * material.clearCoat * BRDF_Specular_GGX_Environment( geometry, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearCoatRoughness );
#endif
}
#define RE_Direct RE_Direct_Physical
Expand Down

0 comments on commit 4e8486f

Please sign in to comment.