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 00000000000000..00e7abad00f74d Binary files /dev/null and b/examples/screenshots/webgpu_lights_dynamic.jpg differ 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/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. * 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;