three
Version:
JavaScript 3D library
902 lines (623 loc) • 19.4 kB
JavaScript
import { NodeUpdateType } from './constants.js';
import { getNodeChildren, getCacheKey, hash } from './NodeUtils.js';
import { EventDispatcher } from '../../core/EventDispatcher.js';
import { MathUtils } from '../../math/MathUtils.js';
let _nodeId = 0;
/**
* Base class for all nodes.
*
* @augments EventDispatcher
*/
class Node extends EventDispatcher {
static get type() {
return 'Node';
}
/**
* Constructs a new node.
*
* @param {?string} nodeType - The node type.
*/
constructor( nodeType = null ) {
super();
/**
* The node type. This represents the result type of the node (e.g. `float` or `vec3`).
*
* @type {?string}
* @default null
*/
this.nodeType = nodeType;
/**
* The update type of the node's {@link Node#update} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateType = NodeUpdateType.NONE;
/**
* The update type of the node's {@link Node#updateBefore} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateBeforeType = NodeUpdateType.NONE;
/**
* The update type of the node's {@link Node#updateAfter} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateAfterType = NodeUpdateType.NONE;
/**
* The UUID of the node.
*
* @type {string}
* @readonly
*/
this.uuid = MathUtils.generateUUID();
/**
* The version of the node. The version automatically is increased when {@link Node#needsUpdate} is set to `true`.
*
* @type {number}
* @readonly
* @default 0
*/
this.version = 0;
/**
* Whether this node is global or not. This property is relevant for the internal
* node caching system. All nodes which should be declared just once should
* set this flag to `true` (a typical example is {@link AttributeNode}).
*
* @type {boolean}
* @default false
*/
this.global = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNode = true;
// private
/**
* The cache key of this node.
*
* @private
* @type {?number}
* @default null
*/
this._cacheKey = null;
/**
* The cache key 's version.
*
* @private
* @type {number}
* @default 0
*/
this._cacheKeyVersion = 0;
Object.defineProperty( this, 'id', { value: _nodeId ++ } );
}
/**
* Set this property to `true` when the node should be regenerated.
*
* @type {boolean}
* @default false
* @param {boolean} value
*/
set needsUpdate( value ) {
if ( value === true ) {
this.version ++;
}
}
/**
* The type of the class. The value is usually the constructor name.
*
* @type {string}
* @readonly
*/
get type() {
return this.constructor.type;
}
/**
* Convenient method for defining {@link Node#update}.
*
* @param {Function} callback - The update method.
* @param {string} updateType - The update type.
* @return {Node} A reference to this node.
*/
onUpdate( callback, updateType ) {
this.updateType = updateType;
this.update = callback.bind( this.getSelf() );
return this;
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `FRAME`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onFrameUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.FRAME );
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `RENDER`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onRenderUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.RENDER );
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `OBJECT`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onObjectUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.OBJECT );
}
/**
* Convenient method for defining {@link Node#updateReference}.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onReference( callback ) {
this.updateReference = callback.bind( this.getSelf() );
return this;
}
/**
* The `this` reference might point to a Proxy so this method can be used
* to get the reference to the actual node instance.
*
* @return {Node} A reference to the node.
*/
getSelf() {
// Returns non-node object.
return this.self || this;
}
/**
* Nodes might refer to other objects like materials. This method allows to dynamically update the reference
* to such objects based on a given state (e.g. the current node frame or builder).
*
* @param {any} state - This method can be invocated in different contexts so `state` can refer to any object type.
* @return {any} The updated reference.
*/
updateReference( /*state*/ ) {
return this;
}
/**
* By default this method returns the value of the {@link Node#global} flag. This method
* can be overwritten in derived classes if an analytical way is required to determine the
* global status.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether this node is global or not.
*/
isGlobal( /*builder*/ ) {
return this.global;
}
/**
* Generator function that can be used to iterate over the child nodes.
*
* @generator
* @yields {Node} A child node.
*/
* getChildren() {
for ( const { childNode } of getNodeChildren( this ) ) {
yield childNode;
}
}
/**
* Calling this method dispatches the `dispose` event. This event can be used
* to register event listeners for clean up tasks.
*/
dispose() {
this.dispatchEvent( { type: 'dispose' } );
}
/**
* Callback for {@link Node#traverse}.
*
* @callback traverseCallback
* @param {Node} node - The current node.
*/
/**
* Can be used to traverse through the node's hierarchy.
*
* @param {traverseCallback} callback - A callback that is executed per node.
*/
traverse( callback ) {
callback( this );
for ( const childNode of this.getChildren() ) {
childNode.traverse( callback );
}
}
/**
* Returns the cache key for this node.
*
* @param {boolean} [force=false] - When set to `true`, a recomputation of the cache key is forced.
* @return {number} The cache key of the node.
*/
getCacheKey( force = false ) {
force = force || this.version !== this._cacheKeyVersion;
if ( force === true || this._cacheKey === null ) {
this._cacheKey = hash( getCacheKey( this, force ), this.customCacheKey() );
this._cacheKeyVersion = this.version;
}
return this._cacheKey;
}
/**
* Generate a custom cache key for this node.
*
* @return {number} The cache key of the node.
*/
customCacheKey() {
return 0;
}
/**
* Returns the references to this node which is by default `this`.
*
* @return {Node} A reference to this node.
*/
getScope() {
return this;
}
/**
* Returns the hash of the node which is used to identify the node. By default it's
* the {@link Node#uuid} however derived node classes might have to overwrite this method
* depending on their implementation.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The hash.
*/
getHash( /*builder*/ ) {
return this.uuid;
}
/**
* Returns the update type of {@link Node#update}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateType() {
return this.updateType;
}
/**
* Returns the update type of {@link Node#updateBefore}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateBeforeType() {
return this.updateBeforeType;
}
/**
* Returns the update type of {@link Node#updateAfter}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateAfterType() {
return this.updateAfterType;
}
/**
* Certain types are composed of multiple elements. For example a `vec3`
* is composed of three `float` values. This method returns the type of
* these elements.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
getElementType( builder ) {
const type = this.getNodeType( builder );
const elementType = builder.getElementType( type );
return elementType;
}
/**
* Returns the node member type for the given name.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The name of the member.
* @return {string} The type of the node.
*/
getMemberType( /*builder, name*/ ) {
return 'void';
}
/**
* Returns the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
getNodeType( builder ) {
const nodeProperties = builder.getNodeProperties( this );
if ( nodeProperties.outputNode ) {
return nodeProperties.outputNode.getNodeType( builder );
}
return this.nodeType;
}
/**
* This method is used during the build process of a node and ensures
* equal nodes are not built multiple times but just once. For example if
* `attribute( 'uv' )` is used multiple times by the user, the build
* process makes sure to process just the first node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The shared node if possible. Otherwise `this` is returned.
*/
getShared( builder ) {
const hash = this.getHash( builder );
const nodeFromHash = builder.getNodeFromHash( hash );
return nodeFromHash || this;
}
/**
* Represents the setup stage which is the first step of the build process, see {@link Node#build} method.
* This method is often overwritten in derived modules to prepare the node which is used as the output/result.
* The output node must be returned in the `return` statement.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?Node} The output node.
*/
setup( builder ) {
const nodeProperties = builder.getNodeProperties( this );
let index = 0;
for ( const childNode of this.getChildren() ) {
nodeProperties[ 'node' + index ++ ] = childNode;
}
// return a outputNode if exists or null
return nodeProperties.outputNode || null;
}
/**
* Represents the analyze stage which is the second step of the build process, see {@link Node#build} method.
* This stage analyzes the node hierarchy and ensures descendent nodes are built.
*
* @param {NodeBuilder} builder - The current node builder.
*/
analyze( builder ) {
const usageCount = builder.increaseUsage( this );
if ( usageCount === 1 ) {
// node flow children
const nodeProperties = builder.getNodeProperties( this );
for ( const childNode of Object.values( nodeProperties ) ) {
if ( childNode && childNode.isNode === true ) {
childNode.build( builder );
}
}
}
}
/**
* Represents the generate stage which is the third step of the build process, see {@link Node#build} method.
* This state builds the output node and returns the resulting shader string.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?string} output - Can be used to define the output type.
* @return {?string} The generated shader string.
*/
generate( builder, output ) {
const { outputNode } = builder.getNodeProperties( this );
if ( outputNode && outputNode.isNode === true ) {
return outputNode.build( builder, output );
}
}
/**
* The method can be implemented to update the node's internal state before it is used to render an object.
* The {@link Node#updateBeforeType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
updateBefore( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* The method can be implemented to update the node's internal state after it was used to render an object.
* The {@link Node#updateAfterType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
updateAfter( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* The method can be implemented to update the node's internal state when it is used to render an object.
* The {@link Node#updateType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
update( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* This method performs the build of a node. The behavior of this method as well as its return value depend
* on the current build stage (setup, analyze or generate).
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?string} output - Can be used to define the output type.
* @return {?string} When this method is executed in the setup or analyze stage, `null` is returned. In the generate stage, the generated shader string.
*/
build( builder, output = null ) {
const refNode = this.getShared( builder );
if ( this !== refNode ) {
return refNode.build( builder, output );
}
builder.addNode( this );
builder.addChain( this );
/* Build stages expected results:
- "setup" -> Node
- "analyze" -> null
- "generate" -> String
*/
let result = null;
const buildStage = builder.getBuildStage();
if ( buildStage === 'setup' ) {
this.updateReference( builder );
const properties = builder.getNodeProperties( this );
if ( properties.initialized !== true ) {
//const stackNodesBeforeSetup = builder.stack.nodes.length;
properties.initialized = true;
const outputNode = this.setup( builder ); // return a node or null
const isNodeOutput = outputNode && outputNode.isNode === true;
/*if ( isNodeOutput && builder.stack.nodes.length !== stackNodesBeforeSetup ) {
// !! no outputNode !!
//outputNode = builder.stack;
}*/
for ( const childNode of Object.values( properties ) ) {
if ( childNode && childNode.isNode === true ) {
childNode.build( builder );
}
}
if ( isNodeOutput ) {
outputNode.build( builder );
}
properties.outputNode = outputNode;
}
} else if ( buildStage === 'analyze' ) {
this.analyze( builder );
} else if ( buildStage === 'generate' ) {
const isGenerateOnce = this.generate.length === 1;
if ( isGenerateOnce ) {
const type = this.getNodeType( builder );
const nodeData = builder.getDataFromNode( this );
result = nodeData.snippet;
if ( result === undefined ) {
if ( nodeData.generated === undefined ) {
nodeData.generated = true;
result = this.generate( builder ) || '';
nodeData.snippet = result;
} else {
console.warn( 'THREE.Node: Recursion detected.', this );
result = '';
}
} else if ( nodeData.flowCodes !== undefined && builder.context.nodeBlock !== undefined ) {
builder.addFlowCodeHierarchy( this, builder.context.nodeBlock );
}
result = builder.format( result, type, output );
} else {
result = this.generate( builder, output ) || '';
}
}
builder.removeChain( this );
builder.addSequentialNode( this );
return result;
}
/**
* Returns the child nodes as a JSON object.
*
* @return {Array<Object>} An iterable list of serialized child objects as JSON.
*/
getSerializeChildren() {
return getNodeChildren( this );
}
/**
* Serializes the node to JSON.
*
* @param {Object} json - The output JSON object.
*/
serialize( json ) {
const nodeChildren = this.getSerializeChildren();
const inputNodes = {};
for ( const { property, index, childNode } of nodeChildren ) {
if ( index !== undefined ) {
if ( inputNodes[ property ] === undefined ) {
inputNodes[ property ] = Number.isInteger( index ) ? [] : {};
}
inputNodes[ property ][ index ] = childNode.toJSON( json.meta ).uuid;
} else {
inputNodes[ property ] = childNode.toJSON( json.meta ).uuid;
}
}
if ( Object.keys( inputNodes ).length > 0 ) {
json.inputNodes = inputNodes;
}
}
/**
* Deserializes the node from the given JSON.
*
* @param {Object} json - The JSON object.
*/
deserialize( json ) {
if ( json.inputNodes !== undefined ) {
const nodes = json.meta.nodes;
for ( const property in json.inputNodes ) {
if ( Array.isArray( json.inputNodes[ property ] ) ) {
const inputArray = [];
for ( const uuid of json.inputNodes[ property ] ) {
inputArray.push( nodes[ uuid ] );
}
this[ property ] = inputArray;
} else if ( typeof json.inputNodes[ property ] === 'object' ) {
const inputObject = {};
for ( const subProperty in json.inputNodes[ property ] ) {
const uuid = json.inputNodes[ property ][ subProperty ];
inputObject[ subProperty ] = nodes[ uuid ];
}
this[ property ] = inputObject;
} else {
const uuid = json.inputNodes[ property ];
this[ property ] = nodes[ uuid ];
}
}
}
}
/**
* Serializes the node into the three.js JSON Object/Scene format.
*
* @param {?Object} meta - An optional JSON object that already holds serialized data from other scene objects.
* @return {Object} The serialized node.
*/
toJSON( meta ) {
const { uuid, type } = this;
const isRoot = ( meta === undefined || typeof meta === 'string' );
if ( isRoot ) {
meta = {
textures: {},
images: {},
nodes: {}
};
}
// serialize
let data = meta.nodes[ uuid ];
if ( data === undefined ) {
data = {
uuid,
type,
meta,
metadata: {
version: 4.6,
type: 'Node',
generator: 'Node.toJSON'
}
};
if ( isRoot !== true ) meta.nodes[ data.uuid ] = data;
this.serialize( data );
delete data.meta;
}
// TODO: Copied from Object3D.toJSON
function extractFromCache( cache ) {
const values = [];
for ( const key in cache ) {
const data = cache[ key ];
delete data.metadata;
values.push( data );
}
return values;
}
if ( isRoot ) {
const textures = extractFromCache( meta.textures );
const images = extractFromCache( meta.images );
const nodes = extractFromCache( meta.nodes );
if ( textures.length > 0 ) data.textures = textures;
if ( images.length > 0 ) data.images = images;
if ( nodes.length > 0 ) data.nodes = nodes;
}
return data;
}
}
export default Node;