Преглед изворни кода

WebGPURenderer: Introduce `ProjectorLight` (#31022)

* Texture: Introduce `width`, `height`, `depth`

* Fix `lightShadowMatrix()` if `renderer.shadowMap.enabled` is `false`

* Remove `spotLight.attenuationNode`

* Introduce `ProjectorLight`

* add `webgpu_lights_projector` example

* Update webgpu_lights_projector.jpg

* improve cache key

* optimize for mobile

* Update webgpu_lights_projector.jpg
sunag пре 8 месеци
родитељ
комит
6224432091

+ 1 - 0
examples/files.json

@@ -342,6 +342,7 @@
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
 		"webgpu_lights_physical",
+		"webgpu_lights_projector",
 		"webgpu_lights_rectarealight",
 		"webgpu_lights_selective",
 		"webgpu_lights_spotlight",

BIN
examples/screenshots/webgpu_lights_projector.jpg


+ 297 - 0
examples/webgpu_lights_projector.html

@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - projector light</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - projector light<br />
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<video id="video" loop muted crossOrigin="anonymous" playsinline style="display:none">
+			<source src="textures/sintel.ogv" type='video/ogg; codecs="theora, vorbis"'>
+			<source src="textures/sintel.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
+		</video>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { Fn, color, mx_worley_noise_float, time } from 'three/tsl';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let renderer, scene, camera;
+
+			let projectorLight, lightHelper;
+
+			init();
+
+			function init() {
+
+				// Renderer
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = 1;
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 7, 4, 1 );
+
+				// Controls
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.maxPolarAngle = Math.PI / 2;
+				controls.target.set( 0, 1, 0 );
+				controls.update();
+
+				// Textures
+
+				const loader = new THREE.TextureLoader().setPath( 'textures/' );
+
+				// Lights
+
+				const causticEffect = Fn( ( [ projectorUV ] ) => {
+
+					const waterLayer0 = mx_worley_noise_float( projectorUV.mul( 10 ).add( time ) );
+
+					const caustic = waterLayer0.mul( color( 0x5abcd8 ) ).mul( 2 );
+
+					return caustic;
+
+				} );
+
+
+				const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
+				scene.add( ambient );
+
+				projectorLight = new THREE.ProjectorLight( 0xffffff, 100 );
+				projectorLight.colorNode = causticEffect;
+				projectorLight.position.set( 2.5, 5, 2.5 );
+				projectorLight.angle = Math.PI / 6;
+				projectorLight.penumbra = 1;
+				projectorLight.decay = 2;
+				projectorLight.distance = 0;
+
+				projectorLight.castShadow = true;
+				projectorLight.shadow.mapSize.width = 1024;
+				projectorLight.shadow.mapSize.height = 1024;
+				projectorLight.shadow.camera.near = 1;
+				projectorLight.shadow.camera.far = 10;
+				projectorLight.shadow.focus = 1;
+				projectorLight.shadow.bias = - .003;
+				scene.add( projectorLight );
+
+				lightHelper = new THREE.SpotLightHelper( projectorLight );
+				scene.add( lightHelper );
+
+				//
+
+				const geometry = new THREE.PlaneGeometry( 200, 200 );
+				const material = new THREE.MeshLambertMaterial( { color: 0xbcbcbc } );
+
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.position.set( 0, - 1, 0 );
+				mesh.rotation.x = - Math.PI / 2;
+				mesh.receiveShadow = true;
+				scene.add( mesh );
+
+				// Models
+
+				new PLYLoader().load( 'models/ply/binary/Lucy100k.ply', function ( geometry ) {
+
+					geometry.scale( 0.0024, 0.0024, 0.0024 );
+					geometry.computeVertexNormals();
+
+					const material = new THREE.MeshLambertMaterial();
+
+					const mesh = new THREE.Mesh( geometry, material );
+					mesh.rotation.y = - Math.PI / 2;
+					mesh.position.y = 0.8;
+					mesh.castShadow = true;
+					mesh.receiveShadow = true;
+					scene.add( mesh );
+
+				} );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// GUI
+
+				const gui = new GUI();
+
+				const params = {
+					type: 'procedural',
+					color: projectorLight.color.getHex(),
+					intensity: projectorLight.intensity,
+					distance: projectorLight.distance,
+					angle: projectorLight.angle,
+					penumbra: projectorLight.penumbra,
+					decay: projectorLight.decay,
+					focus: projectorLight.shadow.focus,
+					shadows: true,
+				};
+
+				let videoTexture, mapTexture;
+
+				gui.add( params, 'type', [ 'procedural', 'video', 'texture' ] ).onChange( function ( val ) {
+
+					projectorLight.colorNode = null;
+					projectorLight.map = null;
+
+					if ( val === 'procedural' ) {
+
+						projectorLight.colorNode = causticEffect;
+
+						focus.setValue( 1 );
+
+					} else if ( val === 'video' ) {
+
+						if ( videoTexture === undefined ) {
+
+							const video = document.getElementById( 'video' );
+							video.play();
+
+							videoTexture = new THREE.VideoTexture( video );
+
+						}
+
+						projectorLight.map = videoTexture;
+
+						focus.setValue( .46 );
+
+					} else if ( val === 'texture' ) {
+
+						mapTexture = loader.load( 'colors.png' );
+						mapTexture.minFilter = THREE.LinearFilter;
+						mapTexture.magFilter = THREE.LinearFilter;
+						mapTexture.generateMipmaps = false;
+						mapTexture.colorSpace = THREE.SRGBColorSpace;
+
+						projectorLight.map = mapTexture;
+
+						focus.setValue( 1 );
+
+					}
+
+				} );
+
+				gui.addColor( params, 'color' ).onChange( function ( val ) {
+
+					projectorLight.color.setHex( val );
+
+				} );
+
+				gui.add( params, 'intensity', 0, 500 ).onChange( function ( val ) {
+
+					projectorLight.intensity = val;
+
+				} );
+
+
+				gui.add( params, 'distance', 0, 20 ).onChange( function ( val ) {
+
+					projectorLight.distance = val;
+
+				} );
+
+				gui.add( params, 'angle', 0, Math.PI / 3 ).onChange( function ( val ) {
+
+					projectorLight.angle = val;
+
+				} );
+
+				gui.add( params, 'penumbra', 0, 1 ).onChange( function ( val ) {
+
+					projectorLight.penumbra = val;
+
+				} );
+
+				gui.add( params, 'decay', 1, 2 ).onChange( function ( val ) {
+
+					projectorLight.decay = val;
+
+				} );
+
+				const focus = gui.add( params, 'focus', 0, 1 ).onChange( function ( val ) {
+
+					projectorLight.shadow.focus = val;
+
+				} );
+
+				gui.add( params, 'shadows' ).onChange( function ( val ) {
+
+					renderer.shadowMap.enabled = val;
+
+					scene.traverse( function ( child ) {
+
+						if ( child.material ) {
+
+							child.material.needsUpdate = true;
+
+						}
+
+					} );
+
+				} );
+
+				gui.open();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				const time = performance.now() / 3000;
+
+				projectorLight.position.x = Math.cos( time ) * 2.5;
+				projectorLight.position.z = Math.sin( time ) * 2.5;
+
+				lightHelper.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+
+</html>

+ 0 - 35
examples/webgpu_lights_spotlight.html

@@ -26,7 +26,6 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { Fn, vec2, length, uniform, abs, max, min, sub, div, saturate, acos } from 'three/tsl';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
@@ -95,30 +94,6 @@
 				const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
 				scene.add( ambient );
 
-				const boxAttenuationFn = Fn( ( [ lightNode ], builder ) => {
-
-					const light = lightNode.light;
-
-					const sdBox = Fn( ( [ p, b ] ) => {
-
-						const d = vec2( abs( p ).sub( b ) ).toVar();
-
-						return length( max( d, 0.0 ) ).add( min( max( d.x, d.y ), 0.0 ) );
-
-					} );
-
-					const penumbraCos = uniform( 'float' ).onRenderUpdate( () => Math.min( Math.cos( light.angle * ( 1 - light.penumbra ) ), .99999 ) );
-					const spotLightCoord = lightNode.getSpotLightCoord( builder );
-					const coord = spotLightCoord.xyz.div( spotLightCoord.w );
-
-					const boxDist = sdBox( coord.xy.sub( vec2( 0.5 ) ), vec2( 0.5 ) );
-					const angleFactor = div( -1.0, sub( 1.0, acos( penumbraCos ) ).sub( 1.0 ) );
-					const attenuation = saturate( boxDist.mul( - 2.0 ).mul( angleFactor ) );
-
-					return attenuation;
-
-				} );
-
 				spotLight = new THREE.SpotLight( 0xffffff, 100 );
 				spotLight.map = textures[ 'disturb.jpg' ];
 				spotLight.position.set( 2.5, 5, 2.5 );
@@ -252,16 +227,6 @@
 
 				} );
 
-				gui.add( params, 'customAttenuation' ).name( 'custom attenuation' ).onChange( function ( val ) {
-
-					spotLight.attenuationNode = val ? boxAttenuationFn : null;
-
-					aspectGUI.setValue( 1 ).enable( val );
-
-				} );
-
-				const aspectGUI = gui.add( spotLight.shadow, 'aspect', 0, 2 ).enable( false );
-
 				gui.open();
 
 			}

+ 1 - 0
src/Three.WebGPU.Nodes.js

@@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
 export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
 export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
 export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
+export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
 export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
 export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
 export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';

+ 1 - 0
src/Three.WebGPU.js

@@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
 export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
 export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
 export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
+export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
 export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
 export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
 export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';

+ 46 - 0
src/lights/webgpu/ProjectorLight.js

@@ -0,0 +1,46 @@
+import { SpotLight } from '../SpotLight.js';
+
+/**
+ * A projector light version of {@link SpotLight}. Can only be used with {@link WebGPURenderer}.
+ *
+ * @augments SpotLight
+ */
+class ProjectorLight extends SpotLight {
+
+	/**
+	 * Constructs a new projector light.
+	 *
+	 * @param {(number|Color|string)} [color=0xffffff] - The light's color.
+	 * @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).
+	 * @param {number} [distance=0] - Maximum range of the light. `0` means no limit.
+	 * @param {number} [angle=Math.PI/3] - Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.
+	 * @param {number} [penumbra=0] - Percent of the spotlight cone that is attenuated due to penumbra. Value range is `[0,1]`.
+	 * @param {number} [decay=2] - The amount the light dims along the distance of the light.
+	 */
+	constructor( color, intensity, distance, angle, penumbra, decay ) {
+
+		super( color, intensity, distance, angle, penumbra, decay );
+
+		/**
+		 * Aspect ratio of the light. Set to `null` to use the texture aspect ratio.
+		 *
+		 * @type {number}
+		 * @default null
+		 */
+		this.aspect = null;
+
+	}
+
+	copy( source, recursive ) {
+
+		super.copy( source, recursive );
+
+		this.aspect = source.aspect;
+
+		return this;
+
+	}
+
+}
+
+export default ProjectorLight;

+ 1 - 0
src/nodes/Nodes.js

@@ -120,6 +120,7 @@ export { default as DirectionalLightNode } from './lighting/DirectionalLightNode
 export { default as RectAreaLightNode } from './lighting/RectAreaLightNode.js';
 export { default as SpotLightNode } from './lighting/SpotLightNode.js';
 export { default as IESSpotLightNode } from './lighting/IESSpotLightNode.js';
+export { default as ProjectorLightNode } from './lighting/ProjectorLightNode.js';
 export { default as AmbientLightNode } from './lighting/AmbientLightNode.js';
 export { default as LightsNode } from './lighting/LightsNode.js';
 export { default as LightingNode } from './lighting/LightingNode.js';

+ 2 - 1
src/nodes/lighting/IESSpotLightNode.js

@@ -18,10 +18,11 @@ class IESSpotLightNode extends SpotLightNode {
 	/**
 	 * Overwrites the default implementation to compute an IES conform spot attenuation.
 	 *
+	 * @param {NodeBuilder} builder - The node builder.
 	 * @param {Node<float>} angleCosine - The angle to compute the spot attenuation for.
 	 * @return {Node<float>} The spot attenuation.
 	 */
-	getSpotAttenuation( angleCosine ) {
+	getSpotAttenuation( builder, angleCosine ) {
 
 		const iesMap = this.light.iesMap;
 

+ 2 - 2
src/nodes/lighting/LightsNode.js

@@ -126,9 +126,9 @@ class LightsNode extends Node {
 			if ( light.isSpotLight === true ) {
 
 				const hashMap = ( light.map !== null ) ? light.map.id : - 1;
-				const hashAttenuation = ( light.attenuationNode ) ? light.attenuationNode.id : - 1;
+				const hashColorNode = ( light.colorNode ) ? light.colorNode.getCacheKey() : - 1;
 
-				hashData.push( hashMap, hashAttenuation );
+				hashData.push( hashMap, hashColorNode );
 
 			}
 

+ 78 - 0
src/nodes/lighting/ProjectorLightNode.js

@@ -0,0 +1,78 @@
+import SpotLightNode from './SpotLightNode.js';
+
+import { Fn, vec2 } from '../tsl/TSLCore.js';
+import { length, min, max, saturate, acos } from '../math/MathNode.js';
+import { div, sub } from '../math/OperatorNode.js';
+
+const sdBox = /*@__PURE__*/ Fn( ( [ p, b ] ) => {
+
+	const d = p.abs().sub( b );
+
+	return length( max( d, 0.0 ) ).add( min( max( d.x, d.y ), 0.0 ) );
+
+} );
+
+/**
+ * An implementation of a projector light node.
+ *
+ * @augments SpotLightNode
+ */
+class ProjectorLightNode extends SpotLightNode {
+
+	static get type() {
+
+		return 'ProjectorLightNode';
+
+	}
+
+	update( frame ) {
+
+		super.update( frame );
+
+		const light = this.light;
+
+		this.penumbraCosNode.value = Math.min( Math.cos( light.angle * ( 1 - light.penumbra ) ), .99999 );
+
+		if ( light.aspect === null ) {
+
+			let aspect = 1;
+
+			if ( light.map !== null ) {
+
+				aspect = light.map.width / light.map.height;
+
+			}
+
+			light.shadow.aspect = aspect;
+
+		} else {
+
+			light.shadow.aspect = light.aspect;
+
+		}
+
+	}
+
+	/**
+	 * Overwrites the default implementation to compute projection attenuation.
+	 *
+	 * @param {NodeBuilder} builder - The node builder.
+	 * @return {Node<float>} The spot attenuation.
+	 */
+	getSpotAttenuation( builder ) {
+
+		const penumbraCos = this.penumbraCosNode;
+		const spotLightCoord = this.getLightCoord( builder );
+		const coord = spotLightCoord.xyz.div( spotLightCoord.w );
+
+		const boxDist = sdBox( coord.xy.sub( vec2( 0.5 ) ), vec2( 0.5 ) );
+		const angleFactor = div( - 1.0, sub( 1.0, acos( penumbraCos ) ).sub( 1.0 ) );
+		const attenuation = saturate( boxDist.mul( - 2.0 ).mul( angleFactor ) );
+
+		return attenuation;
+
+	}
+
+}
+
+export default ProjectorLightNode;

+ 27 - 8
src/nodes/lighting/SpotLightNode.js

@@ -56,6 +56,13 @@ class SpotLightNode extends AnalyticLightNode {
 		 */
 		this.decayExponentNode = uniform( 0 ).setGroup( renderGroup );
 
+		/**
+		 * Uniform node representing the light color.
+		 *
+		 * @type {UniformNode<Color>}
+		 */
+		this.colorNode = uniform( this.color ).setGroup( renderGroup );
+
 	}
 
 	/**
@@ -80,10 +87,11 @@ class SpotLightNode extends AnalyticLightNode {
 	/**
 	 * Computes the spot attenuation for the given angle.
 	 *
+	 * @param {NodeBuilder} builder - The node builder.
 	 * @param {Node<float>} angleCosine - The angle to compute the spot attenuation for.
 	 * @return {Node<float>} The spot attenuation.
 	 */
-	getSpotAttenuation( angleCosine ) {
+	getSpotAttenuation( builder, angleCosine ) {
 
 		const { coneCosNode, penumbraCosNode } = this;
 
@@ -91,7 +99,7 @@ class SpotLightNode extends AnalyticLightNode {
 
 	}
 
-	getSpotLightCoord( builder ) {
+	getLightCoord( builder ) {
 
 		const properties = builder.getNodeProperties( this );
 		let projectionUV = properties.projectionUV;
@@ -117,7 +125,7 @@ class SpotLightNode extends AnalyticLightNode {
 		const lightDirection = lightVector.normalize();
 		const angleCos = lightDirection.dot( lightTargetDirection( light ) );
 
-		const spotAttenuation = light.attenuationNode ? light.attenuationNode( this ) : this.getSpotAttenuation( angleCos );
+		const spotAttenuation = this.getSpotAttenuation( builder, angleCos );
 
 		const lightDistance = lightVector.length();
 
@@ -129,14 +137,25 @@ class SpotLightNode extends AnalyticLightNode {
 
 		let lightColor = colorNode.mul( spotAttenuation ).mul( lightAttenuation );
 
-		if ( light.map ) {
+		let projected, lightCoord;
+
+		if ( light.colorNode ) {
+
+			lightCoord = this.getLightCoord( builder );
+			projected = light.colorNode( lightCoord );
+
+		} else if ( light.map ) {
+
+			lightCoord = this.getLightCoord( builder );
+			projected = texture( light.map, lightCoord.xy ).onRenderUpdate( () => light.map );
+
+		}
 
-			const spotLightCoord = this.getSpotLightCoord( builder );
-			const projectedTexture = texture( light.map, spotLightCoord.xy ).onRenderUpdate( () => light.map );
+		if ( projected ) {
 
-			const inSpotLightMap = spotLightCoord.mul( 2. ).sub( 1. ).abs().lessThan( 1. ).all();
+			const inSpotLightMap = lightCoord.mul( 2. ).sub( 1. ).abs().lessThan( 1. ).all();
 
-			lightColor = inSpotLightMap.select( lightColor.mul( projectedTexture ), lightColor );
+			lightColor = inSpotLightMap.select( lightColor.mul( projected ), lightColor );
 
 		}
 

+ 3 - 0
src/nodes/tsl/TSLCore.js

@@ -621,6 +621,9 @@ export const Fn = ( jsFunc, layout = null ) => {
 	fn.shaderNode = shaderNode;
 	fn.id = shaderNode.id;
 
+	fn.getNodeType = ( ...params ) => shaderNode.getNodeType( ...params );
+	fn.getCacheKey = ( ...params ) => shaderNode.getCacheKey( ...params );
+
 	fn.setLayout = ( layout ) => {
 
 		shaderNode.setLayout( layout );

+ 4 - 1
src/renderers/webgpu/nodes/BasicNodeLibrary.js

@@ -9,6 +9,7 @@ import { AmbientLight } from '../../../lights/AmbientLight.js';
 import { HemisphereLight } from '../../../lights/HemisphereLight.js';
 import { LightProbe } from '../../../lights/LightProbe.js';
 import IESSpotLight from '../../../lights/webgpu/IESSpotLight.js';
+import ProjectorLight from '../../../lights/webgpu/ProjectorLight.js';
 import {
 	PointLightNode,
 	DirectionalLightNode,
@@ -17,7 +18,8 @@ import {
 	AmbientLightNode,
 	HemisphereLightNode,
 	LightProbeNode,
-	IESSpotLightNode
+	IESSpotLightNode,
+	ProjectorLightNode
 } from '../../../nodes/Nodes.js';
 
 // Tone Mapping
@@ -48,6 +50,7 @@ class BasicNodeLibrary extends NodeLibrary {
 		this.addLight( HemisphereLightNode, HemisphereLight );
 		this.addLight( LightProbeNode, LightProbe );
 		this.addLight( IESSpotLightNode, IESSpotLight );
+		this.addLight( ProjectorLightNode, ProjectorLight );
 
 		this.addToneMapping( linearToneMapping, LinearToneMapping );
 		this.addToneMapping( reinhardToneMapping, ReinhardToneMapping );

+ 4 - 1
src/renderers/webgpu/nodes/StandardNodeLibrary.js

@@ -28,6 +28,7 @@ import { AmbientLight } from '../../../lights/AmbientLight.js';
 import { HemisphereLight } from '../../../lights/HemisphereLight.js';
 import { LightProbe } from '../../../lights/LightProbe.js';
 import IESSpotLight from '../../../lights/webgpu/IESSpotLight.js';
+import ProjectorLight from '../../../lights/webgpu/ProjectorLight.js';
 import {
 	PointLightNode,
 	DirectionalLightNode,
@@ -36,7 +37,8 @@ import {
 	AmbientLightNode,
 	HemisphereLightNode,
 	LightProbeNode,
-	IESSpotLightNode
+	IESSpotLightNode,
+	ProjectorLightNode
 } from '../../../nodes/Nodes.js';
 
 // Tone Mapping
@@ -82,6 +84,7 @@ class StandardNodeLibrary extends NodeLibrary {
 		this.addLight( HemisphereLightNode, HemisphereLight );
 		this.addLight( LightProbeNode, LightProbe );
 		this.addLight( IESSpotLightNode, IESSpotLight );
+		this.addLight( ProjectorLightNode, ProjectorLight );
 
 		this.addToneMapping( linearToneMapping, LinearToneMapping );
 		this.addToneMapping( reinhardToneMapping, ReinhardToneMapping );

粤ICP备19079148号