@openhps/core
Version:
Open Hybrid Positioning System - Core component
1,692 lines (1,547 loc) • 63.1 kB
JavaScript
import NodeUniform from './NodeUniform.js';
import NodeAttribute from './NodeAttribute.js';
import NodeVarying from './NodeVarying.js';
import NodeVar from './NodeVar.js';
import NodeCode from './NodeCode.js';
import NodeCache from './NodeCache.js';
import ParameterNode from './ParameterNode.js';
import StructType from './StructType.js';
import FunctionNode from '../code/FunctionNode.js';
import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
import { getTypeFromLength } from './NodeUtils.js';
import { NodeUpdateType, defaultBuildStages, shaderStages } from './constants.js';
import { NumberNodeUniform, Vector2NodeUniform, Vector3NodeUniform, Vector4NodeUniform, ColorNodeUniform, Matrix2NodeUniform, Matrix3NodeUniform, Matrix4NodeUniform } from '../../renderers/common/nodes/NodeUniform.js';
import { stack } from './StackNode.js';
import { getCurrentStack, setCurrentStack } from '../tsl/TSLBase.js';
import CubeRenderTarget from '../../renderers/common/CubeRenderTarget.js';
import ChainMap from '../../renderers/common/ChainMap.js';
import BindGroup from '../../renderers/common/BindGroup.js';
import { REVISION, IntType, UnsignedIntType, LinearFilter, LinearMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapLinearFilter } from '../../constants.js';
import { RenderTarget } from '../../core/RenderTarget.js';
import { Color } from '../../math/Color.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { Float16BufferAttribute } from '../../core/BufferAttribute.js';
const rendererCache = new WeakMap();
const typeFromArray = new Map([[Int8Array, 'int'], [Int16Array, 'int'], [Int32Array, 'int'], [Uint8Array, 'uint'], [Uint16Array, 'uint'], [Uint32Array, 'uint'], [Float32Array, 'float']]);
const toFloat = value => {
if (/e/g.test(value)) {
return String(value).replace(/\+/g, '');
} else {
value = Number(value);
return value + (value % 1 ? '' : '.0');
}
};
/**
* Base class for builders which generate a shader program based
* on a 3D object and its node material definition.
*/
class NodeBuilder {
/**
* Constructs a new node builder.
*
* @param {Object3D} object - The 3D object.
* @param {Renderer} renderer - The current renderer.
* @param {NodeParser} parser - A reference to a node parser.
*/
constructor(object, renderer, parser) {
/**
* The 3D object.
*
* @type {Object3D}
*/
this.object = object;
/**
* The material of the 3D object.
*
* @type {?Material}
*/
this.material = object && object.material || null;
/**
* The geometry of the 3D object.
*
* @type {?BufferGeometry}
*/
this.geometry = object && object.geometry || null;
/**
* The current renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* A reference to a node parser.
*
* @type {NodeParser}
*/
this.parser = parser;
/**
* The scene the 3D object belongs to.
*
* @type {?Scene}
* @default null
*/
this.scene = null;
/**
* The camera the 3D object is rendered with.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
/**
* A list of all nodes the builder is processing
* for this 3D object.
*
* @type {Array<Node>}
*/
this.nodes = [];
/**
* A list of all sequential nodes.
*
* @type {Array<Node>}
*/
this.sequentialNodes = [];
/**
* A list of all nodes which {@link Node#update} method should be executed.
*
* @type {Array<Node>}
*/
this.updateNodes = [];
/**
* A list of all nodes which {@link Node#updateBefore} method should be executed.
*
* @type {Array<Node>}
*/
this.updateBeforeNodes = [];
/**
* A list of all nodes which {@link Node#updateAfter} method should be executed.
*
* @type {Array<Node>}
*/
this.updateAfterNodes = [];
/**
* A dictionary that assigns each node to a unique hash.
*
* @type {Object<number,Node>}
*/
this.hashNodes = {};
/**
* A reference to a node material observer.
*
* @type {?NodeMaterialObserver}
* @default null
*/
this.observer = null;
/**
* A reference to the current lights node.
*
* @type {?LightsNode}
* @default null
*/
this.lightsNode = null;
/**
* A reference to the current environment node.
*
* @type {?Node}
* @default null
*/
this.environmentNode = null;
/**
* A reference to the current fog node.
*
* @type {?FogNode}
* @default null
*/
this.fogNode = null;
/**
* The current clipping context.
*
* @type {?ClippingContext}
*/
this.clippingContext = null;
/**
* The generated vertex shader.
*
* @type {?string}
*/
this.vertexShader = null;
/**
* The generated fragment shader.
*
* @type {?string}
*/
this.fragmentShader = null;
/**
* The generated compute shader.
*
* @type {?string}
*/
this.computeShader = null;
/**
* Nodes used in the primary flow of code generation.
*
* @type {Object<string,Array<Node>>}
*/
this.flowNodes = {
vertex: [],
fragment: [],
compute: []
};
/**
* Nodes code from `.flowNodes`.
*
* @type {Object<string,string>}
*/
this.flowCode = {
vertex: '',
fragment: '',
compute: ''
};
/**
* This dictionary holds the node uniforms of the builder.
* The uniforms are maintained in an array for each shader stage.
*
* @type {Object}
*/
this.uniforms = {
vertex: [],
fragment: [],
compute: [],
index: 0
};
/**
* This dictionary holds the output structs of the builder.
* The structs are maintained in an array for each shader stage.
*
* @type {Object}
*/
this.structs = {
vertex: [],
fragment: [],
compute: [],
index: 0
};
/**
* This dictionary holds the bindings for each shader stage.
*
* @type {Object}
*/
this.bindings = {
vertex: {},
fragment: {},
compute: {}
};
/**
* This dictionary maintains the binding indices per bind group.
*
* @type {Object}
*/
this.bindingsIndexes = {};
/**
* Reference to the array of bind groups.
*
* @type {?Array<BindGroup>}
*/
this.bindGroups = null;
/**
* This array holds the node attributes of this builder
* created via {@link AttributeNode}.
*
* @type {Array<NodeAttribute>}
*/
this.attributes = [];
/**
* This array holds the node attributes of this builder
* created via {@link BufferAttributeNode}.
*
* @type {Array<NodeAttribute>}
*/
this.bufferAttributes = [];
/**
* This array holds the node varyings of this builder.
*
* @type {Array<NodeVarying>}
*/
this.varyings = [];
/**
* This dictionary holds the (native) node codes of this builder.
* The codes are maintained in an array for each shader stage.
*
* @type {Object<string,Array<NodeCode>>}
*/
this.codes = {};
/**
* This dictionary holds the node variables of this builder.
* The variables are maintained in an array for each shader stage.
* This dictionary is also used to count the number of variables
* according to their type (const, vars).
*
* @type {Object<string,Array<NodeVar>|number>}
*/
this.vars = {};
/**
* This dictionary holds the declarations for each shader stage.
*
* @type {Object}
*/
this.declarations = {};
/**
* Current code flow.
* All code generated in this stack will be stored in `.flow`.
*
* @type {{code: string}}
*/
this.flow = {
code: ''
};
/**
* A chain of nodes.
* Used to check recursive calls in node-graph.
*
* @type {Array<Node>}
*/
this.chaining = [];
/**
* The current stack.
* This reflects the current process in the code block hierarchy,
* it is useful to know if the current process is inside a conditional for example.
*
* @type {StackNode}
*/
this.stack = stack();
/**
* List of stack nodes.
* The current stack hierarchy is stored in an array.
*
* @type {Array<StackNode>}
*/
this.stacks = [];
/**
* A tab value. Used for shader string generation.
*
* @type {string}
* @default '\t'
*/
this.tab = '\t';
/**
* Reference to the current function node.
*
* @type {?FunctionNode}
* @default null
*/
this.currentFunctionNode = null;
/**
* The builder's context.
*
* @type {Object}
*/
this.context = {
material: this.material
};
/**
* The builder's cache.
*
* @type {NodeCache}
*/
this.cache = new NodeCache();
/**
* Since the {@link NodeBuilder#cache} might be temporarily
* overwritten by other caches, this member retains the reference
* to the builder's own cache.
*
* @type {NodeCache}
* @default this.cache
*/
this.globalCache = this.cache;
this.flowsData = new WeakMap();
/**
* The current shader stage.
*
* @type {?('vertex'|'fragment'|'compute'|'any')}
*/
this.shaderStage = null;
/**
* The current build stage.
*
* @type {?('setup'|'analyze'|'generate')}
*/
this.buildStage = null;
}
/**
* Returns the bind groups of the current renderer.
*
* @return {ChainMap} The cache.
*/
getBindGroupsCache() {
let bindGroupsCache = rendererCache.get(this.renderer);
if (bindGroupsCache === undefined) {
bindGroupsCache = new ChainMap();
rendererCache.set(this.renderer, bindGroupsCache);
}
return bindGroupsCache;
}
/**
* Factory method for creating an instance of {@link RenderTarget} with the given
* dimensions and options.
*
* @param {number} width - The width of the render target.
* @param {number} height - The height of the render target.
* @param {Object} options - The options of the render target.
* @return {RenderTarget} The render target.
*/
createRenderTarget(width, height, options) {
return new RenderTarget(width, height, options);
}
/**
* Factory method for creating an instance of {@link CubeRenderTarget} with the given
* dimensions and options.
*
* @param {number} size - The size of the cube render target.
* @param {Object} options - The options of the cube render target.
* @return {CubeRenderTarget} The cube render target.
*/
createCubeRenderTarget(size, options) {
return new CubeRenderTarget(size, options);
}
/**
* Whether the given node is included in the internal array of nodes or not.
*
* @param {Node} node - The node to test.
* @return {boolean} Whether the given node is included in the internal array of nodes or not.
*/
includes(node) {
return this.nodes.includes(node);
}
/**
* Returns the output struct name which is required by
* {@link OutputStructNode}.
*
* @abstract
* @return {string} The name of the output struct.
*/
getOutputStructName() {}
/**
* Returns a bind group for the given group name and binding.
*
* @private
* @param {string} groupName - The group name.
* @param {Array<NodeUniformsGroup>} bindings - List of bindings.
* @return {BindGroup} The bind group
*/
_getBindGroup(groupName, bindings) {
const bindGroupsCache = this.getBindGroupsCache();
//
const bindingsArray = [];
let sharedGroup = true;
for (const binding of bindings) {
bindingsArray.push(binding);
sharedGroup = sharedGroup && binding.groupNode.shared !== true;
}
//
let bindGroup;
if (sharedGroup) {
bindGroup = bindGroupsCache.get(bindingsArray);
if (bindGroup === undefined) {
bindGroup = new BindGroup(groupName, bindingsArray, this.bindingsIndexes[groupName].group, bindingsArray);
bindGroupsCache.set(bindingsArray, bindGroup);
}
} else {
bindGroup = new BindGroup(groupName, bindingsArray, this.bindingsIndexes[groupName].group, bindingsArray);
}
return bindGroup;
}
/**
* Returns an array of node uniform groups for the given group name and shader stage.
*
* @param {string} groupName - The group name.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {Array<NodeUniformsGroup>} The array of node uniform groups.
*/
getBindGroupArray(groupName, shaderStage) {
const bindings = this.bindings[shaderStage];
let bindGroup = bindings[groupName];
if (bindGroup === undefined) {
if (this.bindingsIndexes[groupName] === undefined) {
this.bindingsIndexes[groupName] = {
binding: 0,
group: Object.keys(this.bindingsIndexes).length
};
}
bindings[groupName] = bindGroup = [];
}
return bindGroup;
}
/**
* Returns a list bindings of all shader stages separated by groups.
*
* @return {Array<BindGroup>} The list of bindings.
*/
getBindings() {
let bindingsGroups = this.bindGroups;
if (bindingsGroups === null) {
const groups = {};
const bindings = this.bindings;
for (const shaderStage of shaderStages) {
for (const groupName in bindings[shaderStage]) {
const uniforms = bindings[shaderStage][groupName];
const groupUniforms = groups[groupName] || (groups[groupName] = []);
groupUniforms.push(...uniforms);
}
}
bindingsGroups = [];
for (const groupName in groups) {
const group = groups[groupName];
const bindingsGroup = this._getBindGroup(groupName, group);
bindingsGroups.push(bindingsGroup);
}
this.bindGroups = bindingsGroups;
}
return bindingsGroups;
}
/**
* Sorts the bind groups and updates {@link NodeBuilder#bindingsIndexes}.
*/
sortBindingGroups() {
const bindingsGroups = this.getBindings();
bindingsGroups.sort((a, b) => a.bindings[0].groupNode.order - b.bindings[0].groupNode.order);
for (let i = 0; i < bindingsGroups.length; i++) {
const bindingGroup = bindingsGroups[i];
this.bindingsIndexes[bindingGroup.name].group = i;
bindingGroup.index = i;
}
}
/**
* The builder maintains each node in a hash-based dictionary.
* This method sets the given node (value) with the given hash (key) into this dictionary.
*
* @param {Node} node - The node to add.
* @param {number} hash - The hash of the node.
*/
setHashNode(node, hash) {
this.hashNodes[hash] = node;
}
/**
* Adds a node to this builder.
*
* @param {Node} node - The node to add.
*/
addNode(node) {
if (this.nodes.includes(node) === false) {
this.nodes.push(node);
this.setHashNode(node, node.getHash(this));
}
}
/**
* It is used to add Nodes that will be used as FRAME and RENDER events,
* and need to follow a certain sequence in the calls to work correctly.
* This function should be called after 'setup()' in the 'build()' process to ensure that the child nodes are processed first.
*
* @param {Node} node - The node to add.
*/
addSequentialNode(node) {
if (this.sequentialNodes.includes(node) === false) {
this.sequentialNodes.push(node);
}
}
/**
* Checks the update types of nodes
*/
buildUpdateNodes() {
for (const node of this.nodes) {
const updateType = node.getUpdateType();
if (updateType !== NodeUpdateType.NONE) {
this.updateNodes.push(node.getSelf());
}
}
for (const node of this.sequentialNodes) {
const updateBeforeType = node.getUpdateBeforeType();
const updateAfterType = node.getUpdateAfterType();
if (updateBeforeType !== NodeUpdateType.NONE) {
this.updateBeforeNodes.push(node.getSelf());
}
if (updateAfterType !== NodeUpdateType.NONE) {
this.updateAfterNodes.push(node.getSelf());
}
}
}
/**
* A reference the current node which is the
* last node in the chain of nodes.
*
* @type {Node}
*/
get currentNode() {
return this.chaining[this.chaining.length - 1];
}
/**
* Whether the given texture is filtered or not.
*
* @param {Texture} texture - The texture to check.
* @return {boolean} Whether the given texture is filtered or not.
*/
isFilteredTexture(texture) {
return texture.magFilter === LinearFilter || texture.magFilter === LinearMipmapNearestFilter || texture.magFilter === NearestMipmapLinearFilter || texture.magFilter === LinearMipmapLinearFilter || texture.minFilter === LinearFilter || texture.minFilter === LinearMipmapNearestFilter || texture.minFilter === NearestMipmapLinearFilter || texture.minFilter === LinearMipmapLinearFilter;
}
/**
* Adds the given node to the internal node chain.
* This is used to check recursive calls in node-graph.
*
* @param {Node} node - The node to add.
*/
addChain(node) {
/*
if ( this.chaining.indexOf( node ) !== - 1 ) {
console.warn( 'Recursive node: ', node );
}
*/
this.chaining.push(node);
}
/**
* Removes the given node from the internal node chain.
*
* @param {Node} node - The node to remove.
*/
removeChain(node) {
const lastChain = this.chaining.pop();
if (lastChain !== node) {
throw new Error('NodeBuilder: Invalid node chaining!');
}
}
/**
* Returns the native shader method name for a given generic name. E.g.
* the method name `textureDimensions` matches the WGSL name but must be
* resolved to `textureSize` in GLSL.
*
* @abstract
* @param {string} method - The method name to resolve.
* @return {string} The resolved method name.
*/
getMethod(method) {
return method;
}
/**
* Returns a node for the given hash, see {@link NodeBuilder#setHashNode}.
*
* @param {number} hash - The hash of the node.
* @return {Node} The found node.
*/
getNodeFromHash(hash) {
return this.hashNodes[hash];
}
/**
* Adds the Node to a target flow so that it can generate code in the 'generate' process.
*
* @param {('vertex'|'fragment'|'compute')} shaderStage - The shader stage.
* @param {Node} node - The node to add.
* @return {Node} The node.
*/
addFlow(shaderStage, node) {
this.flowNodes[shaderStage].push(node);
return node;
}
/**
* Sets builder's context.
*
* @param {Object} context - The context to set.
*/
setContext(context) {
this.context = context;
}
/**
* Returns the builder's current context.
*
* @return {Object} The builder's current context.
*/
getContext() {
return this.context;
}
/**
* Gets a context used in shader construction that can be shared across different materials.
* This is necessary since the renderer cache can reuse shaders generated in one material and use them in another.
*
* @return {Object} The builder's current context without material.
*/
getSharedContext() {
const context = {
...this.context
};
delete context.material;
return this.context;
}
/**
* Sets builder's cache.
*
* @param {NodeCache} cache - The cache to set.
*/
setCache(cache) {
this.cache = cache;
}
/**
* Returns the builder's current cache.
*
* @return {NodeCache} The builder's current cache.
*/
getCache() {
return this.cache;
}
/**
* Returns a cache for the given node.
*
* @param {Node} node - The node.
* @param {boolean} [parent=true] - Whether this node refers to a shared parent cache or not.
* @return {NodeCache} The cache.
*/
getCacheFromNode(node, parent = true) {
const data = this.getDataFromNode(node);
if (data.cache === undefined) data.cache = new NodeCache(parent ? this.getCache() : null);
return data.cache;
}
/**
* Whether the requested feature is available or not.
*
* @abstract
* @param {string} name - The requested feature.
* @return {boolean} Whether the requested feature is supported or not.
*/
isAvailable( /*name*/
) {
return false;
}
/**
* Returns the vertexIndex input variable as a native shader string.
*
* @abstract
* @return {string} The instanceIndex shader string.
*/
getVertexIndex() {
console.warn('Abstract function.');
}
/**
* Returns the instanceIndex input variable as a native shader string.
*
* @abstract
* @return {string} The instanceIndex shader string.
*/
getInstanceIndex() {
console.warn('Abstract function.');
}
/**
* Returns the drawIndex input variable as a native shader string.
* Only relevant for WebGL and its `WEBGL_multi_draw` extension.
*
* @abstract
* @return {?string} The drawIndex shader string.
*/
getDrawIndex() {
console.warn('Abstract function.');
}
/**
* Returns the frontFacing input variable as a native shader string.
*
* @abstract
* @return {string} The frontFacing shader string.
*/
getFrontFacing() {
console.warn('Abstract function.');
}
/**
* Returns the fragCoord input variable as a native shader string.
*
* @abstract
* @return {string} The fragCoord shader string.
*/
getFragCoord() {
console.warn('Abstract function.');
}
/**
* Whether to flip texture data along its vertical axis or not. WebGL needs
* this method evaluate to `true`, WebGPU to `false`.
*
* @abstract
* @return {boolean} Whether to flip texture data along its vertical axis or not.
*/
isFlipY() {
return false;
}
/**
* Calling this method increases the usage count for the given node by one.
*
* @param {Node} node - The node to increase the usage count for.
* @return {number} The updated usage count.
*/
increaseUsage(node) {
const nodeData = this.getDataFromNode(node);
nodeData.usageCount = nodeData.usageCount === undefined ? 1 : nodeData.usageCount + 1;
return nodeData.usageCount;
}
/**
* Generates a texture sample shader string for the given texture data.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The texture property name.
* @param {string} uvSnippet - Snippet defining the texture coordinates.
* @return {string} The generated shader string.
*/
generateTexture( /* texture, textureProperty, uvSnippet */
) {
console.warn('Abstract function.');
}
/**
* Generates a texture LOD shader string for the given texture data.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The texture property name.
* @param {string} uvSnippet - Snippet defining the texture coordinates.
* @param {?string} depthSnippet - Snippet defining the 0-based texture array index to sample.
* @param {string} levelSnippet - Snippet defining the mip level.
* @return {string} The generated shader string.
*/
generateTextureLod( /* texture, textureProperty, uvSnippet, depthSnippet, levelSnippet */
) {
console.warn('Abstract function.');
}
/**
* Generates the array declaration string.
*
* @param {string} type - The type.
* @param {?number} [count] - The count.
* @return {string} The generated value as a shader string.
*/
generateArrayDeclaration(type, count) {
return this.getType(type) + '[ ' + count + ' ]';
}
/**
* Generates the array shader string for the given type and value.
*
* @param {string} type - The type.
* @param {?number} [count] - The count.
* @param {?Array<Node>} [values=null] - The default values.
* @return {string} The generated value as a shader string.
*/
generateArray(type, count, values = null) {
let snippet = this.generateArrayDeclaration(type, count) + '( ';
for (let i = 0; i < count; i++) {
const value = values ? values[i] : null;
if (value !== null) {
snippet += value.build(this, type);
} else {
snippet += this.generateConst(type);
}
if (i < count - 1) snippet += ', ';
}
snippet += ' )';
return snippet;
}
/**
* Generates the struct shader string.
*
* @param {string} type - The type.
* @param {Array<Object>} [membersLayout] - The count.
* @param {?Array<Node>} [values=null] - The default values.
* @return {string} The generated value as a shader string.
*/
generateStruct(type, membersLayout, values = null) {
const snippets = [];
for (const member of membersLayout) {
const {
name,
type
} = member;
if (values && values[name] && values[name].isNode) {
snippets.push(values[name].build(this, type));
} else {
snippets.push(this.generateConst(type));
}
}
return type + '( ' + snippets.join(', ') + ' )';
}
/**
* Generates the shader string for the given type and value.
*
* @param {string} type - The type.
* @param {?any} [value=null] - The value.
* @return {string} The generated value as a shader string.
*/
generateConst(type, value = null) {
if (value === null) {
if (type === 'float' || type === 'int' || type === 'uint') value = 0;else if (type === 'bool') value = false;else if (type === 'color') value = new Color();else if (type === 'vec2') value = new Vector2();else if (type === 'vec3') value = new Vector3();else if (type === 'vec4') value = new Vector4();
}
if (type === 'float') return toFloat(value);
if (type === 'int') return `${Math.round(value)}`;
if (type === 'uint') return value >= 0 ? `${Math.round(value)}u` : '0u';
if (type === 'bool') return value ? 'true' : 'false';
if (type === 'color') return `${this.getType('vec3')}( ${toFloat(value.r)}, ${toFloat(value.g)}, ${toFloat(value.b)} )`;
const typeLength = this.getTypeLength(type);
const componentType = this.getComponentType(type);
const generateConst = value => this.generateConst(componentType, value);
if (typeLength === 2) {
return `${this.getType(type)}( ${generateConst(value.x)}, ${generateConst(value.y)} )`;
} else if (typeLength === 3) {
return `${this.getType(type)}( ${generateConst(value.x)}, ${generateConst(value.y)}, ${generateConst(value.z)} )`;
} else if (typeLength === 4 && type !== 'mat2') {
return `${this.getType(type)}( ${generateConst(value.x)}, ${generateConst(value.y)}, ${generateConst(value.z)}, ${generateConst(value.w)} )`;
} else if (typeLength >= 4 && value && (value.isMatrix2 || value.isMatrix3 || value.isMatrix4)) {
return `${this.getType(type)}( ${value.elements.map(generateConst).join(', ')} )`;
} else if (typeLength > 4) {
return `${this.getType(type)}()`;
}
throw new Error(`NodeBuilder: Type '${type}' not found in generate constant attempt.`);
}
/**
* It might be necessary to convert certain data types to different ones
* so this method can be used to hide the conversion.
*
* @param {string} type - The type.
* @return {string} The updated type.
*/
getType(type) {
if (type === 'color') return 'vec3';
return type;
}
/**
* Whether the given attribute name is defined in the geometry or not.
*
* @param {string} name - The attribute name.
* @return {boolean} Whether the given attribute name is defined in the geometry.
*/
hasGeometryAttribute(name) {
return this.geometry && this.geometry.getAttribute(name) !== undefined;
}
/**
* Returns a node attribute for the given name and type.
*
* @param {string} name - The attribute's name.
* @param {string} type - The attribute's type.
* @return {NodeAttribute} The node attribute.
*/
getAttribute(name, type) {
const attributes = this.attributes;
// find attribute
for (const attribute of attributes) {
if (attribute.name === name) {
return attribute;
}
}
// create a new if no exist
const attribute = new NodeAttribute(name, type);
this.registerDeclaration(attribute);
attributes.push(attribute);
return attribute;
}
/**
* Returns for the given node and shader stage the property name for the shader.
*
* @param {Node} node - The node.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The property name.
*/
getPropertyName(node /*, shaderStage*/) {
return node.name;
}
/**
* Whether the given type is a vector type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a vector type or not.
*/
isVector(type) {
return /vec\d/.test(type);
}
/**
* Whether the given type is a matrix type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a matrix type or not.
*/
isMatrix(type) {
return /mat\d/.test(type);
}
/**
* Whether the given type is a reference type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a reference type or not.
*/
isReference(type) {
return type === 'void' || type === 'property' || type === 'sampler' || type === 'samplerComparison' || type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'depthTexture' || type === 'texture3D';
}
/**
* Checks if the given texture requires a manual conversion to the working color space.
*
* @abstract
* @param {Texture} texture - The texture to check.
* @return {boolean} Whether the given texture requires a conversion to working color space or not.
*/
needsToWorkingColorSpace( /*texture*/
) {
return false;
}
/**
* Returns the component type of a given texture.
*
* @param {Texture} texture - The texture.
* @return {string} The component type.
*/
getComponentTypeFromTexture(texture) {
const type = texture.type;
if (texture.isDataTexture) {
if (type === IntType) return 'int';
if (type === UnsignedIntType) return 'uint';
}
return 'float';
}
/**
* Returns the element type for a given type.
*
* @param {string} type - The type.
* @return {string} The element type.
*/
getElementType(type) {
if (type === 'mat2') return 'vec2';
if (type === 'mat3') return 'vec3';
if (type === 'mat4') return 'vec4';
return this.getComponentType(type);
}
/**
* Returns the component type for a given type.
*
* @param {string} type - The type.
* @return {string} The component type.
*/
getComponentType(type) {
type = this.getVectorType(type);
if (type === 'float' || type === 'bool' || type === 'int' || type === 'uint') return type;
const componentType = /(b|i|u|)(vec|mat)([2-4])/.exec(type);
if (componentType === null) return null;
if (componentType[1] === 'b') return 'bool';
if (componentType[1] === 'i') return 'int';
if (componentType[1] === 'u') return 'uint';
return 'float';
}
/**
* Returns the vector type for a given type.
*
* @param {string} type - The type.
* @return {string} The vector type.
*/
getVectorType(type) {
if (type === 'color') return 'vec3';
if (type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D') return 'vec4';
return type;
}
/**
* Returns the data type for the given the length and component type.
*
* @param {number} length - The length.
* @param {string} [componentType='float'] - The component type.
* @return {string} The type.
*/
getTypeFromLength(length, componentType = 'float') {
if (length === 1) return componentType;
let baseType = getTypeFromLength(length);
const prefix = componentType === 'float' ? '' : componentType[0];
// fix edge case for mat2x2 being same size as vec4
if (/mat2/.test(componentType) === true) {
baseType = baseType.replace('vec', 'mat');
}
return prefix + baseType;
}
/**
* Returns the type for a given typed array.
*
* @param {TypedArray} array - The typed array.
* @return {string} The type.
*/
getTypeFromArray(array) {
return typeFromArray.get(array.constructor);
}
/**
* Returns the type is an integer type.
*
* @param {string} type - The type.
* @return {boolean} Whether the type is an integer type or not.
*/
isInteger(type) {
return /int|uint|(i|u)vec/.test(type);
}
/**
* Returns the type for a given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @return {string} The type.
*/
getTypeFromAttribute(attribute) {
let dataAttribute = attribute;
if (attribute.isInterleavedBufferAttribute) dataAttribute = attribute.data;
const array = dataAttribute.array;
const itemSize = attribute.itemSize;
const normalized = attribute.normalized;
let arrayType;
if (!(attribute instanceof Float16BufferAttribute) && normalized !== true) {
arrayType = this.getTypeFromArray(array);
}
return this.getTypeFromLength(itemSize, arrayType);
}
/**
* Returns the length for the given data type.
*
* @param {string} type - The data type.
* @return {number} The length.
*/
getTypeLength(type) {
const vecType = this.getVectorType(type);
const vecNum = /vec([2-4])/.exec(vecType);
if (vecNum !== null) return Number(vecNum[1]);
if (vecType === 'float' || vecType === 'bool' || vecType === 'int' || vecType === 'uint') return 1;
if (/mat2/.test(type) === true) return 4;
if (/mat3/.test(type) === true) return 9;
if (/mat4/.test(type) === true) return 16;
return 0;
}
/**
* Returns the vector type for a given matrix type.
*
* @param {string} type - The matrix type.
* @return {string} The vector type.
*/
getVectorFromMatrix(type) {
return type.replace('mat', 'vec');
}
/**
* For a given type this method changes the component type to the
* given value. E.g. `vec4` should be changed to the new component type
* `uint` which results in `uvec4`.
*
* @param {string} type - The type.
* @param {string} newComponentType - The new component type.
* @return {string} The new type.
*/
changeComponentType(type, newComponentType) {
return this.getTypeFromLength(this.getTypeLength(type), newComponentType);
}
/**
* Returns the integer type pendant for the given type.
*
* @param {string} type - The type.
* @return {string} The integer type.
*/
getIntegerType(type) {
const componentType = this.getComponentType(type);
if (componentType === 'int' || componentType === 'uint') return type;
return this.changeComponentType(type, 'int');
}
/**
* Adds a stack node to the internal stack.
*
* @return {StackNode} The added stack node.
*/
addStack() {
this.stack = stack(this.stack);
this.stacks.push(getCurrentStack() || this.stack);
setCurrentStack(this.stack);
return this.stack;
}
/**
* Removes the last stack node from the internal stack.
*
* @return {StackNode} The removed stack node.
*/
removeStack() {
const lastStack = this.stack;
this.stack = lastStack.parent;
setCurrentStack(this.stacks.pop());
return lastStack;
}
/**
* The builder maintains (cached) data for each node during the building process. This method
* can be used to get these data for a specific shader stage and cache.
*
* @param {Node} node - The node to get the data for.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {?NodeCache} cache - An optional cache.
* @return {Object} The node data.
*/
getDataFromNode(node, shaderStage = this.shaderStage, cache = null) {
cache = cache === null ? node.isGlobal(this) ? this.globalCache : this.cache : cache;
let nodeData = cache.getData(node);
if (nodeData === undefined) {
nodeData = {};
cache.setData(node, nodeData);
}
if (nodeData[shaderStage] === undefined) nodeData[shaderStage] = {};
return nodeData[shaderStage];
}
/**
* Returns the properties for the given node and shader stage.
*
* @param {Node} node - The node to get the properties for.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage='any'] - The shader stage.
* @return {Object} The node properties.
*/
getNodeProperties(node, shaderStage = 'any') {
const nodeData = this.getDataFromNode(node, shaderStage);
return nodeData.properties || (nodeData.properties = {
outputNode: null
});
}
/**
* Returns an instance of {@link NodeAttribute} for the given buffer attribute node.
*
* @param {BufferAttributeNode} node - The buffer attribute node.
* @param {string} type - The node type.
* @return {NodeAttribute} The node attribute.
*/
getBufferAttributeFromNode(node, type) {
const nodeData = this.getDataFromNode(node);
let bufferAttribute = nodeData.bufferAttribute;
if (bufferAttribute === undefined) {
const index = this.uniforms.index++;
bufferAttribute = new NodeAttribute('nodeAttribute' + index, type, node);
this.bufferAttributes.push(bufferAttribute);
nodeData.bufferAttribute = bufferAttribute;
}
return bufferAttribute;
}
/**
* Returns an instance of {@link StructType} for the given output struct node.
*
* @param {OutputStructNode} node - The output struct node.
* @param {Array<Object>} membersLayout - The output struct types.
* @param {?string} [name=null] - The name of the struct.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @return {StructType} The struct type attribute.
*/
getStructTypeFromNode(node, membersLayout, name = null, shaderStage = this.shaderStage) {
const nodeData = this.getDataFromNode(node, shaderStage, this.globalCache);
let structType = nodeData.structType;
if (structType === undefined) {
const index = this.structs.index++;
if (name === null) name = 'StructType' + index;
structType = new StructType(name, membersLayout);
this.structs[shaderStage].push(structType);
nodeData.structType = structType;
}
return structType;
}
/**
* Returns an instance of {@link StructType} for the given output struct node.
*
* @param {OutputStructNode} node - The output struct node.
* @param {Array<Object>} membersLayout - The output struct types.
* @return {StructType} The struct type attribute.
*/
getOutputStructTypeFromNode(node, membersLayout) {
const structType = this.getStructTypeFromNode(node, membersLayout, 'OutputType', 'fragment');
structType.output = true;
return structType;
}
/**
* Returns an instance of {@link NodeUniform} for the given uniform node.
*
* @param {UniformNode} node - The uniform node.
* @param {string} type - The uniform type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {?string} name - The name of the uniform.
* @return {NodeUniform} The node uniform.
*/
getUniformFromNode(node, type, shaderStage = this.shaderStage, name = null) {
const nodeData = this.getDataFromNode(node, shaderStage, this.globalCache);
let nodeUniform = nodeData.uniform;
if (nodeUniform === undefined) {
const index = this.uniforms.index++;
nodeUniform = new NodeUniform(name || 'nodeUniform' + index, type, node);
this.uniforms[shaderStage].push(nodeUniform);
this.registerDeclaration(nodeUniform);
nodeData.uniform = nodeUniform;
}
return nodeUniform;
}
/**
* Returns the array length.
*
* @param {Node} node - The node.
* @return {?number} The array length.
*/
getArrayCount(node) {
let count = null;
if (node.isArrayNode) count = node.count;else if (node.isVarNode && node.node.isArrayNode) count = node.node.count;
return count;
}
/**
* Returns an instance of {@link NodeVar} for the given variable node.
*
* @param {VarNode} node - The variable node.
* @param {?string} name - The variable's name.
* @param {string} [type=node.getNodeType( this )] - The variable's type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {boolean} [readOnly=false] - Whether the variable is read-only or not.
*
* @return {NodeVar} The node variable.
*/
getVarFromNode(node, name = null, type = node.getNodeType(this), shaderStage = this.shaderStage, readOnly = false) {
const nodeData = this.getDataFromNode(node, shaderStage);
let nodeVar = nodeData.variable;
if (nodeVar === undefined) {
const idNS = readOnly ? '_const' : '_var';
const vars = this.vars[shaderStage] || (this.vars[shaderStage] = []);
const id = this.vars[idNS] || (this.vars[idNS] = 0);
if (name === null) {
name = (readOnly ? 'nodeConst' : 'nodeVar') + id;
this.vars[idNS]++;
}
//
const count = this.getArrayCount(node);
nodeVar = new NodeVar(name, type, readOnly, count);
if (!readOnly) {
vars.push(nodeVar);
}
this.registerDeclaration(nodeVar);
nodeData.variable = nodeVar;
}
return nodeVar;
}
/**
* Returns whether a Node or its flow is deterministic, useful for use in `const`.
*
* @param {Node} node - The varying node.
* @return {boolean} Returns true if deterministic.
*/
isDeterministic(node) {
if (node.isMathNode) {
return this.isDeterministic(node.aNode) && (node.bNode ? this.isDeterministic(node.bNode) : true) && (node.cNode ? this.isDeterministic(node.cNode) : true);
} else if (node.isOperatorNode) {
return this.isDeterministic(node.aNode) && (node.bNode ? this.isDeterministic(node.bNode) : true);
} else if (node.isArrayNode) {
if (node.values !== null) {
for (const n of node.values) {
if (!this.isDeterministic(n)) {
return false;
}
}
}
return true;
} else if (node.isConstNode) {
return true;
}
return false;
}
/**
* Returns an instance of {@link NodeVarying} for the given varying node.
*
* @param {(VaryingNode|PropertyNode)} node - The varying node.
* @param {?string} name - The varying's name.
* @param {string} [type=node.getNodeType( this )] - The varying's type.
* @return {NodeVar} The node varying.
*/
getVaryingFromNode(node, name = null, type = node.getNodeType(this)) {
const nodeData = this.getDataFromNode(node, 'any');
let nodeVarying = nodeData.varying;
if (nodeVarying === undefined) {
const varyings = this.varyings;
const index = varyings.length;
if (name === null) name = 'nodeVarying' + index;
nodeVarying = new NodeVarying(name, type);
varyings.push(nodeVarying);
this.registerDeclaration(nodeVarying);
nodeData.varying = nodeVarying;
}
return nodeVarying;
}
/**
* Registers a node declaration in the current shader stage.
*
* @param {Object} node - The node to be registered.
*/
registerDeclaration(node) {
const shaderStage = this.shaderStage;
const declarations = this.declarations[shaderStage] || (this.declarations[shaderStage] = {});
const property = this.getPropertyName(node);
let index = 1;
let name = property;
// Automatically renames the property if the name is already in use.
while (declarations[name] !== undefined) {
name = property + '_' + index++;
}
declarations[name] = node;
if (index > 1) {
node.name = name;
console.warn(`THREE.TSL: Declaration name '${property}' of '${node.type}' already in use. Renamed to '${name}'.`);
}
}
/**
* Returns an instance of {@link NodeCode} for the given code node.
*
* @param {CodeNode} node - The code node.
* @param {string} type - The node type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @return {NodeCode} The node code.
*/
getCodeFromNode(node, type, shaderStage = this.shaderStage) {
const nodeData = this.getDataFromNode(node);
let nodeCode = nodeData.code;
if (nodeCode === undefined) {
const codes = this.codes[shaderStage] || (this.codes[shaderStage] = []);
const index = codes.length;
nodeCode = new NodeCode('nodeCode' + index, type);
codes.push(nodeCode);
nodeData.code = nodeCode;
}
return nodeCode;
}
/**
* Adds a code flow based on the code-block hierarchy.
* This is used so that code-blocks like If,Else create their variables locally if the Node
* is only used inside one of these conditionals in the current shader stage.
*
* @param {Node} node - The node to add.
* @param {Node} nodeBlock - Node-based code-block. Usually 'ConditionalNode'.
*/
addFlowCodeHierarchy(node, nodeBlock) {
const {
flowCodes,
flowCodeBlock
} = this.getDataFromNode(node);
let needsFlowCode = true;
let nodeBlockHierarchy = nodeBlock;
while (nodeBlockHierarchy) {
if (flowCodeBlock.get(nodeBlockHierarchy) === true) {
needsFlowCode = false;
break;
}
nodeBlockHierarchy = this.getDataFromNode(nodeBlockHierarchy).parentNodeBlock;
}
if (needsFlowCode) {
for (const flowCode of flowCodes) {
this.addLineFlowCode(flowCode);
}
}
}
/**
* Add a inline-code to the current flow code-block.
*
* @param {Node} node - The node to add.
* @param {string} code - The code to add.
* @param {Node} nodeBlock - Current ConditionalNode
*/
addLineFlowCodeBlock(node, code, nodeBlock) {
const nodeData = this.getDataFromNode(node);
const flowCodes = nodeData.flowCodes || (nodeData.flowCodes = []);
const codeBlock = nodeData.flowCodeBlock || (nodeData.flowCodeBlock = new WeakMap());
flowCodes.push(code);
codeBlock.set(nodeBlock, true);
}
/**
* Add a inline-code to the current flow.
*
* @param {string} code - The code to add.
* @param {?Node} [node= null] - Optional Node, can help the system understand if the Node is part of a code-block.
* @return {NodeBuilder} A reference to this node builder.
*/
addLineFlowCode(code, node = null) {
if (code === '') return this;
if (node !== null && this.context.nodeBlock) {
this.addLineFlowCodeBlock(node, code, this.context.nodeBlock);
}
code = this.tab + code;
if (!/;\s*$/.test(code)) {
code = code + ';\n';
}
this.flow.code += code;
return this;
}
/**
* Adds a code to the current code flow.
*
* @param {string} code - Shader code.
* @return {NodeBuilder} A reference to this node builder.
*/
addFlowCode(code) {
this.flow.code += code;
return this;
}
/**
* Add tab in the code that will be generated so that other snippets respect the current tabulation.
* Typically used in codes with If,Else.
*
* @return {NodeBuilder} A reference to this node builder.
*/
addFlowTab() {
this.tab += '\t';
return this;
}
/**
* Removes a tab.
*
* @return {NodeBuilder} A reference to this node builder.
*/
removeFlowTab() {
this.tab = this.tab.slice(0, -1);
return this;
}
/**
* Gets the current flow data based on a Node.
*
* @param {Node} node - Node that the flow was started.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {Object} The flow data.
*/
getFlowData(node /*, shaderStage*/) {
return this.flowsData.get(node);
}
/**
* Executes the node flow based on a root node to generate the final shader code.
*
* @param {Node} node - The node to execute.
* @return {Object} The code flow.
*/
flowNode(node) {
const output = node.getNodeType(this);
const flowData = this.flowChildNode(node, output);
this.flowsData.set(node, flowData);
return flowData;
}
/**
* Includes a node in the current function node.
*
* @param {Node} node - The node to include.
* @returns {void}
*/
addInclude(node) {
if (this.currentFunctionNode !== null) {
this.currentFunctionNode.includes.push(node);
}
}
/**
* Returns the native shader operator name for a given generic name.
* It is a similar type of method like {@link NodeBuilder#getMethod}.
*
* @param {ShaderNodeInternal} shaderNode - The shader node to build the function node with.
* @return {FunctionNode} The build function node.
*/
buildFunctionNode(shaderNode) {
const fn = new FunctionNode();
const previous = this.currentFunctionNode;
this.currentFunctionNode = fn;
fn.code = this.buildFunctionCode(shaderNode);
this.currentFunctionNode = previous;
return fn;
}
/**
* Generates a code flow based on a TSL function: Fn().
*
* @param {ShaderNodeInternal} shaderNode - A function code will be generated based on the input.
* @return {Object}
*/
flowShaderNode(shaderNode) {
const layout = shaderNode.layout;
const inputs = {
[Symbol.iterator]() {
let index = 0;
const values = Object.values(this);
return {
next: () => ({
value: values[index],