浏览代码

Restore energy loss in indirect specular lighting by approximating
multiscattering via Fdez-Agüera's "A Multiple-Scattering Microfacet
Model for Real-Time Image-based Lighting".

Jordan Santell 6 年之前
父节点
当前提交
4e8486fb11

+ 1 - 0
examples/files.js

@@ -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",

+ 179 - 0
examples/webgl_furnace_test.html

@@ -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 - 0
src/materials/MeshPhysicalMaterial.js

@@ -5,6 +5,8 @@ import { MeshStandardMaterial } from './MeshStandardMaterial.js';
  *
  * parameters = {
  *  reflectivity: <float>
+ *  clearCoat: <float>
+ *  clearCoatRoughness: <float>
  * }
  */
 

+ 41 - 8
src/renderers/shaders/ShaderChunk/bsdfs.glsl.js

@@ -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 )
@@ -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 */ ) {
 

+ 1 - 1
src/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js

@@ -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
 `;

+ 31 - 4
src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js

@@ -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 ) );
@@ -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