import Node from '../core/Node.js';
import { nodeObject, vec3 } from '../tsl/TSLBase.js';
import { hashArray } from '../core/NodeUtils.js';
/** @module LightsNode **/
const sortLights = ( lights ) => {
return lights.sort( ( a, b ) => a.id - b.id );
};
const getLightNodeById = ( id, lightNodes ) => {
for ( const lightNode of lightNodes ) {
if ( lightNode.isAnalyticLightNode && lightNode.light.id === id ) {
return lightNode;
}
}
return null;
};
const _lightsNodeRef = /*@__PURE__*/ new WeakMap();
/**
* This node represents the scene's lighting and manages the lighting model's life cycle
* for the current build 3D object. It is responsible for computing the total outgoing
* light in a given lighting context.
*
* @augments Node
*/
class LightsNode extends Node {
static get type() {
return 'LightsNode';
}
/**
* Constructs a new lights node.
*/
constructor() {
super( 'vec3' );
/**
* A node representing the total diffuse light.
*
* @type {Node<vec3>}
*/
this.totalDiffuseNode = vec3().toVar( 'totalDiffuse' );
/**
* A node representing the total specular light.
*
* @type {Node<vec3>}
*/
this.totalSpecularNode = vec3().toVar( 'totalSpecular' );
/**
* A node representing the outgoing light.
*
* @type {Node<vec3>}
*/
this.outgoingLightNode = vec3().toVar( 'outgoingLight' );
/**
* An array representing the lights in the scene.
*
* @private
* @type {Array<Light>}
*/
this._lights = [];
/**
* For each light in the scene, this node will create a
* corresponding light node.
*
* @private
* @type {Array<LightingNode>?}
* @default null
*/
this._lightNodes = null;
/**
* A hash for identifying the current light nodes setup.
*
* @private
* @type {String?}
* @default null
*/
this._lightNodesHash = null;
/**
* `LightsNode` sets this property to `true` by default.
*
* @type {Boolean}
* @default true
*/
this.global = true;
}
/**
* Overwrites the default {@link Node#customCacheKey} implementation by including the
* light IDs into the cache key.
*
* @return {Number} The custom cache key.
*/
customCacheKey() {
const lightIDs = [];
const lights = this._lights;
for ( let i = 0; i < lights.length; i ++ ) {
lightIDs.push( lights[ i ].id );
}
return hashArray( lightIDs );
}
/**
* Computes a hash value for identifying the current light nodes setup.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {String} The computed hash.
*/
getHash( builder ) {
if ( this._lightNodesHash === null ) {
if ( this._lightNodes === null ) this.setupLightsNode( builder );
const hash = [];
for ( const lightNode of this._lightNodes ) {
hash.push( lightNode.getSelf().getHash() );
}
this._lightNodesHash = 'lights-' + hash.join( ',' );
}
return this._lightNodesHash;
}
analyze( builder ) {
const properties = builder.getDataFromNode( this );
for ( const node of properties.nodes ) {
node.build( builder );
}
}
/**
* Creates lighting nodes for each scene light. This makes it possible to further
* process lights in the node system.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
*/
setupLightsNode( builder ) {
const lightNodes = [];
const previousLightNodes = this._lightNodes;
const lights = sortLights( this._lights );
const nodeLibrary = builder.renderer.library;
for ( const light of lights ) {
if ( light.isNode ) {
lightNodes.push( nodeObject( light ) );
} else {
let lightNode = null;
if ( previousLightNodes !== null ) {
lightNode = getLightNodeById( light.id, previousLightNodes ); // reuse existing light node
}
if ( lightNode === null ) {
// find the corresponding node type for a given light
const lightNodeClass = nodeLibrary.getLightNodeClass( light.constructor );
if ( lightNodeClass === null ) {
console.warn( `LightsNode.setupNodeLights: Light node not found for ${ light.constructor.name }` );
continue;
}
let lightNode = null;
if ( ! _lightsNodeRef.has( light ) ) {
lightNode = nodeObject( new lightNodeClass( light ) );
_lightsNodeRef.set( light, lightNode );
} else {
lightNode = _lightsNodeRef.get( light );
}
lightNodes.push( lightNode );
}
}
}
this._lightNodes = lightNodes;
}
/**
* Setups the internal lights by building all respective
* light nodes.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Array<LightingNode>} lightNodes - An array of lighting nodes.
*/
setupLights( builder, lightNodes ) {
for ( const lightNode of lightNodes ) {
lightNode.build( builder );
}
}
/**
* The implementation makes sure that for each light in the scene
* there is a corresponding light node. By building the light nodes
* and evaluating the lighting model the outgoing light is computed.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {Node<vec3>} A node representing the outgoing light.
*/
setup( builder ) {
if ( this._lightNodes === null ) this.setupLightsNode( builder );
const context = builder.context;
const lightingModel = context.lightingModel;
let outgoingLightNode = this.outgoingLightNode;
if ( lightingModel ) {
const { _lightNodes, totalDiffuseNode, totalSpecularNode } = this;
context.outgoingLight = outgoingLightNode;
const stack = builder.addStack();
//
const properties = builder.getDataFromNode( this );
properties.nodes = stack.nodes;
//
lightingModel.start( context, stack, builder );
// lights
this.setupLights( builder, _lightNodes );
//
lightingModel.indirect( context, stack, builder );
//
const { backdrop, backdropAlpha } = context;
const { directDiffuse, directSpecular, indirectDiffuse, indirectSpecular } = context.reflectedLight;
let totalDiffuse = directDiffuse.add( indirectDiffuse );
if ( backdrop !== null ) {
if ( backdropAlpha !== null ) {
totalDiffuse = vec3( backdropAlpha.mix( totalDiffuse, backdrop ) );
} else {
totalDiffuse = vec3( backdrop );
}
context.material.transparent = true;
}
totalDiffuseNode.assign( totalDiffuse );
totalSpecularNode.assign( directSpecular.add( indirectSpecular ) );
outgoingLightNode.assign( totalDiffuseNode.add( totalSpecularNode ) );
//
lightingModel.finish( context, stack, builder );
//
outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );
}
return outgoingLightNode;
}
/**
* Configures this node with an array of lights.
*
* @param {Array<Light>} lights - An array of lights.
* @return {LightsNode} A reference to this node.
*/
setLights( lights ) {
this._lights = lights;
this._lightNodes = null;
this._lightNodesHash = null;
return this;
}
/**
* Returns an array of the scene's lights.
*
* @return {Array<Light>} The scene's lights.
*/
getLights() {
return this._lights;
}
/**
* Whether the scene has lights or not.
*
* @type {Boolean}
*/
get hasLights() {
return this._lights.length > 0;
}
}
export default LightsNode;
/**
* TSL function for creating an instance of `LightsNode` and configuring
* it with the given array of lights.
*
* @function
* @param {Array<Light>} lights - An array of lights.
* @return {LightsNode} The created lights node.
*/
export const lights = ( lights = [] ) => nodeObject( new LightsNode() ).setLights( lights );