Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
82 changes: 82 additions & 0 deletions examples/jsm/lighting/DynamicLighting.js
Original file line number Diff line number Diff line change
@@ -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 changes: 300 additions & 0 deletions examples/jsm/tsl/lighting/DynamicLightsNode.js
Original file line number Diff line number Diff line change
@@ -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 );
Loading