ソースを参照

Addons: WebGPU CSM shadows - using shadowNode (#29610)

* CSM shadows - using shadowNode

* remove accidentally included file

* remove test logging

* Update CSMShadowNode.js

Fix main frustum with WebGL backend.

* rework

* handle camera changes

* CSMShadowNode: Clean up.

* CMSShadowNode: Fix cascade sampling.

* E2E: Update screenshot.

* CSMShadowNode: Clean up.

---------

Co-authored-by: aardgoose <angus.sawyer@email.com>
Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
aardgoose 1 年間 前
コミット
59100c44cb

+ 1 - 0
examples/files.json

@@ -411,6 +411,7 @@
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
 		"webgpu_shadowmap",
+		"webgpu_shadowmap_csm",
 		"webgpu_shadowmap_opacity",
 		"webgpu_shadowmap_progressive",
 		"webgpu_shadowmap_vsm",

+ 2 - 2
examples/jsm/csm/CSM.js

@@ -11,7 +11,7 @@ import { CSMFrustum } from './CSMFrustum.js';
 import { CSMShader } from './CSMShader.js';
 
 const _cameraToLightMatrix = new Matrix4();
-const _lightSpaceFrustum = new CSMFrustum();
+const _lightSpaceFrustum = new CSMFrustum( { webGL: true } );
 const _center = new Vector3();
 const _bbox = new Box3();
 const _uniformArray = [];
@@ -38,7 +38,7 @@ export class CSM {
 		this.lightMargin = data.lightMargin || 200;
 		this.customSplitsCallback = data.customSplitsCallback;
 		this.fade = false;
-		this.mainFrustum = new CSMFrustum();
+		this.mainFrustum = new CSMFrustum( { webGL: true } );
 		this.frustums = [];
 		this.breaks = [];
 

+ 7 - 4
examples/jsm/csm/CSMFrustum.js

@@ -8,6 +8,8 @@ class CSMFrustum {
 
 		data = data || {};
 
+		this.zNear = data.webGL === true ? - 1 : 0;
+
 		this.vertices = {
 			near: [
 				new Vector3(),
@@ -33,6 +35,7 @@ class CSMFrustum {
 
 	setFromProjectionMatrix( projectionMatrix, maxFar ) {
 
+		const zNear = this.zNear;
 		const isOrthographic = projectionMatrix.elements[ 2 * 4 + 3 ] === 0;
 
 		inverseProjectionMatrix.copy( projectionMatrix ).invert();
@@ -42,10 +45,10 @@ class CSMFrustum {
 		// 2 --- 1
 		// clip space spans from [-1, 1]
 
-		this.vertices.near[ 0 ].set( 1, 1, - 1 );
-		this.vertices.near[ 1 ].set( 1, - 1, - 1 );
-		this.vertices.near[ 2 ].set( - 1, - 1, - 1 );
-		this.vertices.near[ 3 ].set( - 1, 1, - 1 );
+		this.vertices.near[ 0 ].set( 1, 1, zNear );
+		this.vertices.near[ 1 ].set( 1, - 1, zNear );
+		this.vertices.near[ 2 ].set( - 1, - 1, zNear );
+		this.vertices.near[ 3 ].set( - 1, 1, zNear );
 		this.vertices.near.forEach( function ( v ) {
 
 			v.applyMatrix4( inverseProjectionMatrix );

+ 2 - 0
examples/jsm/csm/CSMHelper.js

@@ -78,6 +78,8 @@ class CSMHelper extends Group {
 		const cascadePlanes = this.cascadePlanes;
 		const shadowLines = this.shadowLines;
 
+		if ( camera === null ) return;
+
 		this.position.copy( camera.position );
 		this.quaternion.copy( camera.quaternion );
 		this.scale.copy( camera.scale );

+ 435 - 0
examples/jsm/csm/CSMShadowNode.js

@@ -0,0 +1,435 @@
+import {
+	Vector2,
+	Vector3,
+	MathUtils,
+	Matrix4,
+	Box3,
+	Object3D,
+	WebGLCoordinateSystem
+} from 'three';
+
+import { CSMFrustum } from './CSMFrustum.js';
+import { viewZToOrthographicDepth, reference, uniform, float, vec4, vec2, If, Fn, min, renderGroup, positionView, Node, NodeUpdateType, shadow } from 'three/tsl';
+
+const _cameraToLightMatrix = new Matrix4();
+const _lightSpaceFrustum = new CSMFrustum();
+const _center = new Vector3();
+const _bbox = new Box3();
+const _uniformArray = [];
+const _logArray = [];
+const _lightDirection = new Vector3();
+const _lightOrientationMatrix = new Matrix4();
+const _lightOrientationMatrixInverse = new Matrix4();
+const _up = new Vector3( 0, 1, 0 );
+
+class LwLight extends Object3D {
+
+	constructor() {
+
+		super();
+
+		this.target = new Object3D();
+
+	}
+
+}
+
+class CSMShadowNode extends Node {
+
+	constructor( light, data = {} ) {
+
+		super();
+
+		this.light = light;
+		this.camera = null;
+		this.cascades = data.cascades || 3;
+		this.maxFar = data.maxFar || 100000;
+		this.mode = data.mode || 'practical';
+		this.lightMargin = data.lightMargin || 200;
+		this.customSplitsCallback = data.customSplitsCallback;
+
+		this.fade = false;
+
+		this.breaks = [];
+
+		this._cascades = [];
+		this.mainFrustum = null;
+		this.frustums = [];
+		this.updateBeforeType = NodeUpdateType.FRAME;
+
+		this.lights = [];
+
+		this._shadowNodes = [];
+
+	}
+
+	init( { camera, renderer } ) {
+
+		this.camera = camera;
+
+		const data = { webGL: renderer.coordinateSystem === WebGLCoordinateSystem };
+		this.mainFrustum = new CSMFrustum( data );
+
+		const light = this.light;
+		const parent = light.parent;
+
+		for ( let i = 0; i < this.cascades; i ++ ) {
+
+			const lwLight = new LwLight();
+			const lShadow = light.shadow.clone();
+			lShadow.bias = lShadow.bias * ( i + 1 );
+
+			this.lights.push( lwLight );
+
+			parent.add( lwLight );
+			parent.add( lwLight.target );
+
+			lwLight.shadow = lShadow;
+
+			this._shadowNodes.push( shadow( lwLight, lShadow ) );
+
+			this._cascades.push( new Vector2() );
+
+		}
+
+		this.updateFrustums();
+
+	}
+
+	initCascades() {
+
+		const camera = this.camera;
+		camera.updateProjectionMatrix();
+
+		this.mainFrustum.setFromProjectionMatrix( camera.projectionMatrix, this.maxFar );
+		this.mainFrustum.split( this.breaks, this.frustums );
+
+	}
+
+	getBreaks() {
+
+		const camera = this.camera;
+		const far = Math.min( camera.far, this.maxFar );
+
+		this.breaks.length = 0;
+
+		switch ( this.mode ) {
+
+			case 'uniform':
+				uniformSplit( this.cascades, camera.near, far, this.breaks );
+				break;
+
+			case 'logarithmic':
+				logarithmicSplit( this.cascades, camera.near, far, this.breaks );
+				break;
+
+			case 'practical':
+				practicalSplit( this.cascades, camera.near, far, 0.5, this.breaks );
+				break;
+
+			case 'custom':
+				if ( this.customSplitsCallback === undefined ) console.error( 'CSM: Custom split scheme callback not defined.' );
+				this.customSplitsCallback( this.cascades, camera.near, far, this.breaks );
+				break;
+
+		}
+
+		function uniformSplit( amount, near, far, target ) {
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( ( near + ( far - near ) * i / amount ) / far );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+		function logarithmicSplit( amount, near, far, target ) {
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( ( near * ( far / near ) ** ( i / amount ) ) / far );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+		function practicalSplit( amount, near, far, lambda, target ) {
+
+			_uniformArray.length = 0;
+			_logArray.length = 0;
+			logarithmicSplit( amount, near, far, _logArray );
+			uniformSplit( amount, near, far, _uniformArray );
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+	}
+
+	setLightBreaks() {
+
+		for ( let i = 0, l = this.cascades; i < l; i ++ ) {
+
+			const amount = this.breaks[ i ];
+			const prev = this.breaks[ i - 1 ] || 0;
+
+			this._cascades[ i ].set( prev, amount );
+
+		}
+
+	}
+
+	updateShadowBounds() {
+
+		const frustums = this.frustums;
+
+		for ( let i = 0; i < frustums.length; i ++ ) {
+
+			const shadowCam = this.lights[ i ].shadow.camera;
+			const frustum = this.frustums[ i ];
+
+			// Get the two points that represent that furthest points on the frustum assuming
+			// that's either the diagonal across the far plane or the diagonal across the whole
+			// frustum itself.
+			const nearVerts = frustum.vertices.near;
+			const farVerts = frustum.vertices.far;
+			const point1 = farVerts[ 0 ];
+
+			let point2;
+
+			if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) {
+
+				point2 = farVerts[ 2 ];
+
+			} else {
+
+				point2 = nearVerts[ 2 ];
+
+			}
+
+			let squaredBBWidth = point1.distanceTo( point2 );
+
+			if ( this.fade ) {
+
+				// expand the shadow extents by the fade margin if fade is enabled.
+				const camera = this.camera;
+				const far = Math.max( camera.far, this.maxFar );
+				const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near );
+				const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near );
+
+				squaredBBWidth += margin;
+
+			}
+
+			shadowCam.left = - squaredBBWidth / 2;
+			shadowCam.right = squaredBBWidth / 2;
+			shadowCam.top = squaredBBWidth / 2;
+			shadowCam.bottom = - squaredBBWidth / 2;
+			shadowCam.updateProjectionMatrix();
+
+		}
+
+	}
+
+	updateFrustums() {
+
+		this.getBreaks();
+		this.initCascades();
+		this.updateShadowBounds();
+		this.setLightBreaks();
+
+	}
+
+	setupFade() {
+
+		const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup ).label( 'cameraNear' );
+		const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cacades' );
+
+		const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
+			.onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
+
+		const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
+		const lastCascade = this.cascades - 1;
+
+		return Fn( () => {
+
+			const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
+
+			const cascade = vec2().toVar( 'cascade' );
+			const cascadeCenter = float().toVar( 'cascadeCenter' );
+
+			const margin = float().toVar( 'margin' );
+
+			const csmX = float().toVar( 'csmX' );
+			const csmY = float().toVar( 'csmY' );
+
+			for ( let i = 0; i < this.cascades; i ++ ) {
+
+				const isLastCascade = i === lastCascade;
+
+				cascade.assign( cascades.element( i ) );
+
+				cascadeCenter.assign( cascade.x.add( cascade.y ).div( 2.0 ) );
+
+				const closestEdge = linearDepth.lessThan( cascadeCenter ).select( cascade.x, cascade.y );
+
+				margin.assign( float( 0.25 ).mul( closestEdge.pow( 2.0 ) ) );
+
+				csmX.assign( cascade.x.sub( margin.div( 2.0 ) ) );
+
+				if ( isLastCascade ) {
+
+					csmY.assign( cascade.y );
+
+				} else {
+
+					csmY.assign( cascade.y.add( margin.div( 2.0 ) ) );
+
+				}
+
+				const inRange = linearDepth.greaterThanEqual( csmX ).and( linearDepth.lessThanEqual( csmY ) );
+
+				If( inRange, () => {
+
+					const dist = min( linearDepth.sub( csmX ), csmY.sub( linearDepth ) ).toVar();
+
+					let ratio = dist.div( margin ).clamp( 0.0, 1.0 );
+
+					if ( i === 0 ) {
+
+						// dont fade at nearest edge
+						ratio = linearDepth.greaterThan( cascadeCenter ).select( ratio, 1 );
+
+					}
+
+					ret.subAssign( this._shadowNodes[ i ].oneMinus().mul( ratio ) );
+
+				} );
+
+			}
+
+			return ret;
+
+		} )();
+
+	}
+
+	setupStandard() {
+
+		const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup ).label( 'cameraNear' );
+		const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cacades' );
+
+		const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
+			.onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
+
+		const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
+
+		return Fn( () => {
+
+			const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
+			const cascade = vec2().toVar( 'cascade' );
+
+			for ( let i = 0; i < this.cascades; i ++ ) {
+
+				cascade.assign( cascades.element( i ) );
+
+				If( linearDepth.greaterThanEqual( cascade.x ).and( linearDepth.lessThanEqual( cascade.y ) ), () => {
+
+					ret.assign( this._shadowNodes[ i ] );
+
+				} );
+
+			}
+
+			return ret;
+
+		} )();
+
+	}
+
+	setup( builder ) {
+
+		if ( this.camera === null ) this.init( builder );
+
+		return this.fade === true ? this.setupFade() : this.setupStandard();
+
+	}
+
+	updateBefore( /*builder*/ ) {
+
+		const light = this.light;
+		const camera = this.camera;
+		const frustums = this.frustums;
+
+		_lightDirection.subVectors( light.target.position, light.position ).normalize();
+
+		// for each frustum we need to find its min-max box aligned with the light orientation
+		// the position in _lightOrientationMatrix does not matter, as we transform there and back
+		_lightOrientationMatrix.lookAt( light.position, light.target.position, _up );
+		_lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert();
+
+		for ( let i = 0; i < frustums.length; i ++ ) {
+
+			const lwLight = this.lights[ i ];
+			const shadow = lwLight.shadow;
+			const shadowCam = shadow.camera;
+			const texelWidth = ( shadowCam.right - shadowCam.left ) / shadow.mapSize.width;
+			const texelHeight = ( shadowCam.top - shadowCam.bottom ) / shadow.mapSize.height;
+
+			_cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, camera.matrixWorld );
+			frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum );
+
+			const nearVerts = _lightSpaceFrustum.vertices.near;
+			const farVerts = _lightSpaceFrustum.vertices.far;
+
+			_bbox.makeEmpty();
+
+			for ( let j = 0; j < 4; j ++ ) {
+
+				_bbox.expandByPoint( nearVerts[ j ] );
+				_bbox.expandByPoint( farVerts[ j ] );
+
+			}
+
+			_bbox.getCenter( _center );
+			_center.z = _bbox.max.z + this.lightMargin;
+			_center.x = Math.floor( _center.x / texelWidth ) * texelWidth;
+			_center.y = Math.floor( _center.y / texelHeight ) * texelHeight;
+			_center.applyMatrix4( _lightOrientationMatrix );
+
+			lwLight.position.copy( _center );
+			lwLight.target.position.copy( _center );
+			lwLight.target.position.add( _lightDirection );
+
+		}
+
+	}
+
+	dispose() {
+
+		for ( let i = 0; i < this.lights.length; i ++ ) {
+
+			const light = this.lights[ i ];
+			const parent = light.parent;
+
+			parent.remove( light.target );
+			parent.remove( light );
+
+		}
+
+	}
+
+}
+
+export { CSMShadowNode };

BIN
examples/screenshots/webgpu_shadowmap_csm.jpg


+ 320 - 0
examples/webgpu_shadowmap_csm.html

@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - cascaded shadow maps</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - cascaded shadow maps<br>
+			by <a href="https://github.com/strandedkitty/" target="_blank" rel="noopener">StrandedKitty</a> (<a href="https://github.com/strandedkitty/three-csm" target="_blank" rel="noopener">original repository</a>)
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { CSMShadowNode } from 'three/addons/csm/CSMShadowNode.js';
+			import { CSMHelper } from 'three/addons/csm/CSMHelper.js';
+
+			let renderer, scene, camera, orthoCamera, controls, csm, csmHelper, csmDirectionalLight;
+
+			const params = {
+				orthographic: false,
+				fade: false,
+				shadows: true,
+				maxFar: 1000,
+				mode: 'practical',
+				lightX: - 1,
+				lightY: - 1,
+				lightZ: - 1,
+				margin: 100,
+				shadowNear: 1,
+				shadowFar: 2000,
+				autoUpdateHelper: true,
+				updateHelper: function () {
+
+					csmHelper.update();
+
+				}
+			};
+
+			init();
+
+			function updateOrthoCamera() {
+
+				const size = controls.target.distanceTo( camera.position );
+				const aspect = camera.aspect;
+
+				orthoCamera.left = size * aspect / - 2;
+				orthoCamera.right = size * aspect / 2;
+
+				orthoCamera.top = size / 2;
+				orthoCamera.bottom = size / - 2;
+				orthoCamera.position.copy( camera.position );
+				orthoCamera.rotation.copy( camera.rotation );
+				orthoCamera.updateProjectionMatrix();
+
+			}
+
+			function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( '#454e61' );
+				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 5000 );
+				orthoCamera = new THREE.OrthographicCamera();
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+				renderer.shadowMap.enabled = params.shadows;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.maxPolarAngle = Math.PI / 2;
+				camera.position.set( 60, 60, 0 );
+				controls.target = new THREE.Vector3( - 100, 10, 0 );
+				controls.update();
+
+				const ambientLight = new THREE.AmbientLight( 0xffffff, 1.5 );
+				scene.add( ambientLight );
+
+				const additionalDirectionalLight = new THREE.DirectionalLight( 0x000020, 1.5 );
+				additionalDirectionalLight.position.set( params.lightX, params.lightY, params.lightZ ).normalize().multiplyScalar( - 200 );
+				scene.add( additionalDirectionalLight );
+
+				csmDirectionalLight = new THREE.DirectionalLight( 0xffffff, 3.0 );
+
+				csmDirectionalLight.castShadow = true;
+				csmDirectionalLight.shadow.mapSize.width = 2048;
+				csmDirectionalLight.shadow.mapSize.height = 2048;
+				csmDirectionalLight.shadow.camera.near = params.shadowNear;
+				csmDirectionalLight.shadow.camera.far = params.shadowFar;
+				csmDirectionalLight.shadow.camera.top = 1000;
+				csmDirectionalLight.shadow.camera.bottom = - 1000;
+				csmDirectionalLight.shadow.camera.left = - 1000;
+				csmDirectionalLight.shadow.camera.right = 1000;
+				csmDirectionalLight.shadow.bias = - 0.001;
+
+				csm = new CSMShadowNode( csmDirectionalLight, { cascades: 4, maxFar: params.maxFar, mode: params.mode } );
+
+				csmDirectionalLight.position.set( params.lightX, params.lightY, params.lightZ ).normalize().multiplyScalar( - 200 );
+
+				csmDirectionalLight.shadow.shadowNode = csm;
+
+				scene.add( csmDirectionalLight );
+
+				csmHelper = new CSMHelper( csm );
+				csmHelper.visible = false;
+				scene.add( csmHelper );
+
+				const floorMaterial = new THREE.MeshPhongMaterial( { color: '#252a34' } );
+
+				const floor = new THREE.Mesh( new THREE.PlaneGeometry( 10000, 10000, 8, 8 ), floorMaterial );
+				floor.rotation.x = - Math.PI / 2;
+				floor.castShadow = true;
+				floor.receiveShadow = true;
+				scene.add( floor );
+
+				const material1 = new THREE.MeshPhongMaterial( { color: '#08d9d6' } );
+
+				const material2 = new THREE.MeshPhongMaterial( { color: '#ff2e63' } );
+
+				const geometry = new THREE.BoxGeometry( 10, 10, 10 );
+
+				for ( let i = 0; i < 40; i ++ ) {
+
+					const cube1 = new THREE.Mesh( geometry, i % 2 === 0 ? material1 : material2 );
+					cube1.castShadow = true;
+					cube1.receiveShadow = true;
+					scene.add( cube1 );
+					cube1.position.set( - i * 25, 20, 30 );
+					cube1.scale.y = Math.random() * 2 + 6;
+
+					const cube2 = new THREE.Mesh( geometry, i % 2 === 0 ? material2 : material1 );
+					cube2.castShadow = true;
+					cube2.receiveShadow = true;
+					scene.add( cube2 );
+					cube2.position.set( - i * 25, 20, - 30 );
+					cube2.scale.y = Math.random() * 2 + 6;
+
+				}
+
+				const gui = new GUI();
+
+				gui.add( params, 'orthographic' ).onChange( function ( value ) {
+
+					csm.camera = value ? orthoCamera : camera;
+					csm.updateFrustums();
+
+				} );
+
+				// gui.add( params, 'fade' ).onChange( function ( value ) {
+
+				// csm.fade = value;
+				// csm.updateFrustums();
+				// TODO: Changing "fade" requires toggling shadows right now
+
+				// } );
+
+				gui.add( params, 'shadows' ).onChange( function ( value ) {
+
+					renderer.shadowMap.enabled = value;
+
+				} );
+
+				gui.add( params, 'maxFar', 1, 5000 ).step( 1 ).name( 'max shadow far' ).onChange( function ( value ) {
+
+					csm.maxFar = value;
+					csm.updateFrustums();
+
+				} );
+
+				gui.add( params, 'mode', [ 'uniform', 'logarithmic', 'practical' ] ).name( 'frustum split mode' ).onChange( function ( value ) {
+
+					csm.mode = value;
+					csm.updateFrustums();
+
+				} );
+
+				gui.add( params, 'lightX', - 1, 1 ).name( 'light direction x' ).onChange( function () {
+
+					csmDirectionalLight.position.set( params.lightX, params.lightY, params.lightZ ).normalize().multiplyScalar( - 200 );
+
+				} );
+
+				gui.add( params, 'lightY', - 1, 1 ).name( 'light direction y' ).onChange( function () {
+
+					csmDirectionalLight.position.set( params.lightX, params.lightY, params.lightZ ).normalize().multiplyScalar( - 200 );
+
+				} );
+
+				gui.add( params, 'lightZ', - 1, 1 ).name( 'light direction z' ).onChange( function () {
+
+					csmDirectionalLight.position.set( params.lightX, params.lightY, params.lightZ ).normalize().multiplyScalar( - 200 );
+
+				} );
+
+				gui.add( params, 'margin', 0, 200 ).name( 'light margin' ).onChange( function ( value ) {
+
+					csm.lightMargin = value;
+
+				} );
+
+				gui.add( params, 'shadowNear', 1, 10000 ).name( 'shadow near' ).onChange( function ( value ) {
+
+					for ( let i = 0; i < csm.lights.length; i ++ ) {
+
+						csm.lights[ i ].shadow.camera.near = value;
+						csm.lights[ i ].shadow.camera.updateProjectionMatrix();
+
+					}
+
+				} );
+
+				gui.add( params, 'shadowFar', 1, 10000 ).name( 'shadow far' ).onChange( function ( value ) {
+
+					for ( let i = 0; i < csm.lights.length; i ++ ) {
+
+						csm.lights[ i ].shadow.camera.far = value;
+						csm.lights[ i ].shadow.camera.updateProjectionMatrix();
+
+					}
+
+				} );
+
+				const helperFolder = gui.addFolder( 'helper' );
+
+				helperFolder.add( csmHelper, 'visible' );
+
+				helperFolder.add( csmHelper, 'displayFrustum' ).onChange( function () {
+
+					csmHelper.updateVisibility();
+
+				} );
+
+				helperFolder.add( csmHelper, 'displayPlanes' ).onChange( function () {
+
+					csmHelper.updateVisibility();
+
+				} );
+
+				helperFolder.add( csmHelper, 'displayShadowBounds' ).onChange( function () {
+
+					csmHelper.updateVisibility();
+
+				} );
+
+				helperFolder.add( params, 'autoUpdateHelper' ).name( 'auto update' );
+
+				helperFolder.add( params, 'updateHelper' ).name( 'update' );
+
+				helperFolder.open();
+
+				window.addEventListener( 'resize', function () {
+
+					camera.aspect = window.innerWidth / window.innerHeight;
+					camera.updateProjectionMatrix();
+
+					updateOrthoCamera();
+					csm.updateFrustums();
+
+					renderer.setSize( window.innerWidth, window.innerHeight );
+
+				} );
+
+			}
+
+			function animate() {
+
+				camera.updateMatrixWorld();
+				controls.update();
+
+				if ( params.orthographic ) {
+
+					updateOrthoCamera();
+					csm.updateFrustums();
+
+					if ( params.autoUpdateHelper ) {
+
+						csmHelper.update();
+
+					}
+
+					renderer.render( scene, orthoCamera );
+
+				} else {
+
+					if ( params.autoUpdateHelper ) {
+
+						csmHelper.update();
+
+					}
+
+					renderer.render( scene, camera );
+
+				}
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 14 - 1
src/nodes/lighting/AnalyticLightNode.js

@@ -5,6 +5,7 @@ import { Color } from '../../math/Color.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
 import { hash } from '../core/NodeUtils.js';
 import { shadow } from './ShadowNode.js';
+import { nodeObject } from '../tsl/TSLCore.js';
 
 class AnalyticLightNode extends LightingNode {
 
@@ -56,7 +57,19 @@ class AnalyticLightNode extends LightingNode {
 
 		if ( shadowColorNode === null ) {
 
-			const shadowNode = shadow( this.light );
+			const customShadowNode = this.light.shadow.shadowNode;
+
+			let shadowNode;
+
+			if ( customShadowNode !== undefined ) {
+
+				shadowNode = nodeObject( customShadowNode );
+
+			} else {
+
+				shadowNode = shadow( this.light );
+
+			}
 
 			this.shadowNode = shadowNode;
 

粤ICP备19079148号