Browse Source

Merge branch 'physical_lights' of http://github.com/bhouston/three.js into dev

# Conflicts:
#	src/renderers/shaders/ShaderChunk/ambient_pars.glsl
Mr.doob 9 years ago
parent
commit
9d3cf8c2f5

+ 14 - 4
docs/api/lights/AmbientLight.html

@@ -24,7 +24,7 @@
 		<div>[example:canvas_sandbox sandbox ]</div>
 		<div>[example:webgl_animation_cloth animation / cloth ]</div>
 		<div>[example:webgl_animation_skinning_blending animation / skinning / blending ]</div>
-		
+
 <code>var light = new THREE.AmbientLight( 0x404040 ); // soft white light
 scene.add( light );</code>
 
@@ -39,19 +39,29 @@ scene.add( light );</code>
 		This creates an Ambientlight with a color.
 		</div>
 
+		<h2>Properties</h2>
+
+		<h3>[property:Float intensity]</h3>
+		<div>
+			Light's intensity.<br />
+			In "physically correct" mode, the product of color * intensity is interpreted as luminous irradiance measured in lux at the material surface.<br/>
+			Default — *1.0*.
+		</div>
+
+
 		<h2>Methods</h2>
-		
+
 		<h3>[method:AmbientLight clone]()</h3>
 		<div>
 		<br />
 		It returns a clone of Ambientlight.
 		</div>
-		
+
 		<h3>[method:JSON toJSON]()</h3>
 		<div>
 		Return Ambientlight data in JSON format.
 		</div>
-		
+
 		<h2>Source</h2>
 
 		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]

+ 1 - 0
docs/api/lights/DirectionalLight.html

@@ -60,6 +60,7 @@ scene.add( directionalLight );</code>
 		<h3>[property:Float intensity]</h3>
 		<div>
 			Light's intensity.<br />
+			In "physically correct" mode, the product of intensity * color is interpreted as luminous irradiance measured in lux at the material's surface.<br/>
 			Default — *1.0*.
 		</div>
 

+ 6 - 5
docs/api/lights/HemisphereLight.html

@@ -13,16 +13,16 @@
 		<h1>[name]</h1>
 
 		<div class="desc">A light source positioned directly above the scene.</div>
-		
+
 		<h2>Example</h2>
-		
+
 		<div>[example:webgl_lights_hemisphere lights / hemisphere ]</div>
 		<div>[example:misc_controls_pointerlock controls / pointerlock ]</div>
 		<div>[example:webgl_decals decals ]</div>
 		<div>[example:webgl_loader_collada_kinematics loader / collada / kinematics ]</div>
 		<div>[example:webgl_materials_lightmap materials / lightmap ]</div>
 		<div>[example:webgl_shaders_ocean shaders / ocean ]</div>
-		
+
 <code>var light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
 scene.add( light );</code>
 
@@ -47,17 +47,18 @@ scene.add( light );</code>
 		<h3>[property:Float intensity]</h3>
 		<div>
 			Light's intensity.<br />
+			In "physically correct" mode, the product of intensity * color (or intensity * groundColor) is interpreted as luminous irradiance measured in lux at the material's surface.<br/>
 			Default — *1.0*.
 		</div>
 
 		<h2>Methods</h2>
-		
+
 		<h3>[method:HemisphereLight clone]()</h3>
 		<div>
 		<br />
 		It returns a clone of HemisphereLight.
 		</div>
-		
+
 		<h3>[method:JSON toJSON]()</h3>
 		<div>
 		Return HemisphereLight data in JSON format.

+ 15 - 6
docs/api/lights/PointLight.html

@@ -28,7 +28,7 @@
 		<div>[example:webgl_geometry_large_mesh geometry / large / mesh ]</div>
 		<div>[example:webgl_geometry_text geometry / text ]</div>
 		<div>[example:webgl_lensflares lensflares ]</div>
-		
+
 		<code>var light = new THREE.PointLight( 0xff0000, 1, 100 );
 light.position.set( 50, 50, 50 );
 scene.add( light );</code>
@@ -46,7 +46,7 @@ scene.add( light );</code>
 		</div>
 		<div>
 		Creates a light at a specific position in the scene.  The light shines in all directions (roughly similar to a light bulb.)
-	
+
 		</div>
 
 
@@ -55,29 +55,38 @@ scene.add( light );</code>
 		<h3>[property:Float intensity]</h3>
 		<div>
 			Light's intensity.<br />
+			In "physically correct" mode, the product of color * intensity is interpreted as luminous intensity measured in candela.<br/>
 			Default - *1.0*.
 		</div>
 
+		<h3>[property:Float power]</h3>
+		<div>
+			Light's power.<br />
+			In "physically correct" mode, the luminous power of the light measured in lumens.<br/>
+			Default - *4PI*.
+		</div>
+
 		<h3>[property:Float distance]</h3>
 		<div>
 			If non-zero, light will attenuate linearly from maximum intensity at light *position* down to zero at *distance*.<br />
 			Default — *0.0*.
 		</div>
-		
+
 		<h3>[property:Float decay]</h3>
 		<div>
 			The amount the light dims along the distance of the light<br />
+			In "physically correct" mode, decay = 2 leads to physically realistic light falloff.<br/>
 			Default — *1*.
 		</div>
-		
+
 		<h2>Methods</h2>
-		
+
 		<h3>[method:PointLight clone]()</h3>
 		<div>
 		<br />
 		It returns a clone of PointLight.
 		</div>
-		
+
 		<h3>[method:JSON toJSON]()</h3>
 		<div>
 		Return PointLight data in JSON format.

+ 9 - 0
docs/api/lights/SpotLight.html

@@ -77,9 +77,17 @@
 		<h3>[property:Float intensity]</h3>
 		<div>
 			Light's intensity.<br />
+			In "physically correct" mode, the product of color * intensity is interpreted as luminous intensity measured in candela.<br/>
 			Default — *1.0*.
 		</div>
 
+		<h3>[property:Float power]</h3>
+		<div>
+			Light's power.<br />
+			In "physically correct" mode, the luminous power of the light measured in lumens.<br/>
+			Default - *4PI*.
+		</div>
+
 		<h3>[property:Float distance]</h3>
 		<div>
 			If non-zero, light will attenuate linearly from maximum intensity at light *position* down to zero at *distance*.<br />
@@ -101,6 +109,7 @@
 		<h3>[property:Float decay]</h3>
 		<div>
 			The amount the light dims along the distance of the light<br />
+			In "physically correct" mode, decay = 2 leads to physically realistic light falloff.<br/>
 			Default — *1*.
 		</div>
 

+ 1 - 0
examples/files.js

@@ -57,6 +57,7 @@ var files = {
 		"webgl_kinect",
 		"webgl_lensflares",
 		"webgl_lights_hemisphere",
+		"webgl_lights_physical",
 		"webgl_lights_pointlights",
 		"webgl_lights_pointlights2",
 		"webgl_lines_colors",

BIN
examples/textures/hardwood2_bump.jpg


BIN
examples/textures/hardwood2_diffuse.jpg


BIN
examples/textures/hardwood2_roughness.jpg


+ 315 - 0
examples/webgl_lights_physical.html

@@ -0,0 +1,315 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - lights - physical lights</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-color: #000;
+				margin: 0px;
+				overflow: hidden;
+			}
+
+			#info {
+				position: absolute;
+				top: 0px; width: 100%;
+				color: #ffffff;
+				padding: 5px;
+				font-family: Monospace;
+				font-size: 13px;
+				text-align: center;
+			}
+
+			a {
+				color: #ff0080;
+				text-decoration: none;
+			}
+
+			a:hover {
+				color: #0080ff;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="container"></div>
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - Physically accurate lighting example using a incandescent bulb - by <a href="http://clara.io" target="_blank">Ben Houston</a><br />
+			Using real world scale: Brick cube is 1 meter in size.  Light is 2 meters from floor.  Globe is 25 cm in diameter.<br/>
+			Using Reinhard inline tonemapping with real-world light falloff (decay = 2).
+		</div>
+
+		<script src="../build/three.min.js"></script>
+		<script src="../examples/js/libs/stats.min.js"></script>
+		<script src="../examples/js/libs/dat.gui.min.js"></script>
+		<script src="../examples/js/controls/OrbitControls.js"></script>
+		<script src="js/Detector.js"></script>
+
+		<script>
+
+			if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
+
+			var camera, scene, renderer,
+			bulbLight, bulbMat, ambientLight,
+			object, loader, stats;
+			var ballMat, cubeMat, floorMat;
+
+
+			// ref for lumens: http://www.power-sure.com/lumens.htm
+			var bulbLuminousPowers = {
+				"110000 lm (1000W)": 110000,
+				"3500 lm (300W)": 3500,
+				"1700 lm (100W)": 1700,
+				"800 lm (60W)": 800,
+				"400 lm (40W)": 400,
+				"180 lm (25W)": 180,
+				"20 lm (4W)": 20,
+				"Off": 0
+			};
+
+			// ref for solar irradiances: https://en.wikipedia.org/wiki/Lux
+			var hemiLuminousIrradiances = {
+				"0.0001 lx (Moonless Night)": 0.0001,
+				"0.002 lx (Night Airglow)": 0.002,
+				"0.5 lx (Full Moon)": 0.5,
+				"3.4 lx (City Twilight)": 3.4,
+				"50 lx (Living Room)": 50,
+				"100 lx (Very Overcast)": 100,
+				"350 lx (Office Room)": 350,
+				"400 lx (Sunrise/Sunset)": 400,
+				"1000 lx (Overcast)": 1000,
+				"18000 lx (Daylight)": 18000,
+				"50000 lx (Direct Sun)": 50000,
+			};
+
+			var params = {
+				shadows: true,
+				exposure: 0.68,
+				bulbPower: Object.keys( bulbLuminousPowers )[2],
+				hemiIrradiance: Object.keys( hemiLuminousIrradiances )[0]
+			};
+
+
+			var clock = new THREE.Clock();
+
+			init();
+			animate();
+
+			function init() {
+
+				var container = document.getElementById( 'container' );
+
+				stats = new Stats();
+				stats.domElement.style.position = 'absolute';
+				stats.domElement.style.top = '0px';
+
+				container.appendChild( stats.domElement );
+
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.x = -4;
+				camera.position.z = 4;
+				camera.position.y = 3;
+
+				scene = new THREE.Scene();
+
+				var bulbGeometry = new THREE.SphereGeometry( 0.02, 16, 8 );
+				bulbLight = new THREE.PointLight( 0xffee88, 1, 100, 2 );
+
+				bulbMat = new THREE.MeshStandardMaterial( {
+					emissive: 0xffffee,
+					emissiveIntensity: 1,
+					color: 0x000000
+				});
+				bulbLight.add( new THREE.Mesh( bulbGeometry, bulbMat ) );
+				bulbLight.position.set( 0, 2, 0 );
+				bulbLight.castShadow = true;
+				scene.add( bulbLight );
+
+				hemiLight = new THREE.HemisphereLight( 0xddeeff, 0x0f0e0d, 0.02 );
+				scene.add( hemiLight );
+
+				floorMat = new THREE.MeshStandardMaterial( {
+					roughness: 0.8,
+					color: 0xffffff,
+					metalness: 0.2,
+					bumpScale: 0.0005,
+				});
+				var textureLoader = new THREE.TextureLoader();
+				textureLoader.load( "../examples/textures/hardwood2_diffuse.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					floorMat.map = map;
+					floorMat.needsUpdate = true;
+				} );
+				textureLoader.load( "../examples/textures/hardwood2_bump.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					floorMat.bumpMap = map;
+					floorMat.needsUpdate = true;
+				} );
+				textureLoader.load( "../examples/textures/hardwood2_roughness.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					floorMat.roughnessMap = map;
+					floorMat.needsUpdate = true;
+				} );
+
+				cubeMat = new THREE.MeshStandardMaterial( {
+					roughness: 0.7,
+					color: 0xffffff,
+					bumpScale: 0.002,
+					metalness: 0.2
+				});
+				textureLoader.load( "../examples/textures/brick_diffuse.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					cubeMat.map = map;
+					cubeMat.needsUpdate = true;
+				} );
+				textureLoader.load( "../examples/textures/brick_bump.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					cubeMat.bumpMap = map;
+					cubeMat.needsUpdate = true;
+				} );
+
+				ballMat = new THREE.MeshStandardMaterial( {
+					roughness: 0,
+					metalness: 0.0,
+					color: 0xffffff
+				});
+				textureLoader.load( "../examples/textures/planets/earth_atmos_2048.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					ballMat.map = map;
+					ballMat.needsUpdate = true;
+				} );
+				textureLoader.load( "../examples/textures/planets/earth_specular_2048.jpg", function( map ) {
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					ballMat.roughnessMap = map;
+					ballMat.needsUpdate = true;
+				} );
+
+				var floorGeometry = new THREE.PlaneBufferGeometry( 20, 20 );
+				var floorMesh = new THREE.Mesh( floorGeometry, floorMat );
+				floorMesh.receiveShadow = true;
+				floorMesh.rotation.x = -Math.PI / 2.0;
+				scene.add( floorMesh );
+
+				var ballGeometry = new THREE.SphereGeometry( 0.1213, 32, 32 );
+				var ballMesh = new THREE.Mesh( ballGeometry, ballMat );
+				ballMesh.position.set( 1, 0.1213, 1 );
+				ballMesh.castShadow = true;
+				scene.add( ballMesh );
+
+				var boxGeometry = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
+				var boxMesh = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh.position.set( -0.5, 0.25, -1 );
+				boxMesh.castShadow = true;
+				scene.add( boxMesh );
+
+				var boxMesh2 = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh2.position.set( 0, 0.25, -5 );
+				boxMesh2.castShadow = true;
+				scene.add( boxMesh2 );
+
+				var boxMesh3 = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh3.position.set( 7, 0.25, 0 );
+				boxMesh3.castShadow = true;
+				scene.add( boxMesh3 );
+
+
+				renderer = new THREE.WebGLRenderer();
+				renderer.physicallyCorrectLights = true;
+				renderer.gammaInput = true;
+				renderer.gammaOutput = true;
+				renderer.shadowMap.enabled = true;
+				renderer.toneMapping = THREE.ReinhardToneMapping;
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+
+				var controls = new THREE.OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 0, 0 );
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+
+				var gui = new dat.GUI();
+
+				gui.add( params, 'hemiIrradiance', Object.keys( hemiLuminousIrradiances ) );
+				gui.add( params, 'bulbPower', Object.keys( bulbLuminousPowers ) );
+				gui.add( params, 'exposure', 0, 1 );
+				gui.add( params, 'shadows' );
+				gui.open();
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+
+			}
+
+			var previousShadowMap = false;
+
+			function render() {
+
+				renderer.toneMappingExposure = Math.pow( params.exposure, 5.0 ); // to allow for very bright scenes.
+				renderer.shadowMap.enabled = params.shadows;
+				bulbLight.castShadow = params.shadows;
+				if( params.shadows !== previousShadowMap ) {
+					ballMat.needsUpdate = true;
+					cubeMat.needsUpdate = true;
+					floorMat.needsUpdate = true;
+					previousShadowMap = params.shadows;
+				}
+				bulbLight.power = bulbLuminousPowers[ params.bulbPower ];
+				bulbMat.emissiveIntensity = bulbLight.intensity / Math.pow( 0.02, 2.0 ); // convert from intensity to irradiance at bulb surface
+
+				hemiLight.intensity = hemiLuminousIrradiances[ params.hemiIrradiance ];
+				var time = Date.now() * 0.0005;
+				var delta = clock.getDelta();
+
+				bulbLight.position.y = Math.cos( time ) * 0.75 + 1.25;
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 20 - 0
src/lights/PointLight.js

@@ -19,6 +19,26 @@ THREE.PointLight = function ( color, intensity, distance, decay ) {
 THREE.PointLight.prototype = Object.create( THREE.Light.prototype );
 THREE.PointLight.prototype.constructor = THREE.PointLight;
 
+Object.defineProperty( THREE.PointLight.prototype, "power", {
+
+	get: function () {
+
+		// intensity = power per solid angle.
+		// ref: equation (15) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+		return this.intensity * 4 * Math.PI;
+
+	},
+
+	set: function ( power ) {
+
+		// intensity = power per solid angle.
+		// ref: equation (15) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+		this.intensity = power / ( 4 * Math.PI );
+
+	}
+
+} );
+
 THREE.PointLight.prototype.copy = function ( source ) {
 
 	THREE.Light.prototype.copy.call( this, source );

+ 20 - 0
src/lights/SpotLight.js

@@ -25,6 +25,26 @@ THREE.SpotLight = function ( color, intensity, distance, angle, penumbra, decay
 THREE.SpotLight.prototype = Object.create( THREE.Light.prototype );
 THREE.SpotLight.prototype.constructor = THREE.SpotLight;
 
+Object.defineProperty( THREE.SpotLight.prototype, "power", {
+
+	get: function () {
+
+		// intensity = power per solid angle.
+		// ref: equation (17) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+		return this.intensity * Math.PI;
+
+	},
+
+	set: function ( power ) {
+
+		// intensity = power per solid angle.
+		// ref: equation (17) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+		this.intensity = power / Math.PI;
+
+	}
+
+} );
+
 THREE.SpotLight.prototype.copy = function ( source ) {
 
 	THREE.Light.prototype.copy.call( this, source );

+ 4 - 0
src/renderers/WebGLRenderer.js

@@ -55,6 +55,10 @@ THREE.WebGLRenderer = function ( parameters ) {
 	this.gammaInput = false;
 	this.gammaOutput = false;
 
+	// physical lights
+
+	this.physicallyCorrectLights = false;
+
 	// tone mapping
 
 	this.toneMapping = THREE.LinearToneMapping;

+ 15 - 0
src/renderers/shaders/ShaderChunk/ambient_pars.glsl

@@ -0,0 +1,15 @@
+uniform vec3 ambientLightColor;
+
+vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
+
+	vec3 irradiance = ambientLightColor;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI;
+
+	#endif
+
+	return irradiance;
+
+}

+ 24 - 12
src/renderers/shaders/ShaderChunk/bsdfs.glsl

@@ -4,18 +4,30 @@ bool testLightInRange( const in float lightDistance, const in float cutoffDistan
 
 }
 
-float calcLightAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {
+float punctualLightIntensityToIrradianceFactor( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {
 
-	if ( decayExponent > 0.0 ) {
+		if( decayExponent > 0.0 ) {
 
-	  return pow( saturate( -lightDistance / cutoffDistance + 1.0 ), decayExponent );
+#if defined ( PHYSICALLY_CORRECT_LIGHTS )
 
-	}
+			// based upon Frostbite 3 Moving to Physically-based Rendering
+			// http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+			// this is intended to be used on spot and point lights who are represented as luminous intensity
+			// but who must be converted to luminous irradiance for surface lighting calculation
+			float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );
+			float maxDistanceCutoffFactor = exp2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ) ;
+			return distanceFalloff * maxDistanceCutoffFactor;
 
-	return 1.0;
+#else
 
-}
+			return pow( saturate( -lightDistance / cutoffDistance + 1.0 ), decayExponent );
+
+#endif
 
+		}
+
+		return 1.0;
+}
 
 vec3 BRDF_Diffuse_Lambert( const in vec3 diffuseColor ) {
 
@@ -46,9 +58,9 @@ float G_GGX_Smith( const in float alpha, const in float dotNL, const in float do
 
 	float a2 = alpha * alpha;
 
-	float gl = dotNL + pow( a2 + ( 1.0 - a2 ) * dotNL * dotNL, 0.5 );
+	float gl = dotNL + pow( a2 + ( 1.0 - a2 ) * pow2( dotNL ), 0.5 );
 
-	float gv = dotNV + pow( a2 + ( 1.0 - a2 ) * dotNV * dotNV, 0.5 );
+	float gv = dotNV + pow( a2 + ( 1.0 - a2 ) * pow2( dotNV ), 0.5 );
 
 	return 1.0 / ( gl * gv );
 
@@ -60,11 +72,11 @@ float G_GGX_Smith( const in float alpha, const in float dotNL, const in float do
 // alpha is "roughness squared" in Disney’s reparameterization
 float D_GGX( const in float alpha, const in float dotNH ) {
 
-	float a2 = alpha * alpha;
+	float a2 = pow2( alpha );
 
-	float denom = dotNH * dotNH * ( a2 - 1.0 ) + 1.0; // avoid alpha = 0 with dotNH = 1
+	float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; // avoid alpha = 0 with dotNH = 1
 
-	return RECIPROCAL_PI * a2 / ( denom * denom );
+	return RECIPROCAL_PI * a2 / pow2( denom );
 
 }
 
@@ -146,7 +158,7 @@ vec3 BRDF_Specular_BlinnPhong( const in IncidentLight incidentLight, const in Ge
 
 // source: http://simonstechblog.blogspot.ca/2011/12/microfacet-brdf.html
 float GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {
-	return ( 2.0 / square( ggxRoughness + 0.0001 ) - 2.0 );
+	return ( 2.0 / pow2( ggxRoughness + 0.0001 ) - 2.0 );
 }
 
 float BlinnExponentToGGXRoughness( const in float blinnExponent ) {

+ 3 - 1
src/renderers/shaders/ShaderChunk/common.glsl

@@ -8,7 +8,9 @@
 #define saturate(a) clamp( a, 0.0, 1.0 )
 #define whiteCompliment(a) ( 1.0 - saturate( a ) )
 
-float square( const in float x ) { return x*x; }
+float pow2( const in float x ) { return x*x; }
+float pow3( const in float x ) { return x*x*x; }
+float pow4( const in float x ) { float x2 = x*x; return x2*x2; }
 float average( const in vec3 color ) { return dot( color, vec3( 0.3333 ) ); }
 
 

+ 1 - 1
src/renderers/shaders/ShaderChunk/emissivemap_fragment.glsl

@@ -4,6 +4,6 @@
 
 	emissiveColor.rgb = emissiveMapTexelToLinear( emissiveColor ).rgb;
 
-	totalEmissiveLight *= emissiveColor.rgb;
+	totalEmissiveRadiance *= emissiveColor.rgb;
 
 #endif

+ 3 - 3
src/renderers/shaders/ShaderChunk/lights_lambert_vertex.glsl

@@ -24,7 +24,7 @@ vec3 directLightColor_Diffuse;
 
 	for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
 
-		directLight = getPointDirectLight( pointLights[ i ], geometry );
+		directLight = getPointDirectLightIrradiance( pointLights[ i ], geometry );
 
 		dotNL = dot( geometry.normal, directLight.direction );
 		directLightColor_Diffuse = PI * directLight.color;
@@ -45,7 +45,7 @@ vec3 directLightColor_Diffuse;
 
 	for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
 
-		directLight = getSpotDirectLight( spotLights[ i ], geometry );
+		directLight = getSpotDirectLightIrradiance( spotLights[ i ], geometry );
 
 		dotNL = dot( geometry.normal, directLight.direction );
 		directLightColor_Diffuse = PI * directLight.color;
@@ -65,7 +65,7 @@ vec3 directLightColor_Diffuse;
 
 	for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
 
-		directLight = getDirectionalDirectLight( directionalLights[ i ], geometry );
+		directLight = getDirectionalDirectLightIrradiance( directionalLights[ i ], geometry );
 
 		dotNL = dot( geometry.normal, directLight.direction );
 		directLightColor_Diffuse = PI * directLight.color;

+ 27 - 9
src/renderers/shaders/ShaderChunk/lights_pars.glsl

@@ -2,7 +2,15 @@ uniform vec3 ambientLightColor;
 
 vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 
-	return PI * ambientLightColor;
+	vec3 irradiance = ambientLightColor;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI;
+
+	#endif
+
+	return irradiance;
 
 }
 
@@ -20,7 +28,7 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 
 	uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
 
-	IncidentLight getDirectionalDirectLight( const in DirectionalLight directionalLight, const in GeometricContext geometry ) {
+	IncidentLight getDirectionalDirectLightIrradiance( const in DirectionalLight directionalLight, const in GeometricContext geometry ) {
 
 		IncidentLight directLight;
 
@@ -51,7 +59,7 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 
 	uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
 
-	IncidentLight getPointDirectLight( const in PointLight pointLight, const in GeometricContext geometry ) {
+	IncidentLight getPointDirectLightIrradiance( const in PointLight pointLight, const in GeometricContext geometry ) {
 
 		IncidentLight directLight;
 
@@ -63,7 +71,8 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 		if ( testLightInRange( lightDistance, pointLight.distance ) ) {
 
 			directLight.color = pointLight.color;
-			directLight.color *= calcLightAttenuation( lightDistance, pointLight.distance, pointLight.decay );
+			directLight.color *= punctualLightIntensityToIrradianceFactor( lightDistance, pointLight.distance, pointLight.decay );
+
 			directLight.visible = true;
 
 		} else {
@@ -99,7 +108,7 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 
 	uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];
 
-	IncidentLight getSpotDirectLight( const in SpotLight spotLight, const in GeometricContext geometry ) {
+	IncidentLight getSpotDirectLightIrradiance( const in SpotLight spotLight, const in GeometricContext geometry ) {
 
 		IncidentLight directLight;
 
@@ -114,7 +123,8 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 			float spotEffect = smoothstep( spotLight.coneCos, spotLight.penumbraCos, angleCos );
 
 			directLight.color = spotLight.color;
-			directLight.color *= ( spotEffect * calcLightAttenuation( lightDistance, spotLight.distance, spotLight.decay ) );
+			directLight.color *= spotEffect * punctualLightIntensityToIrradianceFactor( lightDistance, spotLight.distance, spotLight.decay );
+
 			directLight.visible = true;
 
 		} else {
@@ -146,7 +156,15 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 		float dotNL = dot( geometry.normal, hemiLight.direction );
 		float hemiDiffuseWeight = 0.5 * dotNL + 0.5;
 
-		return PI * mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );
+		vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );
+
+		#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+			irradiance *= PI;
+
+		#endif
+
+		return irradiance;
 
 	}
 
@@ -208,10 +226,10 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
 	float getSpecularMIPLevel( const in float blinnShininessExponent, const in int maxMIPLevel ) {
 
 		//float envMapWidth = pow( 2.0, maxMIPLevelScalar );
-		//float desiredMIPLevel = log2( envMapWidth * sqrt( 3.0 ) ) - 0.5 * log2( square( blinnShininessExponent ) + 1.0 );
+		//float desiredMIPLevel = log2( envMapWidth * sqrt( 3.0 ) ) - 0.5 * log2( pow2( blinnShininessExponent ) + 1.0 );
 
 		float maxMIPLevelScalar = float( maxMIPLevel );
-		float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( square( blinnShininessExponent ) + 1.0 );
+		float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( pow2( blinnShininessExponent ) + 1.0 );
 
 		// clamp to allowable LOD ranges.
 		return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );

+ 7 - 1
src/renderers/shaders/ShaderChunk/lights_phong_pars_fragment.glsl

@@ -26,7 +26,13 @@ void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in Geometri
 
 	float dotNL = saturate( dot( geometry.normal, directLight.direction ) );
 
-	vec3 irradiance = dotNL * PI * directLight.color; // punctual light
+	vec3 irradiance = dotNL * directLight.color;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI; // punctual light
+
+	#endif
 
 	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
 	reflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;

+ 7 - 1
src/renderers/shaders/ShaderChunk/lights_standard_pars_fragment.glsl

@@ -10,7 +10,13 @@ void RE_Direct_Standard( const in IncidentLight directLight, const in GeometricC
 
 	float dotNL = saturate( dot( geometry.normal, directLight.direction ) );
 
-	vec3 irradiance = dotNL * PI * directLight.color; // punctual light
+	vec3 irradiance = dotNL * directLight.color;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI; // punctual light
+
+	#endif
 
 	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
 

+ 12 - 4
src/renderers/shaders/ShaderChunk/lights_template.glsl

@@ -29,7 +29,7 @@ IncidentLight directLight;
 
 		pointLight = pointLights[ i ];
 
-		directLight = getPointDirectLight( pointLight, geometry );
+		directLight = getPointDirectLightIrradiance( pointLight, geometry );
 
 		#ifdef USE_SHADOWMAP
 		directLight.color *= all( bvec2( pointLight.shadow, directLight.visible ) ) ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ] ) : 1.0;
@@ -49,7 +49,7 @@ IncidentLight directLight;
 
 		spotLight = spotLights[ i ];
 
-		directLight = getSpotDirectLight( spotLight, geometry );
+		directLight = getSpotDirectLightIrradiance( spotLight, geometry );
 
 		#ifdef USE_SHADOWMAP
 		directLight.color *= all( bvec2( spotLight.shadow, directLight.visible ) ) ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;
@@ -69,7 +69,7 @@ IncidentLight directLight;
 
 		directionalLight = directionalLights[ i ];
 
-		directLight = getDirectionalDirectLight( directionalLight, geometry );
+		directLight = getDirectionalDirectLightIrradiance( directionalLight, geometry );
 
 		#ifdef USE_SHADOWMAP
 		directLight.color *= all( bvec2( directionalLight.shadow, directLight.visible ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
@@ -87,7 +87,15 @@ IncidentLight directLight;
 
 	#ifdef USE_LIGHTMAP
 
-		irradiance += PI * texture2D( lightMap, vUv2 ).xyz * lightMapIntensity; // factor of PI should not be present; included here to prevent breakage
+		vec3 lightMapIrradiance = texture2D( lightMap, vUv2 ).xyz * lightMapIntensity;
+
+		#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+			lightMapIrradiance *= PI; // factor of PI should not be present; included here to prevent breakage
+
+		#endif
+
+		irradiance += lightMapIrradiance;
 
 	#endif
 

+ 2 - 2
src/renderers/shaders/ShaderLib/meshlambert_frag.glsl

@@ -32,7 +32,7 @@ void main() {
 
 	vec4 diffuseColor = vec4( diffuse, opacity );
 	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
-	vec3 totalEmissiveLight = emissive;
+	vec3 totalEmissiveRadiance = emissive;
 
 	#include <logdepthbuf_fragment>
 	#include <map_fragment>
@@ -64,7 +64,7 @@ void main() {
 	// modulation
 	#include <aomap_fragment>
 
-	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveLight;
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
 
 	#include <envmap_fragment>
 

+ 2 - 2
src/renderers/shaders/ShaderLib/meshphong_frag.glsl

@@ -30,7 +30,7 @@ void main() {
 
 	vec4 diffuseColor = vec4( diffuse, opacity );
 	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
-	vec3 totalEmissiveLight = emissive;
+	vec3 totalEmissiveRadiance = emissive;
 
 	#include <logdepthbuf_fragment>
 	#include <map_fragment>
@@ -48,7 +48,7 @@ void main() {
 	// modulation
 	#include <aomap_fragment>
 
-	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
 
 	#include <envmap_fragment>
 

+ 2 - 2
src/renderers/shaders/ShaderLib/meshstandard_frag.glsl

@@ -42,7 +42,7 @@ void main() {
 
 	vec4 diffuseColor = vec4( diffuse, opacity );
 	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
-	vec3 totalEmissiveLight = emissive;
+	vec3 totalEmissiveRadiance = emissive;
 
 	#include <logdepthbuf_fragment>
 	#include <map_fragment>
@@ -62,7 +62,7 @@ void main() {
 	// modulation
 	#include <aomap_fragment>
 
-	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
 
 	gl_FragColor = vec4( outgoingLight, diffuseColor.a );
 

+ 2 - 0
src/renderers/webgl/WebGLProgram.js

@@ -524,6 +524,8 @@ THREE.WebGLProgram = ( function () {
 
 				parameters.premultipliedAlpha ? "#define PREMULTIPLIED_ALPHA" : '',
 
+				parameters.physicallyCorrectLights ? "#define PHYSICALLY_CORRECT_LIGHTS" : '',
+
 				parameters.logarithmicDepthBuffer ? '#define USE_LOGDEPTHBUF' : '',
 				parameters.logarithmicDepthBuffer && renderer.extensions.get( 'EXT_frag_depth' ) ? '#define USE_LOGDEPTHBUF_EXT' : '',
 

+ 2 - 1
src/renderers/webgl/WebGLPrograms.js

@@ -23,7 +23,7 @@ THREE.WebGLPrograms = function ( renderer, capabilities ) {
 		"maxBones", "useVertexTexture", "morphTargets", "morphNormals",
 		"maxMorphTargets", "maxMorphNormals", "premultipliedAlpha",
 		"numDirLights", "numPointLights", "numSpotLights", "numHemiLights",
-		"shadowMapEnabled", "pointLightShadows", "toneMapping",
+		"shadowMapEnabled", "pointLightShadows", "toneMapping", 'physicallyCorrectLights',
 		"shadowMapType",
 		"alphaTest", "doubleSided", "flipSided"
 	];
@@ -176,6 +176,7 @@ THREE.WebGLPrograms = function ( renderer, capabilities ) {
 			shadowMapType: renderer.shadowMap.type,
 
 			toneMapping: renderer.toneMapping,
+			physicallyCorrectLights: renderer.physicallyCorrectLights,
 
 			premultipliedAlpha: ( material.blending === THREE.PremultipliedAlphaBlending ),
 			alphaTest: material.alphaTest,