Browse Source

WebGLRenderer: Implemented DirectionalLightShadow autoFit.

Mr.doob 3 months ago
parent
commit
aa06ce41d0

+ 263 - 0
examples/reproduce_shadow.html

@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+	<title>three.js webgl - shadow map - automatic</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: #cce0ff;
+			color: #000;
+			margin: 0;
+		}
+
+		a {
+			color: #080;
+		}
+
+		canvas {
+			position: absolute;
+			top: 0;
+			left: 0;
+		}
+	</style>
+</head>
+
+<body>
+
+	<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - automatic shadow map<br />
+		<label><input type="checkbox" id="autoUpdate" checked> Auto Fit Shadow Camera</label><br />
+		Use WASD or Arrow Keys to drive the car.
+	</div>
+
+	<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+	<script type="module">
+
+		import * as THREE from 'three';
+		import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewer.js';
+
+		let camera, scene, renderer;
+		let light, shadowCameraHelper, hud;
+		let car;
+		let frameCount = 0;
+
+		const keys = {
+			ArrowUp: false,
+			ArrowDown: false,
+			ArrowLeft: false,
+			ArrowRight: false,
+			w: false,
+			s: false,
+			a: false,
+			d: false
+		};
+
+		const carSpeed = 0.5;
+		const carTurnSpeed = 0.05;
+
+		init();
+		animate();
+
+		function init() {
+
+			const container = document.createElement( 'div' );
+			document.body.appendChild( container );
+
+			// scene
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0xcce0ff );
+
+			// camera
+
+			camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+			camera.position.set( 0, 20, 40 );
+
+			// lights
+
+			scene.add( new THREE.AmbientLight( 0x666666 ) );
+
+			light = new THREE.DirectionalLight( 0xdfebff, 1.75 );
+			light.position.set( 50, 200, 100 );
+			light.castShadow = true;
+			light.shadow.mapSize.width = 2048;
+			light.shadow.mapSize.height = 2048;
+			light.shadow.autoFit = true;
+			light.shadow.cascadeCount = 3;
+
+			scene.add( light );
+
+			// helper
+
+			shadowCameraHelper = new THREE.CameraHelper( light.shadow.camera );
+			scene.add( shadowCameraHelper );
+
+			// ground with terrain
+
+			const groundGeometry = new THREE.PlaneGeometry( 2000, 2000, 100, 100 );
+			const positions = groundGeometry.attributes.position;
+
+			for ( let i = 0; i < positions.count; i ++ ) {
+
+				const x = positions.getX( i );
+				const z = positions.getY( i );
+				const y = Math.sin( x * 0.01 ) * 20 + Math.cos( z * 0.01 ) * 20;
+				positions.setZ( i, y );
+
+			}
+
+			groundGeometry.computeVertexNormals();
+
+			const groundMaterial = new THREE.MeshPhongMaterial( { color: 0x999999 } );
+			const ground = new THREE.Mesh( groundGeometry, groundMaterial );
+			ground.rotation.x = - Math.PI / 2;
+			ground.receiveShadow = true;
+			scene.add( ground );
+
+			// car
+
+			const geometry = new THREE.BoxGeometry( 5, 5, 10 );
+			const material = new THREE.MeshPhongMaterial( { color: 0xff0000 } );
+			car = new THREE.Mesh( geometry, material );
+			car.position.y = 2.5;
+			car.castShadow = true;
+			car.receiveShadow = true;
+			scene.add( car );
+
+			// cubes
+
+			for ( let i = 0; i < 200; i ++ ) {
+
+				const geometry = new THREE.BoxGeometry( 5, 100, 5 );
+				const material = new THREE.MeshPhongMaterial( { color: Math.random() * 0xffffff } );
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.position.x = Math.random() * 1000 - 500;
+				mesh.position.y = 10;
+				mesh.position.z = Math.random() * 1000 - 500;
+				mesh.castShadow = true;
+				mesh.receiveShadow = true;
+				scene.add( mesh );
+
+			}
+
+			// renderer
+
+			renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.shadowMap.enabled = true;
+			renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+			container.appendChild( renderer.domElement );
+
+			// hud
+
+			hud = new ShadowMapViewer( light );
+			hud.position.x = 10;
+			hud.position.y = 10;
+			hud.size.width = 256;
+			hud.size.height = 256;
+			hud.update();
+
+			// events
+
+			window.addEventListener( 'resize', onWindowResize );
+			window.addEventListener( 'keydown', onKeyDown );
+			window.addEventListener( 'keyup', onKeyUp );
+
+		}
+
+		function onWindowResize() {
+
+			camera.aspect = window.innerWidth / window.innerHeight;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize( window.innerWidth, window.innerHeight );
+
+			hud.updateForWindowResize();
+
+		}
+
+		function onKeyDown( event ) {
+
+			if ( keys.hasOwnProperty( event.key ) ) keys[ event.key ] = true;
+
+		}
+
+		function onKeyUp( event ) {
+
+			if ( keys.hasOwnProperty( event.key ) ) keys[ event.key ] = false;
+
+		}
+
+		//
+
+		function animate() {
+
+			requestAnimationFrame( animate );
+
+			const autoUpdate = document.getElementById( 'autoUpdate' ).checked;
+
+			light.shadow.autoFit = autoUpdate;
+
+			if ( car ) {
+
+				if ( keys.ArrowUp || keys.w ) car.translateZ( carSpeed );
+				if ( keys.ArrowDown || keys.s ) car.translateZ( - carSpeed );
+				if ( keys.ArrowLeft || keys.a ) car.rotateY( carTurnSpeed );
+				if ( keys.ArrowRight || keys.d ) car.rotateY( - carTurnSpeed );
+
+				// Update car height to follow terrain
+				const x = car.position.x;
+				const z = car.position.z;
+				const terrainHeight = Math.sin( x * 0.01 ) * 20 + Math.cos( z * 0.01 ) * 20;
+				car.position.y = terrainHeight + 2.5;
+
+				// Camera follow
+				const relativeCameraOffset = new THREE.Vector3( 0, 10, - 20 );
+				const cameraOffset = relativeCameraOffset.applyMatrix4( car.matrixWorld );
+
+				camera.position.lerp( cameraOffset, 0.1 );
+				camera.lookAt( car.position );
+
+			}
+
+			renderer.render( scene, camera );
+
+			hud.render( renderer );
+
+			shadowCameraHelper.update();
+
+			/*
+			if ( frameCount < 100 ) {
+
+				console.log( 'Shadow Camera:', {
+					left: light.shadow.camera.left,
+					right: light.shadow.camera.right,
+					top: light.shadow.camera.top,
+					bottom: light.shadow.camera.bottom,
+					near: light.shadow.camera.near,
+					far: light.shadow.camera.far,
+					position: light.shadow.camera.position.toArray(),
+					zoom: light.shadow.camera.zoom
+				} );
+				frameCount ++;
+		
+			}
+			*/
+
+		}
+
+	</script>
+</body>
+
+</html>

+ 45 - 0
src/lights/DirectionalLightShadow.js

@@ -1,5 +1,6 @@
 import { LightShadow } from './LightShadow.js';
 import { OrthographicCamera } from '../cameras/OrthographicCamera.js';
+import { Matrix4 } from '../math/Matrix4.js';
 
 /**
  * Represents the shadow configuration of directional lights.
@@ -24,8 +25,52 @@ class DirectionalLightShadow extends LightShadow {
 		 */
 		this.isDirectionalLightShadow = true;
 
+		this.autoFit = true;
+
+	}
+
+	updateMatrices( light ) {
+
+		if ( this.autoFit === true ) {
+
+			const shadowCamera = this.camera;
+			const shadowMatrix = this.matrix;
+
+			_projScreenMatrix.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
+			this._frustum.setFromProjectionMatrix( _projScreenMatrix, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );
+
+			if ( shadowCamera.reversedDepth ) {
+
+				shadowMatrix.set(
+					0.5, 0.0, 0.0, 0.5,
+					0.0, 0.5, 0.0, 0.5,
+					0.0, 0.0, 1.0, 0.0,
+					0.0, 0.0, 0.0, 1.0
+				);
+
+			} else {
+
+				shadowMatrix.set(
+					0.5, 0.0, 0.0, 0.5,
+					0.0, 0.5, 0.0, 0.5,
+					0.0, 0.0, 0.5, 0.5,
+					0.0, 0.0, 0.0, 1.0
+				);
+
+			}
+
+			shadowMatrix.multiply( _projScreenMatrix );
+
+		} else {
+
+			super.updateMatrices( light );
+
+		}
+
 	}
 
 }
 
+const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
+
 export { DirectionalLightShadow };

+ 86 - 0
src/renderers/webgl/WebGLShadowMap.js

@@ -7,7 +7,10 @@ import { BufferAttribute } from '../../core/BufferAttribute.js';
 import { BufferGeometry } from '../../core/BufferGeometry.js';
 import { Mesh } from '../../objects/Mesh.js';
 import { Vector4 } from '../../math/Vector4.js';
+import { Vector3 } from '../../math/Vector3.js';
 import { Vector2 } from '../../math/Vector2.js';
+import { Matrix4 } from '../../math/Matrix4.js';
+import { Box3 } from '../../math/Box3.js';
 import { Frustum } from '../../math/Frustum.js';
 
 import * as vsm from '../shaders/ShaderLib/vsm.glsl.js';
@@ -22,6 +25,26 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
 
 		_viewport = new Vector4(),
 
+		_projScreenMatrix = new Matrix4(),
+		_shadowCameraInverseMatrix = new Matrix4(),
+		_lightDirection = new Vector3(),
+		_lightPositionWorld = new Vector3(),
+		_targetPositionWorld = new Vector3(),
+		_frustumCenter = new Vector3(),
+		_shadowCameraViewMatrix = new Matrix4(),
+		_shadowBox = new Box3(),
+		_tempVector3 = new Vector3(),
+		_frustumCornersNDC = [
+			new Vector3( - 1, - 1, - 1 ), new Vector3( 1, - 1, - 1 ),
+			new Vector3( - 1, 1, - 1 ), new Vector3( 1, 1, - 1 ),
+			new Vector3( - 1, - 1, 1 ), new Vector3( 1, - 1, 1 ),
+			new Vector3( - 1, 1, 1 ), new Vector3( 1, 1, 1 )
+		],
+		_frustumCorners = [
+			new Vector3(), new Vector3(), new Vector3(), new Vector3(),
+			new Vector3(), new Vector3(), new Vector3(), new Vector3()
+		],
+
 		_depthMaterial = new MeshDepthMaterial( { depthPacking: RGBADepthPacking } ),
 		_distanceMaterial = new MeshDistanceMaterial(),
 
@@ -120,6 +143,69 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
 
 			if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;
 
+			if ( shadow.isDirectionalLightShadow && shadow.autoFit ) {
+
+				// Calculate view frustum corners in world space
+				_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
+				_frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
+				_shadowCameraInverseMatrix.copy( _projScreenMatrix ).invert();
+
+				_frustumCenter.set( 0, 0, 0 );
+
+				for ( let i = 0; i < 8; i ++ ) {
+
+					_frustumCorners[ i ].copy( _frustumCornersNDC[ i ] ).applyMatrix4( _shadowCameraInverseMatrix );
+					_frustumCenter.add( _frustumCorners[ i ] );
+
+				}
+
+				_frustumCenter.multiplyScalar( 1 / 8 );
+
+				// Position shadow camera to look at frustum center
+				_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
+				_targetPositionWorld.setFromMatrixPosition( light.target.matrixWorld );
+				_lightDirection.subVectors( _targetPositionWorld, _lightPositionWorld ).normalize();
+
+				shadow.camera.position.copy( _frustumCenter ).addScaledVector( _lightDirection, - 500 );
+				shadow.camera.lookAt( _frustumCenter );
+				shadow.camera.updateMatrixWorld();
+
+				// Calculate shadow camera bounds
+				_shadowCameraViewMatrix.copy( shadow.camera.matrixWorld ).invert();
+				_shadowBox.makeEmpty();
+
+				for ( let i = 0; i < 8; i ++ ) {
+
+					_tempVector3.copy( _frustumCorners[ i ] ).applyMatrix4( _shadowCameraViewMatrix );
+					_shadowBox.expandByPoint( _tempVector3 );
+
+				}
+
+				// Snap to texel grid to stabilize shadows
+				const shadowMapSize = shadow.mapSize;
+				const xPixelSize = ( _shadowBox.max.x - _shadowBox.min.x ) / shadowMapSize.x;
+				const yPixelSize = ( _shadowBox.max.y - _shadowBox.min.y ) / shadowMapSize.y;
+				const originX = _shadowCameraViewMatrix.elements[ 12 ];
+				const originY = _shadowCameraViewMatrix.elements[ 13 ];
+
+				_shadowBox.min.x = Math.floor( ( _shadowBox.min.x - originX ) / xPixelSize ) * xPixelSize + originX;
+				_shadowBox.max.x = Math.ceil( ( _shadowBox.max.x - originX ) / xPixelSize ) * xPixelSize + originX;
+				_shadowBox.min.y = Math.floor( ( _shadowBox.min.y - originY ) / yPixelSize ) * yPixelSize + originY;
+				_shadowBox.max.y = Math.ceil( ( _shadowBox.max.y - originY ) / yPixelSize ) * yPixelSize + originY;
+
+				// Update shadow camera properties
+				const zRange = _shadowBox.max.z - _shadowBox.min.z;
+				shadow.camera.left = _shadowBox.min.x;
+				shadow.camera.right = _shadowBox.max.x;
+				shadow.camera.top = _shadowBox.max.y;
+				shadow.camera.bottom = _shadowBox.min.y;
+				shadow.camera.near = - _shadowBox.max.z - zRange;
+				shadow.camera.far = - _shadowBox.min.z + zRange * 0.1;
+
+				shadow.camera.updateProjectionMatrix();
+
+			}
+
 			_shadowMapSize.copy( shadow.mapSize );
 
 			const shadowFrameExtents = shadow.getFrameExtents();

粤ICP备19079148号