Przeglądaj źródła

WebGPURenderer: Introduce Dynamic Lights (#33042)

Renaud Rohlinger 1 miesiąc temu
rodzic
commit
bf65eaf73a

+ 1 - 0
examples/files.json

@@ -346,6 +346,7 @@
 		"webgpu_lightprobe",
 		"webgpu_lightprobe_cubecamera",
 		"webgpu_lights_custom",
+		"webgpu_lights_dynamic",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
 		"webgpu_lights_physical",

+ 82 - 0
examples/jsm/lighting/DynamicLighting.js

@@ -0,0 +1,82 @@
+import { Lighting, LightsNode } from 'three/webgpu';
+import DynamicLightsNode from '../tsl/lighting/DynamicLightsNode.js';
+
+const _defaultLights = /*@__PURE__*/ new LightsNode();
+
+/**
+ * A custom lighting implementation that batches supported analytic lights into
+ * uniform arrays so light count changes do not recompile materials.
+ *
+ * ```js
+ * const lighting = new DynamicLighting( { maxPointLights: 64 } );
+ * renderer.lighting = lighting;
+ * ```
+ *
+ * @augments Lighting
+ * @three_import import { DynamicLighting } from 'three/addons/lighting/DynamicLighting.js';
+ */
+export class DynamicLighting extends Lighting {
+
+	/**
+	 * Constructs a new dynamic lighting system.
+	 *
+	 * @param {Object} [options={}] - Dynamic lighting configuration.
+	 * @param {number} [options.maxDirectionalLights=8] - Maximum number of batched directional lights.
+	 * @param {number} [options.maxPointLights=16] - Maximum number of batched point lights.
+	 * @param {number} [options.maxSpotLights=16] - Maximum number of batched spot lights.
+	 * @param {number} [options.maxHemisphereLights=4] - Maximum number of batched hemisphere lights.
+	 */
+	constructor( options = {} ) {
+
+		super();
+
+		this.options = {
+			maxDirectionalLights: 8,
+			maxPointLights: 16,
+			maxSpotLights: 16,
+			maxHemisphereLights: 4,
+			...options
+		};
+
+		this._nodes = new WeakMap();
+
+	}
+
+	/**
+	 * Creates a new dynamic lights node for the given array of lights.
+	 *
+	 * @param {Array<Light>} lights - The lights to bind to the node.
+	 * @return {DynamicLightsNode} The dynamic lights node.
+	 */
+	createNode( lights = [] ) {
+
+		return new DynamicLightsNode( this.options ).setLights( lights );
+
+	}
+
+	/**
+	 * Returns a lights node for the given scene.
+	 *
+	 * @param {Scene} scene - The scene.
+	 * @return {LightsNode} The lights node.
+	 */
+	getNode( scene ) {
+
+		if ( scene.isQuadMesh ) return _defaultLights;
+
+		let node = this._nodes.get( scene );
+
+		if ( node === undefined ) {
+
+			node = this.createNode();
+			this._nodes.set( scene, node );
+
+		}
+
+		return node;
+
+	}
+
+}
+
+export default DynamicLighting;

+ 300 - 0
examples/jsm/tsl/lighting/DynamicLightsNode.js

@@ -0,0 +1,300 @@
+import { LightsNode, NodeUtils, warn } from 'three/webgpu';
+import { nodeObject } from 'three/tsl';
+
+import AmbientLightDataNode from './data/AmbientLightDataNode.js';
+import DirectionalLightDataNode from './data/DirectionalLightDataNode.js';
+import PointLightDataNode from './data/PointLightDataNode.js';
+import SpotLightDataNode from './data/SpotLightDataNode.js';
+import HemisphereLightDataNode from './data/HemisphereLightDataNode.js';
+
+const _lightNodeRef = /*@__PURE__*/ new WeakMap();
+const _hashData = [];
+
+const _lightTypeToDataNode = {
+	AmbientLight: AmbientLightDataNode,
+	DirectionalLight: DirectionalLightDataNode,
+	PointLight: PointLightDataNode,
+	SpotLight: SpotLightDataNode,
+	HemisphereLight: HemisphereLightDataNode
+};
+
+const _lightTypeToMaxProp = {
+	DirectionalLight: 'maxDirectionalLights',
+	PointLight: 'maxPointLights',
+	SpotLight: 'maxSpotLights',
+	HemisphereLight: 'maxHemisphereLights'
+};
+
+const sortLights = ( lights ) => lights.sort( ( a, b ) => a.id - b.id );
+
+const isSpecialSpotLight = ( light ) => {
+
+	return light.isSpotLight === true && ( light.map !== null || light.colorNode !== undefined );
+
+};
+
+const canBatchLight = ( light ) => {
+
+	return light.isNode !== true &&
+		light.castShadow !== true &&
+		isSpecialSpotLight( light ) === false &&
+		_lightTypeToDataNode[ light.constructor.name ] !== undefined;
+
+};
+
+const getOrCreateLightNode = ( light, nodeLibrary ) => {
+
+	const lightNodeClass = nodeLibrary.getLightNodeClass( light.constructor );
+
+	if ( lightNodeClass === null ) {
+
+		warn( `DynamicLightsNode: Light node not found for ${ light.constructor.name }.` );
+		return null;
+
+	}
+
+	if ( _lightNodeRef.has( light ) === false ) {
+
+		_lightNodeRef.set( light, new lightNodeClass( light ) );
+
+	}
+
+	return _lightNodeRef.get( light );
+
+};
+
+/**
+ * A custom version of `LightsNode` that batches supported analytic lights into
+ * uniform arrays and loops.
+ *
+ * Unsupported lights, node lights, shadow-casting lights, and projected spot
+ * lights keep the default per-light path.
+ *
+ * @augments LightsNode
+ * @three_import import { DynamicLightsNode } from 'three/addons/tsl/lighting/DynamicLightsNode.js';
+ */
+class DynamicLightsNode extends LightsNode {
+
+	static get type() {
+
+		return 'DynamicLightsNode';
+
+	}
+
+	/**
+	 * Constructs a new dynamic lights node.
+	 *
+	 * @param {Object} [options={}] - Dynamic lighting configuration.
+	 * @param {number} [options.maxDirectionalLights=8] - Maximum number of batched directional lights.
+	 * @param {number} [options.maxPointLights=16] - Maximum number of batched point lights.
+	 * @param {number} [options.maxSpotLights=16] - Maximum number of batched spot lights.
+	 * @param {number} [options.maxHemisphereLights=4] - Maximum number of batched hemisphere lights.
+	 */
+	constructor( options = {} ) {
+
+		super();
+
+		this.maxDirectionalLights = options.maxDirectionalLights !== undefined ? options.maxDirectionalLights : 8;
+		this.maxPointLights = options.maxPointLights !== undefined ? options.maxPointLights : 16;
+		this.maxSpotLights = options.maxSpotLights !== undefined ? options.maxSpotLights : 16;
+		this.maxHemisphereLights = options.maxHemisphereLights !== undefined ? options.maxHemisphereLights : 4;
+
+		this._dataNodes = new Map();
+
+	}
+
+	customCacheKey() {
+
+		const typeSet = new Set();
+
+		for ( let i = 0; i < this._lights.length; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			if ( canBatchLight( light ) ) {
+
+				typeSet.add( light.constructor.name );
+
+			} else {
+
+				_hashData.push( light.id );
+				_hashData.push( light.castShadow ? 1 : 0 );
+
+				if ( light.isSpotLight === true ) {
+
+					const hashMap = light.map !== null ? light.map.id : - 1;
+					const hashColorNode = light.colorNode ? light.colorNode.getCacheKey() : - 1;
+
+					_hashData.push( hashMap, hashColorNode );
+
+				}
+
+			}
+
+		}
+
+		for ( const typeName of this._dataNodes.keys() ) {
+
+			typeSet.add( typeName );
+
+		}
+
+		for ( const typeName of [ ...typeSet ].sort() ) {
+
+			_hashData.push( NodeUtils.hashString( typeName ) );
+
+		}
+
+		const cacheKey = NodeUtils.hashArray( _hashData );
+
+		_hashData.length = 0;
+
+		return cacheKey;
+
+	}
+
+	setupLightsNode( builder ) {
+
+		const lightNodes = [];
+		const lightsByType = new Map();
+		const lights = sortLights( this._lights );
+		const nodeLibrary = builder.renderer.library;
+
+		for ( const light of lights ) {
+
+			if ( light.isNode === true ) {
+
+				lightNodes.push( nodeObject( light ) );
+				continue;
+
+			}
+
+			if ( canBatchLight( light ) ) {
+
+				const typeName = light.constructor.name;
+				const typeLights = lightsByType.get( typeName );
+
+				if ( typeLights === undefined ) {
+
+					lightsByType.set( typeName, [ light ] );
+
+				} else {
+
+					typeLights.push( light );
+
+				}
+
+				continue;
+
+			}
+
+			const lightNode = getOrCreateLightNode( light, nodeLibrary );
+
+			if ( lightNode !== null ) {
+
+				lightNodes.push( lightNode );
+
+			}
+
+		}
+
+		for ( const [ typeName, typeLights ] of lightsByType ) {
+
+			let dataNode = this._dataNodes.get( typeName );
+
+			if ( dataNode === undefined ) {
+
+				const DataNodeClass = _lightTypeToDataNode[ typeName ];
+				const maxProp = _lightTypeToMaxProp[ typeName ];
+				const maxCount = maxProp !== undefined ? this[ maxProp ] : undefined;
+
+				dataNode = maxCount !== undefined ? new DataNodeClass( maxCount ) : new DataNodeClass();
+
+				this._dataNodes.set( typeName, dataNode );
+
+			}
+
+			dataNode.setLights( typeLights );
+			lightNodes.push( dataNode );
+
+		}
+
+		for ( const [ typeName, dataNode ] of this._dataNodes ) {
+
+			if ( lightsByType.has( typeName ) === false ) {
+
+				dataNode.setLights( [] );
+				lightNodes.push( dataNode );
+
+			}
+
+		}
+
+		this._lightNodes = lightNodes;
+
+	}
+
+	setLights( lights ) {
+
+		super.setLights( lights );
+
+		if ( this._dataNodes.size > 0 ) {
+
+			this._updateDataNodeLights( lights );
+
+		}
+
+		return this;
+
+	}
+
+	_updateDataNodeLights( lights ) {
+
+		const lightsByType = new Map();
+
+		for ( const light of lights ) {
+
+			if ( canBatchLight( light ) === false ) continue;
+
+			const typeName = light.constructor.name;
+			const typeLights = lightsByType.get( typeName );
+
+			if ( typeLights === undefined ) {
+
+				lightsByType.set( typeName, [ light ] );
+
+			} else {
+
+				typeLights.push( light );
+
+			}
+
+		}
+
+		for ( const [ typeName, dataNode ] of this._dataNodes ) {
+
+			dataNode.setLights( lightsByType.get( typeName ) || [] );
+
+		}
+
+	}
+
+	get hasLights() {
+
+		return super.hasLights || this._dataNodes.size > 0;
+
+	}
+
+}
+
+export default DynamicLightsNode;
+
+/**
+ * TSL function that creates a dynamic lights node.
+ *
+ * @tsl
+ * @function
+ * @param {Object} [options={}] - Dynamic lighting configuration.
+ * @return {DynamicLightsNode} The created dynamic lights node.
+ */
+export const dynamicLights = ( options = {} ) => new DynamicLightsNode( options );

+ 61 - 0
examples/jsm/tsl/lighting/data/AmbientLightDataNode.js

@@ -0,0 +1,61 @@
+import { Color, Node } from 'three/webgpu';
+import { NodeUpdateType, renderGroup, uniform } from 'three/tsl';
+
+/**
+ * Batched data node for ambient lights in dynamic lighting mode.
+ *
+ * @augments Node
+ */
+class AmbientLightDataNode extends Node {
+
+	static get type() {
+
+		return 'AmbientLightDataNode';
+
+	}
+
+	constructor() {
+
+		super();
+
+		this._color = new Color();
+		this._lights = [];
+
+		this.colorNode = uniform( this._color ).setGroup( renderGroup );
+		this.updateType = NodeUpdateType.RENDER;
+
+	}
+
+	setLights( lights ) {
+
+		this._lights = lights;
+
+		return this;
+
+	}
+
+	update() {
+
+		this._color.setScalar( 0 );
+
+		for ( let i = 0; i < this._lights.length; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			this._color.r += light.color.r * light.intensity;
+			this._color.g += light.color.g * light.intensity;
+			this._color.b += light.color.b * light.intensity;
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		builder.context.irradiance.addAssign( this.colorNode );
+
+	}
+
+}
+
+export default AmbientLightDataNode;

+ 111 - 0
examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js

@@ -0,0 +1,111 @@
+import { Color, Node, Vector3 } from 'three/webgpu';
+import { Loop, NodeUpdateType, renderGroup, uniform, uniformArray, vec3 } from 'three/tsl';
+
+const _lightPosition = /*@__PURE__*/ new Vector3();
+const _targetPosition = /*@__PURE__*/ new Vector3();
+
+const warn = ( message ) => {
+
+	console.warn( `THREE.DirectionalLightDataNode: ${ message }` );
+
+};
+
+/**
+ * Batched data node for directional lights in dynamic lighting mode.
+ *
+ * @augments Node
+ */
+class DirectionalLightDataNode extends Node {
+
+	static get type() {
+
+		return 'DirectionalLightDataNode';
+
+	}
+
+	constructor( maxCount = 8 ) {
+
+		super();
+
+		this.maxCount = maxCount;
+		this._lights = [];
+		this._colors = [];
+		this._directions = [];
+
+		for ( let i = 0; i < maxCount; i ++ ) {
+
+			this._colors.push( new Color() );
+			this._directions.push( new Vector3() );
+
+		}
+
+		this.colorsNode = uniformArray( this._colors, 'color' ).setGroup( renderGroup );
+		this.directionsNode = uniformArray( this._directions, 'vec3' ).setGroup( renderGroup );
+		this.countNode = uniform( 0, 'int' ).setGroup( renderGroup );
+		this.updateType = NodeUpdateType.RENDER;
+
+	}
+
+	setLights( lights ) {
+
+		if ( lights.length > this.maxCount ) {
+
+			warn( `${ lights.length } lights exceed the configured max of ${ this.maxCount }. Excess lights are ignored.` );
+
+		}
+
+		this._lights = lights;
+
+		return this;
+
+	}
+
+	update( { camera } ) {
+
+		const count = Math.min( this._lights.length, this.maxCount );
+
+		this.countNode.value = count;
+
+		for ( let i = 0; i < count; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			this._colors[ i ].copy( light.color ).multiplyScalar( light.intensity );
+
+			_lightPosition.setFromMatrixPosition( light.matrixWorld );
+			_targetPosition.setFromMatrixPosition( light.target.matrixWorld );
+
+			this._directions[ i ].subVectors( _lightPosition, _targetPosition ).transformDirection( camera.matrixWorldInverse );
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		const { lightingModel, reflectedLight } = builder.context;
+		const dynDiffuse = vec3( 0 ).toVar( 'dynDirectionalDiffuse' );
+		const dynSpecular = vec3( 0 ).toVar( 'dynDirectionalSpecular' );
+
+		Loop( this.countNode, ( { i } ) => {
+
+			const lightColor = this.colorsNode.element( i ).toVar();
+			const lightDirection = this.directionsNode.element( i ).normalize().toVar();
+
+			lightingModel.direct( {
+				lightDirection,
+				lightColor,
+				lightNode: { light: {}, shadowNode: null },
+				reflectedLight: { directDiffuse: dynDiffuse, directSpecular: dynSpecular }
+			}, builder );
+
+		} );
+
+		reflectedLight.directDiffuse.addAssign( dynDiffuse );
+		reflectedLight.directSpecular.addAssign( dynSpecular );
+
+	}
+
+}
+
+export default DirectionalLightDataNode;

+ 99 - 0
examples/jsm/tsl/lighting/data/HemisphereLightDataNode.js

@@ -0,0 +1,99 @@
+import { Color, Node, Vector3 } from 'three/webgpu';
+import { Loop, NodeUpdateType, mix, normalWorld, renderGroup, uniform, uniformArray } from 'three/tsl';
+
+const warn = ( message ) => {
+
+	console.warn( `THREE.HemisphereLightDataNode: ${ message }` );
+
+};
+
+/**
+ * Batched data node for hemisphere lights in dynamic lighting mode.
+ *
+ * @augments Node
+ */
+class HemisphereLightDataNode extends Node {
+
+	static get type() {
+
+		return 'HemisphereLightDataNode';
+
+	}
+
+	constructor( maxCount = 4 ) {
+
+		super();
+
+		this.maxCount = maxCount;
+		this._lights = [];
+		this._skyColors = [];
+		this._groundColors = [];
+		this._directions = [];
+
+		for ( let i = 0; i < maxCount; i ++ ) {
+
+			this._skyColors.push( new Color() );
+			this._groundColors.push( new Color() );
+			this._directions.push( new Vector3() );
+
+		}
+
+		this.skyColorsNode = uniformArray( this._skyColors, 'color' ).setGroup( renderGroup );
+		this.groundColorsNode = uniformArray( this._groundColors, 'color' ).setGroup( renderGroup );
+		this.directionsNode = uniformArray( this._directions, 'vec3' ).setGroup( renderGroup );
+		this.countNode = uniform( 0, 'int' ).setGroup( renderGroup );
+		this.updateType = NodeUpdateType.RENDER;
+
+	}
+
+	setLights( lights ) {
+
+		if ( lights.length > this.maxCount ) {
+
+			warn( `${ lights.length } lights exceed the configured max of ${ this.maxCount }. Excess lights are ignored.` );
+
+		}
+
+		this._lights = lights;
+
+		return this;
+
+	}
+
+	update() {
+
+		const count = Math.min( this._lights.length, this.maxCount );
+
+		this.countNode.value = count;
+
+		for ( let i = 0; i < count; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			this._skyColors[ i ].copy( light.color ).multiplyScalar( light.intensity );
+			this._groundColors[ i ].copy( light.groundColor ).multiplyScalar( light.intensity );
+			this._directions[ i ].setFromMatrixPosition( light.matrixWorld ).normalize();
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		Loop( this.countNode, ( { i } ) => {
+
+			const skyColor = this.skyColorsNode.element( i );
+			const groundColor = this.groundColorsNode.element( i );
+			const lightDirection = this.directionsNode.element( i );
+			const hemiDiffuseWeight = normalWorld.dot( lightDirection ).mul( 0.5 ).add( 0.5 );
+			const irradiance = mix( groundColor, skyColor, hemiDiffuseWeight );
+
+			builder.context.irradiance.addAssign( irradiance );
+
+		} );
+
+	}
+
+}
+
+export default HemisphereLightDataNode;

+ 134 - 0
examples/jsm/tsl/lighting/data/PointLightDataNode.js

@@ -0,0 +1,134 @@
+import { Color, Node, Vector3, Vector4 } from 'three/webgpu';
+import { Loop, NodeUpdateType, getDistanceAttenuation, positionView, renderGroup, uniform, uniformArray, vec3 } from 'three/tsl';
+
+const _position = /*@__PURE__*/ new Vector3();
+
+const warn = ( message ) => {
+
+	console.warn( `THREE.PointLightDataNode: ${ message }` );
+
+};
+
+/**
+ * Batched data node for point lights in dynamic lighting mode.
+ *
+ * @augments Node
+ */
+class PointLightDataNode extends Node {
+
+	static get type() {
+
+		return 'PointLightDataNode';
+
+	}
+
+	constructor( maxCount = 16 ) {
+
+		super();
+
+		this.maxCount = maxCount;
+		this._lights = [];
+		this._colors = [];
+		this._positionsAndCutoff = [];
+		this._decays = [];
+
+		for ( let i = 0; i < maxCount; i ++ ) {
+
+			this._colors.push( new Color() );
+			this._positionsAndCutoff.push( new Vector4() );
+			this._decays.push( new Vector4() );
+
+		}
+
+		this.colorsNode = uniformArray( this._colors, 'color' ).setGroup( renderGroup );
+		this.positionsAndCutoffNode = uniformArray( this._positionsAndCutoff, 'vec4' ).setGroup( renderGroup );
+		this.decaysNode = uniformArray( this._decays, 'vec4' ).setGroup( renderGroup );
+		this.countNode = uniform( 0, 'int' ).setGroup( renderGroup );
+		this.updateType = NodeUpdateType.RENDER;
+
+	}
+
+	setLights( lights ) {
+
+		if ( lights.length > this.maxCount ) {
+
+			warn( `${ lights.length } lights exceed the configured max of ${ this.maxCount }. Excess lights are ignored.` );
+
+		}
+
+		this._lights = lights;
+
+		return this;
+
+	}
+
+	update( { camera } ) {
+
+		const count = Math.min( this._lights.length, this.maxCount );
+
+		this.countNode.value = count;
+
+		for ( let i = 0; i < count; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			this._colors[ i ].copy( light.color ).multiplyScalar( light.intensity );
+
+			_position.setFromMatrixPosition( light.matrixWorld );
+			_position.applyMatrix4( camera.matrixWorldInverse );
+
+			const positionAndCutoff = this._positionsAndCutoff[ i ];
+			positionAndCutoff.x = _position.x;
+			positionAndCutoff.y = _position.y;
+			positionAndCutoff.z = _position.z;
+			positionAndCutoff.w = light.distance;
+
+			this._decays[ i ].x = light.decay;
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		const surfacePosition = builder.context.positionView || positionView;
+		const { lightingModel, reflectedLight } = builder.context;
+		const dynDiffuse = vec3( 0 ).toVar( 'dynPointDiffuse' );
+		const dynSpecular = vec3( 0 ).toVar( 'dynPointSpecular' );
+
+		Loop( this.countNode, ( { i } ) => {
+
+			const positionAndCutoff = this.positionsAndCutoffNode.element( i );
+			const lightViewPosition = positionAndCutoff.xyz;
+			const cutoffDistance = positionAndCutoff.w;
+			const decayExponent = this.decaysNode.element( i ).x;
+
+			const lightVector = lightViewPosition.sub( surfacePosition ).toVar();
+			const lightDirection = lightVector.normalize().toVar();
+			const lightDistance = lightVector.length();
+
+			const attenuation = getDistanceAttenuation( {
+				lightDistance,
+				cutoffDistance,
+				decayExponent
+			} );
+
+			const lightColor = this.colorsNode.element( i ).mul( attenuation ).toVar();
+
+			lightingModel.direct( {
+				lightDirection,
+				lightColor,
+				lightNode: { light: {}, shadowNode: null },
+				reflectedLight: { directDiffuse: dynDiffuse, directSpecular: dynSpecular }
+			}, builder );
+
+		} );
+
+		reflectedLight.directDiffuse.addAssign( dynDiffuse );
+		reflectedLight.directSpecular.addAssign( dynSpecular );
+
+	}
+
+}
+
+export default PointLightDataNode;

+ 161 - 0
examples/jsm/tsl/lighting/data/SpotLightDataNode.js

@@ -0,0 +1,161 @@
+import { Color, Node, Vector3, Vector4 } from 'three/webgpu';
+import { Loop, NodeUpdateType, getDistanceAttenuation, positionView, renderGroup, smoothstep, uniform, uniformArray, vec3 } from 'three/tsl';
+
+const _lightPosition = /*@__PURE__*/ new Vector3();
+const _targetPosition = /*@__PURE__*/ new Vector3();
+
+const warn = ( message ) => {
+
+	console.warn( `THREE.SpotLightDataNode: ${ message }` );
+
+};
+
+/**
+ * Batched data node for simple spot lights in dynamic lighting mode.
+ *
+ * Projected spot lights keep the default per-light path.
+ *
+ * @augments Node
+ */
+class SpotLightDataNode extends Node {
+
+	static get type() {
+
+		return 'SpotLightDataNode';
+
+	}
+
+	constructor( maxCount = 16 ) {
+
+		super();
+
+		this.maxCount = maxCount;
+		this._lights = [];
+		this._colors = [];
+		this._positionsAndCutoff = [];
+		this._directionsAndDecay = [];
+		this._cones = [];
+
+		for ( let i = 0; i < maxCount; i ++ ) {
+
+			this._colors.push( new Color() );
+			this._positionsAndCutoff.push( new Vector4() );
+			this._directionsAndDecay.push( new Vector4() );
+			this._cones.push( new Vector4() );
+
+		}
+
+		this.colorsNode = uniformArray( this._colors, 'color' ).setGroup( renderGroup );
+		this.positionsAndCutoffNode = uniformArray( this._positionsAndCutoff, 'vec4' ).setGroup( renderGroup );
+		this.directionsAndDecayNode = uniformArray( this._directionsAndDecay, 'vec4' ).setGroup( renderGroup );
+		this.conesNode = uniformArray( this._cones, 'vec4' ).setGroup( renderGroup );
+		this.countNode = uniform( 0, 'int' ).setGroup( renderGroup );
+		this.updateType = NodeUpdateType.RENDER;
+
+	}
+
+	setLights( lights ) {
+
+		if ( lights.length > this.maxCount ) {
+
+			warn( `${ lights.length } lights exceed the configured max of ${ this.maxCount }. Excess lights are ignored.` );
+
+		}
+
+		this._lights = lights;
+
+		return this;
+
+	}
+
+	update( { camera } ) {
+
+		const count = Math.min( this._lights.length, this.maxCount );
+
+		this.countNode.value = count;
+
+		for ( let i = 0; i < count; i ++ ) {
+
+			const light = this._lights[ i ];
+
+			this._colors[ i ].copy( light.color ).multiplyScalar( light.intensity );
+
+			_lightPosition.setFromMatrixPosition( light.matrixWorld );
+			_lightPosition.applyMatrix4( camera.matrixWorldInverse );
+
+			const positionAndCutoff = this._positionsAndCutoff[ i ];
+			positionAndCutoff.x = _lightPosition.x;
+			positionAndCutoff.y = _lightPosition.y;
+			positionAndCutoff.z = _lightPosition.z;
+			positionAndCutoff.w = light.distance;
+
+			_lightPosition.setFromMatrixPosition( light.matrixWorld );
+			_targetPosition.setFromMatrixPosition( light.target.matrixWorld );
+			_lightPosition.sub( _targetPosition ).transformDirection( camera.matrixWorldInverse );
+
+			const directionAndDecay = this._directionsAndDecay[ i ];
+			directionAndDecay.x = _lightPosition.x;
+			directionAndDecay.y = _lightPosition.y;
+			directionAndDecay.z = _lightPosition.z;
+			directionAndDecay.w = light.decay;
+
+			const cone = this._cones[ i ];
+			cone.x = Math.cos( light.angle );
+			cone.y = Math.cos( light.angle * ( 1 - light.penumbra ) );
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		const surfacePosition = builder.context.positionView || positionView;
+		const { lightingModel, reflectedLight } = builder.context;
+		const dynDiffuse = vec3( 0 ).toVar( 'dynSpotDiffuse' );
+		const dynSpecular = vec3( 0 ).toVar( 'dynSpotSpecular' );
+
+		Loop( this.countNode, ( { i } ) => {
+
+			const positionAndCutoff = this.positionsAndCutoffNode.element( i );
+			const lightViewPosition = positionAndCutoff.xyz;
+			const cutoffDistance = positionAndCutoff.w;
+
+			const directionAndDecay = this.directionsAndDecayNode.element( i );
+			const spotDirection = directionAndDecay.xyz;
+			const decayExponent = directionAndDecay.w;
+
+			const cone = this.conesNode.element( i );
+			const coneCos = cone.x;
+			const penumbraCos = cone.y;
+
+			const lightVector = lightViewPosition.sub( surfacePosition ).toVar();
+			const lightDirection = lightVector.normalize().toVar();
+			const lightDistance = lightVector.length();
+
+			const angleCos = lightDirection.dot( spotDirection );
+			const spotAttenuation = smoothstep( coneCos, penumbraCos, angleCos );
+			const distanceAttenuation = getDistanceAttenuation( {
+				lightDistance,
+				cutoffDistance,
+				decayExponent
+			} );
+
+			const lightColor = this.colorsNode.element( i ).mul( spotAttenuation ).mul( distanceAttenuation ).toVar();
+
+			lightingModel.direct( {
+				lightDirection,
+				lightColor,
+				lightNode: { light: {}, shadowNode: null },
+				reflectedLight: { directDiffuse: dynDiffuse, directSpecular: dynSpecular }
+			}, builder );
+
+		} );
+
+		reflectedLight.directDiffuse.addAssign( dynDiffuse );
+		reflectedLight.directSpecular.addAssign( dynSpecular );
+
+	}
+
+}
+
+export default SpotLightDataNode;

BIN
examples/screenshots/webgpu_lights_dynamic.jpg


+ 331 - 0
examples/webgpu_lights_dynamic.html

@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - dynamic lights</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="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Dynamic Lights</span>
+			</div>
+
+			<small>
+				Opt-in DynamicLighting avoids shader recompilation when adding/removing supported lights.<br />
+				100 meshes with 50 unique PBR materials. Use the Inspector to explore the scene.
+			</small>
+		</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>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { DynamicLighting } from 'three/addons/lighting/DynamicLighting.js';
+			import { Inspector } from 'three/addons/inspector/Inspector.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, controls, timer, stats;
+
+			const pointLights = [];
+			let autoAddInterval = null;
+
+			const params = {
+				dynamic: true,
+				autoAdd: false,
+				lightCount: 2,
+				addLight() {
+
+					addLight();
+
+				},
+				removeLight() {
+
+					removeLight();
+
+				},
+				removeAll() {
+
+					removeAllLights();
+
+				}
+			};
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 200 );
+				camera.position.set( 0, 15, 30 );
+
+				scene = new THREE.Scene();
+
+				timer = new THREE.Timer();
+				timer.connect( document );
+
+				// Stats
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				// Floor
+
+				const floorGeometry = new THREE.PlaneGeometry( 120, 120 );
+				const floorMaterial = new THREE.MeshStandardMaterial( { color: 0x444444, roughness: 0.8 } );
+				const floor = new THREE.Mesh( floorGeometry, floorMaterial );
+				floor.rotation.x = - Math.PI / 2;
+				scene.add( floor );
+
+				// Shared geometries
+
+				const sphereGeometry = new THREE.SphereGeometry( 0.8, 128, 128 );
+				const boxGeometry = new THREE.BoxGeometry( 1.2, 1.2, 1.2, 64, 64, 64 );
+				const torusGeometry = new THREE.TorusGeometry( 0.6, 0.25, 128, 128 );
+				const cylinderGeometry = new THREE.CylinderGeometry( 0.5, 0.5, 1.4, 128, 64 );
+				const coneGeometry = new THREE.ConeGeometry( 0.6, 1.4, 128, 64 );
+
+				const geometries = [ sphereGeometry, boxGeometry, torusGeometry, cylinderGeometry, coneGeometry ];
+
+				// 100 meshes — first 50 with unique PBR materials, remaining 50 reuse them
+
+				const uniqueMaterials = [];
+
+				for ( let i = 0; i < 50; i ++ ) {
+
+					const material = new THREE.MeshStandardMaterial( {
+						color: new THREE.Color().setHSL( i / 50, 0.6 + Math.random() * 0.4, 0.35 + Math.random() * 0.3 ),
+						roughness: Math.random(),
+						metalness: Math.random(),
+					} );
+					material.name = 'Standard_' + i;
+
+					uniqueMaterials.push( material );
+
+				}
+
+				const meshesPerRing = 10;
+
+				for ( let i = 0; i < 100; i ++ ) {
+
+					const material = i < 50 ? uniqueMaterials[ i ] : uniqueMaterials[ i - 50 ];
+					const geometry = geometries[ i % geometries.length ];
+					const mesh = new THREE.Mesh( geometry, material );
+
+					const ring = Math.floor( i / meshesPerRing );
+					const indexInRing = i % meshesPerRing;
+					const angle = ( indexInRing / meshesPerRing ) * Math.PI * 2 + ring * 0.3;
+					const radius = 6 + ring * 4;
+
+					mesh.position.set(
+						Math.cos( angle ) * radius,
+						0.7 + Math.random() * 2,
+						Math.sin( angle ) * radius
+					);
+
+					mesh.rotation.set(
+						Math.random() * Math.PI,
+						Math.random() * Math.PI,
+						0
+					);
+
+					scene.add( mesh );
+
+				}
+
+				// Center sphere
+
+				const centerMaterial = new THREE.MeshStandardMaterial( { color: 0xffffff, roughness: 0.1, metalness: 0.9 } );
+				const centerSphere = new THREE.Mesh( new THREE.SphereGeometry( 2, 128, 128 ), centerMaterial );
+				centerSphere.position.y = 2;
+				scene.add( centerSphere );
+
+				// Ambient light
+
+				scene.add( new THREE.AmbientLight( 0x404040, 0.5 ) );
+
+				// Start with a couple point lights
+
+				addLight();
+				addLight();
+
+				// Renderer
+
+				createRenderer();
+
+				// Inspector GUI
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function createRenderer() {
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.inspector = new Inspector();
+				document.body.appendChild( renderer.domElement );
+
+				if ( params.dynamic ) renderer.lighting = new DynamicLighting();
+
+				// Inspector GUI
+
+				const gui = renderer.inspector.createParameters( 'Dynamic Lights' );
+
+				gui.add( params, 'dynamic' ).name( 'dynamic mode' ).onChange( () => {
+
+					renderer.dispose();
+					document.body.removeChild( renderer.domElement );
+					clearInterval( autoAddInterval );
+					autoAddInterval = null;
+					params.autoAdd = false;
+					createRenderer();
+
+				} );
+
+				gui.add( params, 'autoAdd' ).name( 'auto-add lights' ).onChange( ( value ) => {
+
+					if ( value ) {
+
+						autoAddInterval = setInterval( () => {
+
+							addLight();
+
+						}, 500 );
+
+					} else {
+
+						clearInterval( autoAddInterval );
+						autoAddInterval = null;
+
+					}
+
+				} );
+
+				gui.add( params, 'addLight' ).name( 'add light' );
+				gui.add( params, 'removeLight' ).name( 'remove light' );
+				gui.add( params, 'removeAll' ).name( 'remove all lights' );
+				gui.add( params, 'lightCount' ).name( 'point lights' ).listen();
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true;
+				controls.target.set( 0, 2, 0 );
+				controls.update();
+
+			}
+
+			function addLight() {
+
+				const color = new THREE.Color().setHSL( Math.random(), 0.8, 0.5 );
+				const light = new THREE.PointLight( color, 1000 );
+
+				const angle = Math.random() * Math.PI * 2;
+				const radius = 5 + Math.random() * 20;
+				light.position.set(
+					Math.cos( angle ) * radius,
+					1 + Math.random() * 6,
+					Math.sin( angle ) * radius
+				);
+
+				light.userData.angle = angle;
+				light.userData.radius = radius;
+				light.userData.speed = 0.2 + Math.random() * 0.8;
+				light.userData.baseY = light.position.y;
+
+				// Visual indicator
+				const sphere = new THREE.Mesh(
+					new THREE.SphereGeometry( 0.15, 8, 8 ),
+					new THREE.MeshBasicMaterial( { color: color } )
+				);
+				light.add( sphere );
+
+				scene.add( light );
+				pointLights.push( light );
+
+				params.lightCount = pointLights.length;
+
+			}
+
+			function removeLight() {
+
+				if ( pointLights.length === 0 ) return;
+
+				const light = pointLights.pop();
+				scene.remove( light );
+				light.dispose();
+
+				params.lightCount = pointLights.length;
+
+			}
+
+			function removeAllLights() {
+
+				while ( pointLights.length > 0 ) {
+
+					const light = pointLights.pop();
+					scene.remove( light );
+					light.dispose();
+
+				}
+
+				params.lightCount = 0;
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				timer.update();
+
+				const time = timer.getElapsed();
+
+				controls.update();
+
+				// Animate lights
+
+				for ( let i = 0; i < pointLights.length; i ++ ) {
+
+					const light = pointLights[ i ];
+					const d = light.userData;
+					const t = time * d.speed + d.angle;
+
+					light.position.x = Math.cos( t ) * d.radius;
+					light.position.z = Math.sin( t ) * d.radius;
+					light.position.y = d.baseY + Math.sin( t * 2 ) * 0.5;
+
+				}
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 6 - 27
src/nodes/lighting/LightsNode.js

@@ -213,14 +213,12 @@ class LightsNode extends Node {
 
 				if ( previousLightNodes !== null ) {
 
-					lightNode = getLightNodeById( light.id, previousLightNodes ); // reuse existing light node
+					lightNode = getLightNodeById( light.id, previousLightNodes );
 
 				}
 
 				if ( lightNode === null ) {
 
-					// find the corresponding node type for a given light
-
 					const lightNodeClass = nodeLibrary.getLightNodeClass( light.constructor );
 
 					if ( lightNodeClass === null ) {
@@ -230,23 +228,18 @@ class LightsNode extends Node {
 
 					}
 
-					let lightNode = null;
-
-					if ( ! _lightsNodeRef.has( light ) ) {
-
-						lightNode = new lightNodeClass( light );
-						_lightsNodeRef.set( light, lightNode );
+					if ( _lightsNodeRef.has( light ) === false ) {
 
-					} else {
-
-						lightNode = _lightsNodeRef.get( light );
+						_lightsNodeRef.set( light, new lightNodeClass( light ) );
 
 					}
 
-					lightNodes.push( lightNode );
+					lightNode = _lightsNodeRef.get( light );
 
 				}
 
+				lightNodes.push( lightNode );
+
 			}
 
 		}
@@ -325,8 +318,6 @@ class LightsNode extends Node {
 
 		builder.lightsNode = this;
 
-		//
-
 		let outgoingLightNode = this.outgoingLightNode;
 
 		const context = builder.context;
@@ -342,16 +333,10 @@ class LightsNode extends Node {
 
 			const stack = builder.addStack();
 
-			//
-
 			properties.nodes = stack.nodes;
 
-			//
-
 			lightingModel.start( builder );
 
-			//
-
 			const { backdrop, backdropAlpha } = context;
 			const { directDiffuse, directSpecular, indirectDiffuse, indirectSpecular } = context.reflectedLight;
 
@@ -376,12 +361,8 @@ class LightsNode extends Node {
 
 			outgoingLightNode.assign( totalDiffuseNode.add( totalSpecularNode ) );
 
-			//
-
 			lightingModel.finish( builder );
 
-			//
-
 			outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );
 
 		} else {
@@ -390,8 +371,6 @@ class LightsNode extends Node {
 
 		}
 
-		//
-
 		builder.lightsNode = currentLightsNode;
 
 		return outgoingLightNode;

粤ICP备19079148号