From bf65eaf73a9b7b6ebf9edd383b10616bf06e94af Mon Sep 17 00:00:00 2001 From: Renaud Rohlinger Date: Thu, 12 Mar 2026 11:24:49 +0900 Subject: [PATCH 1/2] WebGPURenderer: Introduce Dynamic Lights (#33042) --- examples/files.json | 1 + examples/jsm/lighting/DynamicLighting.js | 82 +++++ .../jsm/tsl/lighting/DynamicLightsNode.js | 300 ++++++++++++++++ .../tsl/lighting/data/AmbientLightDataNode.js | 61 ++++ .../lighting/data/DirectionalLightDataNode.js | 111 ++++++ .../lighting/data/HemisphereLightDataNode.js | 99 ++++++ .../tsl/lighting/data/PointLightDataNode.js | 134 +++++++ .../tsl/lighting/data/SpotLightDataNode.js | 161 +++++++++ .../screenshots/webgpu_lights_dynamic.jpg | Bin 0 -> 28671 bytes examples/webgpu_lights_dynamic.html | 331 ++++++++++++++++++ src/nodes/lighting/LightsNode.js | 33 +- 11 files changed, 1286 insertions(+), 27 deletions(-) create mode 100644 examples/jsm/lighting/DynamicLighting.js create mode 100644 examples/jsm/tsl/lighting/DynamicLightsNode.js create mode 100644 examples/jsm/tsl/lighting/data/AmbientLightDataNode.js create mode 100644 examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js create mode 100644 examples/jsm/tsl/lighting/data/HemisphereLightDataNode.js create mode 100644 examples/jsm/tsl/lighting/data/PointLightDataNode.js create mode 100644 examples/jsm/tsl/lighting/data/SpotLightDataNode.js create mode 100644 examples/screenshots/webgpu_lights_dynamic.jpg create mode 100644 examples/webgpu_lights_dynamic.html diff --git a/examples/files.json b/examples/files.json index 023ca2c07e02e8..731fca9bb6d45f 100644 --- a/examples/files.json +++ b/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", diff --git a/examples/jsm/lighting/DynamicLighting.js b/examples/jsm/lighting/DynamicLighting.js new file mode 100644 index 00000000000000..0b67296ffcf976 --- /dev/null +++ b/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} 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; diff --git a/examples/jsm/tsl/lighting/DynamicLightsNode.js b/examples/jsm/tsl/lighting/DynamicLightsNode.js new file mode 100644 index 00000000000000..ec8649fdcbf6c1 --- /dev/null +++ b/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 ); diff --git a/examples/jsm/tsl/lighting/data/AmbientLightDataNode.js b/examples/jsm/tsl/lighting/data/AmbientLightDataNode.js new file mode 100644 index 00000000000000..382f33cbea0e3c --- /dev/null +++ b/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; diff --git a/examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js b/examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js new file mode 100644 index 00000000000000..893972d1d13847 --- /dev/null +++ b/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; diff --git a/examples/jsm/tsl/lighting/data/HemisphereLightDataNode.js b/examples/jsm/tsl/lighting/data/HemisphereLightDataNode.js new file mode 100644 index 00000000000000..93a8e3ab8cc749 --- /dev/null +++ b/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; diff --git a/examples/jsm/tsl/lighting/data/PointLightDataNode.js b/examples/jsm/tsl/lighting/data/PointLightDataNode.js new file mode 100644 index 00000000000000..33a07e8cb3273a --- /dev/null +++ b/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; diff --git a/examples/jsm/tsl/lighting/data/SpotLightDataNode.js b/examples/jsm/tsl/lighting/data/SpotLightDataNode.js new file mode 100644 index 00000000000000..63025a48857673 --- /dev/null +++ b/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; diff --git a/examples/screenshots/webgpu_lights_dynamic.jpg b/examples/screenshots/webgpu_lights_dynamic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..00e7abad00f74da77a7c69b6d9407ce06b5c100b GIT binary patch literal 28671 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 BGp+yt literal 0 HcmV?d00001 diff --git a/examples/webgpu_lights_dynamic.html b/examples/webgpu_lights_dynamic.html new file mode 100644 index 00000000000000..a62fb73783fa10 --- /dev/null +++ b/examples/webgpu_lights_dynamic.html @@ -0,0 +1,331 @@ + + + + three.js webgpu - dynamic lights + + + + + + +
+ + +
+ three.jsDynamic Lights +
+ + + Opt-in DynamicLighting avoids shader recompilation when adding/removing supported lights.
+ 100 meshes with 50 unique PBR materials. Use the Inspector to explore the scene. +
+
+ + + + + + diff --git a/src/nodes/lighting/LightsNode.js b/src/nodes/lighting/LightsNode.js index 1d7f1a8a25fdcc..3d5dfc68a28fda 100644 --- a/src/nodes/lighting/LightsNode.js +++ b/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; From 4fad923a7f1be3024aa4040d9413c134236317b1 Mon Sep 17 00:00:00 2001 From: PoseidonEnergy <109360495+PoseidonEnergy@users.noreply.github.com> Date: Thu, 12 Mar 2026 04:55:55 -0500 Subject: [PATCH 2/2] Math: Make use of Static initialization blocks. (#33140) --- src/math/Matrix2.js | 22 +++++++++++++--------- src/math/Matrix3.js | 22 +++++++++++++--------- src/math/Matrix4.js | 22 +++++++++++++--------- src/math/Vector2.js | 18 +++++++++++------- src/math/Vector3.js | 20 ++++++++++++-------- src/math/Vector4.js | 22 +++++++++++++--------- 6 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/math/Matrix2.js b/src/math/Matrix2.js index 3e096aefc66892..b7b82c1dd0d3e0 100644 --- a/src/math/Matrix2.js +++ b/src/math/Matrix2.js @@ -26,6 +26,19 @@ */ export class Matrix2 { + static { + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + Matrix2.prototype.isMatrix2 = true; + + } + /** * Constructs a new 2x2 matrix. The arguments are supposed to be * in row-major order. If no arguments are provided, the constructor @@ -38,15 +51,6 @@ export class Matrix2 { */ constructor( n11, n12, n21, n22 ) { - /** - * This flag can be used for type testing. - * - * @type {boolean} - * @readonly - * @default true - */ - Matrix2.prototype.isMatrix2 = true; - /** * A column-major list of matrix values. * diff --git a/src/math/Matrix3.js b/src/math/Matrix3.js index 250dc5aa5abc36..8fd40cb24ab09c 100644 --- a/src/math/Matrix3.js +++ b/src/math/Matrix3.js @@ -28,6 +28,19 @@ */ class Matrix3 { + static { + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + Matrix3.prototype.isMatrix3 = true; + + } + /** * Constructs a new 3x3 matrix. The arguments are supposed to be * in row-major order. If no arguments are provided, the constructor @@ -45,15 +58,6 @@ class Matrix3 { */ constructor( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) { - /** - * This flag can be used for type testing. - * - * @type {boolean} - * @readonly - * @default true - */ - Matrix3.prototype.isMatrix3 = true; - /** * A column-major list of matrix values. * diff --git a/src/math/Matrix4.js b/src/math/Matrix4.js index 78bb134b363b87..efacbf44ec420d 100644 --- a/src/math/Matrix4.js +++ b/src/math/Matrix4.js @@ -41,6 +41,19 @@ import { Vector3 } from './Vector3.js'; */ class Matrix4 { + static { + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + Matrix4.prototype.isMatrix4 = true; + + } + /** * Constructs a new 4x4 matrix. The arguments are supposed to be * in row-major order. If no arguments are provided, the constructor @@ -65,15 +78,6 @@ class Matrix4 { */ constructor( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) { - /** - * This flag can be used for type testing. - * - * @type {boolean} - * @readonly - * @default true - */ - Matrix4.prototype.isMatrix4 = true; - /** * A column-major list of matrix values. * diff --git a/src/math/Vector2.js b/src/math/Vector2.js index 60c53582d228b6..7113eb6aace566 100644 --- a/src/math/Vector2.js +++ b/src/math/Vector2.js @@ -27,13 +27,7 @@ import { clamp } from './MathUtils.js'; */ class Vector2 { - /** - * Constructs a new 2D vector. - * - * @param {number} [x=0] - The x value of this vector. - * @param {number} [y=0] - The y value of this vector. - */ - constructor( x = 0, y = 0 ) { + static { /** * This flag can be used for type testing. @@ -44,6 +38,16 @@ class Vector2 { */ Vector2.prototype.isVector2 = true; + } + + /** + * Constructs a new 2D vector. + * + * @param {number} [x=0] - The x value of this vector. + * @param {number} [y=0] - The y value of this vector. + */ + constructor( x = 0, y = 0 ) { + /** * The x value of this vector. * diff --git a/src/math/Vector3.js b/src/math/Vector3.js index fee3a6db5ef445..8c76998107c815 100644 --- a/src/math/Vector3.js +++ b/src/math/Vector3.js @@ -28,14 +28,7 @@ import { Quaternion } from './Quaternion.js'; */ class Vector3 { - /** - * Constructs a new 3D vector. - * - * @param {number} [x=0] - The x value of this vector. - * @param {number} [y=0] - The y value of this vector. - * @param {number} [z=0] - The z value of this vector. - */ - constructor( x = 0, y = 0, z = 0 ) { + static { /** * This flag can be used for type testing. @@ -46,6 +39,17 @@ class Vector3 { */ Vector3.prototype.isVector3 = true; + } + + /** + * Constructs a new 3D vector. + * + * @param {number} [x=0] - The x value of this vector. + * @param {number} [y=0] - The y value of this vector. + * @param {number} [z=0] - The z value of this vector. + */ + constructor( x = 0, y = 0, z = 0 ) { + /** * The x value of this vector. * diff --git a/src/math/Vector4.js b/src/math/Vector4.js index 52c7d29407bf5f..445e40eea9c9bf 100644 --- a/src/math/Vector4.js +++ b/src/math/Vector4.js @@ -26,15 +26,7 @@ import { clamp } from './MathUtils.js'; */ class Vector4 { - /** - * Constructs a new 4D vector. - * - * @param {number} [x=0] - The x value of this vector. - * @param {number} [y=0] - The y value of this vector. - * @param {number} [z=0] - The z value of this vector. - * @param {number} [w=1] - The w value of this vector. - */ - constructor( x = 0, y = 0, z = 0, w = 1 ) { + static { /** * This flag can be used for type testing. @@ -45,6 +37,18 @@ class Vector4 { */ Vector4.prototype.isVector4 = true; + } + + /** + * Constructs a new 4D vector. + * + * @param {number} [x=0] - The x value of this vector. + * @param {number} [y=0] - The y value of this vector. + * @param {number} [z=0] - The z value of this vector. + * @param {number} [w=1] - The w value of this vector. + */ + constructor( x = 0, y = 0, z = 0, w = 1 ) { + /** * The x value of this vector. *