three
Version:
JavaScript 3D library
41,904 lines • 1.07 MB
JavaScript
/**
* @license
* Copyright 2010-2026 Three.js Authors
* SPDX-License-Identifier: MIT
*/
import { Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, error, UnsignedIntType, IntType, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, AlphaFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, EventDispatcher, generateUUID, warn, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBufferAttribute, InterleavedBuffer, DynamicDrawUsage, NoColorSpace, log as log$1, warnOnce, NormalBlending, SrcAlphaFactor, OneMinusSrcAlphaFactor, AddEquation, MaterialBlending, NoBlending, Sphere, BackSide, DoubleSide, Texture, Compatibility, LessCompare, LessEqualCompare, GreaterCompare, GreaterEqualCompare, NearestFilter, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, RenderTarget, Object3D, HalfFloatType, LinearMipMapLinearFilter, Plane, CubeTexture, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking, ObjectSpaceNormalMap, RED_GREEN_RGTC2_Format, RG11_EAC_Format, DataTexture, RGBAFormat, FloatType, InstancedBufferAttribute, DataArrayTexture, RenderObjectRefreshType, Material, Mesh, OrthographicCamera, BufferGeometry, Float32BufferAttribute, BufferAttribute, UVMapping, LinearSRGBColorSpace, UnsignedInt248Type, VSMShadowMap, PCFShadowMap, LinearFilter, BasicShadowMap, CubeDepthTexture, BoxGeometry, Scene, CubeCamera, floorPowerOfTwo, ClampToEdgeWrapping } from './three.core.js';
/**
* Possible shader stages.
*
* @property {string} VERTEX The vertex shader stage.
* @property {string} FRAGMENT The fragment shader stage.
*/
const NodeShaderStage = {
VERTEX: 'vertex',
FRAGMENT: 'fragment'
};
/**
* Update types of a node.
*
* @property {string} NONE The update method is not executed.
* @property {string} FRAME The update method is executed per frame.
* @property {string} RENDER The update method is executed per render. A frame might be produced by multiple render calls so this value allows more detailed updates than FRAME.
* @property {string} OBJECT The update method is executed per {@link Object3D} that uses the node for rendering.
*/
const NodeUpdateType = {
NONE: 'none',
FRAME: 'frame',
RENDER: 'render',
OBJECT: 'object'
};
/**
* Data types of a node.
*
* @property {string} BOOLEAN Boolean type.
* @property {string} INTEGER Integer type.
* @property {string} FLOAT Float type.
* @property {string} VECTOR2 Two-dimensional vector type.
* @property {string} VECTOR3 Three-dimensional vector type.
* @property {string} VECTOR4 Four-dimensional vector type.
* @property {string} MATRIX2 2x2 matrix type.
* @property {string} MATRIX3 3x3 matrix type.
* @property {string} MATRIX4 4x4 matrix type.
*/
const NodeType = {
BOOLEAN: 'bool',
INTEGER: 'int',
FLOAT: 'float',
VECTOR2: 'vec2',
VECTOR3: 'vec3',
VECTOR4: 'vec4',
MATRIX2: 'mat2',
MATRIX3: 'mat3',
MATRIX4: 'mat4'
};
/**
* Access types of a node. These are relevant for compute and storage usage.
*
* @property {string} READ_ONLY Read-only access
* @property {string} WRITE_ONLY Write-only access.
* @property {string} READ_WRITE Read and write access.
*/
const NodeAccess = {
READ_ONLY: 'readOnly',
WRITE_ONLY: 'writeOnly',
READ_WRITE: 'readWrite',
};
const defaultShaderStages = [ 'fragment', 'vertex' ];
const defaultBuildStages = [ 'setup', 'analyze', 'generate' ];
const shaderStages = [ ...defaultShaderStages, 'compute' ];
const vectorComponents = [ 'x', 'y', 'z', 'w' ];
// Pre-compiled RegExp patterns for ignored files
const IGNORED_FILES = [
/^StackTrace\.js$/,
/^TSLCore\.js$/,
/^.*Node\.js$/,
/^three\.webgpu.*\.js$/
];
/**
* Parses the stack trace and filters out ignored files.
* Returns an array with function name, file, line, and column.
*/
function getFilteredStack( stack ) {
// Pattern to extract function name, file, line, and column from different browsers
// Chrome: "at functionName (file.js:1:2)" or "at file.js:1:2"
// Firefox: "functionName@file.js:1:2"
const regex = /(?:at\s+(.+?)\s+\()?(?:(.+?)@)?([^@\s()]+):(\d+):(\d+)/;
return stack.split( '\n' )
.map( line => {
const match = line.match( regex );
if ( ! match ) return null; // Skip if line format is invalid
// Chrome: match[1], Firefox: match[2]
const fn = match[ 1 ] || match[ 2 ] || '';
const file = match[ 3 ].split( '?' )[ 0 ]; // Clean file name (Vite/HMR)
const lineNum = parseInt( match[ 4 ], 10 );
const column = parseInt( match[ 5 ], 10 );
// Extract only the filename from full path
const fileName = file.split( '/' ).pop();
return {
fn: fn,
file: fileName,
line: lineNum,
column: column
};
} )
.filter( frame => {
// Only keep frames that are valid and not in the ignore list
return frame && ! IGNORED_FILES.some( regex => regex.test( frame.file ) );
} );
}
/**
* Class representing a stack trace for debugging purposes.
*/
class StackTrace {
/**
* Creates a StackTrace instance by capturing and filtering the current stack trace.
*
* @param {Error|string|null} stackMessage - An optional stack trace to use instead of capturing a new one.
*/
constructor( stackMessage = null ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStackTrace = true;
/**
* The stack trace.
*
* @type {Array<{fn: string, file: string, line: number, column: number}>}
*/
this.stack = getFilteredStack( stackMessage ? stackMessage : new Error().stack );
}
/**
* Returns a formatted location string of the top stack frame.
*
* @returns {string} The formatted stack trace message.
*/
getLocation() {
if ( this.stack.length === 0 ) {
return '[Unknown location]';
}
const mainStack = this.stack[ 0 ];
const fn = mainStack.fn;
const fnName = fn ? `"${ fn }()" at ` : '';
return `${fnName}"${mainStack.file}:${mainStack.line}"`; // :${mainStack.column}
}
/**
* Returns the full error message including the stack trace.
*
* @param {string} message - The error message.
* @returns {string} The full error message with stack trace.
*/
getError( message ) {
if ( this.stack.length === 0 ) {
return message;
}
// Output: "Error: message\n at functionName (file.js:line:column)"
const stackString = this.stack.map( frame => {
const location = `${ frame.file }:${ frame.line }:${ frame.column }`;
if ( frame.fn ) {
return ` at ${ frame.fn } (${ location })`;
}
return ` at ${ location }`;
} ).join( '\n' );
return `${ message }\n${ stackString }`;
}
}
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
function cyrb53( value, seed = 0 ) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
if ( Array.isArray( value ) ) {
for ( let i = 0, val; i < value.length; i ++ ) {
val = value[ i ];
h1 = Math.imul( h1 ^ val, 2654435761 );
h2 = Math.imul( h2 ^ val, 1597334677 );
}
} else {
for ( let i = 0, ch; i < value.length; i ++ ) {
ch = value.charCodeAt( i );
h1 = Math.imul( h1 ^ ch, 2654435761 );
h2 = Math.imul( h2 ^ ch, 1597334677 );
}
}
h1 = Math.imul( h1 ^ ( h1 >>> 16 ), 2246822507 );
h1 ^= Math.imul( h2 ^ ( h2 >>> 13 ), 3266489909 );
h2 = Math.imul( h2 ^ ( h2 >>> 16 ), 2246822507 );
h2 ^= Math.imul( h1 ^ ( h1 >>> 13 ), 3266489909 );
return 4294967296 * ( 2097151 & h2 ) + ( h1 >>> 0 );
}
/**
* Computes a hash for the given string.
*
* @private
* @method
* @param {string} str - The string to be hashed.
* @return {number} The hash.
*/
const hashString = ( str ) => cyrb53( str );
/**
* Computes a hash for the given array.
*
* @private
* @method
* @param {Array<number>} array - The array to be hashed.
* @return {number} The hash.
*/
const hashArray = ( array ) => cyrb53( array );
/**
* Computes a hash for the given list of parameters.
*
* @private
* @method
* @param {...number} params - A list of parameters.
* @return {number} The hash.
*/
const hash$1 = ( ...params ) => cyrb53( params );
const typeFromLength = /*@__PURE__*/ new Map( [
[ 1, 'float' ],
[ 2, 'vec2' ],
[ 3, 'vec3' ],
[ 4, 'vec4' ],
[ 9, 'mat3' ],
[ 16, 'mat4' ]
] );
const dataFromObject = /*@__PURE__*/ new WeakMap();
/**
* Returns the data type for the given the length.
*
* @private
* @method
* @param {number} length - The length.
* @return {string} The data type.
*/
function getTypeFromLength( length ) {
return typeFromLength.get( length );
}
/**
* Returns the typed array for the given data type.
*
* @private
* @method
* @param {string} type - The data type.
* @return {TypedArray} The typed array.
*/
function getTypedArrayFromType( type ) {
// Handle component type for vectors and matrices
if ( /[iu]?vec\d/.test( type ) ) {
// Handle int vectors
if ( type.startsWith( 'ivec' ) ) return Int32Array;
// Handle uint vectors
if ( type.startsWith( 'uvec' ) ) return Uint32Array;
// Default to float vectors
return Float32Array;
}
// Handle matrices (always float)
if ( /mat\d/.test( type ) ) return Float32Array;
// Basic types
if ( /float/.test( type ) ) return Float32Array;
if ( /uint/.test( type ) ) return Uint32Array;
if ( /int/.test( type ) ) return Int32Array;
throw new Error( `THREE.NodeUtils: Unsupported type: ${type}` );
}
/**
* Returns the length for the given data type.
*
* @private
* @method
* @param {string} type - The data type.
* @return {number} The length.
*/
function getLengthFromType( type ) {
if ( /float|int|uint|bool/.test( type ) ) return 1;
if ( /vec2/.test( type ) ) return 2;
if ( /vec3/.test( type ) ) return 3;
if ( /vec4/.test( type ) ) return 4;
if ( /mat2/.test( type ) ) return 4;
if ( /mat3/.test( type ) ) return 9;
if ( /mat4/.test( type ) ) return 16;
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
}
/**
* Returns the gpu memory length for the given data type in 4-byte elements.
*
* @private
* @method
* @param {string} type - The data type.
* @return {number} The memory length in 4-byte elements.
*/
function getMemoryLengthFromType( type ) {
if ( /float|int|uint|bool/.test( type ) ) return 1;
if ( /vec2/.test( type ) ) return 2;
if ( /vec3/.test( type ) ) return 3;
if ( /vec4/.test( type ) ) return 4;
if ( /mat2/.test( type ) ) return 4;
if ( /mat3/.test( type ) ) return 12;
if ( /mat4/.test( type ) ) return 16;
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
}
/**
* Returns the alignment requirement for the given data type in 4-byte elements.
*
* @private
* @method
* @param {string} type - The data type.
* @return {number} The alignment requirement in 4-byte elements.
*/
function getAlignmentFromType( type ) {
if ( /float|int|uint|bool/.test( type ) ) return 1;
if ( /vec2/.test( type ) ) return 2;
if ( /vec3/.test( type ) ) return 4;
if ( /vec4/.test( type ) ) return 4;
if ( /mat2/.test( type ) ) return 2;
if ( /mat3/.test( type ) ) return 4;
if ( /mat4/.test( type ) ) return 4;
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
}
/**
* Returns the data type for the given value.
*
* @private
* @method
* @param {any} value - The value.
* @return {?string} The data type.
*/
function getValueType( value ) {
if ( value === undefined || value === null ) return null;
const typeOf = typeof value;
if ( value.isNode === true ) {
return 'node';
} else if ( typeOf === 'number' ) {
return 'float';
} else if ( typeOf === 'boolean' ) {
return 'bool';
} else if ( typeOf === 'string' ) {
return 'string';
} else if ( typeOf === 'function' ) {
return 'shader';
} else if ( value.isVector2 === true ) {
return 'vec2';
} else if ( value.isVector3 === true ) {
return 'vec3';
} else if ( value.isVector4 === true ) {
return 'vec4';
} else if ( value.isMatrix2 === true ) {
return 'mat2';
} else if ( value.isMatrix3 === true ) {
return 'mat3';
} else if ( value.isMatrix4 === true ) {
return 'mat4';
} else if ( value.isColor === true ) {
return 'color';
} else if ( value instanceof ArrayBuffer ) {
return 'ArrayBuffer';
}
return null;
}
/**
* Returns the node data type for the given texture.
*
* @private
* @method
* @param {Texture} texture - The texture.
* @return {string} The data type.
*/
function getTextureType( texture ) {
if ( texture.isDepthTexture === true ) {
return 'float';
}
const format = texture.format;
let length;
if ( format === RedFormat || format === RedIntegerFormat || format === DepthFormat || format === DepthStencilFormat || format === AlphaFormat ) {
length = 1;
} else if ( format === RGFormat || format === RGIntegerFormat ) {
length = 2;
} else if ( format === RGBFormat || format === RGBIntegerFormat ) {
length = 3;
} else {
length = 4;
}
let componentType;
if ( texture.type === UnsignedIntType ) {
componentType = 'uint';
} else if ( texture.type === IntType ) {
componentType = 'int';
} else {
componentType = 'float';
}
if ( length === 1 ) return componentType;
let baseType = getTypeFromLength( length );
if ( componentType !== 'float' ) {
baseType = componentType[ 0 ] + baseType;
}
return baseType;
}
/**
* Returns the value/object for the given data type and parameters.
*
* @private
* @method
* @param {string} type - The given type.
* @param {...any} params - A parameter list.
* @return {any} The value/object.
*/
function getValueFromType( type, ...params ) {
const last4 = type ? type.slice( -4 ) : undefined;
if ( params.length === 1 ) { // ensure same behaviour as in NodeBuilder.format()
if ( last4 === 'vec2' ) params = [ params[ 0 ], params[ 0 ] ];
else if ( last4 === 'vec3' ) params = [ params[ 0 ], params[ 0 ], params[ 0 ] ];
else if ( last4 === 'vec4' ) params = [ params[ 0 ], params[ 0 ], params[ 0 ], params[ 0 ] ];
}
if ( type === 'color' ) {
return new Color( ...params );
} else if ( last4 === 'vec2' ) {
return new Vector2( ...params );
} else if ( last4 === 'vec3' ) {
return new Vector3( ...params );
} else if ( last4 === 'vec4' ) {
return new Vector4( ...params );
} else if ( last4 === 'mat2' ) {
return new Matrix2( ...params );
} else if ( last4 === 'mat3' ) {
return new Matrix3( ...params );
} else if ( last4 === 'mat4' ) {
return new Matrix4( ...params );
} else if ( type === 'bool' ) {
return params[ 0 ] || false;
} else if ( ( type === 'float' ) || ( type === 'int' ) || ( type === 'uint' ) ) {
return params[ 0 ] || 0;
} else if ( type === 'string' ) {
return params[ 0 ] || '';
} else if ( type === 'ArrayBuffer' ) {
return base64ToArrayBuffer( params[ 0 ] );
}
return null;
}
/**
* Gets the object data that can be shared between different rendering steps.
*
* @private
* @param {Object} object - The object to get the data for.
* @return {Object} The object data.
*/
function getDataFromObject( object ) {
let data = dataFromObject.get( object );
if ( data === undefined ) {
data = {};
dataFromObject.set( object, data );
}
return data;
}
/**
* Converts the given array buffer to a Base64 string.
*
* @private
* @method
* @param {ArrayBuffer} arrayBuffer - The array buffer.
* @return {string} The Base64 string.
*/
function arrayBufferToBase64( arrayBuffer ) {
let chars = '';
const array = new Uint8Array( arrayBuffer );
for ( let i = 0; i < array.length; i ++ ) {
chars += String.fromCharCode( array[ i ] );
}
return btoa( chars );
}
/**
* Merges the given node build contexts into a new context, the data of the source context takes precedence.
* The node overrides of both contexts are merged as well, so the overrides of the target context still
* apply unless the source context overrides the same node.
*
* @private
* @method
* @param {Object} target - The context to merge into, e.g. the outer context.
* @param {Object} source - The context to merge, e.g. the inner context.
* @return {Object} The merged context.
*/
function mergeContext( target, source ) {
const context = { ...target, ...source };
if ( target.overrideNodes !== undefined && source.overrideNodes !== undefined && target.overrideNodes !== source.overrideNodes ) {
context.overrideNodes = new Map( [ ...target.overrideNodes, ...source.overrideNodes ] );
}
return context;
}
/**
* Converts the given Base64 string to an array buffer.
*
* @private
* @method
* @param {string} base64 - The Base64 string.
* @return {ArrayBuffer} The array buffer.
*/
function base64ToArrayBuffer( base64 ) {
return Uint8Array.from( atob( base64 ), c => c.charCodeAt( 0 ) ).buffer;
}
function isArrayAsParameter( params ) {
return ( params[ 0 ] !== undefined && params[ 0 ] !== null ) && ( params[ 0 ].isNode || Object.getPrototypeOf( params[ 0 ] ) !== Object.prototype );
}
var NodeUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
arrayBufferToBase64: arrayBufferToBase64,
base64ToArrayBuffer: base64ToArrayBuffer,
getAlignmentFromType: getAlignmentFromType,
getDataFromObject: getDataFromObject,
getLengthFromType: getLengthFromType,
getMemoryLengthFromType: getMemoryLengthFromType,
getTextureType: getTextureType,
getTypeFromLength: getTypeFromLength,
getTypedArrayFromType: getTypedArrayFromType,
getValueFromType: getValueFromType,
getValueType: getValueType,
hash: hash$1,
hashArray: hashArray,
hashString: hashString,
isArrayAsParameter: isArrayAsParameter,
mergeContext: mergeContext
});
const _parentBuildStage = {
analyze: 'setup',
generate: 'analyze'
};
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 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;
/**
* The name of the node.
*
* @type {string}
* @default ''
*/
this.name = '';
/**
* 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;
/**
* Create a list of parents for this node during the build process.
*
* @type {boolean}
* @default false
*/
this.parents = false;
/**
* Whether this node can be assigned. It is generated as an expression,
* unless it is assigned, in which case it is declared as a variable where it was created.
*
* @type {boolean}
* @default false
*/
this.assignable = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNode = true;
// private
this._inspectorNode = null;
/**
* The cache key of this node.
*
* @private
* @type {?number}
* @default null
*/
this._cacheKey = null;
/**
* The UUID of the node.
*
* @type {string}
* @default null
* @private
*/
this._uuid = null;
/**
* The cache key's version.
*
* @private
* @type {number}
* @default 0
*/
this._cacheKeyVersion = 0;
/**
* The unique ID of the node.
*
* @type {number}
* @readonly
*/
this.id = _nodeId ++;
/**
* The stack trace of the node for debugging purposes.
*
* @type {?string}
* @default null
*/
this.stackTrace = null;
if ( Node.captureStackTrace === true ) {
this.stackTrace = new StackTrace();
}
}
/**
* Whether this node allows caching its result in a temporary variable.
* Caching is enabled by default. Override this method to disable it.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether temporary caching is allowed.
*/
isCacheable( /*builder*/ ) {
return true;
}
/**
* Makes this node assignable and adds it to the current stack,
* so it can be declared where it was created if it is assigned later.
* Only use it with nodes that are not shared, see {@link Node#assignable}.
*
* @return {Node} A reference to this node.
*/
toAssignable() {
this.assignable = true;
return this.toStack();
}
/**
* Checks if this node is assignable without a forced declaration.
*
* @param {NodeBuilder} builder - The node builder.
* @returns {boolean} Whether this node is assignable without a forced declaration.
*/
isAssignable( builder ) {
const data = builder.getDataFromNode( this );
if ( data.forceDeclaration === true ) return false;
return this.assignable;
}
/**
* Checks if this node is the target of an assignment.
*
* @param {NodeBuilder} builder - The node builder.
* @returns {boolean} Whether this node is assigned.
*/
isAssign( builder ) {
const data = builder.getDataFromNode( this );
return data.assign;
}
/**
* Declares the assignable node before the loops where it is used if it has no stack, and
* decides if a value that cannot be cached must be evaluated once at its declaration.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Object} data - The node data.
* @param {Node} [valueNode=this] - The node that defines the value of the assignable node.
*/
_updateAssignable( builder, data, valueNode = this ) {
const nodeLoop = builder.context.nodeLoop;
if ( data.stack === undefined && builder.buildStage === 'setup' ) {
// A node outside of the stacks of this build, e.g. created outside of a function, is declared
// before the outermost loop where its value does not change, otherwise every iteration would repeat it.
data.stack = builder.declareBeforeBlock( this );
} else if ( this.assignable === true && nodeLoop && builder.buildStage === 'analyze' ) {
if ( valueNode.isCacheable( builder ) === false && builder.isDeterministic( valueNode ) === false ) {
// A value that cannot be cached, e.g. a function call, is evaluated once at its declaration
// if it is used in a loop that runs after it, otherwise the loop would repeat it.
const declarationIndex = builder.activeStacks.indexOf( data.stack );
const loopIndex = builder.activeStacks.indexOf( builder.getDataFromNode( nodeLoop ).stack );
if ( declarationIndex !== -1 && loopIndex >= declarationIndex ) data.forceDeclaration = true;
}
}
}
/**
* Whether `build()` declares this assignable node as a variable when it is assigned.
* Nodes that wrap an assignable value, like {@link VarNode}, declare it themselves.
*
* @private
* @return {boolean} Whether `build()` declares the assignable node.
*/
_declaresAssignable() {
return true;
}
/**
* Declares a variable for this node in the current flow, initialized with the given snippet.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {string} snippet - The initial value.
* @param {string} type - The variable type.
* @param {boolean} readOnly - Whether the variable is read-only.
* @param {string} property - The node data property that holds the variable.
* @return {string} The property name of the variable.
*/
_declareVariable( builder, snippet, type, readOnly, property ) {
const nodeVar = builder.getVarFromNode( this, null, type, undefined, readOnly, true, property );
const propertyName = builder.getPropertyName( nodeVar );
const declarationPrefix = readOnly
? builder.generateLetStatement( nodeVar.type, propertyName, nodeVar.count )
: builder.generateVarStatement( nodeVar.type, propertyName, nodeVar.count );
builder.addLineFlowCode( `${ declarationPrefix } = ${ snippet }`, this );
return propertyName;
}
/**
* Whether this assignable node must be declared as a variable, because it is assigned
* or must be evaluated once at its declaration.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Object} nodeData - The node data.
* @return {boolean} Whether the assignable node is a variable.
*/
_isVariable( builder, nodeData ) {
return ( nodeData.assign === true || nodeData.forceDeclaration === true ) && this.getNodeType( builder ) !== 'void';
}
/**
* Generates an assignable node as a variable. The variable is declared the first
* time it is generated, which is its stack position, and then referenced by name.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Object} nodeData - The node data.
* @param {?string} output - The output type.
* @return {string} The generated shader string.
*/
_buildVariable( builder, nodeData, output ) {
const nodeType = this.getNodeType( builder );
const type = builder.getVectorType( nodeType );
if ( nodeData.propertyName === undefined ) {
const snippet = this.generate.length < 2
? builder.format( this.generate( builder ) || '', nodeType, type )
: this.generate( builder, type ) || '';
nodeData.propertyName = this._declareVariable( builder, snippet, type, false, 'variable' );
}
return builder.format( nodeData.propertyName, type, output );
}
/**
* Whether the generated value is a variable declared in the scope of the function,
* so it keeps its value after the block where it was generated, e.g. a {@link VarNode}.
*
* @private
* @return {boolean} Whether the generated value is a variable of the function scope.
*/
_isFunctionScope() {
return false;
}
/**
* A generated value is only visible in the block where it was declared and in its inner blocks.
* A variable of the function scope is also visible in a later block of the same parent and guard,
* e.g. a second `if ( c )` of an immutable condition `c`, since that block only runs if its block ran.
* This method discards the generated value if it is not visible in the current block.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Object} nodeData - The node data.
*/
_checkFlowBlock( builder, nodeData ) {
if ( nodeData.flowBlock !== undefined ) {
const { guard, parent } = nodeData.flowBlock;
const isFunctionScope = guard !== undefined && this._isFunctionScope();
let flowBlock = builder.flowBlock;
while ( flowBlock !== null && flowBlock !== nodeData.flowBlock ) {
if ( isFunctionScope && flowBlock.guard === guard && flowBlock.parent === parent ) break;
flowBlock = flowBlock.parent;
}
if ( flowBlock === null ) {
nodeData.flowBlock = undefined;
nodeData.propertyName = undefined;
nodeData.snippet = undefined;
nodeData.generated = undefined;
}
}
}
/**
* Keeps the block where a value was generated, so it is only reused where it is visible.
* A global node is a declaration visible in any block, unless it emitted code in this block.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Object} nodeData - The node data.
* @param {boolean} isCached - Whether the value was generated before.
* @param {number} flowCodeLength - The length of the flow code before the value was generated.
*/
_setFlowBlock( builder, nodeData, isCached, flowCodeLength ) {
const isLocal = this.isGlobal( builder ) === false || builder.flow.code.length !== flowCodeLength;
if ( isCached === false && ( nodeData.propertyName !== undefined || nodeData.snippet !== undefined ) && isLocal && builder.flowBlock !== null ) {
nodeData.flowBlock = builder.flowBlock;
}
}
/**
* 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 UUID of the node.
*
* @type {string}
* @readonly
*/
get uuid() {
if ( this._uuid === null ) {
this._uuid = generateUUID();
}
return this._uuid;
}
/**
* 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 );
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 );
return 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 cache referring to the current shader-stage.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether this node is global or not.
*/
isGlobal( /*builder*/ ) {
return this.global;
}
/**
* Returns the child nodes of this node.
*
* @return {Array<Node>} The child nodes.
*/
getChildren() {
const children = this._getChildren();
const nodes = [];
for ( let i = 0; i < children.length; i ++ ) {
nodes.push( children[ i ].childNode );
}
return nodes;
}
/**
* 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 child nodes of this node.
*
* @private
* @param {Set<Node>} [ignores=new Set()] - A set of nodes to ignore during the search to avoid circular references.
* @returns {Array<Object>} An array of objects describing the child nodes.
*/
_getChildren( ignores = new Set() ) {
const children = [];
// avoid circular references
ignores.add( this );
const properties = Object.getOwnPropertyNames( this );
for ( let p = 0; p < properties.length; p ++ ) {
const property = properties[ p ];
// Ignore private properties.
if ( property.charCodeAt( 0 ) === 95 ) continue;
const object = this[ property ];
// Ignore primitives and ignored nodes.
if ( object === null || ( typeof object !== 'object' && typeof object !== 'function' ) || ignores.has( object ) ) continue;
if ( Array.isArray( object ) === true ) {
for ( let i = 0; i < object.length; i ++ ) {
const child = object[ i ];
if ( child && child.isNode === true ) {
children.push( { property, index: i, childNode: child } );
}
}
} else if ( object && object.isNode === true ) {
children.push( { property, childNode: object } );
} else if ( object && Object.getPrototypeOf( object ) === Object.prototype ) {
for ( const subProperty in object ) {
// Ignore private sub-properties.
if ( subProperty.startsWith( '_' ) === true ) continue;
const child = object[ subProperty ];
if ( child && child.isNode === true ) {
children.push( { property, index: subProperty, childNode: child } );
}
}
}
}
//
return children;
}
/**
* Returns the cache key for this node.
*
* @param {boolean} [force=false] - When set to `true`, a recomputation of the cache key is forced.
* @param {Set<Node>} [ignores=null] - A set of nodes to ignore during the computation of the cache key.
* @return {number} The cache key of the node.
*/
getCacheKey( force = false, ignores = null ) {
force = force || this.version !== this._cacheKeyVersion;
if ( force === true || this._cacheKey === null ) {
if ( ignores === null ) ignores = new Set();
//
const values = [];
for ( const { property, childNode } of this._getChildren( ignores ) ) {
values.push( hashString( property.slice( 0, -4 ) ), childNode.getCacheKey( force, ignores ) );
}
//
this._cacheKey = hash$1( hashArray( values ), 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 this.id;
}
/**
* 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 String( this.id );
}
/**
* Returns the update type of {@link Node#update}.
*
* @param {NodeFrame} [frame] - The current node frame.
* @return {NodeUpdateType} The update type.
*/
getUpdateType( /*frame*/ ) {
return this.updateType;
}
/**
* Returns the update type of {@link Node#updateBefore}.
*
* @param {NodeFrame} [frame] - The current node frame.
* @return {NodeUpdateType} The update type.
*/
getUpdateBeforeType( /*frame*/ ) {
return this.updateBeforeType;
}
/**
* Returns the update type of {@link Node#updateAfter}.
*
* @param {NodeFrame} [frame] - The current node frame.
* @return {NodeUpdateType} The update type.
*/
getUpdateAfterType( /*frame*/ ) {
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.
* @param {string} [output=null] - The output of the node.
* @return {string} The type of the node.
*/
getNodeType( builder, output = null ) {
const nodeData = builder.getDataFromNode( this );
let type;
if ( output !== null ) {
nodeData.typeFromOutput = nodeData.typeFromOutput || {};
type = nodeData.typeFromOutput[ output ];
if ( type === undefined ) {
type = this.generateNodeType( builder, output );
nodeData.typeFromOutput[ output ] = type;
}
} else {
type = nodeData.type;
if ( type === undefined ) {
type = this.generateNodeType( builder );
nodeData.type = type;
}
}
return type;
}
/**
* Returns the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} [output=null] - The output of the node.
* @return {string} The type of the node.
*/
generateNodeType( builder, output = null ) {
const nodeProperties = builder.getNodeProperties( this );
if ( nodeProperties.outputNode ) {
return nodeProperties.outputNode.getNodeType( builder, output );
}
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. It also handles
* node overrides if an override context is set.
*
* @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 );
let sharedNode = null;
if ( nodeFromHash && nodeFromHash !== this ) {
sharedNode = nodeFromHash;
} else if ( builder.context.overrideNodes ) {
const callback = builder.context.overrideNodes.get( this );
if ( callback ) {
// the overrides are cached per callback, nested contexts can override the same node with different nodes
const nodeData = builder.getDataFromNode( this );
const overrides = nodeData.overrides || ( nodeData.overrides = new Map() );
if ( overrides.has( callback ) === false ) {
// avoid recursive override calls
overrides.set( callback, null );
// cancel the override for use the same node inside the callback
sharedNode = callback( builder ).overrideNode( this, null );
overrides.set( callback, sharedNode );
} else {
sharedNode = overrides.get( callback );
}
}
}
return sharedNode || this;
}
/**
* Returns the number of elements in the node array.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?number} The number of elements in the node array.
*/
getArrayCount( /*builder*/ ) {
return null;
}
/**
* 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 a node's output/result.
* If an output node is prepared, then it must be returned in the `return` statement of the derived module's setup function.
*
* @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.
* @param {?Node} output - The target output node.
*/
analyze( builder, output = null ) {
const usageCount = builder.increaseUsage( this );
if ( this.parents === true ) {
const nodeData = builder.getDataFromNode( this, 'any' );
nodeData.stages = nodeData.stages || {};
nodeData.stages[ builder.shaderStage ] = nodeData.stages[ builder.shaderStage ] || [];
nodeData.stages[ builder.shaderStage ].push( output );
}
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, this );
}
}
}
}
/**
* 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*/ ) {
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*/ ) {
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*/ ) {
warn( 'Abstract function.' );
}
/**
* Registers the inspector node added with `toInspector()` in the builder, so it is updated every frame.
* Nodes that override `build()` without always calling `super.build()` must call this method.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
*/
_setupInspector( builder ) {
if ( this._inspectorNode !== null && builder.buildStage === 'setup' ) {
builder.addNode( this._inspectorNode );
}
}
/**
* This method performs the build of a node. The behavior and return value depend on the current build stage:
* - **setup**: Prepares the node and its children for the build process. This process can also create new nodes. Returns the node itself or a variant.
* - **analyze**: Analyzes the node hierarchy for optimizations in the code generation stage. Returns `null`.
* - **generate**: Generates the shader code for the node. Returns the generated shader string.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?(string|Node)} [output=null] - Can be used to define the output type.
* @return {?(Node|string)} The result of the build process, depending on the build stage.
*/
build( builder, output = null ) {
this._setupInspector( builder );
const refNode = this.getShared( builder );
if ( this !== refNode ) {
return refNode.build( builder, output );
}
//
const nodeData = builder.getDataFromNode( this );
if ( this.assignable === true && this._declaresAssignable() ) this._updateAssignable( builder, nodeData );
nodeData.buildStages = nodeData.buildStages || { setup: false, analyze: false, generate: false };
nodeData.buildStages[ builder.buildStage ] = true;
const parentBuildStage = _parentBuildStage[ builder.buildStage ];
if ( parentBuildStage && nodeData.buildStages[ parentBuildStage ] !== true ) {
// force parent build stage (setup or analyze)
const previousBuildStage = builder.getBuildStage();
builder.setBuildStage( parentBuildStage );
this.build( builder );
builder.setBuildStage( previousBuildStage );
}
//
builder.addChain( this );
/* Build stages expected results:
- "setup" -> Node
- "analyze" -> null
- "generate" -> String
*/
let result = null;
const buildStage = builder.getBuildStage();
if ( buildStage === 'setup' ) {
builder.addNode( this );
this.updateReference( builder );
const properties = builder.getNodeProperties( this );
if ( properties.initialized !== true ) {
//const stackNodesBeforeSetup = builder.stack.nodes.length;
properties.initialized = true;
properties.outputNode = this.setup( builder ) || properties.outputNode || null;
/*if ( isNodeOutput && builder.stack.nodes.length !== stackNodesBeforeSetup ) {
// !! no outputNode !!
//outputNode = builder.stack;
}*/
for ( const childNode of Object.values( properties ) ) {
if ( childNode && childNode.isNode === true ) {
if ( childNode.parents === true ) {
const childProperties = builder.getNodeProperties( childNode );
childProperties.parents = childProperties.parents || [];
childProperties.parents.push( this );
}
childNode.build( builder );
}
}
builder.addSequentialNode( this );
}
result = properties.outputNode;
} else if ( buildStage === 'analyze' ) {
this.analyze( builder, output );
} else if ( buildStage === 'generate' ) {
this._checkFlowBlock( builder, nodeData );
const isCached = nodeData.propertyName !== undefined || nodeData.snippet !== undefined;
const flowCodeLength = builder.flow.code.length;
// References must be generated directly, even if a cached value exists.
const allowedCache = this.isCacheable( builder ) && builder.isReference( output ) === false;
const type = allowedCache ? builder.getVectorType( this.getNodeType( builder, output ) ) : null;
const cacheResult = allowedCache && type !== 'void' && output !== 'void' && nodeData.usageCount > 1;
const generateOutput = cacheResult ? type : output;
if ( this.assignable === true && this._declaresAssignable() && this._isVariable( builder, nodeData ) ) {
result = this._buildVariable( builder, nodeData, output );
} else if ( allowedCache && nodeData.propertyName !== undefined ) {
result = builder.format( nodeData.propertyName, type, output );
} else {
// If generate has just one argument, it means the output type is not required.
// This means that the node does not handle output conversions internally,
// so the value is stored in a cache and the builder handles the conversion
// for all requested output types.
const isGenerateOnce = this.generate.length < 2;
if ( isGenerateOnce ) {
const type = this.getNodeType( builder );
result = nodeData.snippet;
if ( result === undefined ) {
if ( nodeData.generated === undefined ) {
nodeData.generated = true;
result = this.generate( builder ) || '';
nodeData.snippet = result;
} else {
warn( 'Node: Recursion detected.', this );
result = '/* Recursion detected. */';
}
}
result = builder.format( result, type, generateOutput );
} else {
result = this.generate( builder, generateOutput ) || '';
}
if ( result === '' && generateOutput !== null && generateOutput !== 'void' && generateOutput !== 'OutputType' ) {
// if no snippet is generated, return a default value
error( `TSL: Invalid generated code, expected a "${ generateOutput }".` );
result = builder.generateConst( generateOutput );
}
if ( cacheResult ) {
// Use a dedicated property, the node may already own a variable.
const propertyName = this._declareVariable( builder, result, type, nodeData.assign !== true, 'cacheVariable' );
nodeData.snippet = result;
nodeData.propertyName = propertyName;
result = builder.format( propertyName, type, output );
}
}
this._setFlowBlock( builder, nodeData, isCached, flowCodeLength );
}
builder.removeChain( this );
return result;
}
/**
* Returns the child nodes as a JSON object.
*
* @return {Generator<Object>} An iterable list of serialized child objects as JSON.
*/
getSerializeChildren() {
return this._getChildren();
}
/**
* 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.7,
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;
}
}
/**
* Enables or disables the automatic capturing of stack traces for nodes.
*
* @type {boolean}
* @default false
*/
Node.captureStackTrace = false;
/**
* Base class for representing element access on an array-like
* node data structures.
*
* @augments Node
*/
class ArrayElementNode extends Node {
static get type() {
return 'ArrayElementNode';
}
/**
* Constructs an array element node.
*
* @param {Node} node - The array-like node.
* @param {Node} indexNode - The index node that defines the element access.
*/
constructor( node, indexNode ) {
super();
/**
* The array-like node.
*
* @type {Node}
*/
this.node = node;
/**
* The index node that defines the element access.
*
* @type {Node}
*/
this.indexNode = indexNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isArrayElementNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns the scope of the array-like node so assignments to elements
* mark the underlying value as mutable.
*
* @return {Node} The scope of the node.
*/
getScope() {
return this.node.getScope();
}
/**
* This method is overwritten since the node type is inferred from the array-like node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.node.getElementType( builder );
}
/**
* This method is overwritten since the member type is inferred from the array-like node.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The member name.
* @return {string} The member type.
*/
getMemberType( builder, name ) {
return this.node.getMemberType( builder, name );
}
generate( builder ) {
const indexType = this.indexNode.getNodeType( builder );
const nodeSnippet = this.node.build( builder );
const indexSnippet = this.indexNode.build( builder, ! builder.isVector( indexType ) && builder.isInteger( indexType ) ? indexType : 'uint' );
return `${ nodeSnippet }[ ${ indexSnippet } ]`;
}
}
/**
* This module is part of the TSL core and usually not used in app level code.
* It represents a convert operation during the shader generation process
* meaning it converts the data type of a node to a target data type.
*
* @augments Node
*/
class ConvertNode extends Node {
static get type() {
return 'ConvertNode';
}
/**
* Constructs a new convert node.
*
* @param {Node} node - The node which type should be converted.
* @param {string} convertTo - The target node type. Multiple types can be defined by separating them with a `|` sign.
*/
constructor( node, convertTo ) {
super();
/**
* The node which type should be converted.
*
* @type {Node}
*/
this.node = node;
/**
* The target node type. Multiple types can be defined by separating them with a `|` sign.
*
* @type {string}
*/
this.convertTo = convertTo;
}
/**
* This method is overwritten since the implementation tries to infer the best
* matching type from the {@link ConvertNode#convertTo} property.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
const requestType = this.node.getNodeType( builder );
let convertTo = null;
for ( const overloadingType of this.convertTo.split( '|' ) ) {
if ( convertTo === null || builder.getTypeLength( requestType ) === builder.getTypeLength( overloadingType ) ) {
convertTo = overloadingType;
}
}
return convertTo;
}
serialize( data ) {
super.serialize( data );
data.convertTo = this.convertTo;
}
deserialize( data ) {
super.deserialize( data );
this.convertTo = data.convertTo;
}
generate( builder, output ) {
const node = this.node;
const type = this.getNodeType( builder );
const snippet = node.build( builder, type );
return builder.format( snippet, type, output );
}
}
/**
* This module is part of the TSL core and usually not used in app level code.
* It represents a join operation during the shader generation process.
* For example in can compose/join two single floats into a `vec2` type.
*
* @augments Node
*/
class JoinNode extends Node {
static get type() {
return 'JoinNode';
}
/**
* Constructs a new join node.
*
* @param {Array<Node>} nodes - An array of nodes that should be joined.
* @param {?string} [nodeType=null] - The node type.
*/
constructor( nodes = [], nodeType = null ) {
super( nodeType );
/**
* An array of nodes that should be joined.
*
* @type {Array<Node>}
*/
this.nodes = nodes;
}
/**
* This method is overwritten since the node type must be inferred from the
* joined data length if not explicitly defined.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.nodeType !== null ) {
return builder.getVectorType( this.nodeType );
}
return builder.getTypeFromLength( this.nodes.reduce( ( count, cur ) => count + builder.getTypeLength( cur.getNodeType( builder ) ), 0 ) );
}
generate( builder, output ) {
const type = this.getNodeType( builder );
const maxLength = builder.getTypeLength( type );
const nodes = this.nodes;
const primitiveType = builder.getComponentType( type );
const snippetValues = [];
let length = 0;
for ( const input of nodes ) {
if ( length >= maxLength ) {
error( `TSL: Length of parameters exceeds maximum length of function '${ type }()' type.`, this.stackTrace );
break;
}
let inputType = input.getNodeType( builder );
let inputTypeLength = builder.getTypeLength( inputType );
let inputSnippet;
if ( length + inputTypeLength > maxLength ) {
error( `TSL: Length of '${ type }()' data exceeds maximum length of output type.`, this.stackTrace );
inputTypeLength = maxLength - length;
inputType = builder.getTypeFromLength( inputTypeLength );
}
length += inputTypeLength;
inputSnippet = input.build( builder, inputType );
const inputPrimitiveType = builder.getComponentType( inputType );
if ( inputPrimitiveType !== primitiveType ) {
const targetType = builder.getTypeFromLength( inputTypeLength, primitiveType );
inputSnippet = builder.format( inputSnippet, inputType, targetType );
}
snippetValues.push( inputSnippet );
}
const snippet = `${ builder.getType( type ) }( ${ snippetValues.join( ', ' ) } )`;
return builder.format( snippet, type, output );
}
}
const _stringVectorComponents = vectorComponents.join( '' );
/**
* This module is part of the TSL core and usually not used in app level code.
* `SplitNode` represents a property access operation which means it is
* used to implement any `.xyzw`, `.rgba` and `stpq` usage on node objects.
* For example:
* ```js
* const redValue = color.r;
* ```
*
* @augments Node
*/
class SplitNode extends Node {
static get type() {
return 'SplitNode';
}
/**
* Constructs a new split node.
*
* @param {Node} node - The node that should be accessed.
* @param {string} [components='x'] - The components that should be accessed.
*/
constructor( node, components = 'x' ) {
super();
/**
* The node that should be accessed.
*
* @type {Node}
*/
this.node = node;
/**
* The components that should be accessed.
*
* @type {string}
*/
this.components = components;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSplitNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns the vector length which is computed based on the requested components.
*
* @return {number} The vector length.
*/
getVectorLength() {
let vectorLength = this.components.length;
for ( const c of this.components ) {
vectorLength = Math.max( vectorComponents.indexOf( c ) + 1, vectorLength );
}
return vectorLength;
}
/**
* Returns the component type of the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The component type.
*/
getComponentType( builder ) {
return builder.getComponentType( this.node.getNodeType( builder ) );
}
/**
* This method is overwritten since the node type is inferred from requested components.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return builder.getTypeFromLength( this.components.length, this.getComponentType( builder ) );
}
/**
* Returns the scope of the node.
*
* @return {Node} The scope of the node.
*/
getScope() {
return this.node.getScope();
}
generate( builder, output ) {
const node = this.node;
const nodeTypeLength = builder.getTypeLength( node.getNodeType( builder ) );
let snippet = null;
if ( nodeTypeLength > 1 ) {
let type = null;
const componentsLength = this.getVectorLength();
if ( componentsLength >= nodeTypeLength ) {
// needed expand the input node
type = builder.getTypeFromLength( this.getVectorLength(), this.getComponentType( builder ) );
}
const nodeSnippet = node.build( builder, type );
if ( this.components.length === nodeTypeLength && this.components === _stringVectorComponents.slice( 0, this.components.length ) ) {
// unnecessary swizzle
snippet = builder.format( nodeSnippet, type, output );
} else {
snippet = builder.format( `${nodeSnippet}.${this.components}`, this.getNodeType( builder ), output );
}
} else {
// ignore .components if .node returns float/integer
snippet = node.build( builder, output );
}
return snippet;
}
serialize( data ) {
super.serialize( data );
data.components = this.components;
}
deserialize( data ) {
super.deserialize( data );
this.components = data.components;
}
}
/**
* This module is part of the TSL core and usually not used in app level code.
* `SetNode` represents a set operation which means it is used to implement any
* `setXYZW()`, `setRGBA()` and `setSTPQ()` method invocations on node objects.
* For example:
* ```js
* materialLine.colorNode = color( 0, 0, 0 ).setR( float( 1 ) );
* ```
*
* @augments Node
*/
class SetNode extends Node {
static get type() {
return 'SetNode';
}
/**
* Constructs a new set node.
*
* @param {Node} sourceNode - The node that should be updated.
* @param {string} components - The components that should be updated.
* @param {Node} targetNode - The value node.
*/
constructor( sourceNode, components, targetNode ) {
super();
/**
* The node that should be updated.
*
* @type {Node}
*/
this.sourceNode = sourceNode;
/**
* The components that should be updated.
*
* @type {string}
*/
this.components = components;
/**
* The value node.
*
* @type {Node}
*/
this.targetNode = targetNode;
}
/**
* This method is overwritten since the node type is inferred from {@link SetNode#sourceNode}.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.sourceNode.getNodeType( builder );
}
generate( builder ) {
const { sourceNode, components, targetNode } = this;
const sourceType = this.getNodeType( builder );
const componentType = builder.getComponentType( targetNode.getNodeType( builder ) );
const targetType = builder.getTypeFromLength( components.length, componentType );
const targetSnippet = targetNode.build( builder, targetType );
const sourceSnippet = sourceNode.build( builder, sourceType );
const length = builder.getTypeLength( sourceType );
const snippetValues = [];
for ( let i = 0; i < length; i ++ ) {
const component = vectorComponents[ i ];
if ( component === components[ 0 ] ) {
snippetValues.push( targetSnippet );
i += components.length - 1;
} else {
snippetValues.push( sourceSnippet + '.' + component );
}
}
return `${ builder.getType( sourceType ) }( ${ snippetValues.join( ', ' ) } )`;
}
}
/**
* This module is part of the TSL core and usually not used in app level code.
* It represents a flip operation during the shader generation process
* meaning it flips normalized values with the following formula:
* ```
* x = 1 - x;
* ```
* `FlipNode` is internally used to implement any `flipXYZW()`, `flipRGBA()` and
* `flipSTPQ()` method invocations on node objects. For example:
* ```js
* uvNode = uvNode.flipY();
* ```
*
* @augments Node
*/
class FlipNode extends Node {
static get type() {
return 'FlipNode';
}
/**
* Constructs a new flip node.
*
* @param {Node} sourceNode - The node which component(s) should be flipped.
* @param {string} components - The components that should be flipped e.g. `'x'` or `'xy'`.
*/
constructor( sourceNode, components ) {
super();
/**
* The node which component(s) should be flipped.
*
* @type {Node}
*/
this.sourceNode = sourceNode;
/**
* The components that should be flipped e.g. `'x'` or `'xy'`.
*
* @type {string}
*/
this.components = components;
}
/**
* This method is overwritten since the node type is inferred from the source node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.sourceNode.getNodeType( builder );
}
generate( builder ) {
const { components, sourceNode } = this;
const sourceType = this.getNodeType( builder );
const sourceSnippet = sourceNode.build( builder );
const sourceCache = builder.getVarFromNode( this );
const sourceProperty = builder.getPropertyName( sourceCache );
builder.addLineFlowCode( sourceProperty + ' = ' + sourceSnippet, this );
const length = builder.getTypeLength( sourceType );
const snippetValues = [];
let componentIndex = 0;
for ( let i = 0; i < length; i ++ ) {
const component = vectorComponents[ i ];
if ( component === components[ componentIndex ] ) {
snippetValues.push( '1.0 - ' + ( sourceProperty + '.' + component ) );
componentIndex ++;
} else {
snippetValues.push( sourceProperty + '.' + component );
}
}
return `${ builder.getType( sourceType ) }( ${ snippetValues.join( ', ' ) } )`;
}
}
/**
* Base class for representing data input nodes.
*
* @augments Node
*/
class InputNode extends Node {
static get type() {
return 'InputNode';
}
/**
* Constructs a new input node.
*
* @param {any} value - The value of this node. This can be any JS primitive, functions, array buffers or even three.js objects (vector, matrices, colors).
* @param {?string} nodeType - The node type. If no explicit type is defined, the node tries to derive the type from its value.
*/
constructor( value, nodeType = null ) {
super( nodeType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isInputNode = true;
/**
* The value of this node. This can be any JS primitive, functions, array buffers or even three.js objects (vector, matrices, colors).
*
* @type {any}
*/
this.value = value;
/**
* The precision of the value in the shader.
*
* @type {?('low'|'medium'|'high')}
* @default null
*/
this.precision = null;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( /*builder*/ ) {
if ( this.nodeType === null ) {
return getValueType( this.value );
}
return this.nodeType;
}
/**
* Returns the input type of the node which is by default the node type. Derived modules
* might overwrite this method and use a fixed type or compute one analytically.
*
* A typical example for different input and node types are textures. The input type of a
* normal RGBA texture is `texture` whereas its node type is `vec4`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( builder ) {
return this.getNodeType( builder );
}
/**
* Sets the precision to the given value. The method can be
* overwritten in derived classes if the final precision must be computed
* analytically.
*
* @param {('low'|'medium'|'high')} precision - The precision of the input value in the shader.
* @return {InputNode} A reference to this node.
*/
setPrecision( precision ) {
this.precision = precision;
return this;
}
serialize( data ) {
super.serialize( data );
data.value = this.value;
if ( this.value && this.value.toArray ) data.value = this.value.toArray();
data.valueType = getValueType( this.value );
data.nodeType = this.nodeType;
if ( data.valueType === 'ArrayBuffer' ) data.value = arrayBufferToBase64( data.value );
data.precision = this.precision;
}
deserialize( data ) {
super.deserialize( data );
this.nodeType = data.nodeType;
this.value = Array.isArray( data.value ) ? getValueFromType( data.valueType, ...data.value ) : data.value;
this.precision = data.precision || null;
if ( this.value && this.value.fromArray ) this.value = this.value.fromArray( data.value );
}
generate( /*builder, output*/ ) {
warn( 'Abstract function.' );
}
}
const _regNum = /float|u?int/;
/**
* Class for representing a constant value in the shader.
*
* @augments InputNode
*/
class ConstNode extends InputNode {
static get type() {
return 'ConstNode';
}
/**
* Constructs a new input node.
*
* @param {any} value - The value of this node. Usually a JS primitive or three.js object (vector, matrix, color).
* @param {?string} nodeType - The node type. If no explicit type is defined, the node tries to derive the type from its value.
*/
constructor( value, nodeType = null ) {
super( value, nodeType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isConstNode = true;
/**
* Whether this constant is an implicit number whose type can adapt to other operands.
*
* @type {boolean}
* @default false
*/
this.isWeak = false;
}
/**
* Generates the shader string of the value with the current node builder.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated value as a shader string.
*/
generateConst( builder ) {
return builder.generateConst( this.getNodeType( builder ), this.value );
}
generate( builder, output ) {
const type = this.getNodeType( builder );
if ( _regNum.test( type ) && _regNum.test( output ) ) {
let value = this.value;
// Preserve the declared integer value before converting to the output type.
if ( type === 'int' ) value = Math.trunc( value );
else if ( type === 'uint' ) value = value >= 0 ? Math.trunc( value ) : 0;
return builder.generateConst( output, value );
}
return builder.format( this.generateConst( builder ), type, output );
}
serialize( data ) {
super.serialize( data );
data.isWeak = this.isWeak;
}
deserialize( data ) {
super.deserialize( data );
this.isWeak = data.isWeak === true;
}
}
/**
* Base class for representing member access on an object-like
* node data structures.
*
* @augments Node
*/
class MemberNode extends Node {
static get type() {
return 'MemberNode';
}
/**
* Constructs a member node.
*
* @param {Node} structNode - The struct node.
* @param {string} property - The property name.
*/
constructor( structNode, property ) {
super();
/**
* The struct node.
*
* @type {Node}
*/
this.structNode = structNode;
/**
* The property name.
*
* @type {Node}
*/
this.property = property;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMemberNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns the scope of the struct so assignments to members
* mark the underlying value as mutable.
*
* @return {Node} The scope of the node.
*/
getScope() {
return this.structNode.getScope();
}
hasMember( builder ) {
if ( this.structNode.isMemberNode ) {
if ( this.structNode.hasMember( builder ) === false ) {
return false;
}
}
return this.structNode.getMemberType( builder, this.property ) !== 'void';
}
generateNodeType( builder ) {
if ( this.hasMember( builder ) === false ) {
// default type if member does not exist
return 'float';
}
return this.structNode.getMemberType( builder, this.property );
}
getMemberType( builder, name ) {
if ( this.hasMember( builder ) === false ) {
// default type if member does not exist
return 'float';
}
const type = this.getNodeType( builder );
const struct = builder.getStructTypeNode( type );
return struct.getMemberType( builder, name );
}
generate( builder ) {
if ( this.hasMember( builder ) === false ) {
warn( `TSL: Member "${ this.property }" does not exist in struct.`, this.stackTrace );
const type = this.getNodeType( builder );
return builder.generateConst( type );
}
const propertyName = this.structNode.build( builder );
return propertyName + '.' + this.property;
}
}
let currentStack = null;
const _secureNodeBuilders = new WeakMap();
const NodeElements = new Map();
// Extend Node Class for TSL using prototype
function addMethodChaining( name, nodeElement ) {
// No require StackTrace because this is internal API
if ( NodeElements.has( name ) ) {
warn( `TSL: Redefinition of method chaining '${ name }'.` );
return;
}
if ( typeof nodeElement !== 'function' ) throw new Error( `THREE.TSL: Node element ${ name } is not a function` );
NodeElements.set( name, nodeElement );
if ( name !== 'assign' ) {
// Changing Node prototype to add method chaining
Node.prototype[ name ] = function ( ...params ) {
return this.isStackNode ? this.addToStack( nodeElement( ...params ) ) : nodeElement( this, ...params );
};
// Adding assign method chaining
Node.prototype[ name + 'Assign' ] = function ( ...params ) {
return this.isStackNode ? this.assign( params[ 0 ], nodeElement( ...params ) ) : this.assign( nodeElement( this, ...params ) );
};
}
}
const parseSwizzle = ( props ) => props.replace( /r|s/g, 'x' ).replace( /g|t/g, 'y' ).replace( /b|p/g, 'z' ).replace( /a|q/g, 'w' );
const parseSwizzleAndSort = ( props ) => parseSwizzle( props ).split( '' ).sort().join( '' );
Node.prototype.assign = function ( ...params ) {
if ( this.isStackNode !== true ) {
if ( currentStack !== null ) {
currentStack.assign( this, ...params );
} else {
error( 'TSL: No stack defined for assign operation. Make sure the assign is inside a Fn().', new StackTrace() );
}
return this;
} else {
const nodeElement = NodeElements.get( 'assign' );
return this.addToStack( nodeElement( ...params ) );
}
};
Node.prototype.get = function ( value ) {
return new MemberNode( this, value );
};
// Cache prototype for TSL
const proto = {};
// Cache of the swizzle and element nodes, a proxy of a node has its own cache
const _swizzleCache = new WeakMap();
function getSwizzleCache( node ) {
let cache = _swizzleCache.get( node );
if ( cache === undefined ) {
cache = {};
_swizzleCache.set( node, cache );
}
return cache;
}
// Set swizzle properties for xyzw, rgba, and stpq.
function setProtoSwizzle( property, altA, altB ) {
// swizzle properties
proto[ property ] = proto[ altA ] = proto[ altB ] = {
get() {
const cache = getSwizzleCache( this );
//
let split = cache[ property ];
if ( split === undefined ) {
split = new SplitNode( this, property );
cache[ property ] = split;
}
return split;
},
set( value ) {
this[ property ].assign( nodeObject( value ) );
}
};
// set properties ( swizzle ) and sort to xyzw sequence
const propUpper = property.toUpperCase();
const altAUpper = altA.toUpperCase();
const altBUpper = altB.toUpperCase();
// Set methods for swizzle properties
Node.prototype[ 'set' + propUpper ] = Node.prototype[ 'set' + altAUpper ] = Node.prototype[ 'set' + altBUpper ] = function ( value ) {
const swizzle = parseSwizzleAndSort( property );
return new SetNode( this, swizzle, nodeObject( value ) );
};
// Set methods for flip properties
Node.prototype[ 'flip' + propUpper ] = Node.prototype[ 'flip' + altAUpper ] = Node.prototype[ 'flip' + altBUpper ] = function () {
const swizzle = parseSwizzleAndSort( property );
return new FlipNode( this, swizzle );
};
}
const swizzleA = [ 'x', 'y', 'z', 'w' ];
const swizzleB = [ 'r', 'g', 'b', 'a' ];
const swizzleC = [ 's', 't', 'p', 'q' ];
for ( let a = 0; a < 4; a ++ ) {
let prop = swizzleA[ a ];
let altA = swizzleB[ a ];
let altB = swizzleC[ a ];
setProtoSwizzle( prop, altA, altB );
for ( let b = 0; b < 4; b ++ ) {
prop = swizzleA[ a ] + swizzleA[ b ];
altA = swizzleB[ a ] + swizzleB[ b ];
altB = swizzleC[ a ] + swizzleC[ b ];
setProtoSwizzle( prop, altA, altB );
for ( let c = 0; c < 4; c ++ ) {
prop = swizzleA[ a ] + swizzleA[ b ] + swizzleA[ c ];
altA = swizzleB[ a ] + swizzleB[ b ] + swizzleB[ c ];
altB = swizzleC[ a ] + swizzleC[ b ] + swizzleC[ c ];
setProtoSwizzle( prop, altA, altB );
for ( let d = 0; d < 4; d ++ ) {
prop = swizzleA[ a ] + swizzleA[ b ] + swizzleA[ c ] + swizzleA[ d ];
altA = swizzleB[ a ] + swizzleB[ b ] + swizzleB[ c ] + swizzleB[ d ];
altB = swizzleC[ a ] + swizzleC[ b ] + swizzleC[ c ] + swizzleC[ d ];
setProtoSwizzle( prop, altA, altB );
}
}
}
}
// Set/get static properties for array elements (0-31).
for ( let i = 0; i < 32; i ++ ) {
proto[ i ] = {
get() {
const cache = getSwizzleCache( this );
//
let element = cache[ i ];
if ( element === undefined ) {
element = new ArrayElementNode( this, new ConstNode( i, 'uint' ) );
cache[ i ] = element;
}
return element;
},
set( value ) {
this[ i ].assign( nodeObject( value ) );
}
};
}
/*
// Set properties for width, height, and depth.
function setProtoProperty( property, target ) {
proto[ property ] = {
get() {
const cache = getSwizzleCache( this );
//
let split = cache[ target ];
if ( split === undefined ) {
split = new SplitNode( this, target );
cache[ target ] = split;
}
return split;
},
set( value ) {
this[ target ].assign( nodeObject( value ) );
}
};
}
setProtoProperty( 'width', 'x' );
setProtoProperty( 'height', 'y' );
setProtoProperty( 'depth', 'z' );
*/
Object.defineProperties( Node.prototype, proto );
// --- FINISH ---
const ShaderNodeObject = function ( obj, altType = null ) {
const type = getValueType( obj );
if ( type === 'node' ) {
return obj;
} else if ( ( altType === null && ( type === 'float' || type === 'boolean' ) ) || ( type && type !== 'shader' && type !== 'string' ) ) {
return getConstNode( obj, altType );
} else if ( type === 'shader' ) {
return obj.isFn ? obj : Fn( obj );
}
return obj;
};
const ShaderNodeObjects = function ( objects, altType = null ) {
for ( const name in objects ) {
objects[ name ] = nodeObject( objects[ name ], altType );
}
return objects;
};
const ShaderNodeArray = function ( array, altType = null ) {
const len = array.length;
for ( let i = 0; i < len; i ++ ) {
array[ i ] = nodeObject( array[ i ], altType );
}
return array;
};
const ShaderNodeProxy = function ( NodeClass, scope = null, factor = null, settings = null ) {
function assignNode( node ) {
if ( settings !== null ) {
Object.assign( node, settings );
if ( settings.assignable === true ) {
node = node.toAssignable();
}
}
return node;
}
let fn, name = scope, minParams, maxParams;
function verifyParamsLimit( params ) {
let tslName;
if ( name ) tslName = /[a-z]/i.test( name ) ? name + '()' : name;
else tslName = NodeClass.type;
if ( minParams !== undefined && params.length < minParams ) {
error( `TSL: "${ tslName }" parameter length is less than minimum required.`, new StackTrace() );
return params.concat( new Array( minParams - params.length ).fill( 0 ) );
} else if ( maxParams !== undefined && params.length > maxParams ) {
error( `TSL: "${ tslName }" parameter length exceeds limit.`, new StackTrace() );
return params.slice( 0, maxParams );
}
return params;
}
if ( scope === null ) {
fn = ( ...params ) => {
return assignNode( new NodeClass( ...nodeArray( verifyParamsLimit( params ) ) ) );
};
} else if ( factor !== null ) {
factor = nodeObject( factor );
fn = ( ...params ) => {
return assignNode( new NodeClass( scope, ...nodeArray( verifyParamsLimit( params ) ), factor ) );
};
} else {
fn = ( ...params ) => {
return assignNode( new NodeClass( scope, ...nodeArray( verifyParamsLimit( params ) ) ) );
};
}
fn.setParameterLength = ( ...params ) => {
if ( params.length === 1 ) minParams = maxParams = params[ 0 ];
else if ( params.length === 2 ) [ minParams, maxParams ] = params;
return fn;
};
fn.setName = ( value ) => {
name = value;
return fn;
};
return fn;
};
const ShaderNodeImmutable = function ( NodeClass, ...params ) {
return new NodeClass( ...nodeArray( params ) );
};
class ShaderCallNodeInternal extends Node {
constructor( shaderNode, rawInputs ) {
super();
this.shaderNode = shaderNode;
this.rawInputs = rawInputs;
this.isShaderCallNodeInternal = true;
}
isCacheable( builder ) {
// A call is an expression unless its body has statements.
return this.getOutputNode( builder ).nodes.length === 0;
}
generateNodeType( builder ) {
return this.shaderNode.nodeType || this.getOutputNode( builder ).getNodeType( builder );
}
getElementType( builder ) {
return this.getOutputNode( builder ).getElementType( builder );
}
getMemberType( builder, name ) {
return this.getOutputNode( builder ).getMemberType( builder, name );
}
call( builder ) {
const { shaderNode, rawInputs } = this;
const properties = builder.getNodeProperties( shaderNode );
const subBuild = builder.getClosestSubBuild( shaderNode.subBuilds ) || '';
const subBuildProperty = subBuild || 'default';
if ( properties[ subBuildProperty ] ) {
return properties[ subBuildProperty ];
}
//
const previousSubBuildFn = builder.subBuildFn;
const previousFnCall = builder.fnCall;
builder.subBuildFn = subBuild;
builder.fnCall = this;
let result = null;
// A layout call needs every input. Missing inputs are completed with the default values
// of the function parameters; if they can't be resolved, the function is expanded inline.
const layoutInputs = shaderNode.layout ? builder._getLayoutInputs( shaderNode, rawInputs ) : null;
if ( layoutInputs !== null ) {
// build inputs first, use layout inputs to ensure that no extra parameters are built
const layoutParameters = shaderNode.layout.inputs;
if ( isArrayAsParameter( layoutInputs ) ) {
const rawArrayParameters = layoutInputs;
for ( let i = 0; i < layoutParameters.length; i ++ ) {
const rawParameter = rawArrayParameters[ i ];
if ( rawParameter && rawParameter.isNode ) {
rawParameter.build( builder );
}
}
} else {
const rawObjectParameters = layoutInputs[ 0 ];
for ( const param of layoutParameters ) {
const rawParameter = rawObjectParameters[ param.name ];
if ( rawParameter && rawParameter.isNode ) {
rawParameter.build( builder );
}
}
}
const functionNode = builder.buildFunctionNode( shaderNode );
builder.addInclude( functionNode );
//
const inputs = getLayoutParameters( layoutInputs );
result = functionNode.call( inputs );
} else {
let secureNodeBuilder = _secureNodeBuilders.get( builder );
if ( secureNodeBuilder === undefined ) {
secureNodeBuilder = new Proxy( builder, {
get: ( target, property, receiver ) => {
let value;
if ( Symbol.iterator === property ) {
value = function* () {
yield undefined;
};
} else {
value = Reflect.get( target, property, receiver );
}
return value;
}
} );
_secureNodeBuilders.set( builder, secureNodeBuilder );
}
//
const inputs = rawInputs ? getProxyParameters( rawInputs ) : null;
const hasParameters = Array.isArray( rawInputs ) ? rawInputs.length > 0 : rawInputs !== null;
const jsFunc = shaderNode.jsFunc;
const outputNode = hasParameters || jsFunc.length > 1 ? jsFunc( inputs, secureNodeBuilder ) : jsFunc( secureNodeBuilder );
result = nodeObject( outputNode );
}
builder.subBuildFn = previousSubBuildFn;
builder.fnCall = previousFnCall;
if ( shaderNode.once ) {
properties[ subBuildProperty ] = result;
}
return result;
}
setupOutput( builder ) {
builder.addStack();
builder.stack.outputNode = this.call( builder );
return builder.removeStack();
}
getOutputNode( builder ) {
const properties = builder.getNodeProperties( this );
const subBuildOutput = builder.getSubBuildOutput( this );
properties[ subBuildOutput ] = properties[ subBuildOutput ] || this.setupOutput( builder );
properties[ subBuildOutput ].subBuild = builder.getClosestSubBuild( this );
return properties[ subBuildOutput ];
}
generate( builder, output ) {
return this.getOutputNode( builder ).build( builder, output ) || '';
}
build( builder, output = null ) {
// The call does not use Node.build(), so it handles the inspector node, shared and overridden nodes and assignable state here.
this._setupInspector( builder );
const refNode = this.getShared( builder );
if ( this !== refNode ) {
return refNode.build( builder, output );
}
let result = null;
const buildStage = builder.getBuildStage();
const properties = builder.getNodeProperties( this );
const nodeData = this.assignable === true ? builder.getDataFromNode( this ) : null;
if ( nodeData !== null ) this._updateAssignable( builder, nodeData );
const subBuildOutput = builder.getSubBuildOutput( this );
const outputNode = this.getOutputNode( builder );
const previousFnCall = builder.fnCall;
builder.fnCall = this;
if ( buildStage === 'setup' ) {
const subBuildInitialized = builder.getSubBuildProperty( 'initialized', this );
if ( properties[ subBuildInitialized ] !== true ) {
properties[ subBuildInitialized ] = true;
properties[ subBuildOutput ] = this.getOutputNode( builder );
properties[ subBuildOutput ].build( builder );
// If the shaderNode has subBuilds, add them to the chaining nodes
// so they can be built later in the build process.
if ( this.shaderNode.subBuilds ) {
for ( const node of builder.chaining ) {
const nodeData = builder.getDataFromNode( node, 'any' );
nodeData.subBuilds = nodeData.subBuilds || new Set();
for ( const subBuild of this.shaderNode.subBuilds ) {
nodeData.subBuilds.add( subBuild );
}
//builder.getDataFromNode( node ).subBuilds = nodeData.subBuilds;
}
}
}
result = properties[ subBuildOutput ];
} else if ( buildStage === 'analyze' ) {
// A variable is evaluated once, so its value is analyzed only once.
if ( nodeData === null || this._isVariable( builder, nodeData ) === false || builder.increaseUsage( this ) === 1 ) {
outputNode.build( builder, output );
}
} else if ( buildStage === 'generate' ) {
if ( nodeData !== null && this._isVariable( builder, nodeData ) ) {
this._checkFlowBlock( builder, nodeData );
const isCached = nodeData.propertyName !== undefined;
const flowCodeLength = builder.flow.code.length;
result = this._buildVariable( builder, nodeData, output );
this._setFlowBlock( builder, nodeData, isCached, flowCodeLength );
} else {
result = outputNode.build( builder, output ) || '';
}
}
builder.fnCall = previousFnCall;
return result;
}
}
function getLayoutParameters( params ) {
let output;
nodeObjects( params );
if ( isArrayAsParameter( params ) ) {
output = [ ...params ];
} else {
output = params[ 0 ];
}
return output;
}
function getProxyParameters( params ) {
let index = 0;
nodeObjects( params );
return new Proxy( params, {
get: ( target, property, receiver ) => {
let value;
if ( property === 'length' ) {
value = params.length;
return value;
}
if ( Symbol.iterator === property ) {
value = function* () {
for ( const inputNode of params ) {
yield inputNode;
}
};
} else {
if ( params.length > 0 ) {
if ( Object.getPrototypeOf( params[ 0 ] ) === Object.prototype ) {
const objectTarget = params[ 0 ];
if ( objectTarget[ property ] === undefined ) {
value = objectTarget[ index ++ ];
} else {
value = Reflect.get( objectTarget, property, receiver );
}
} else if ( params[ 0 ] instanceof Node ) {
if ( params[ property ] === undefined ) {
value = params[ index ++ ];
} else {
value = Reflect.get( params, property, receiver );
}
}
} else {
value = Reflect.get( target, property, receiver );
}
value = nodeObject( value );
}
return value;
}
} );
}
class ShaderNodeInternal extends Node {
constructor( jsFunc, nodeType ) {
super( nodeType );
this.jsFunc = jsFunc;
this.layout = null;
this.global = true;
this.once = false;
}
isCacheable( /*builder*/ ) {
return false;
}
setLayout( layout ) {
this.layout = layout;
return this;
}
getLayout() {
return this.layout;
}
call( rawInputs = null ) {
return new ShaderCallNodeInternal( this, rawInputs );
}
setup() {
return this.call();
}
}
const bools = [ false, true ];
const uints = [ 0, 1, 2, 3 ];
const ints = [ -1, -2 ];
const floats = [ 0.5, 1.5, 1 / 3, 1e-6, 1e6, Math.PI, Math.PI * 2, 1 / Math.PI, 2 / Math.PI, 1 / ( Math.PI * 2 ), Math.PI / 2 ];
const boolsCacheMap = new Map();
for ( const bool of bools ) boolsCacheMap.set( bool, new ConstNode( bool ) );
const uintsCacheMap = new Map();
for ( const uint of uints ) uintsCacheMap.set( uint, new ConstNode( uint, 'uint' ) );
const intsCacheMap = new Map();
for ( const value of uintsCacheMap.keys() ) intsCacheMap.set( value, new ConstNode( value, 'int' ) );
for ( const int of ints ) intsCacheMap.set( int, new ConstNode( int, 'int' ) );
const floatsCacheMap = new Map();
for ( const value of intsCacheMap.keys() ) floatsCacheMap.set( value, new ConstNode( value ) );
for ( const float of floats ) floatsCacheMap.set( float, new ConstNode( float ) );
for ( const float of floats ) floatsCacheMap.set( - float, new ConstNode( - float ) );
const cacheMaps = { bool: boolsCacheMap, uint: uintsCacheMap, ints: intsCacheMap, float: floatsCacheMap };
const constNodesCacheMap = new Map( boolsCacheMap );
const getConstNode = ( value, type ) => {
if ( constNodesCacheMap.has( value ) ) {
return constNodesCacheMap.get( value );
} else if ( value.isNode === true ) {
return value;
} else {
const node = new ConstNode( value, type );
// Implicit numbers are weak and can adapt to the type of other operands.
node.isWeak = ! type && typeof value === 'number';
return node;
}
};
for ( const value of floatsCacheMap.keys() ) constNodesCacheMap.set( value, getConstNode( value ) );
const ConvertType = function ( type, cacheMap = null ) {
return ( ...params ) => {
for ( const param of params ) {
if ( param === undefined ) {
error( `TSL: Invalid parameter for the type "${ type }".`, new StackTrace() );
return new ConstNode( 0, type );
}
}
if ( params.length === 0 || ( ! [ 'bool', 'float', 'int', 'uint' ].includes( type ) && params.every( param => {
const paramType = typeof param;
return paramType !== 'object' && paramType !== 'function';
} ) ) ) {
params = [ getValueFromType( type, ...params ) ];
}
if ( params.length === 1 && cacheMap !== null && cacheMap.has( params[ 0 ] ) ) {
const cached = cacheMap.get( params[ 0 ] );
return new ConstNode( cached.value, cached.nodeType ).toAssignable();
}
if ( params.length === 1 ) {
const param = params[ 0 ];
const node = getConstNode( param, type );
if ( node.nodeType === type ) {
// A node given by the user or a shared constant must not become the variable itself.
if ( node === param ) return new ConvertNode( node, type ).toAssignable();
if ( constNodesCacheMap.get( param ) === node ) return new ConstNode( node.value, node.nodeType ).toAssignable();
return node.toAssignable();
}
return new ConvertNode( node, type ).toAssignable();
}
const nodes = params.map( param => getConstNode( param ) );
return new JoinNode( nodes, type ).toAssignable();
};
};
// exports
function defined( value ) {
if ( value && value.isNode ) {
value.traverse( ( node ) => {
if ( node.isConstNode ) {
value = node.value;
}
} );
}
return Boolean( value );
}
// utils
const getConstNodeType = ( value ) => ( value !== undefined && value !== null ) ? ( value.nodeType || value.convertTo || ( typeof value === 'string' ? value : null ) ) : null;
// shader node base
function ShaderNode( jsFunc, nodeType ) {
return new ShaderNodeInternal( jsFunc, nodeType );
}
const nodeObject = ( val, altType = null ) => /* new */ ShaderNodeObject( val, altType );
const nodeObjectAssignable = ( val, altType = null ) => {
let node = nodeObject( val, altType );
// Shared constants need their own assignable node.
if ( node.isConstNode === true && constNodesCacheMap.get( node.value ) === node ) {
const copy = new ConstNode( node.value, node.nodeType );
copy.isWeak = node.isWeak;
node = copy;
}
return node.toAssignable();
};
const nodeObjects = ( val, altType = null ) => new ShaderNodeObjects( val, altType );
const nodeArray = ( val, altType = null ) => new ShaderNodeArray( val, altType );
const nodeProxy = ( NodeClass, scope = null, factor = null, settings = null ) => new ShaderNodeProxy( NodeClass, scope, factor, settings );
const nodeImmutable = ( NodeClass, ...params ) => new ShaderNodeImmutable( NodeClass, ...params );
const nodeProxyAssignable = ( NodeClass, scope = null, factor = null, settings = {} ) => new ShaderNodeProxy( NodeClass, scope, factor, { ...settings, assignable: true } );
const nodeProxyConstructor = ( constructorFunction, nodeInstance ) => {
return new Proxy( constructorFunction, {
get( target, prop, receiver ) {
return Reflect.get( nodeInstance, prop, receiver );
},
set( target, prop, value ) {
return Reflect.set( nodeInstance, prop, value );
}
} );
};
let fnId = 0;
class FnNode extends Node {
constructor( jsFunc, layout = null ) {
super();
let nodeType = null;
if ( layout !== null ) {
if ( typeof layout === 'object' ) {
nodeType = layout.return;
} else {
if ( typeof layout === 'string' ) {
nodeType = layout;
} else {
error( 'TSL: Invalid layout type.', new StackTrace() );
}
layout = null;
}
}
this.shaderNode = new ShaderNode( jsFunc, nodeType );
if ( layout !== null ) {
this.setLayout( layout );
}
this.isFn = true;
}
isCacheable( /*builder*/ ) {
return false;
}
setLayout( layout ) {
const nodeType = this.shaderNode.nodeType;
if ( typeof layout.inputs !== 'object' ) {
const fullLayout = {
name: 'fn' + fnId ++,
type: nodeType,
inputs: []
};
for ( const name in layout ) {
if ( name === 'return' ) continue;
fullLayout.inputs.push( {
name: name,
type: layout[ name ]
} );
}
layout = fullLayout;
}
this.shaderNode.setLayout( layout );
return this;
}
generateNodeType( builder ) {
return this.shaderNode.getNodeType( builder ) || 'float';
}
call( ...params ) {
const fnCall = this.shaderNode.call( params );
// A call without a return value is a statement.
if ( this.shaderNode.nodeType === 'void' ) return fnCall.toStack();
return fnCall.toAssignable();
}
once( subBuilds = null ) {
this.shaderNode.once = true;
this.shaderNode.subBuilds = subBuilds;
return this;
}
generate( builder ) {
const type = this.getNodeType( builder );
error( 'TSL: "Fn()" was declared but not invoked. Try calling it like "Fn()( ...params )".', this.stackTrace );
return builder.generateConst( type );
}
}
function Fn( jsFunc, layout = null ) {
// Layout functions are cached across shaders of the same backend. Their bodies
// must depend only on explicit inputs and constants, not uniforms or material context.
//
// Parameters can have default values. A call without some inputs passes the defaults as
// arguments, so values like `uv()` are evaluated at the call site. The defaults are resolved
// once per function and shared by all calls: a value that must be evaluated per build should
// be a node like `Fn( ... ).once()().toVar( name )`. If a default depends on another
// parameter, that call is expanded inline.
const instance = new FnNode( jsFunc, layout );
return new Proxy( () => {}, {
apply( target, thisArg, params ) {
return instance.call( ...params );
},
get( target, prop, receiver ) {
return Reflect.get( instance, prop, receiver );
},
set( target, prop, value, receiver ) {
return Reflect.set( instance, prop, value, receiver );
}
} );
}
//
const setCurrentStack = ( stack ) => {
currentStack = stack;
};
const getCurrentStack = () => currentStack;
/**
* Represent a conditional node using if/else statements.
*
* ```js
* If( condition, function )
* .ElseIf( condition, function )
* .Else( function )
* ```
* @tsl
* @function
* @param {...any} params - The parameters for the conditional node.
* @returns {StackNode} The conditional node.
*/
const If = ( ...params ) => currentStack.If( ...params );
/**
* Represent a conditional node using switch/case statements.
*
* ```js
* Switch( value )
* .Case( 1, function )
* .Case( 2, 3, 4, function )
* .Default( function )
* ```
* @tsl
* @function
* @param {...any} params - The parameters for the conditional node.
* @returns {StackNode} The conditional node.
*/
const Switch = ( ...params ) => currentStack.Switch( ...params );
/**
* Add the given node to the current stack.
*
* @param {Node} node - The node to add.
* @returns {Node} The node that was added to the stack.
*/
function Stack( node ) {
if ( currentStack ) currentStack.addToStack( node );
return node;
}
addMethodChaining( 'toStack', Stack );
// types
const color = new ConvertType( 'color' );
const float = new ConvertType( 'float', cacheMaps.float );
const int = new ConvertType( 'int', cacheMaps.ints );
const uint = new ConvertType( 'uint', cacheMaps.uint );
const bool = new ConvertType( 'bool', cacheMaps.bool );
const vec2 = new ConvertType( 'vec2' );
const ivec2 = new ConvertType( 'ivec2' );
const uvec2 = new ConvertType( 'uvec2' );
const bvec2 = new ConvertType( 'bvec2' );
const vec3 = new ConvertType( 'vec3' );
const ivec3 = new ConvertType( 'ivec3' );
const uvec3 = new ConvertType( 'uvec3' );
const bvec3 = new ConvertType( 'bvec3' );
const vec4 = new ConvertType( 'vec4' );
const ivec4 = new ConvertType( 'ivec4' );
const uvec4 = new ConvertType( 'uvec4' );
const bvec4 = new ConvertType( 'bvec4' );
const mat2 = new ConvertType( 'mat2' );
const mat3 = new ConvertType( 'mat3' );
const mat4 = new ConvertType( 'mat4' );
addMethodChaining( 'toColor', color );
addMethodChaining( 'toFloat', float );
addMethodChaining( 'toInt', int );
addMethodChaining( 'toUint', uint );
addMethodChaining( 'toBool', bool );
addMethodChaining( 'toVec2', vec2 );
addMethodChaining( 'toIVec2', ivec2 );
addMethodChaining( 'toUVec2', uvec2 );
addMethodChaining( 'toBVec2', bvec2 );
addMethodChaining( 'toVec3', vec3 );
addMethodChaining( 'toIVec3', ivec3 );
addMethodChaining( 'toUVec3', uvec3 );
addMethodChaining( 'toBVec3', bvec3 );
addMethodChaining( 'toVec4', vec4 );
addMethodChaining( 'toIVec4', ivec4 );
addMethodChaining( 'toUVec4', uvec4 );
addMethodChaining( 'toBVec4', bvec4 );
addMethodChaining( 'toMat2', mat2 );
addMethodChaining( 'toMat3', mat3 );
addMethodChaining( 'toMat4', mat4 );
// basic nodes
const element = /*@__PURE__*/ nodeProxy( ArrayElementNode ).setParameterLength( 2 );
const convert = ( node, types ) => new ConvertNode( nodeObject( node ), types );
const split = ( node, channels ) => new SplitNode( nodeObject( node ), channels );
addMethodChaining( 'element', element );
addMethodChaining( 'convert', convert );
/**
* These node represents an assign operation. Meaning a node is assigned
* to another node.
*
* @augments Node
*/
class AssignNode extends Node {
static get type() {
return 'AssignNode';
}
/**
* Constructs a new assign node.
*
* @param {Node} targetNode - The target node.
* @param {Node} sourceNode - The source type.
*/
constructor( targetNode, sourceNode ) {
super();
/**
* The target node.
*
* @type {Node}
*/
this.targetNode = targetNode;
/**
* The source node.
*
* @type {Node}
*/
this.sourceNode = sourceNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isAssignNode = true;
}
/**
* Assignments must not be cached in temporary variables.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Always `false`.
*/
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( builder, output ) {
return output !== 'void' ? this.targetNode.getNodeType( builder ) : 'void';
}
/**
* Whether a split is required when assigning source to target. This can happen when the component length of
* target and source data type does not match.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether a split is required when assigning source to target.
*/
needsSplitAssign( builder ) {
const { targetNode } = this;
if ( builder.isAvailable( 'swizzleAssign' ) === false && targetNode.isSplitNode && targetNode.components.length > 1 ) {
const targetLength = builder.getTypeLength( targetNode.node.getNodeType( builder ) );
const assignDifferentVector = vectorComponents.join( '' ).slice( 0, targetLength ) !== targetNode.components;
return assignDifferentVector;
}
return false;
}
setup( builder ) {
const { targetNode, sourceNode } = this;
const scope = targetNode.getScope();
const scopeData = builder.getDataFromNode( scope );
scopeData.assign = true;
const properties = builder.getNodeProperties( this );
properties.sourceNode = sourceNode;
properties.targetNode = targetNode.context( { assign: true } );
}
generate( builder, output ) {
const { targetNode, sourceNode } = builder.getNodeProperties( this );
const needsSplitAssign = this.needsSplitAssign( builder );
const target = targetNode.build( builder );
const targetType = targetNode.getNodeType( builder );
const source = sourceNode.build( builder, targetType );
const sourceType = sourceNode.getNodeType( builder );
const nodeData = builder.getDataFromNode( this );
//
let snippet;
if ( nodeData.propertyName !== undefined ) {
if ( output !== 'void' ) {
snippet = target;
}
} else if ( needsSplitAssign ) {
const sourceVar = builder.getVarFromNode( this, null, targetType );
const sourceProperty = builder.getPropertyName( sourceVar );
builder.addLineFlowCode( `${ sourceProperty } = ${ source }`, this );
const splitNode = targetNode.node;
const splitTargetNode = splitNode.node.context( { assign: true } );
const targetRoot = splitTargetNode.build( builder );
for ( let i = 0; i < splitNode.components.length; i ++ ) {
const component = splitNode.components[ i ];
builder.addLineFlowCode( `${ targetRoot }.${ component } = ${ sourceProperty }[ ${ i } ]`, this );
}
if ( output !== 'void' ) {
snippet = target;
}
} else {
snippet = `${ target } = ${ source }`;
if ( output === 'void' || sourceType === 'void' ) {
builder.addLineFlowCode( snippet, this );
if ( output !== 'void' ) {
snippet = target;
}
}
}
// The value of an assignment is its target.
nodeData.propertyName = target;
return builder.format( snippet, targetType, output );
}
}
/**
* TSL function for creating an assign node.
*
* @tsl
* @function
* @param {Node} targetNode - The target node.
* @param {Node} sourceNode - The source type.
* @returns {AssignNode}
*/
const assign = /*@__PURE__*/ nodeProxy( AssignNode ).setParameterLength( 2 );
addMethodChaining( 'assign', assign );
/**
* ArrayNode represents a collection of nodes, typically created using the {@link array} function.
* ```js
* const colors = array( [
* vec3( 1, 0, 0 ),
* vec3( 0, 1, 0 ),
* vec3( 0, 0, 1 )
* ] );
*
* const redColor = tintColors.element( 0 );
* ```
*
* @augments Node
*/
class ArrayNode extends Node {
static get type() {
return 'ArrayNode';
}
/**
* Constructs a new array node.
*
* @param {?string} nodeType - The data type of the elements.
* @param {number} count - Size of the array.
* @param {?Array<Node>} [values=null] - Array default values.
*/
constructor( nodeType, count, values = null ) {
super( nodeType );
/**
* Array size.
*
* @type {number}
*/
this.count = count;
/**
* Array default values.
*
* @type {?Array<Node>}
*/
this.values = values;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isArrayNode = true;
}
/**
* Returns the number of elements in the node array.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {number} The number of elements in the node array.
*/
getArrayCount( /*builder*/ ) {
return this.count;
}
/**
* Returns the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
generateNodeType( builder ) {
if ( this.nodeType === null ) {
return this.values[ 0 ].getNodeType( builder );
}
return this.nodeType;
}
/**
* Returns the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
getElementType( builder ) {
return this.getNodeType( builder );
}
/**
* Returns the type of a member variable.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The name of the member variable.
* @return {string} The type of the member variable.
*/
getMemberType( builder, name ) {
if ( this.nodeType === null ) {
return this.values[ 0 ].getMemberType( builder, name );
}
return super.getMemberType( builder, name );
}
/**
* This method builds the output node and returns the resulting array as a shader string.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated shader string.
*/
generate( builder ) {
const type = this.getNodeType( builder );
return builder.generateArray( type, this.count, this.values );
}
}
/**
* TSL function for creating an array node.
*
* @tsl
* @function
* @param {string|Array<Node>} nodeTypeOrValues - A string representing the element type (e.g., 'vec3')
* or an array containing the default values (e.g., [ vec3() ]).
* @param {?number} [count] - Size of the array.
* @returns {ArrayNode}
*/
const array = ( ...params ) => {
let node;
if ( params.length === 1 ) {
const values = params[ 0 ].map( ( value ) => nodeObject( value ) );
node = new ArrayNode( null, values.length, values );
} else {
const nodeType = params[ 0 ];
const count = params[ 1 ];
node = new ArrayNode( nodeType, count );
}
return node;
};
addMethodChaining( 'toArray', ( node, count ) => array( Array( count ).fill( node ) ) );
/**
* This node can be used to group single instances of {@link UniformNode}
* and manage them as a uniform buffer.
*
* In most cases, the predefined nodes `objectGroup`, `renderGroup` and `frameGroup`
* will be used when defining the {@link UniformNode#groupNode} property.
*
* - `objectGroup`: Uniform buffer per object.
* - `renderGroup`: Shared uniform buffer, updated once per render call.
* - `frameGroup`: Shared uniform buffer, updated once per frame.
*
* @augments Node
*/
class UniformGroupNode extends Node {
static get type() {
return 'UniformGroupNode';
}
/**
* Constructs a new uniform group node.
*
* @param {string} name - The name of the uniform group node.
* @param {boolean} [shared=false] - Whether this uniform group node is shared or not.
* @param {number} [order=1] - Influences the internal sorting.
* @param {string|null} [updateType=null] - The update type of the uniform group node.
*/
constructor( name, shared = false, order = 1, updateType = null ) {
super( 'string' );
/**
* The name of the uniform group node.
*
* @type {string}
*/
this.name = name;
/**
* Whether this uniform group node is shared or not.
*
* @type {boolean}
* @default false
*/
this.shared = shared;
/**
* The sort key that determines the order of this group relative to other
* uniform groups. Groups with a lower order are placed first. Shared groups
* typically use a lower order so they precede regular per-object groups.
*
* @type {number}
* @default 1
*/
this.order = order;
/**
* The update type of the uniform group node.
*
* @type {string|null}
* @default null
*/
this.updateType = updateType;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isUniformGroup = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Marks the uniform group node as needing an update.
* This will trigger the necessary updates in the rendering process.
*/
update() {
this.needsUpdate = true;
}
/**
* Serializes the uniform group node to a JSON object.
*
* @param {Object} data - The object to store the serialized data.
*/
serialize( data ) {
super.serialize( data );
data.name = this.name;
data.version = this.version;
data.shared = this.shared;
}
/**
* Deserializes the uniform group node from a JSON object.
*
* @param {Object} data - The object containing the serialized data.
*/
deserialize( data ) {
super.deserialize( data );
this.name = data.name;
this.version = data.version;
this.shared = data.shared;
}
}
/**
* TSL function for creating a uniform group node with the given name.
*
* @tsl
* @function
* @param {string} name - The name of the uniform group node.
* @returns {UniformGroupNode}
*/
const uniformGroup = ( name, order = 1, updateType = null ) => new UniformGroupNode( name, false, order, updateType );
/**
* TSL function for creating a shared uniform group node with the given name and order.
*
* @tsl
* @function
* @param {string} name - The name of the uniform group node.
* @param {number} [order=0] - Influences the internal sorting.
* @returns {UniformGroupNode}
*/
const sharedUniformGroup = ( name, order = 0, updateType = null ) => new UniformGroupNode( name, true, order, updateType );
/**
* TSL object that represents a shared uniform group node which is updated once per frame.
*
* @tsl
* @type {UniformGroupNode}
*/
const frameGroup = /*@__PURE__*/ sharedUniformGroup( 'frame', 0, NodeUpdateType.FRAME );
/**
* TSL object that represents a shared uniform group node which is updated once per render.
*
* @tsl
* @type {UniformGroupNode}
*/
const renderGroup = /*@__PURE__*/ sharedUniformGroup( 'render', 0, NodeUpdateType.RENDER );
/**
* TSL object that represents a uniform group node which is updated once per object.
*
* @tsl
* @type {UniformGroupNode}
*/
const objectGroup = /*@__PURE__*/ uniformGroup( 'object', 1, NodeUpdateType.OBJECT );
/**
* Class for representing a uniform.
*
* @augments InputNode
*/
class UniformNode extends InputNode {
static get type() {
return 'UniformNode';
}
/**
* Constructs a new uniform node.
*
* @param {any} value - The value of this node. Usually a JS primitive or three.js object (vector, matrix, color, texture).
* @param {?string} nodeType - The node type. If no explicit type is defined, the node tries to derive the type from its value.
*/
constructor( value, nodeType = null ) {
super( value, nodeType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isUniformNode = true;
/**
* The name or label of the uniform.
*
* @type {string}
* @default ''
*/
this.name = '';
/**
* The uniform group of this uniform. By default, uniforms are
* managed per object but they might belong to a shared group
* which is updated per frame or render call.
*
* @type {UniformGroupNode}
*/
this.groupNode = objectGroup;
}
/**
* Sets the {@link UniformNode#name} property.
*
* @param {string} name - The name of the uniform.
* @return {UniformNode} A reference to this node.
*/
setName( name ) {
this.name = name;
return this;
}
/**
* Sets the {@link UniformNode#name} property.
*
* @deprecated
* @param {string} name - The name of the uniform.
* @return {UniformNode} A reference to this node.
*/
label( name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() ); // @deprecated r179
return this.setName( name );
}
/**
* Sets the {@link UniformNode#groupNode} property.
*
* @param {UniformGroupNode} group - The uniform group.
* @return {UniformNode} A reference to this node.
*/
setGroup( group ) {
this.groupNode = group;
return this;
}
/**
* Returns the {@link UniformNode#groupNode}.
*
* @return {UniformGroupNode} The uniform group.
*/
getGroup() {
return this.groupNode;
}
/**
* By default, this method returns the result of {@link Node#getHash} but derived
* classes might overwrite this method with a different implementation.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The uniform hash.
*/
getUniformHash( builder ) {
return this.getHash( builder );
}
/**
* Uniform nodes with the same hash share a single uniform. This method returns the node
* the shared uniform refers to which is the first node registered for the hash.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {UniformNode} The node the shared uniform refers to.
*/
getSharedNode( builder ) {
const hash = this.getUniformHash( builder );
let sharedNode = builder.getNodeFromHash( hash );
if ( sharedNode === undefined ) {
builder.setHashNode( this, hash );
sharedNode = this;
}
return sharedNode;
}
onUpdate( callback, updateType ) {
callback = callback.bind( this );
return super.onUpdate( ( frame ) => {
const value = callback( frame, this );
if ( value !== undefined ) {
this.value = value;
}
}, updateType );
}
getInputType( builder ) {
let type = super.getInputType( builder );
if ( type === 'bool' ) {
type = 'uint';
}
return type;
}
generate( builder, output ) {
const type = this.getNodeType( builder );
const sharedNode = this.getSharedNode( builder );
const sharedNodeType = sharedNode.getInputType( builder );
const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage, this.name || builder.context.nodeName );
const uniformName = builder.getPropertyName( nodeUniform );
if ( builder.context.nodeName !== undefined ) delete builder.context.nodeName;
//
let snippet = uniformName;
if ( type === 'bool' ) {
// cache to variable
const nodeData = builder.getDataFromNode( this );
let propertyName = nodeData.propertyName;
if ( propertyName === undefined ) {
const nodeVar = builder.getVarFromNode( this, null, 'bool' );
propertyName = builder.getPropertyName( nodeVar );
nodeData.propertyName = propertyName;
snippet = builder.format( uniformName, sharedNodeType, type );
builder.addLineFlowCode( `${ propertyName } = ${ snippet }`, this );
}
snippet = propertyName;
}
return builder.format( snippet, type, output );
}
}
/**
* TSL function for creating a uniform node.
*
* @tsl
* @function
* @param {any|string} value - The value of this uniform or your type. Usually a JS primitive or three.js object (vector, matrix, color, texture).
* @param {string} [type] - The node type. If no explicit type is defined, the node tries to derive the type from its value.
* @returns {UniformNode}
*/
const uniform = ( value, type ) => {
const nodeType = getConstNodeType( type || value );
if ( nodeType === value ) {
// if the value is a type but no having a value
value = getValueFromType( nodeType );
}
if ( value && value.isNode === true ) {
let v = value.value;
value.traverse( n => {
if ( n.isConstNode === true ) {
v = n.value;
}
} );
value = v;
}
return new UniformNode( value, nodeType );
};
/**
* This class represents a shader property. It can be used
* to explicitly define a property and assign a value to it.
*
* ```js
* const threshold = property( 'float', 'threshold' ).assign( THRESHOLD );
*```
* `PropertyNode` is used by the engine to predefined common material properties
* for TSL code.
*
* @augments Node
*/
class PropertyNode extends Node {
static get type() {
return 'PropertyNode';
}
/**
* Constructs a new property node.
*
* @param {string} nodeType - The type of the node.
* @param {?string} [name=null] - The name of the property in the shader.
* @param {boolean} [varying=false] - Whether this property is a varying or not.
* @param {?Node} [placeholderNode=null] - The placeholder node if not assigned.
*/
constructor( nodeType, name = null, varying = false, placeholderNode = null ) {
super( nodeType );
/**
* The name of the property in the shader. If no name is defined,
* the node system auto-generates one.
*
* @type {?string}
* @default null
*/
this.name = name;
/**
* Whether this property is a varying or not.
*
* @type {boolean}
* @default false
*/
this.varying = varying;
/**
* The placeholder node of the property if it is not assigned.
*
* @type {?Node}
* @default null
*/
this.placeholderNode = nodeObject( placeholderNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPropertyNode = true;
/**
* This flag is used for global cache.
*
* @type {boolean}
* @default true
*/
this.global = true;
}
isCacheable( /*builder*/ ) {
return false;
}
getNodeType( builder ) {
const nodeType = super.getNodeType( builder );
if ( nodeType === 'output' ) {
return builder.getOutputType();
}
return nodeType;
}
customCacheKey() {
return hashString( this.type + ':' + ( this.name || '' ) + ':' + ( this.varying ? '1' : '0' ) );
}
getHash( builder ) {
return this.name || super.getHash( builder );
}
generate( builder ) {
let nodeVar;
if ( this.varying === true ) {
nodeVar = builder.getVaryingFromNode( this, this.name );
nodeVar.needsInterpolation = true;
} else {
nodeVar = builder.getVarFromNode( this, this.name );
if ( this.placeholderNode !== null ) {
if ( builder.hasWriteUsage( this ) === false ) {
const snippet = this.placeholderNode.build( builder, this.getNodeType( builder ) );
builder.addLineFlowCode( `${ builder.getPropertyName( nodeVar ) } = ${ snippet }`, this );
}
}
}
return builder.getPropertyName( nodeVar );
}
}
/**
* TSL function for creating a property node.
*
* @tsl
* @function
* @param {string} type - The type of the node.
* @param {?string} [name=null] - The name of the property in the shader.
* @param {?Node} [placeholderNode=null] - The placeholder node if not assigned.
* @returns {PropertyNode}
*/
const property = ( type, name, placeholderNode = null ) => new PropertyNode( type, name, false, placeholderNode );
/**
* TSL function for creating a varying property node.
*
* @tsl
* @function
* @param {string} type - The type of the node.
* @param {?string} [name=null] - The name of the varying in the shader.
* @param {?Node} [placeholderNode=null] - The placeholder node if not assigned.
* @returns {PropertyNode}
*/
const varyingProperty = ( type, name, placeholderNode = null ) => new PropertyNode( type, name, true, placeholderNode );
/**
* TSL object that represents the shader variable `DiffuseColor`.
*
* @tsl
* @type {PropertyNode<vec4>}
*/
const diffuseColor = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec4', 'DiffuseColor' );
/**
* TSL object that represents the shader variable `DiffuseContribution`.
*
* @tsl
* @type {PropertyNode<vec3>}
*/
const diffuseContribution = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec3', 'DiffuseContribution' );
/**
* TSL object that represents the shader variable `DiffuseRoughness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const diffuseRoughness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'DiffuseRoughness' );
/**
* TSL object that represents the shader variable `EmissiveColor`.
*
* @tsl
* @type {PropertyNode<vec3>}
*/
const emissive = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec3', 'EmissiveColor' );
/**
* TSL object that represents the shader variable `Roughness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const roughness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Roughness' );
/**
* TSL object that represents the shader variable `Metalness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const metalness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Metalness' );
/**
* TSL object that represents the shader variable `Clearcoat`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const clearcoat = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Clearcoat' );
/**
* TSL object that represents the shader variable `ClearcoatRoughness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const clearcoatRoughness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'ClearcoatRoughness' );
/**
* TSL object that represents the shader variable `Sheen`.
*
* @tsl
* @type {PropertyNode<vec3>}
*/
const sheen = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec3', 'Sheen' );
/**
* TSL object that represents the shader variable `SheenRoughness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const sheenRoughness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'SheenRoughness' );
/**
* TSL object that represents the shader variable `Iridescence`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const iridescence = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Iridescence' );
/**
* TSL object that represents the shader variable `IridescenceIOR`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const iridescenceIOR = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'IridescenceIOR' );
/**
* TSL object that represents the shader variable `IridescenceThickness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const iridescenceThickness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'IridescenceThickness' );
/**
* TSL object that represents the shader variable `AlphaT`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const alphaT = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'AlphaT' );
/**
* TSL object that represents the shader variable `Anisotropy`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const anisotropy = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Anisotropy' );
/**
* TSL object that represents the shader variable `AnisotropyT`.
*
* @tsl
* @type {PropertyNode<vec3>}
*/
const anisotropyT = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec3', 'AnisotropyT' );
/**
* TSL object that represents the shader variable `AnisotropyB`.
*
* @tsl
* @type {PropertyNode<vec3>}
*/
const anisotropyB = /*@__PURE__*/ nodeImmutable( PropertyNode, 'vec3', 'AnisotropyB' );
/**
* TSL object that represents the shader variable `SpecularColor`.
*
* @tsl
* @type {PropertyNode<color>}
*/
const specularColor = /*@__PURE__*/ nodeImmutable( PropertyNode, 'color', 'SpecularColor' );
/**
* TSL object that represents the shader variable `SpecularColorBlended`.
*
* @tsl
* @type {PropertyNode<color>}
*/
const specularColorBlended = /*@__PURE__*/ nodeImmutable( PropertyNode, 'color', 'SpecularColorBlended' );
/**
* TSL object that represents the shader variable `SpecularF90`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const specularF90 = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'SpecularF90' );
/**
* TSL object that represents the shader variable `Shininess`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const shininess = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Shininess' );
/**
* TSL object that represents the shader variable `Output`.
*
* @tsl
* @type {PropertyNode<vec4>}
*/
const output = /*@__PURE__*/ nodeImmutable( PropertyNode, 'output', 'Output' );
/**
* TSL object that represents the shader variable `dashSize`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const dashSize = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'dashSize' );
/**
* TSL object that represents the shader variable `gapSize`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const gapSize = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'gapSize' );
/**
* TSL object that represents the shader variable `pointWidth`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const pointWidth = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'pointWidth' );
/**
* TSL object that represents the shader variable `IOR`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const ior = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'IOR' );
/**
* TSL object that represents the shader variable `Transmission`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const transmission = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Transmission' );
/**
* TSL object that represents the shader variable `Thickness`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const thickness = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Thickness' );
/**
* TSL object that represents the shader variable `AttenuationDistance`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const attenuationDistance = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'AttenuationDistance' );
/**
* TSL object that represents the shader variable `AttenuationColor`.
*
* @tsl
* @type {PropertyNode<color>}
*/
const attenuationColor = /*@__PURE__*/ nodeImmutable( PropertyNode, 'color', 'AttenuationColor' );
/**
* TSL object that represents the shader variable `Dispersion`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const dispersion = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Dispersion' );
/**
* TSL object that represents the shader variable `Retroreflectivity`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const retroreflectivity = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Retroreflectivity' );
/**
* TSL object that represents the shader variable `AmbientOcclusion`.
* If no value is assigned to this property, it defaults to a placeholder value of `1.0`.
*
* @tsl
* @type {PropertyNode<float>}
*/
const ambientOcclusion = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'AmbientOcclusion', false, 1 );
/**
* This module represents the call of a {@link FunctionNode}. Developers are usually not confronted
* with this module since they use the predefined TSL syntax `wgslFn` and `glslFn` which encapsulate
* this logic.
*
* @augments Node
*/
class FunctionCallNode extends Node {
static get type() {
return 'FunctionCallNode';
}
/**
* Constructs a new function call node.
*
* @param {?FunctionNode} functionNode - The function node.
* @param {Object<string, Node>} [parameters={}] - The parameters for the function call.
*/
constructor( functionNode = null, parameters = {} ) {
super();
/**
* The function node.
*
* @type {?FunctionNode}
* @default null
*/
this.functionNode = functionNode;
/**
* The parameters of the function call.
*
* @type {Object<string, Node>}
* @default {}
*/
this.parameters = parameters;
}
/**
* Sets the parameters of the function call node.
*
* @param {Object<string, Node>} parameters - The parameters to set.
* @return {FunctionCallNode} A reference to this node.
*/
setParameters( parameters ) {
this.parameters = parameters;
return this;
}
/**
* Returns the parameters of the function call node.
*
* @return {Object<string, Node>} The parameters of this node.
*/
getParameters() {
return this.parameters;
}
/**
* Returns the type of this function call node.
*
* @param {NodeBuilder} builder - The current node builder.
* @returns {string} The type of this node.
*/
generateNodeType( builder ) {
return this.functionNode.getNodeType( builder );
}
/**
* Returns the function node of this function call node.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} [name] - The name of the member.
* @returns {string} The type of the member.
*/
getMemberType( builder, name ) {
return this.functionNode.getMemberType( builder, name );
}
generate( builder ) {
const params = [];
const functionNode = this.functionNode;
const inputs = functionNode.getInputs( builder );
const parameters = this.parameters;
const generateInput = ( node, inputNode ) => {
const type = inputNode.type;
const pointer = type === 'pointer';
let output;
if ( pointer ) output = '&' + node.build( builder );
else output = node.build( builder, type );
return output;
};
if ( Array.isArray( parameters ) ) {
if ( parameters.length > inputs.length ) {
error( 'TSL: The number of provided parameters exceeds the expected number of inputs in \'Fn()\'.' );
parameters.length = inputs.length;
} else if ( parameters.length < inputs.length ) {
error( 'TSL: The number of provided parameters is less than the expected number of inputs in \'Fn()\'.' );
while ( parameters.length < inputs.length ) {
parameters.push( float( 0 ) );
}
}
for ( let i = 0; i < parameters.length; i ++ ) {
params.push( generateInput( parameters[ i ], inputs[ i ] ) );
}
} else {
for ( const inputNode of inputs ) {
const node = parameters[ inputNode.name ];
if ( node !== undefined ) {
params.push( generateInput( node, inputNode ) );
} else {
error( `TSL: Input '${ inputNode.name }' not found in \'Fn()\'.` );
params.push( generateInput( float( 0 ), inputNode ) );
}
}
}
const functionName = functionNode.build( builder, 'property' );
return `${ functionName }( ${ params.join( ', ' ) } )`;
}
}
const call = ( func, ...params ) => {
params = params.length > 1 || ( params[ 0 ] && params[ 0 ].isNode === true ) ? nodeArray( params ) : nodeObjects( params[ 0 ] );
return new FunctionCallNode( nodeObject( func ), params );
};
addMethodChaining( 'call', call );
const _vectorOperators = {
'==': 'equal',
'!=': 'notEqual',
'<': 'lessThan',
'>': 'greaterThan',
'<=': 'lessThanEqual',
'>=': 'greaterThanEqual',
'%': 'mod'
};
/**
* This node represents basic mathematical and logical operations like addition,
* subtraction or comparisons (e.g. `equal()`).
*
* @augments Node
*/
class OperatorNode extends Node {
static get type() {
return 'OperatorNode';
}
/**
* Constructs a new operator node.
*
* @param {string} op - The operator.
* @param {Node} aNode - The first input.
* @param {Node} bNode - The second input.
* @param {...Node} params - Additional input parameters.
*/
constructor( op, aNode, bNode, ...params ) {
super();
if ( params.length > 0 ) {
let finalOp = new OperatorNode( op, aNode, bNode );
for ( let i = 0; i < params.length - 1; i ++ ) {
finalOp = new OperatorNode( op, finalOp, params[ i ] );
}
aNode = finalOp;
bNode = params[ params.length - 1 ];
}
/**
* The operator.
*
* @type {string}
*/
this.op = op;
/**
* The first input.
*
* @type {Node}
*/
this.aNode = aNode;
/**
* The second input.
*
* @type {Node}
*/
this.bNode = bNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOperatorNode = true;
}
/**
* Returns the operator method name.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} output - The output type.
* @returns {string} The operator method name.
*/
getOperatorMethod( builder, output ) {
return builder.getMethod( _vectorOperators[ this.op ], output );
}
/**
* This method is overwritten since the node type is inferred from the operator
* and the input node types.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?string} [output=null] - The output type.
* @return {string} The node type.
*/
generateNodeType( builder, output = null ) {
const op = this.op;
const aNode = this.aNode;
const bNode = this.bNode;
const typeA = aNode.getNodeType( builder );
const typeB = bNode ? bNode.getNodeType( builder ) : null;
if ( typeA === 'void' || typeB === 'void' ) {
return output || 'void';
} else if ( op === '~' || op === '&' || op === '|' || op === '^' || op === '>>' || op === '<<' ) {
return builder.getIntegerType( typeA );
} else if ( op === '&&' || op === '||' || op === '^^' ) {
return 'bool';
} else if ( op === '!' ) {
const typeLength = builder.getTypeLength( typeA );
return typeLength > 1 ? `bvec${ typeLength }` : 'bool';
} else if ( op === '==' || op === '!=' || op === '<' || op === '>' || op === '<=' || op === '>=' ) {
const typeLength = Math.max( builder.getTypeLength( typeA ), builder.getTypeLength( typeB ) );
return typeLength > 1 ? `bvec${ typeLength }` : 'bool';
} else if ( typeB === null ) {
return typeA;
} else {
// Handle matrix operations
if ( builder.isMatrix( typeA ) ) {
if ( builder.isScalar( typeB ) ) {
return typeA; // matrix * scalar = matrix
} else if ( builder.isVector( typeB ) ) {
return builder.getVectorFromMatrix( typeA ); // matrix * vector
} else if ( builder.isMatrix( typeB ) ) {
return typeA; // matrix * matrix
}
} else if ( builder.isMatrix( typeB ) ) {
if ( builder.isScalar( typeA ) ) {
return typeB; // scalar * matrix = matrix
} else if ( builder.isVector( typeA ) ) {
return builder.getVectorFromMatrix( typeB ); // vector * matrix
}
}
// Handle non-matrix cases
// anytype x anytype: use the greater length vector
const type = builder.getTypeLength( typeB ) > builder.getTypeLength( typeA ) ? typeB : typeA;
const promotedType = builder.getPromotedComponentType( aNode, bNode );
return builder.changeComponentType( type, promotedType );
}
}
generate( builder, output ) {
const op = this.op;
const { aNode, bNode } = this;
const type = this.getNodeType( builder, output );
let typeA = null;
let typeB = null;
if ( type !== 'void' ) {
typeA = aNode.getNodeType( builder );
typeB = bNode ? bNode.getNodeType( builder ) : null;
if ( op === '<' || op === '>' || op === '<=' || op === '>=' || op === '==' || op === '!=' ) {
const length = Math.max( builder.getTypeLength( typeA ), builder.getTypeLength( typeB ) );
typeA = typeB = builder.getTypeFromLength( length, builder.getPromotedComponentType( aNode, bNode ) );
} else if ( op === '>>' || op === '<<' ) {
typeA = type;
typeB = builder.changeComponentType( typeB, 'uint' );
} else if ( builder.isMatrix( typeA ) ) {
if ( builder.isScalar( typeB ) ) {
// Keep matrix type for typeA, but ensure typeB stays float
typeB = 'float';
} else if ( builder.isVector( typeB ) ) {
// matrix x vector
typeB = builder.getVectorFromMatrix( typeA );
} else if ( builder.isMatrix( typeB ) ) ; else {
typeA = typeB = type;
}
} else if ( builder.isMatrix( typeB ) ) {
if ( builder.isScalar( typeA ) ) {
// Keep matrix type for typeB, but ensure typeA stays float
typeA = 'float';
} else if ( builder.isVector( typeA ) ) {
// vector x matrix
typeA = builder.getVectorFromMatrix( typeB );
} else {
typeA = typeB = type;
}
} else {
// anytype x anytype
typeA = typeB = type;
}
} else {
typeA = typeB = type;
}
const a = aNode.build( builder, typeA );
const b = bNode ? bNode.build( builder, typeB ) : null;
const fnOpSnippet = builder.getFunctionOperator( op );
if ( output !== 'void' ) {
const isGLSL = builder.renderer.coordinateSystem === WebGLCoordinateSystem;
if ( op === '==' || op === '!=' || op === '<' || op === '>' || op === '<=' || op === '>=' ) {
if ( isGLSL ) {
if ( builder.isVector( typeA ) ) {
return builder.format( `${ this.getOperatorMethod( builder, output ) }( ${ a }, ${ b } )`, type, output );
} else {
return builder.format( `( ${ a } ${ op } ${ b } )`, type, output );
}
} else {
// WGSL
return builder.format( `( ${ a } ${ op } ${ b } )`, type, output );
}
} else if ( op === '%' ) {
if ( builder.isInteger( typeB ) ) {
return builder.format( `( ${ a } % ${ b } )`, type, output );
} else {
return builder.format( `${ this.getOperatorMethod( builder, type ) }( ${ a }, ${ b } )`, type, output );
}
} else if ( op === '!' ) {
if ( isGLSL && builder.isVector( typeA ) ) {
return builder.format( `not( ${a} )`, output );
} else {
// WGSL and scalars on GLSL
return builder.format( `( ${op} ${a} )`, typeA, output );
}
} else if ( op === '~' ) {
return builder.format( `( ${op} ${a} )`, typeA, output );
} else if ( fnOpSnippet ) {
return builder.format( `${ fnOpSnippet }( ${ a }, ${ b } )`, type, output );
} else {
// Handle matrix operations
if ( builder.isMatrix( typeA ) && builder.isScalar( typeB ) ) {
return builder.format( `( ${ b } ${ op } ${ a } )`, type, output );
} else if ( builder.isScalar( typeA ) && builder.isMatrix( typeB ) ) {
return builder.format( `${ a } ${ op } ${ b }`, type, output );
} else {
let snippet = `( ${ a } ${ op } ${ b } )`;
if ( ! isGLSL && type === 'bool' && builder.isVector( typeA ) && builder.isVector( typeB ) ) {
snippet = `all${ snippet }`;
}
return builder.format( snippet, type, output );
}
}
} else if ( typeA !== 'void' ) {
if ( fnOpSnippet ) {
return builder.format( `${ fnOpSnippet }( ${ a }, ${ b } )`, type, output );
} else {
if ( builder.isMatrix( typeA ) && builder.isScalar( typeB ) ) {
return builder.format( `${ b } ${ op } ${ a }`, type, output );
} else {
return builder.format( `${ a } ${ op } ${ b }`, type, output );
}
}
}
}
serialize( data ) {
super.serialize( data );
data.op = this.op;
}
deserialize( data ) {
super.deserialize( data );
this.op = data.op;
}
}
/**
* Returns the addition of two or more value.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @param {...Node} params - Additional input parameters.
* @returns {OperatorNode}
*/
const add = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '+' ).setParameterLength( 2, Infinity ).setName( 'add' );
/**
* Returns the subtraction of two or more value.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @param {...Node} params - Additional input parameters.
* @returns {OperatorNode}
*/
const sub = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '-' ).setParameterLength( 2, Infinity ).setName( 'sub' );
/**
* Returns the multiplication of two or more value.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @param {...Node} params - Additional input parameters.
* @returns {OperatorNode}
*/
const mul = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '*' ).setParameterLength( 2, Infinity ).setName( 'mul' );
/**
* Returns the division of two or more value.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @param {...Node} params - Additional input parameters.
* @returns {OperatorNode}
*/
const div = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '/' ).setParameterLength( 2, Infinity ).setName( 'div' );
/**
* Computes the remainder of dividing the first node by the second one.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const mod = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '%' ).setParameterLength( 2 ).setName( 'mod' );
/**
* Checks if two nodes are equal.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const equal = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '==' ).setParameterLength( 2 ).setName( 'equal' );
/**
* Checks if two nodes are not equal.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const notEqual = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '!=' ).setParameterLength( 2 ).setName( 'notEqual' );
/**
* Checks if the first node is less than the second.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const lessThan = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '<' ).setParameterLength( 2 ).setName( 'lessThan' );
/**
* Checks if the first node is greater than the second.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const greaterThan = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '>' ).setParameterLength( 2 ).setName( 'greaterThan' );
/**
* Checks if the first node is less than or equal to the second.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const lessThanEqual = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '<=' ).setParameterLength( 2 ).setName( 'lessThanEqual' );
/**
* Checks if the first node is greater than or equal to the second.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const greaterThanEqual = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '>=' ).setParameterLength( 2 ).setName( 'greaterThanEqual' );
/**
* Performs a logical AND operation on multiple nodes.
*
* @tsl
* @function
* @param {...Node} nodes - The input nodes to be combined using AND.
* @returns {OperatorNode}
*/
const and = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '&&' ).setParameterLength( 2, Infinity ).setName( 'and' );
/**
* Performs a logical OR operation on multiple nodes.
*
* @tsl
* @function
* @param {...Node} nodes - The input nodes to be combined using OR.
* @returns {OperatorNode}
*/
const or = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '||' ).setParameterLength( 2, Infinity ).setName( 'or' );
/**
* Performs logical NOT on a node.
*
* @tsl
* @function
* @param {Node} value - The value.
* @returns {OperatorNode}
*/
const not = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '!' ).setParameterLength( 1 ).setName( 'not' );
/**
* Performs logical XOR on two nodes.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const xor = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '^^' ).setParameterLength( 2 ).setName( 'xor' );
/**
* Performs bitwise AND on two nodes.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const bitAnd = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '&' ).setParameterLength( 2 ).setName( 'bitAnd' );
/**
* Performs bitwise NOT on a node.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const bitNot = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '~' ).setParameterLength( 1 ).setName( 'bitNot' );
/**
* Performs bitwise OR on two nodes.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const bitOr = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '|' ).setParameterLength( 2 ).setName( 'bitOr' );
/**
* Performs bitwise XOR on two nodes.
*
* @tsl
* @function
* @param {Node} a - The first input.
* @param {Node} b - The second input.
* @returns {OperatorNode}
*/
const bitXor = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '^' ).setParameterLength( 2 ).setName( 'bitXor' );
/**
* Shifts a node to the left.
*
* @tsl
* @function
* @param {Node} a - The node to shift.
* @param {Node} b - The value to shift.
* @returns {OperatorNode}
*/
const shiftLeft = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '<<' ).setParameterLength( 2 ).setName( 'shiftLeft' );
/**
* Shifts a node to the right.
*
* @tsl
* @function
* @param {Node} a - The node to shift.
* @param {Node} b - The value to shift.
* @returns {OperatorNode}
*/
const shiftRight = /*@__PURE__*/ nodeProxyAssignable( OperatorNode, '>>' ).setParameterLength( 2 ).setName( 'shiftRight' );
/**
* Increments a node by 1.
*
* @tsl
* @function
* @param {Node} a - The node to increment.
* @returns {OperatorNode}
*/
const incrementBefore = /*@__PURE__*/ Fn( ( [ a ] ) => {
a.addAssign( 1 );
return a;
} );
/**
* Decrements a node by 1.
*
* @tsl
* @function
* @param {Node} a - The node to decrement.
* @returns {OperatorNode}
*/
const decrementBefore = /*@__PURE__*/ Fn( ( [ a ] ) => {
a.subAssign( 1 );
return a;
} );
/**
* Increments a node by 1 and returns the previous value.
*
* @tsl
* @function
* @param {Node} a - The node to increment.
* @returns {OperatorNode}
*/
const increment = /*@__PURE__*/ Fn( ( [ a ] ) => {
const temp = int( a ).toConst();
a.addAssign( 1 );
return temp;
} );
/**
* Decrements a node by 1 and returns the previous value.
*
* @tsl
* @function
* @param {Node} a - The node to decrement.
* @returns {OperatorNode}
*/
const decrement = /*@__PURE__*/ Fn( ( [ a ] ) => {
const temp = int( a ).toConst();
a.subAssign( 1 );
return temp;
} );
addMethodChaining( 'add', add );
addMethodChaining( 'sub', sub );
addMethodChaining( 'mul', mul );
addMethodChaining( 'div', div );
addMethodChaining( 'mod', mod );
addMethodChaining( 'equal', equal );
addMethodChaining( 'notEqual', notEqual );
addMethodChaining( 'lessThan', lessThan );
addMethodChaining( 'greaterThan', greaterThan );
addMethodChaining( 'lessThanEqual', lessThanEqual );
addMethodChaining( 'greaterThanEqual', greaterThanEqual );
addMethodChaining( 'and', and );
addMethodChaining( 'or', or );
addMethodChaining( 'not', not );
addMethodChaining( 'xor', xor );
addMethodChaining( 'bitAnd', bitAnd );
addMethodChaining( 'bitNot', bitNot );
addMethodChaining( 'bitOr', bitOr );
addMethodChaining( 'bitXor', bitXor );
addMethodChaining( 'shiftLeft', shiftLeft );
addMethodChaining( 'shiftRight', shiftRight );
addMethodChaining( 'incrementBefore', incrementBefore );
addMethodChaining( 'decrementBefore', decrementBefore );
addMethodChaining( 'increment', increment );
addMethodChaining( 'decrement', decrement );
/**
* This node represents a variety of mathematical methods available in shaders.
* They are divided into three categories:
*
* - Methods with one input like `sin`, `cos` or `normalize`.
* - Methods with two inputs like `dot`, `cross` or `pow`.
* - Methods with three inputs like `mix`, `clamp` or `smoothstep`.
*
* @augments Node
*/
class MathNode extends Node {
static get type() {
return 'MathNode';
}
/**
* Constructs a new math node.
*
* @param {string} method - The method name.
* @param {Node} aNode - The first input.
* @param {?Node} [bNode=null] - The second input.
* @param {?Node} [cNode=null] - The third input.
*/
constructor( method, aNode, bNode = null, cNode = null ) {
super();
// Allow the max() and min() functions to take an arbitrary number of arguments.
if ( ( method === MathNode.MAX || method === MathNode.MIN ) && arguments.length > 3 ) {
let finalOp = new MathNode( method, aNode, bNode );
for ( let i = 3; i < arguments.length - 1; i ++ ) {
finalOp = new MathNode( method, finalOp, arguments[ i ] );
}
aNode = finalOp;
bNode = arguments[ arguments.length - 1 ];
cNode = null;
}
/**
* The method name.
*
* @type {string}
*/
this.method = method;
/**
* The first input.
*
* @type {Node}
*/
this.aNode = aNode;
/**
* The second input.
*
* @type {?Node}
* @default null
*/
this.bNode = bNode;
/**
* The third input.
*
* @type {?Node}
* @default null
*/
this.cNode = cNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMathNode = true;
}
/**
* The input type is inferred from the node types of the input nodes.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( builder ) {
const aType = this.aNode.getNodeType( builder );
const bType = this.bNode ? this.bNode.getNodeType( builder ) : null;
const cType = this.cNode ? this.cNode.getNodeType( builder ) : null;
const aLen = builder.isMatrix( aType ) ? 0 : builder.getTypeLength( aType );
const bLen = builder.isMatrix( bType ) ? 0 : builder.getTypeLength( bType );
const cLen = builder.isMatrix( cType ) ? 0 : builder.getTypeLength( cType );
let type;
if ( aLen > bLen && aLen > cLen ) {
type = aType;
} else if ( bLen > cLen ) {
type = bType;
} else if ( cLen > aLen ) {
type = cType;
} else {
type = aType;
}
if ( builder.isMatrix( type ) ) return type;
const promotedType = _floatMethods.has( this.method ) ? 'float' : builder.getPromotedComponentType( this.aNode, this.bNode, this.cNode );
return builder.changeComponentType( type, promotedType );
}
/**
* The selected method as well as the input type determine the node type of this node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
const method = this.method;
if ( method === MathNode.LENGTH || method === MathNode.DISTANCE || method === MathNode.DOT || method === MathNode.DETERMINANT ) {
return 'float';
} else if ( method === MathNode.CROSS ) {
return 'vec3';
} else if ( method === MathNode.ALL || method === MathNode.ANY ) {
return 'bool';
} else if ( method === MathNode.EQUALS ) {
return builder.changeComponentType( this.aNode.getNodeType( builder ), 'bool' );
} else {
return this.getInputType( builder );
}
}
setup( builder ) {
const { aNode, bNode, method } = this;
let outputNode = null;
if ( method === MathNode.ONE_MINUS ) {
outputNode = sub( 1.0, aNode );
} else if ( method === MathNode.RECIPROCAL ) {
outputNode = div( float( 1 ), aNode );
} else if ( method === MathNode.DIFFERENCE ) {
outputNode = abs( sub( aNode, bNode ) );
} else if ( method === MathNode.TRANSFORM_DIRECTION ) {
// pre-multiplies the direction by the matrix and normalizes the result
let matrixNode, directionNode;
if ( builder.isMatrix( aNode.getNodeType( builder ) ) ) {
matrixNode = aNode;
directionNode = bNode;
} else {
matrixNode = bNode;
directionNode = aNode;
}
outputNode = normalize( mul( matrixNode, vec4( vec3( directionNode ), 0.0 ) ).xyz );
}
if ( outputNode !== null ) {
return outputNode;
} else {
return super.setup( builder );
}
}
generate( builder, output ) {
const properties = builder.getNodeProperties( this );
if ( properties.outputNode ) {
return super.generate( builder, output );
}
let method = this.method;
const type = this.getNodeType( builder );
const inputType = this.getInputType( builder );
const a = this.aNode;
const b = this.bNode;
const c = this.cNode;
const coordinateSystem = builder.renderer.coordinateSystem;
if ( method === MathNode.NEGATE ) {
return builder.format( '( - ' + a.build( builder, inputType ) + ' )', type, output );
} else {
const params = [];
if ( method === MathNode.CROSS ) {
params.push(
a.build( builder, type ),
b.build( builder, type )
);
} else if ( coordinateSystem === WebGLCoordinateSystem && method === MathNode.STEP ) {
params.push(
a.build( builder, builder.getTypeLength( a.getNodeType( builder ) ) === 1 ? 'float' : inputType ),
b.build( builder, inputType )
);
} else if ( coordinateSystem === WebGLCoordinateSystem && ( method === MathNode.MIN || method === MathNode.MAX ) ) {
params.push(
a.build( builder, inputType ),
b.build( builder, builder.getTypeLength( b.getNodeType( builder ) ) === 1 ? builder.getComponentType( inputType ) : inputType )
);
} else if ( method === MathNode.REFRACT ) {
params.push(
a.build( builder, inputType ),
b.build( builder, inputType ),
c.build( builder, 'float' )
);
} else if ( method === MathNode.MIX ) {
params.push(
a.build( builder, inputType ),
b.build( builder, inputType ),
c.build( builder, builder.getTypeLength( c.getNodeType( builder ) ) === 1 ? 'float' : inputType )
);
} else {
if ( coordinateSystem === WebGPUCoordinateSystem && method === MathNode.ATAN && b !== null ) {
method = 'atan2';
}
if ( builder.shaderStage !== 'fragment' && ( method === MathNode.DFDX || method === MathNode.DFDY ) ) {
error( `TSL: '${ method }' is not supported in the ${ builder.shaderStage } stage.`, this.stackTrace );
method = '/*' + method + '*/';
}
params.push( a.build( builder, inputType ) );
if ( b !== null ) params.push( b.build( builder, inputType ) );
if ( c !== null ) params.push( c.build( builder, inputType ) );
}
return builder.format( `${ builder.getMethod( method, type ) }( ${params.join( ', ' )} )`, type, output );
}
}
serialize( data ) {
super.serialize( data );
data.method = this.method;
}
deserialize( data ) {
super.deserialize( data );
this.method = data.method;
}
}
// 1 input
MathNode.ALL = 'all';
MathNode.ANY = 'any';
MathNode.RADIANS = 'radians';
MathNode.DEGREES = 'degrees';
MathNode.EXP = 'exp';
MathNode.EXP2 = 'exp2';
MathNode.LOG = 'log';
MathNode.LOG2 = 'log2';
MathNode.SQRT = 'sqrt';
MathNode.INVERSE_SQRT = 'inversesqrt';
MathNode.FLOOR = 'floor';
MathNode.CEIL = 'ceil';
MathNode.NORMALIZE = 'normalize';
MathNode.FRACT = 'fract';
MathNode.SIN = 'sin';
MathNode.SINH = 'sinh';
MathNode.COS = 'cos';
MathNode.COSH = 'cosh';
MathNode.TAN = 'tan';
MathNode.TANH = 'tanh';
MathNode.ASIN = 'asin';
MathNode.ASINH = 'asinh';
MathNode.ACOS = 'acos';
MathNode.ACOSH = 'acosh';
MathNode.ATAN = 'atan';
MathNode.ATANH = 'atanh';
MathNode.ABS = 'abs';
MathNode.SIGN = 'sign';
MathNode.LENGTH = 'length';
MathNode.NEGATE = 'negate';
MathNode.ONE_MINUS = 'oneMinus';
MathNode.DFDX = 'dFdx';
MathNode.DFDY = 'dFdy';
MathNode.ROUND = 'round';
MathNode.RECIPROCAL = 'reciprocal';
MathNode.TRUNC = 'trunc';
MathNode.FWIDTH = 'fwidth';
MathNode.TRANSPOSE = 'transpose';
MathNode.DETERMINANT = 'determinant';
MathNode.INVERSE = 'inverse';
// 2 inputs
MathNode.EQUALS = 'equals';
MathNode.MIN = 'min';
MathNode.MAX = 'max';
MathNode.STEP = 'step';
MathNode.REFLECT = 'reflect';
MathNode.DISTANCE = 'distance';
MathNode.DIFFERENCE = 'difference';
MathNode.DOT = 'dot';
MathNode.CROSS = 'cross';
MathNode.POW = 'pow';
MathNode.TRANSFORM_DIRECTION = 'transformDirection';
// 3 inputs
MathNode.MIX = 'mix';
MathNode.CLAMP = 'clamp';
MathNode.REFRACT = 'refract';
MathNode.SMOOTHSTEP = 'smoothstep';
MathNode.FACEFORWARD = 'faceforward';
// Methods that are only defined for floating-point types.
const _floatMethods = new Set( [
MathNode.RADIANS, MathNode.DEGREES, MathNode.EXP, MathNode.EXP2, MathNode.LOG, MathNode.LOG2,
MathNode.SQRT, MathNode.INVERSE_SQRT, MathNode.FLOOR, MathNode.CEIL, MathNode.NORMALIZE, MathNode.FRACT,
MathNode.SIN, MathNode.SINH, MathNode.COS, MathNode.COSH, MathNode.TAN, MathNode.TANH,
MathNode.ASIN, MathNode.ASINH, MathNode.ACOS, MathNode.ACOSH, MathNode.ATAN, MathNode.ATANH,
MathNode.LENGTH, MathNode.DFDX, MathNode.DFDY, MathNode.ROUND, MathNode.TRUNC, MathNode.FWIDTH, MathNode.RECIPROCAL,
MathNode.STEP, MathNode.REFLECT, MathNode.DISTANCE, MathNode.DOT, MathNode.CROSS, MathNode.POW,
MathNode.MIX, MathNode.REFRACT, MathNode.SMOOTHSTEP, MathNode.FACEFORWARD
] );
// 1 inputs
/**
* A small value used to handle floating-point precision errors.
*
* @tsl
* @type {Node<float>}
*/
const EPSILON = /*@__PURE__*/ float( 1e-6 );
/**
* Represents infinity.
*
* @tsl
* @type {Node<float>}
*/
const INFINITY = /*@__PURE__*/ float( 1e6 );
/**
* Represents PI.
*
* @tsl
* @type {Node<float>}
*/
const PI = /*@__PURE__*/ float( Math.PI );
/**
* Represents PI * 2. Please use the non-deprecated version `TWO_PI`.
*
* @tsl
* @deprecated
* @type {Node<float>}
*/
const PI2 = /*@__PURE__*/ float( Math.PI * 2 ); // @deprecated r181
/**
* Represents PI * 2.
*
* @tsl
* @type {Node<float>}
*/
const TWO_PI = /*@__PURE__*/ float( Math.PI * 2 );
/**
* Represents PI / 2.
*
* @tsl
* @type {Node<float>}
*/
const HALF_PI = /*@__PURE__*/ float( Math.PI * 0.5 );
/**
* Returns `true` if all components of `x` are `true`.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node<bool>}
*/
const all = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ALL ).setParameterLength( 1 );
/**
* Returns `true` if any components of `x` are `true`.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node<bool>}
*/
const any = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ANY ).setParameterLength( 1 );
/**
* Converts a quantity in degrees to radians.
*
* @tsl
* @function
* @param {Node | number} x - The input in degrees.
* @returns {Node}
*/
const radians = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.RADIANS ).setParameterLength( 1 );
/**
* Convert a quantity in radians to degrees.
*
* @tsl
* @function
* @param {Node | number} x - The input in radians.
* @returns {Node}
*/
const degrees = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DEGREES ).setParameterLength( 1 );
/**
* Returns the natural exponentiation of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const exp = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.EXP ).setParameterLength( 1 );
/**
* Returns 2 raised to the power of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const exp2 = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.EXP2 ).setParameterLength( 1 );
/**
* Returns the natural logarithm of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const log = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.LOG ).setParameterLength( 1 );
/**
* Returns the base 2 logarithm of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const log2 = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.LOG2 ).setParameterLength( 1 );
/**
* Returns the square root of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const sqrt = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.SQRT ).setParameterLength( 1 );
/**
* Returns the inverse of the square root of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const inverseSqrt = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.INVERSE_SQRT ).setParameterLength( 1 );
/**
* Finds the nearest integer less than or equal to the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const floor = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.FLOOR ).setParameterLength( 1 );
/**
* Finds the nearest integer that is greater than or equal to the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const ceil = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.CEIL ).setParameterLength( 1 );
/**
* Calculates the unit vector in the same direction as the original vector.
*
* @tsl
* @function
* @param {Node} x - The input vector.
* @returns {Node}
*/
const normalize = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.NORMALIZE ).setParameterLength( 1 );
/**
* Computes the fractional part of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const fract = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.FRACT ).setParameterLength( 1 );
/**
* Returns the sine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const sin = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.SIN ).setParameterLength( 1 );
/**
* Returns the hyperbolic sine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const sinh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.SINH ).setParameterLength( 1 );
/**
* Returns the cosine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const cos = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.COS ).setParameterLength( 1 );
/**
* Returns the hyperbolic cosine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const cosh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.COSH ).setParameterLength( 1 );
/**
* Returns the tangent of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const tan = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.TAN ).setParameterLength( 1 );
/**
* Returns the hyperbolic tangent of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const tanh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.TANH ).setParameterLength( 1 );
/**
* Returns the arcsine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const asin = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ASIN ).setParameterLength( 1 );
/**
* Returns the inverse hyperbolic sine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const asinh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ASINH ).setParameterLength( 1 );
/**
* Returns the arccosine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const acos = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ACOS ).setParameterLength( 1 );
/**
* Returns the inverse hyperbolic cosine of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const acosh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ACOSH ).setParameterLength( 1 );
/**
* Returns the arc-tangent of the parameter.
* If two parameters are provided, the result is `atan2(y/x)`.
*
* @tsl
* @function
* @param {Node | number} y - The y parameter.
* @param {?(Node | number)} x - The x parameter.
* @returns {Node}
*/
const atan = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ATAN ).setParameterLength( 1, 2 );
/**
* Returns the inverse hyperbolic tangent of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const atanh = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ATANH ).setParameterLength( 1 );
/**
* Returns the absolute value of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const abs = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ABS ).setParameterLength( 1 );
/**
* Extracts the sign of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const sign = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.SIGN ).setParameterLength( 1 );
/**
* Calculates the length of a vector.
*
* @tsl
* @function
* @param {Node} x - The parameter.
* @returns {Node<float>}
*/
const length = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.LENGTH ).setParameterLength( 1 );
/**
* Negates the value of the parameter (-x).
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const negate = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.NEGATE ).setParameterLength( 1 );
/**
* Return `1` minus the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const oneMinus = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ONE_MINUS ).setParameterLength( 1 );
/**
* Returns the partial derivative of the parameter with respect to x.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const dFdx = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DFDX ).setParameterLength( 1 );
/**
* Returns the partial derivative of the parameter with respect to y.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const dFdy = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DFDY ).setParameterLength( 1 );
/**
* Rounds the parameter to the nearest integer.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const round = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.ROUND ).setParameterLength( 1 );
/**
* Returns the reciprocal of the parameter `(1/x)`.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const reciprocal = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.RECIPROCAL ).setParameterLength( 1 );
/**
* Truncates the parameter, removing the fractional part.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const trunc = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.TRUNC ).setParameterLength( 1 );
/**
* Returns the sum of the absolute derivatives in x and y.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @returns {Node}
*/
const fwidth = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.FWIDTH ).setParameterLength( 1 );
/**
* Returns the transpose of a matrix.
*
* @tsl
* @function
* @param {Node<mat2|mat3|mat4>} x - The parameter.
* @returns {Node}
*/
const transpose = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.TRANSPOSE ).setParameterLength( 1 );
/**
* Returns the determinant of a matrix.
*
* @tsl
* @function
* @param {Node<mat2|mat3|mat4>} x - The parameter.
* @returns {Node<float>}
*/
const determinant = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DETERMINANT ).setParameterLength( 1 );
/**
* Returns the inverse of a matrix.
*
* @tsl
* @function
* @param {Node<mat2|mat3|mat4>} x - The parameter.
* @returns {Node<mat2|mat3|mat4>}
*/
const inverse = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.INVERSE ).setParameterLength( 1 );
// 2 inputs
/**
* Returns the least of the given values.
*
* @tsl
* @function
* @param {...(Node | number)} values - The values to compare.
* @returns {Node}
*/
const min = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.MIN ).setParameterLength( 2, Infinity );
/**
* Returns the greatest of the given values.
*
* @tsl
* @function
* @param {...(Node | number)} values - The values to compare.
* @returns {Node}
*/
const max = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.MAX ).setParameterLength( 2, Infinity );
/**
* Generate a step function by comparing two values.
*
* @tsl
* @function
* @param {Node | number} x - The y parameter.
* @param {Node | number} y - The x parameter.
* @returns {Node}
*/
const step = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.STEP ).setParameterLength( 2 );
/**
* Calculates the reflection direction for an incident vector.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} I - The incident vector.
* @param {Node<vec2|vec3|vec4>} N - The normal vector.
* @returns {Node<vec2|vec3|vec4>}
*/
const reflect = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.REFLECT ).setParameterLength( 2 );
/**
* Calculates the distance between two points.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} x - The first point.
* @param {Node<vec2|vec3|vec4>} y - The second point.
* @returns {Node<float>}
*/
const distance = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DISTANCE ).setParameterLength( 2 );
/**
* Calculates the absolute difference between two values.
*
* @tsl
* @function
* @param {Node | number} x - The first parameter.
* @param {Node | number} y - The second parameter.
* @returns {Node}
*/
const difference = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DIFFERENCE ).setParameterLength( 2 );
/**
* Calculates the dot product of two vectors.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} x - The first vector.
* @param {Node<vec2|vec3|vec4>} y - The second vector.
* @returns {Node<float>}
*/
const dot = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.DOT ).setParameterLength( 2 );
/**
* Calculates the cross product of two vectors.
*
* @tsl
* @function
* @param {Node<vec2|vec3>} x - The first vector.
* @param {Node<vec2|vec3>} y - The second vector.
* @returns {Node<float|vec3>}
*/
const cross = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.CROSS ).setParameterLength( 2 );
/**
* Return the value of the first parameter raised to the power of the second one.
*
* @tsl
* @function
* @param {Node | number} x - The first parameter.
* @param {Node | number} y - The second parameter.
* @returns {Node}
*/
const pow = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.POW ).setParameterLength( 2 );
/**
* Returns the square of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The first parameter.
* @returns {Node}
*/
const pow2 = ( x ) => mul( x, x );
/**
* Returns the cube of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The first parameter.
* @returns {Node}
*/
const pow3 = ( x ) => mul( x, x, x );
/**
* Returns the fourth power of the parameter.
*
* @tsl
* @function
* @param {Node | number} x - The first parameter.
* @returns {Node}
*/
const pow4 = ( x ) => mul( x, x, x, x );
/**
* Transforms the direction of a vector by a matrix and then normalizes the result.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} direction - The direction vector.
* @param {Node<mat2|mat3|mat4>} matrix - The transformation matrix.
* @returns {Node}
*/
const transformDirection = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.TRANSFORM_DIRECTION ).setParameterLength( 2 );
/**
* Transforms a normal vector by the view matrix and then normalizes the result.
*
* The upper-left 3x3 of the view matrix is assumed to be orthonormal, so the
* normal can be transformed directly without involving the normal matrix.
*
* @tsl
* @function
* @param {Node<vec3>} normal - The normal vector, given in world space.
* @param {Node<mat3|mat4>} viewMatrix - The view matrix.
* @returns {Node<vec3>} The normal vector in view space.
*/
const transformNormalByViewMatrix = ( normal, viewMatrix ) => normalize( mul( viewMatrix, vec4( vec3( normal ), 0.0 ) ).xyz );
/**
* Transforms a normal vector by the inverse of the view matrix and then normalizes the result.
*
* The upper-left 3x3 of the view matrix is assumed to be orthonormal, so post-multiplying
* by the view matrix is equivalent to pre-multiplying by its inverse.
*
* @tsl
* @function
* @param {Node<vec3>} normal - The normal vector, given in view space.
* @param {Node<mat3|mat4>} viewMatrix - The view matrix.
* @returns {Node<vec3>} The normal vector in world space.
*/
const transformNormalByInverseViewMatrix = ( normal, viewMatrix ) => normalize( vec4( vec3( normal ), 0.0 ).mul( viewMatrix ).xyz );
/**
* Returns the cube root of a number.
*
* @tsl
* @function
* @param {Node | number} a - The first parameter.
* @returns {Node}
*/
const cbrt = ( a ) => mul( sign( a ), pow( abs( a ), 1.0 / 3.0 ) );
/**
* Calculate the squared length of a vector.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} a - The vector.
* @returns {Node<float>}
*/
const lengthSq = ( a ) => dot( a, a );
/**
* Linearly interpolates between two values.
*
* @tsl
* @function
* @param {Node | number} a - The first parameter.
* @param {Node | number} b - The second parameter.
* @param {Node | number} t - The interpolation value.
* @returns {Node}
*/
const mix = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.MIX ).setParameterLength( 3 );
/**
* Constrains a value to lie between two further values.
*
* @tsl
* @function
* @param {Node | number} value - The value to constrain.
* @param {Node | number} [low=0] - The lower bound.
* @param {Node | number} [high=1] - The upper bound.
* @returns {Node}
*/
const clamp = ( value, low = 0, high = 1 ) => new MathNode( MathNode.CLAMP, nodeObject( value ), nodeObject( low ), nodeObject( high ) );
/**
* Constrains a value between `0` and `1`.
*
* @tsl
* @function
* @param {Node | number} value - The value to constrain.
* @returns {Node}
*/
const saturate = ( value ) => clamp( value );
/**
* Calculates the refraction direction for an incident vector.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} I - The incident vector.
* @param {Node<vec2|vec3|vec4>} N - The normal vector.
* @param {Node<float>} eta - The ratio of indices of refraction.
* @returns {Node<vec2|vec3|vec4>}
*/
const refract = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.REFRACT ).setParameterLength( 3 );
/**
* Performs a Hermite interpolation between two values.
*
* @tsl
* @function
* @param {Node | number} low - The value of the lower edge of the Hermite function.
* @param {Node | number} high - The value of the upper edge of the Hermite function.
* @param {Node | number} x - The source value for interpolation.
* @returns {Node}
*/
const smoothstep = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.SMOOTHSTEP ).setParameterLength( 3 );
/**
* Returns a vector pointing in the same direction as another.
*
* @tsl
* @function
* @param {Node<vec2|vec3|vec4>} N - The vector to orient.
* @param {Node<vec2|vec3|vec4>} I - The incident vector.
* @param {Node<vec2|vec3|vec4>} Nref - The reference vector.
* @returns {Node<vec2|vec3|vec4>}
*/
const faceForward = /*@__PURE__*/ nodeProxyAssignable( MathNode, MathNode.FACEFORWARD ).setParameterLength( 3 );
/**
* Returns a random value for the given uv.
*
* @tsl
* @function
* @param {Node<vec2>} uv - The uv node.
* @returns {Node<float>}
*/
const rand = /*@__PURE__*/ Fn( ( [ uv ] ) => {
const a = 12.9898, b = 78.233, c = 43758.5453;
const dt = dot( uv.xy, vec2( a, b ) ), sn = mod( dt, PI );
return fract( sin( sn ).mul( c ) );
}, { uv: 'vec2', return: 'float' } );
/**
* Alias for `mix()` with a different parameter order.
*
* @tsl
* @function
* @param {Node | number} t - The interpolation value.
* @param {Node | number} e1 - The first parameter.
* @param {Node | number} e2 - The second parameter.
* @returns {Node}
*/
const mixElement = ( t, e1, e2 ) => mix( e1, e2, t );
/**
* Alias for `smoothstep()` with a different parameter order.
*
* @tsl
* @function
* @param {Node | number} x - The source value for interpolation.
* @param {Node | number} low - The value of the lower edge of the Hermite function.
* @param {Node | number} high - The value of the upper edge of the Hermite function.
* @returns {Node}
*/
const smoothstepElement = ( x, low, high ) => smoothstep( low, high, x );
/**
* Alias for `step()` with a different parameter order.
*
* @tsl
* @function
* @param {Node | number} x - The source value for interpolation.
* @param {Node | number} edge - The edge value.
* @returns {Node}
*/
const stepElement = ( x, edge ) => step( edge, x );
// GLSL alias function
const faceforward = faceForward;
const inversesqrt = inverseSqrt;
// Method chaining
addMethodChaining( 'all', all );
addMethodChaining( 'any', any );
addMethodChaining( 'radians', radians );
addMethodChaining( 'degrees', degrees );
addMethodChaining( 'exp', exp );
addMethodChaining( 'exp2', exp2 );
addMethodChaining( 'log', log );
addMethodChaining( 'log2', log2 );
addMethodChaining( 'sqrt', sqrt );
addMethodChaining( 'inverseSqrt', inverseSqrt );
addMethodChaining( 'floor', floor );
addMethodChaining( 'ceil', ceil );
addMethodChaining( 'normalize', normalize );
addMethodChaining( 'fract', fract );
addMethodChaining( 'sin', sin );
addMethodChaining( 'sinh', sinh );
addMethodChaining( 'cos', cos );
addMethodChaining( 'cosh', cosh );
addMethodChaining( 'tan', tan );
addMethodChaining( 'tanh', tanh );
addMethodChaining( 'asin', asin );
addMethodChaining( 'asinh', asinh );
addMethodChaining( 'acos', acos );
addMethodChaining( 'acosh', acosh );
addMethodChaining( 'atan', atan );
addMethodChaining( 'atanh', atanh );
addMethodChaining( 'abs', abs );
addMethodChaining( 'sign', sign );
addMethodChaining( 'length', length );
addMethodChaining( 'lengthSq', lengthSq );
addMethodChaining( 'negate', negate );
addMethodChaining( 'oneMinus', oneMinus );
addMethodChaining( 'dFdx', dFdx );
addMethodChaining( 'dFdy', dFdy );
addMethodChaining( 'round', round );
addMethodChaining( 'reciprocal', reciprocal );
addMethodChaining( 'trunc', trunc );
addMethodChaining( 'fwidth', fwidth );
addMethodChaining( 'min', min );
addMethodChaining( 'max', max );
addMethodChaining( 'step', stepElement );
addMethodChaining( 'reflect', reflect );
addMethodChaining( 'distance', distance );
addMethodChaining( 'dot', dot );
addMethodChaining( 'cross', cross );
addMethodChaining( 'pow', pow );
addMethodChaining( 'pow2', pow2 );
addMethodChaining( 'pow3', pow3 );
addMethodChaining( 'pow4', pow4 );
addMethodChaining( 'transformDirection', transformDirection );
addMethodChaining( 'transformNormalByViewMatrix', transformNormalByViewMatrix );
addMethodChaining( 'transformNormalByInverseViewMatrix', transformNormalByInverseViewMatrix );
addMethodChaining( 'mix', mixElement );
addMethodChaining( 'clamp', clamp );
addMethodChaining( 'refract', refract );
addMethodChaining( 'smoothstep', smoothstepElement );
addMethodChaining( 'faceForward', faceForward );
addMethodChaining( 'difference', difference );
addMethodChaining( 'saturate', saturate );
addMethodChaining( 'cbrt', cbrt );
addMethodChaining( 'transpose', transpose );
addMethodChaining( 'determinant', determinant );
addMethodChaining( 'inverse', inverse );
addMethodChaining( 'rand', rand );
/**
* Custom error class for node-related errors, including stack trace information.
*/
class NodeError extends Error {
constructor( message, stackTrace = null ) {
super( message );
/**
* The name of the error.
*
* @type {string}
*/
this.name = 'NodeError';
/**
* The stack trace associated with the error.
*
* @type {?StackTrace}
*/
this.stackTrace = stackTrace;
}
}
/**
* Represents a logical `if/else` statement. Can be used as an alternative
* to the `If()`/`Else()` syntax.
*
* The `select()` method is called in a chaining fashion on a condition. The parameter nodes of `select()`
* determine the outcome of the entire statement.
*
* ```js
* velocity = position.greaterThanEqual( limit ).select( velocity.negate(), velocity );
* ```
*
* When the condition is itself a vector (e.g. the `bvec4` produced by
* `someVec4.greaterThanEqual( someOtherVec4 )`), `select()` resolves
* per-component - each output lane picks independently based on its own
* condition component, the same way WGSL's native `select()` and GLSL's
* `mix( x, y, bvecN )` do - rather than picking one branch for the whole
* vector. The condition and values are converted to the largest vector width,
* with the condition converted to boolean components. Scalar values are broadcast.
*
* ```js
* // per-component: each channel picks independently
* const clamped = value.greaterThan( vec3( 1.0 ) ).select( vec3( 1.0 ), value );
* ```
*
* @augments Node
*/
class ConditionalNode extends Node {
static get type() {
return 'ConditionalNode';
}
/**
* Constructs a new conditional node.
*
* @param {Node} condNode - The node that defines the condition.
* @param {Node} ifNode - The node that is evaluate when the condition ends up `true`.
* @param {?Node} [elseNode=null] - The node that is evaluate when the condition ends up `false`.
*/
constructor( condNode, ifNode, elseNode = null ) {
super();
/**
* The node that defines the condition.
*
* @type {Node}
*/
this.condNode = condNode;
/**
* The node that is evaluate when the condition ends up `true`.
*
* @type {Node}
*/
this.ifNode = ifNode;
/**
* The node that is evaluate when the condition ends up `false`.
*
* @type {?Node}
* @default null
*/
this.elseNode = elseNode;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* This method is overwritten since the node type is inferred from the if/else
* nodes.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
const { condNode, ifNode, elseNode } = builder.getNodeProperties( this );
if ( ifNode === undefined ) {
// fallback setup
builder.flowBuildStage( this, 'setup' );
return this.getNodeType( builder );
}
let type = ifNode.getNodeType( builder );
if ( elseNode !== null ) {
const elseType = elseNode.getNodeType( builder );
if ( builder.getTypeLength( elseType ) > builder.getTypeLength( type ) ) {
type = elseType;
}
}
const condLength = builder.getTypeLength( condNode.getNodeType( builder ) );
if ( condLength > 1 && ! builder.isReference( type ) && ( builder.getTypeLength( type ) === 1 || builder.isVector( builder.getVectorType( type ) ) ) ) {
type = builder.getTypeFromLength( Math.max( condLength, builder.getTypeLength( type ) ), builder.getComponentType( type ) );
}
return type;
}
setup( builder ) {
const { condNode, ifNode, elseNode } = this;
const properties = builder.getNodeProperties( this );
properties.condNode = condNode;
properties.ifNode = ifNode;
properties.elseNode = elseNode;
}
generate( builder, output ) {
const type = this.getNodeType( builder );
const nodeData = builder.getDataFromNode( this );
if ( nodeData.propertyName !== undefined ) {
return builder.format( nodeData.propertyName, type, output );
}
const { condNode, ifNode, elseNode } = builder.getNodeProperties( this );
const functionNode = builder.currentFunctionNode;
const needsOutput = output !== 'void';
const nodeProperty = needsOutput ? property( type ).build( builder ) : '';
nodeData.propertyName = nodeProperty;
// A vector condition selects per-component - see getVectorSelect().
const condType = condNode.getNodeType( builder );
const condLength = builder.getTypeLength( condType );
if ( condLength > 1 ) {
const vectorType = builder.getVectorType( type );
if ( builder.isReference( type ) || ! builder.isVector( vectorType ) ) {
throw new NodeError( `TSL: select() with a vector condition ("${ condType }") requires scalar or vector values, received "${ type }".`, this.stackTrace );
}
// No "else": unselected lanes fall back to the type's zero value.
let elseSnippet;
if ( elseNode !== null ) {
elseSnippet = elseNode.build( builder, type );
} else {
elseSnippet = builder.generateConst( type );
}
const boolType = builder.changeComponentType( type, 'bool' );
const condSnippet = condNode.build( builder, boolType );
const ifSnippet = ifNode.build( builder, type );
const mathSnippet = builder.getVectorSelect( condSnippet, ifSnippet, elseSnippet, type );
if ( ! needsOutput ) return '';
builder.addFlowCode( `\n${ builder.tab }${ nodeProperty } = ${ mathSnippet };\n\n` );
return builder.format( nodeProperty, type, output );
}
const nodeSnippet = condNode.build( builder, 'bool' );
const isUniformFlow = builder.context.uniformFlow;
if ( isUniformFlow && elseNode !== null ) {
const ifSnippet = ifNode.build( builder, type );
const elseSnippet = elseNode.build( builder, type );
const mathSnippet = builder.getTernary( nodeSnippet, ifSnippet, elseSnippet );
// TODO: If node property already exists return something else
return builder.format( mathSnippet, type, output );
}
builder.addFlowCode( `\n${ builder.tab }if ( ${ nodeSnippet } ) {\n\n` ).addFlowTab();
// An immutable condition, i.e. a constant, is the guard of the blocks: blocks of the same parent and guard run together.
const condData = builder.getDataFromNode( condNode );
const guard = condData.propertyName === nodeSnippet && condData.assign !== true ? nodeSnippet : undefined;
const flowBlock = builder.flowBlock;
builder.flowBlock = { parent: flowBlock, guard };
let ifSnippet = ifNode.build( builder, type );
builder.flowBlock = flowBlock;
if ( ifSnippet ) {
if ( needsOutput ) {
ifSnippet = nodeProperty + ' = ' + ifSnippet + ';';
} else {
ifSnippet = 'return ' + ifSnippet + ';';
if ( functionNode === null ) {
warn( 'TSL: Return statement used in an inline \'Fn()\'. Define a layout struct to allow return values.', this.stackTrace );
ifSnippet = '// ' + ifSnippet;
}
}
}
builder.removeFlowTab().addFlowCode( builder.tab + '\t' + ifSnippet + '\n\n' + builder.tab + '}' );
if ( elseNode !== null ) {
builder.addFlowCode( ' else {\n\n' ).addFlowTab();
builder.flowBlock = { parent: flowBlock, guard: guard !== undefined ? '!' + guard : undefined };
let elseSnippet = elseNode.build( builder, type );
builder.flowBlock = flowBlock;
if ( elseSnippet ) {
if ( needsOutput ) {
elseSnippet = nodeProperty + ' = ' + elseSnippet + ';';
} else {
elseSnippet = 'return ' + elseSnippet + ';';
if ( functionNode === null ) {
warn( 'TSL: Return statement used in an inline \'Fn()\'. Define a layout struct to allow return values.', this.stackTrace );
elseSnippet = '// ' + elseSnippet;
}
}
}
builder.removeFlowTab().addFlowCode( builder.tab + '\t' + elseSnippet + '\n\n' + builder.tab + '}\n\n' );
} else {
builder.addFlowCode( '\n\n' );
}
return builder.format( nodeProperty, type, output );
}
}
/**
* TSL function for creating a conditional node.
*
* @tsl
* @function
* @param {Node} condNode - The node that defines the condition.
* @param {Node} ifNode - The node that is evaluate when the condition ends up `true`.
* @param {?Node} [elseNode=null] - The node that is evaluate when the condition ends up `false`.
* @returns {ConditionalNode}
*/
const select = /*@__PURE__*/ nodeProxy( ConditionalNode ).setParameterLength( 2, 3 );
addMethodChaining( 'select', select );
/**
* This node can be used as a context management component for another node.
* {@link NodeBuilder} performs its node building process in a specific context and
* this node allows the modify the context. A typical use case is to overwrite `getUV()` e.g.:
*
* ```js
*node.context( { getUV: () => customCoord } );
*\// or
*material.contextNode = context( { getUV: () => customCoord } );
*\// or
*renderer.contextNode = context( { getUV: () => customCoord } );
*\// or
*scenePass.contextNode = context( { getUV: () => customCoord } );
*```
* @augments Node
*/
class ContextNode extends Node {
static get type() {
return 'ContextNode';
}
/**
* Constructs a new context node.
*
* @param {Node} node - The node whose context should be modified.
* @param {Object} [value={}] - The modified context data.
*/
constructor( node = null, value = {} ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isContextNode = true;
/**
* The node whose context should be modified.
*
* @type {Node}
*/
this.node = node;
/**
* The modified context data.
*
* @type {Object}
* @default {}
*/
this.value = value;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* This method is overwritten to ensure it returns the reference to {@link ContextNode#node}.
*
* @return {Node} A reference to {@link ContextNode#node}.
*/
getScope() {
return this.node.getScope();
}
/**
* This method is overwritten to ensure it returns the type of {@link ContextNode#node}.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.node.getNodeType( builder );
}
/**
* Gathers the context data from all parent context nodes.
*
* @return {Object} The gathered context data.
*/
getFlowContextData() {
let data = {};
this.traverse( ( node ) => {
if ( node.isContextNode === true ) {
data = mergeContext( data, node.value );
}
} );
return data;
}
/**
* This method is overwritten to ensure it returns the member type of {@link ContextNode#node}.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The member name.
* @returns {string} The member type.
*/
getMemberType( builder, name ) {
return this.node.getMemberType( builder, name );
}
analyze( builder ) {
const usageCount = builder.increaseUsage( this );
if ( usageCount === 1 ) {
const previousContext = builder.addContext( this.value );
this.node.build( builder, this );
builder.setContext( previousContext );
}
}
setup( builder ) {
const previousContext = builder.addContext( this.value );
this.node.build( builder );
builder.setContext( previousContext );
}
generate( builder, output ) {
const previousContext = builder.addContext( this.value );
const snippet = this.node.build( builder, output );
builder.setContext( previousContext );
return snippet;
}
}
/**
* TSL function for creating a context node.
*
* @tsl
* @function
* @param {Node|Object} [nodeOrValue={}] - The node whose context should be modified or the modified context data.
* @param {Object} [value={}] - The modified context data.
* @returns {ContextNode}
*/
const context = ( nodeOrValue = null, value = {} ) => {
let node = nodeOrValue;
if ( node === null || node.isNode !== true ) {
value = node || value;
node = null;
}
return new ContextNode( node, value );
};
/**
* TSL function for defining a uniformFlow context value for a given node.
*
* @tsl
* @function
* @param {Node} node - The node whose dependencies should all execute within a uniform control-flow path.
* @returns {ContextNode}
*/
const uniformFlow = ( node ) => context( node, { uniformFlow: true } );
/**
* TSL function for defining a name for the context value for a given node.
*
* @tsl
* @function
* @param {Node} node - The node whose context should be modified.
* @param {string} name - The name to set.
* @returns {ContextNode}
*/
const setName = ( node, name ) => context( node, { nodeName: name } );
/**
* TSL function for defining a built-in shadow context for a given node.
*
* @tsl
* @function
* @param {ShadowNode} shadowNode - The shadow node representing the light's shadow.
* @param {Light} light - The light associated with the shadow.
* @param {Node} [node=null] - The node whose context should be modified.
* @returns {ContextNode}
*/
function builtinShadowContext( shadowNode, light, node = null ) {
return context( node, {
getShadow: ( { light: shadowLight, shadowColorNode } ) => {
if ( light === shadowLight ) {
return shadowColorNode.mul( shadowNode );
}
return shadowColorNode;
}
} );
}
/**
* TSL function for defining a built-in ambient occlusion context for a given node.
*
* @tsl
* @function
* @param {Node} aoNode - The ambient occlusion value node to apply.
* @param {Node} [node=null] - The node whose context should be modified.
* @returns {ContextNode}
*/
function builtinAOContext( aoNode, node = null ) {
return context( node, {
getAO: ( inputNode, { material } ) => {
if ( material.transparent === true ) return inputNode;
return inputNode !== null ? inputNode.mul( aoNode ) : aoNode;
}
} );
}
/**
* TSL function for defining a built-in global illumination context for a given node. The AO node
* modulates the indirect lighting of the materials, the GI node is added to their irradiance
* without being modulated by the AO since it already accounts for occlusion.
*
* @tsl
* @function
* @param {Node<float>} aoNode - The ambient occlusion value node to apply.
* @param {Node<vec3>} giNode - The indirect diffuse irradiance node to add.
* @param {Node} [node=null] - The node whose context should be modified.
* @returns {ContextNode}
*/
function builtinGIContext( aoNode, giNode, node = null ) {
// the lighting models multiply the whole indirect diffuse term by the AO, which would occlude
// the GI a second time. Dividing the GI by the AO cancels that out so only the environment
// lighting is modulated.
const ao = aoNode.max( 1e-3 );
return context( node, {
getAO: ( inputNode, { material } ) => {
if ( material.transparent === true ) return inputNode;
return inputNode !== null ? inputNode.mul( ao ) : ao;
},
getGI: ( inputNode, { material } ) => {
if ( material.transparent === true ) return inputNode;
const gi = giNode.div( ao );
return inputNode !== null ? inputNode.add( gi ) : gi;
}
} );
}
/**
* TSL function for defining a label context value for a given node.
*
* @tsl
* @function
* @deprecated
* @param {Node} node - The node whose context should be modified.
* @param {string} name - The name/label to set.
* @returns {ContextNode}
*/
function label( node, name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.' ); // @deprecated r179
return setName( node, name );
}
addMethodChaining( 'context', context );
addMethodChaining( 'label', label );
addMethodChaining( 'uniformFlow', uniformFlow );
addMethodChaining( 'setName', setName );
addMethodChaining( 'builtinShadowContext', ( node, shadowNode, light ) => builtinShadowContext( shadowNode, light, node ) );
addMethodChaining( 'builtinAOContext', ( node, aoValue ) => builtinAOContext( aoValue, node ) );
addMethodChaining( 'builtinGIContext', ( node, aoValue, giValue ) => builtinGIContext( aoValue, giValue, node ) );
/**
* Class for representing shader variables as nodes. Variables are created from
* existing nodes like the following:
*
* ```js
* const depth = sampleDepth( uvNode ).toVar( 'depth' );
* ```
*
* @augments Node
*/
class VarNode extends Node {
static get type() {
return 'VarNode';
}
/**
* Constructs a new variable node.
*
* @param {Node} node - The node for which a variable should be created.
* @param {?string} [name=null] - The name of the variable in the shader.
* @param {boolean} [readOnly=false] - The read-only flag.
*/
constructor( node, name = null, readOnly = false ) {
super();
/**
* The node for which a variable should be created.
*
* @type {Node}
*/
this.node = node;
/**
* The name of the variable in the shader. If no name is defined,
* the node system auto-generates one.
*
* @type {?string}
* @default null
*/
this.name = name;
/**
* `VarNode` sets this property to `true` by default.
*
* @type {boolean}
* @default true
*/
this.global = true;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVarNode = true;
/**
*
* The read-only flag.
*
* @type {boolean}
* @default false
*/
this.readOnly = readOnly;
/**
*
* Add this flag to the node system to indicate that this node require parents.
*
* @type {boolean}
* @default true
*/
this.parents = true;
/**
* Whether this node can be generated as an expression until it is assigned.
*
* @type {boolean}
* @default false
*/
this.assignable = false;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the assignable flag for this node.
*
* An assignable node is generated as an expression until it is assigned,
* at which point it is declared as a variable.
*
* @param {boolean} value - The value to set for the assignable flag.
* @returns {VarNode} This node.
*/
setAssignable( value ) {
this.assignable = value;
return this;
}
/**
* Returns the assignable flag of this node.
*
* @return {boolean} The assignable flag.
*/
getAssignable() {
return this.assignable;
}
getMemberType( builder, name ) {
return this.node.getMemberType( builder, name );
}
getElementType( builder ) {
return this.node.getElementType( builder );
}
generateNodeType( builder ) {
return this.node.getNodeType( builder );
}
getArrayCount( builder ) {
return this.node.getArrayCount( builder );
}
_declaresAssignable() {
return false;
}
_isFunctionScope() {
// see `generate()`, a read-only or local variable is declared where it is generated
return this.readOnly === false && this.assignable === false;
}
build( builder, output = null ) {
this._setupInspector( builder );
const refNode = this.getShared( builder );
if ( this !== refNode ) {
return refNode.build( builder, output );
}
const data = builder.getDataFromNode( this );
this._updateAssignable( builder, data, this.node );
// An assignable node that is not assigned is just its value.
if ( this.assignable && data.forceDeclaration !== true && data.assign !== true ) {
return this.node.build( builder, output );
}
return super.build( builder, output );
}
generate( builder ) {
const { node, name, readOnly } = this;
const nodeType = this.getNodeType( builder );
if ( nodeType == 'void' ) {
if ( this.isAssignable( builder ) !== true ) {
error( 'TSL: ".toVar()" can not be used with void type.', this.stackTrace );
}
const snippet = node.build( builder );
return snippet;
}
const vectorType = builder.getVectorType( nodeType );
const snippet = node.build( builder, vectorType );
const nodeVar = builder.getVarFromNode( this, name, vectorType, undefined, readOnly, this.assignable );
const propertyName = builder.getPropertyName( nodeVar );
let declarationPrefix = propertyName;
if ( nodeVar.readOnly ) {
const count = node.getArrayCount( builder );
declarationPrefix = builder.isDeterministic( node )
? builder.generateConstStatement( nodeVar.type, propertyName, count )
: builder.generateLetStatement( nodeVar.type, propertyName, count );
} else if ( nodeVar.local ) {
declarationPrefix = builder.generateVarStatement( nodeVar.type, propertyName, nodeVar.count );
}
builder.addLineFlowCode( `${ declarationPrefix } = ${ snippet }`, this );
return propertyName;
}
}
/**
* TSL function for creating a var node.
*
* @tsl
* @function
* @param {Node} node - The node for which a variable should be created.
* @param {?string} name - The name of the variable in the shader.
* @returns {VarNode}
*/
const createVar = /*@__PURE__*/ nodeProxy( VarNode );
/**
* TSL function for creating a var node.
*
* @tsl
* @function
* @param {Node} node - The node for which a variable should be created.
* @param {?string} name - The name of the variable in the shader.
* @returns {VarNode}
*/
const Var = ( node, name = null ) => createVar( node, name ).toStack();
/**
* TSL function for creating a const node.
*
* @tsl
* @function
* @param {Node} node - The node for which a constant should be created.
* @param {?string} name - The name of the constant in the shader.
* @returns {VarNode}
*/
const Const = ( node, name = null ) => createVar( node, name, true ).toStack();
// Method chaining
addMethodChaining( 'toVar', Var );
addMethodChaining( 'toConst', Const );
/**
* This node is used to build a sub-build in the node system.
*
* @augments Node
* @param {Node} node - The node to be built in the sub-build.
* @param {string} name - The name of the sub-build.
* @param {?string} [nodeType=null] - The type of the node, if known.
*/
class SubBuildNode extends Node {
static get type() {
return 'SubBuild';
}
constructor( node, name, nodeType = null ) {
super( nodeType );
/**
* The node to be built in the sub-build.
*
* @type {Node}
*/
this.node = node;
/**
* The name of the sub-build.
*
* @type {string}
*/
this.name = name;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSubBuildNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( builder ) {
if ( this.nodeType !== null ) return this.nodeType;
builder.addSubBuild( this.name );
const nodeType = this.node.getNodeType( builder );
builder.removeSubBuild();
return nodeType;
}
build( builder, ...params ) {
this._setupInspector( builder );
builder.addSubBuild( this.name );
const data = this.node.build( builder, ...params );
builder.removeSubBuild();
return data;
}
}
/**
* Creates a new sub-build node.
*
* @tsl
* @function
* @param {Node} node - The node to be built in the sub-build.
* @param {string} name - The name of the sub-build.
* @param {?string} [type=null] - The type of the node, if known.
* @returns {Node} A node object wrapping the SubBuildNode instance.
*/
const subBuild = ( node, name, type = null ) => new SubBuildNode( nodeObject( node ), name, type );
/**
* Class for representing shader varyings as nodes. Varyings are create from
* existing nodes like the following:
*
* ```js
* const positionLocal = positionGeometry.toVarying( 'vPositionLocal' );
* ```
*
* @augments Node
*/
class VaryingNode extends Node {
static get type() {
return 'VaryingNode';
}
/**
* Constructs a new varying node.
*
* @param {Node} node - The node for which a varying should be created.
* @param {?string} name - The name of the varying in the shader.
*/
constructor( node, name = null ) {
super();
/**
* The node for which a varying should be created.
*
* @type {Node}
*/
this.node = subBuild( node, 'VERTEX' );
/**
* The name of the varying in the shader. If no name is defined,
* the node system auto-generates one.
*
* @type {?string}
* @default null
*/
this.name = name;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVaryingNode = true;
/**
* The interpolation type of the varying data.
*
* @type {?string}
* @default null
*/
this.interpolationType = null;
/**
* The interpolation sampling type of varying data.
*
* @type {?string}
* @default null
*/
this.interpolationSampling = null;
/**
* This flag is used for global cache.
*
* @type {boolean}
* @default true
*/
this.global = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Defines the interpolation type of the varying.
*
* @param {string} type - The interpolation type.
* @param {?string} sampling - The interpolation sampling type
* @return {VaryingNode} A reference to this node.
*/
setInterpolation( type, sampling = null ) {
this.interpolationType = type;
this.interpolationSampling = sampling;
return this;
}
getHash( builder ) {
return this.name || super.getHash( builder );
}
generateNodeType( builder ) {
// VaryingNode is auto type
return this.node.getNodeType( builder );
}
/**
* This method performs the setup of a varying node with the current node builder.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {NodeVarying} The node varying from the node builder.
*/
setupVarying( builder ) {
const properties = builder.getNodeProperties( this );
let varying = properties.varying;
if ( varying === undefined ) {
const name = this.name;
const type = this.getNodeType( builder );
const interpolationType = this.interpolationType;
const interpolationSampling = this.interpolationSampling;
properties.varying = varying = builder.getVaryingFromNode( this, name, type, interpolationType, interpolationSampling );
properties.node = subBuild( this.node, 'VERTEX' );
}
// this property can be used to check if the varying can be optimized for a variable
varying.needsInterpolation || ( varying.needsInterpolation = ( builder.shaderStage === 'fragment' ) );
return varying;
}
setup( builder ) {
// the context can evaluate varyings directly in the fragment stage, e.g. if the surface is reconstructed per fragment.
// indices and attributes create their own varyings in the fragment stage, so their varyings are kept unless the
// context provides the attribute
if ( builder.context.inlineVaryings === true && builder.shaderStage === 'fragment' ) {
let node = this.node;
while ( node.isSubBuildNode === true ) node = node.node;
if ( node.isIndexNode !== true && ( node.isAttributeNode !== true || node.getContextAttribute( builder ) ) ) return this.node;
}
this.setupVarying( builder );
builder.flowNodeFromShaderStage( NodeShaderStage.VERTEX, this.node );
}
analyze( builder ) {
// TODO: Node.build() could pass through the output node of setup() directly, so overrides of analyze() and generate() don't need to handle it twice
if ( builder.getNodeProperties( this ).outputNode ) return super.analyze( builder );
this.setupVarying( builder );
builder.flowNodeFromShaderStage( NodeShaderStage.VERTEX, this.node );
}
generate( builder ) {
const { outputNode } = builder.getNodeProperties( this );
if ( outputNode ) return outputNode.build( builder, this.getNodeType( builder ) );
const propertyKey = builder.getSubBuildProperty( 'property', builder.currentStack );
const properties = builder.getNodeProperties( this );
const varying = this.setupVarying( builder );
// The vertex assignment is emitted once per block, from the fragment stage it is emitted outside of any block.
const flowBlock = builder.shaderStage === NodeShaderStage.VERTEX ? builder.flowBlock : null;
if ( properties[ propertyKey ] !== flowBlock ) {
const type = this.getNodeType( builder );
const propertyName = builder.getPropertyName( varying, NodeShaderStage.VERTEX );
if ( builder.shaderStage === NodeShaderStage.VERTEX ) {
const snippet = properties.node.build( builder, type );
builder.addLineFlowCode( `${ propertyName } = ${ snippet }`, this );
} else {
// force node run in vertex stage
builder.flowNodeFromShaderStage( NodeShaderStage.VERTEX, properties.node, type, propertyName );
}
properties[ propertyKey ] = flowBlock;
}
return builder.getPropertyName( varying );
}
}
/**
* TSL function for creating a varying node.
*
* @tsl
* @function
* @param {Node} node - The node for which a varying should be created.
* @param {?string} name - The name of the varying in the shader.
* @returns {VaryingNode}
*/
const varying = /*@__PURE__*/ nodeProxy( VaryingNode ).setParameterLength( 1, 2 );
/**
* Computes a node in the vertex stage.
*
* @tsl
* @function
* @param {Node} node - The node which should be executed in the vertex stage.
* @returns {VaryingNode}
*/
const vertexStage = ( node ) => varying( node );
addMethodChaining( 'toVarying', varying );
addMethodChaining( 'toVertexStage', vertexStage );
/**
* Converts the given color value from sRGB to linear-sRGB color space.
*
* @tsl
* @function
* @param {Node<vec3>} color - The sRGB color.
* @return {Node<vec3>} The linear-sRGB color.
*/
const sRGBTransferEOTF = /*@__PURE__*/ Fn( ( [ color ] ) => {
const a = color.mul( 0.9478672986 ).add( 0.0521327014 ).pow( 2.4 );
const b = color.mul( 0.0773993808 );
const factor = color.lessThanEqual( 0.04045 );
const rgbResult = mix( a, b, factor );
return rgbResult;
}, { color: 'vec3', return: 'vec3' } );
/**
* Converts the given color value from linear-sRGB to sRGB color space.
*
* @tsl
* @function
* @param {Node<vec3>} color - The linear-sRGB color.
* @return {Node<vec3>} The sRGB color.
*/
const sRGBTransferOETF = /*@__PURE__*/ Fn( ( [ color ] ) => {
const a = color.pow( 0.41666 ).mul( 1.055 ).sub( 0.055 );
const b = color.mul( 12.92 );
const factor = color.lessThanEqual( 0.0031308 );
const rgbResult = mix( a, b, factor );
return rgbResult;
}, { color: 'vec3', return: 'vec3' } );
const WORKING_COLOR_SPACE = 'WorkingColorSpace';
const OUTPUT_COLOR_SPACE = 'OutputColorSpace';
/**
* This node represents a color space conversion. Meaning it converts
* a color value from a source to a target color space.
*
* @augments Node
*/
class ColorSpaceNode extends Node {
static get type() {
return 'ColorSpaceNode';
}
/**
* Constructs a new color space node.
*
* @param {Node} colorNode - Represents the color to convert.
* @param {string} source - The source color space.
* @param {string} target - The target color space.
*/
constructor( colorNode, source, target ) {
super( 'vec4' );
/**
* Represents the color to convert.
*
* @type {Node}
*/
this.colorNode = colorNode;
/**
* The source color space.
*
* @type {string}
*/
this.source = source;
/**
* The target color space.
*
* @type {string}
*/
this.target = target;
}
/**
* This method resolves the constants `WORKING_COLOR_SPACE` and
* `OUTPUT_COLOR_SPACE` based on the current configuration of the
* color management and renderer.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} colorSpace - The color space to resolve.
* @return {string} The resolved color space.
*/
resolveColorSpace( builder, colorSpace ) {
if ( colorSpace === WORKING_COLOR_SPACE ) {
return ColorManagement.workingColorSpace;
} else if ( colorSpace === OUTPUT_COLOR_SPACE ) {
return builder.context.outputColorSpace || builder.renderer.outputColorSpace;
}
return colorSpace;
}
setup( builder ) {
const { colorNode } = this;
const source = this.resolveColorSpace( builder, this.source );
const target = this.resolveColorSpace( builder, this.target );
let outputNode = colorNode;
if ( ColorManagement.enabled === false || source === target || ! source || ! target ) {
return outputNode;
}
if ( ColorManagement.getTransfer( source ) === SRGBTransfer ) {
outputNode = vec4( sRGBTransferEOTF( outputNode.rgb ), outputNode.a );
}
if ( ColorManagement.getPrimaries( source ) !== ColorManagement.getPrimaries( target ) ) {
outputNode = vec4(
mat3( ColorManagement._getMatrix( new Matrix3(), source, target ) ).mul( outputNode.rgb ),
outputNode.a
);
}
if ( ColorManagement.getTransfer( target ) === SRGBTransfer ) {
outputNode = vec4( sRGBTransferOETF( outputNode.rgb ), outputNode.a );
}
return outputNode;
}
}
/**
* TSL function for converting a given color node from the current working color space to the given color space.
*
* @tsl
* @function
* @param {Node} node - Represents the node to convert.
* @param {string} targetColorSpace - The target color space.
* @returns {ColorSpaceNode}
*/
const workingToColorSpace = ( node, targetColorSpace ) => new ColorSpaceNode( nodeObject( node ), WORKING_COLOR_SPACE, targetColorSpace );
/**
* TSL function for converting a given color node from the given color space to the current working color space.
*
* @tsl
* @function
* @param {Node} node - Represents the node to convert.
* @param {string} sourceColorSpace - The source color space.
* @returns {ColorSpaceNode}
*/
const colorSpaceToWorking = ( node, sourceColorSpace ) => new ColorSpaceNode( nodeObject( node ), sourceColorSpace, WORKING_COLOR_SPACE );
/**
* TSL function for converting a given color node from one color space to another one.
*
* @tsl
* @function
* @param {Node} node - Represents the node to convert.
* @param {string} sourceColorSpace - The source color space.
* @param {string} targetColorSpace - The target color space.
* @returns {ColorSpaceNode}
*/
const convertColorSpace = ( node, sourceColorSpace, targetColorSpace ) => new ColorSpaceNode( nodeObject( node ), sourceColorSpace, targetColorSpace );
addMethodChaining( 'workingToColorSpace', workingToColorSpace );
addMethodChaining( 'colorSpaceToWorking', colorSpaceToWorking );
/**
* This class is only relevant if the referenced property is array-like.
* In this case, `ReferenceElementNode` allows to refer to a specific
* element inside the data structure via an index.
*
* @augments ArrayElementNode
*/
class ReferenceElementNode extends ArrayElementNode {
static get type() {
return 'ReferenceElementNode';
}
/**
* Constructs a new reference element node.
*
* @param {(ReferenceBaseNode|ReferenceNode)} referenceNode - The reference node.
* @param {Node} indexNode - The index node that defines the element access.
*/
constructor( referenceNode, indexNode ) {
super( referenceNode, indexNode );
/**
* Similar to {@link ReferenceNode#reference}, an additional
* property references to the current node.
*
* @type {?(ReferenceBaseNode|ReferenceNode)}
* @default null
*/
this.referenceNode = referenceNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isReferenceElementNode = true;
}
/**
* This method is overwritten since the node type is inferred from
* the uniform type of the reference node.
*
* @return {string} The node type.
*/
generateNodeType() {
return this.referenceNode.uniformType;
}
generate( builder ) {
const snippet = super.generate( builder );
const arrayType = this.referenceNode.getNodeType( builder );
const elementType = this.getNodeType( builder );
return builder.format( snippet, arrayType, elementType );
}
}
// TODO: Avoid duplicated code and use only ReferenceBaseNode or ReferenceNode
/**
* Base class for nodes which establishes a reference to a property of another object.
* In this way, the value of the node is automatically linked to the value of
* referenced object. Reference nodes internally represent the linked value
* as a uniform.
*
* @augments Node
*/
class ReferenceBaseNode extends Node {
static get type() {
return 'ReferenceBaseNode';
}
/**
* Constructs a new reference base node.
*
* @param {string} property - The name of the property the node refers to.
* @param {string} uniformType - The uniform type that should be used to represent the property value.
* @param {?Object} [object=null] - The object the property belongs to.
* @param {?number} [count=null] - When the linked property is an array-like, this parameter defines its length.
*/
constructor( property, uniformType, object = null, count = null ) {
super();
/**
* The name of the property the node refers to.
*
* @type {string}
*/
this.property = property;
/**
* The uniform type that should be used to represent the property value.
*
* @type {string}
*/
this.uniformType = uniformType;
/**
* The object the property belongs to.
*
* @type {?Object}
* @default null
*/
this.object = object;
/**
* When the linked property is an array, this parameter defines its length.
*
* @type {?number}
* @default null
*/
this.count = count;
/**
* The property name might have dots so nested properties can be referred.
* The hierarchy of the names is stored inside this array.
*
* @type {Array<string>}
*/
this.properties = property.split( '.' );
/**
* Points to the current referred object. This property exists next to {@link ReferenceNode#object}
* since the final reference might be updated from calling code.
*
* @type {?Object}
* @default null
*/
this.reference = object;
/**
* The uniform node that holds the value of the reference node.
*
* @type {UniformNode}
* @default null
*/
this.node = null;
/**
* The uniform group of the internal uniform.
*
* @type {UniformGroupNode}
* @default null
*/
this.group = null;
/**
* Overwritten since reference nodes are updated per object.
*
* @type {string}
* @default 'object'
*/
this.updateType = NodeUpdateType.OBJECT;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the uniform group for this reference node.
*
* @param {UniformGroupNode} group - The uniform group to set.
* @return {ReferenceBaseNode} A reference to this node.
*/
setGroup( group ) {
this.group = group;
return this;
}
/**
* When the referred property is array-like, this method can be used
* to access elements via an index node.
*
* @param {IndexNode} indexNode - indexNode.
* @return {ReferenceElementNode} A reference to an element.
*/
element( indexNode ) {
return new ReferenceElementNode( this, nodeObject( indexNode ) );
}
/**
* Sets the node type which automatically defines the internal
* uniform type.
*
* @param {string} uniformType - The type to set.
*/
setNodeType( uniformType ) {
const node = uniform( null, uniformType );
if ( this.group !== null ) {
node.setGroup( this.group );
}
this.node = node;
}
/**
* This method is overwritten since the node type is inferred from
* the type of the reference node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.node === null ) {
this.updateReference( builder );
this.updateValue();
}
return this.node.getNodeType( builder );
}
/**
* Returns the property value from the given referred object.
*
* @param {Object} [object=this.reference] - The object to retrieve the property value from.
* @return {any} The value.
*/
getValueFromReference( object = this.reference ) {
const { properties } = this;
let value = object[ properties[ 0 ] ];
for ( let i = 1; i < properties.length; i ++ ) {
value = value[ properties[ i ] ];
}
return value;
}
/**
* Allows to update the reference based on the given state. The state is only
* evaluated {@link ReferenceBaseNode#object} is not set.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Object} The updated reference.
*/
updateReference( state ) {
this.reference = this.object !== null ? this.object : state.object;
return this.reference;
}
/**
* The output of the reference node is the internal uniform node.
*
* @return {UniformNode} The output node.
*/
setup() {
this.updateValue();
return this.node;
}
/**
* Overwritten to update the internal uniform value.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( /*frame*/ ) {
this.updateValue();
}
/**
* Retrieves the value from the referred object property and uses it
* to updated the internal uniform.
*/
updateValue() {
if ( this.node === null ) this.setNodeType( this.uniformType );
const value = this.getValueFromReference();
if ( Array.isArray( value ) ) {
this.node.array = value;
} else {
this.node.value = value;
}
}
}
/**
* TSL function for creating a reference base node.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {string} type - The uniform type that should be used to represent the property value.
* @param {Object} object - The object the property belongs to.
* @returns {ReferenceBaseNode}
*/
const reference$1 = ( name, type, object ) => new ReferenceBaseNode( name, type, object );
/**
* This node is a special type of reference node which is intended
* for linking renderer properties with node values.
* ```js
* const exposureNode = rendererReference( 'toneMappingExposure', 'float', renderer );
* ```
* When changing `renderer.toneMappingExposure`, the node value of `exposureNode` will
* automatically be updated.
*
* @augments ReferenceBaseNode
*/
class RendererReferenceNode extends ReferenceBaseNode {
static get type() {
return 'RendererReferenceNode';
}
/**
* Constructs a new renderer reference node.
*
* @param {string} property - The name of the property the node refers to.
* @param {string} inputType - The uniform type that should be used to represent the property value.
* @param {?Renderer} [renderer=null] - The renderer the property belongs to. When no renderer is set,
* the node refers to the renderer of the current state.
*/
constructor( property, inputType, renderer = null ) {
super( property, inputType, renderer );
/**
* The renderer the property belongs to. When no renderer is set,
* the node refers to the renderer of the current state.
*
* @type {?Renderer}
* @default null
*/
this.renderer = renderer;
this.setGroup( renderGroup );
}
/**
* Updates the reference based on the given state. The state is only evaluated
* {@link RendererReferenceNode#renderer} is not set.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Object} The updated reference.
*/
updateReference( state ) {
this.reference = this.renderer !== null ? this.renderer : state.renderer;
return this.reference;
}
}
/**
* TSL function for creating a renderer reference node.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {string} type - The uniform type that should be used to represent the property value.
* @param {?Renderer} [renderer=null] - The renderer the property belongs to. When no renderer is set,
* the node refers to the renderer of the current state.
* @returns {RendererReferenceNode}
*/
const rendererReference = ( name, type, renderer = null ) => new RendererReferenceNode( name, type, renderer );
/**
* This node represents a tone mapping operation.
*
* @augments Node
*/
class ToneMappingNode extends Node {
static get type() {
return 'ToneMappingNode';
}
/**
* Constructs a new tone mapping node.
*
* @param {number} toneMapping - The tone mapping type.
* @param {Node} exposureNode - The tone mapping exposure.
* @param {Node} [colorNode=null] - The color node to process.
*/
constructor( toneMapping, exposureNode = toneMappingExposure, colorNode = null ) {
super( 'vec3' );
/**
* The tone mapping type.
*
* @private
* @type {number}
*/
this._toneMapping = toneMapping;
/**
* The tone mapping exposure.
*
* @type {Node}
* @default null
*/
this.exposureNode = exposureNode;
/**
* Represents the color to process.
*
* @type {?Node}
* @default null
*/
this.colorNode = colorNode;
}
/**
* Overwrites the default `customCacheKey()` implementation by including the tone
* mapping type into the cache key.
*
* @return {number} The hash.
*/
customCacheKey() {
return hash$1( this._toneMapping );
}
/**
* Sets the tone mapping type.
*
* @param {number} value - The tone mapping type.
* @return {ToneMappingNode} A reference to this node.
*/
setToneMapping( value ) {
this._toneMapping = value;
return this;
}
/**
* Gets the tone mapping type.
*
* @returns {number} The tone mapping type.
*/
getToneMapping() {
return this._toneMapping;
}
setup( builder ) {
const colorNode = this.colorNode || builder.context.color;
const toneMapping = this._toneMapping;
if ( toneMapping === NoToneMapping ) return colorNode;
let outputNode = null;
const toneMappingFn = builder.renderer.library.getToneMappingFunction( toneMapping );
if ( toneMappingFn !== null ) {
outputNode = vec4( toneMappingFn( colorNode.rgb, this.exposureNode ), colorNode.a );
} else {
error( 'ToneMappingNode: Unsupported Tone Mapping configuration.', toneMapping );
outputNode = colorNode;
}
return outputNode;
}
}
/**
* TSL function for creating a tone mapping node.
*
* @tsl
* @function
* @param {number} mapping - The tone mapping type.
* @param {Node<float> | number} exposure - The tone mapping exposure.
* @param {Node<vec3> | Color} color - The color node to process.
* @returns {ToneMappingNode<vec3>}
*/
const toneMapping = ( mapping, exposure, color ) => new ToneMappingNode( mapping, nodeObject( exposure ), nodeObject( color ) );
/**
* TSL object that represents the global tone mapping exposure of the renderer.
*
* @tsl
* @type {RendererReferenceNode<vec3>}
*/
const toneMappingExposure = /*@__PURE__*/ rendererReference( 'toneMappingExposure', 'float' );
addMethodChaining( 'toneMapping', ( color, mapping, exposure ) => toneMapping( mapping, exposure, color ) );
/**
* Internal buffer attribute library.
*
* @private
* @type {WeakMap<TypedArray, InterleavedBuffer>}
*/
const _bufferLib = new WeakMap();
/**
* Internal method for retrieving or creating interleaved buffers.
*
* @private
* @param {TypedArray} value - The attribute data.
* @param {number} itemSize - The attribute item size.
* @returns {InterleavedBuffer} The interleaved buffer.
*/
function _getBufferAttribute( value, itemSize ) {
let buffer = _bufferLib.get( value );
if ( buffer === undefined ) {
buffer = new InterleavedBuffer( value, itemSize );
_bufferLib.set( value, buffer );
}
return buffer;
}
/**
* In earlier `three.js` versions it was only possible to define attribute data
* on geometry level. With `BufferAttributeNode`, it is also possible to do this
* on the node level.
* ```js
* const geometry = new THREE.PlaneGeometry();
* const positionAttribute = geometry.getAttribute( 'position' );
*
* const colors = [];
* for ( let i = 0; i < position.count; i ++ ) {
* colors.push( 1, 0, 0 );
* }
*
* material.colorNode = bufferAttribute( new THREE.Float32BufferAttribute( colors, 3 ) );
* ```
* This new approach is especially interesting when geometry data are generated via
* compute shaders. The below line converts a storage buffer into an attribute node.
* ```js
* material.positionNode = positionBuffer.toAttribute();
* ```
* @augments InputNode
*/
class BufferAttributeNode extends InputNode {
static get type() {
return 'BufferAttributeNode';
}
/**
* Constructs a new buffer attribute node.
*
* @param {BufferAttribute|InterleavedBufferAttribute|InterleavedBuffer|TypedArray} value - The attribute data.
* @param {?string} [bufferType=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [bufferStride=0] - The buffer stride.
* @param {number} [bufferOffset=0] - The buffer offset.
*/
constructor( value, bufferType = null, bufferStride = 0, bufferOffset = 0 ) {
super( value, bufferType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBufferNode = true;
/**
* The buffer type (e.g. `'vec3'`).
*
* @type {?string}
* @default null
*/
this.bufferType = bufferType;
/**
* The buffer stride.
*
* @type {number}
* @default 0
*/
this.bufferStride = bufferStride;
/**
* The buffer offset.
*
* @type {number}
* @default 0
*/
this.bufferOffset = bufferOffset;
/**
* The usage property. Set this to `THREE.DynamicDrawUsage` via `.setUsage()`,
* if you are planning to update the attribute data per frame.
*
* @type {number}
* @default StaticDrawUsage
*/
this.usage = StaticDrawUsage;
/**
* Whether the attribute is instanced or not.
*
* @type {boolean}
* @default false
*/
this.instanced = false;
/**
* A reference to the buffer attribute.
*
* @type {?BufferAttribute}
* @default null
*/
this.attribute = null;
/**
* `BufferAttributeNode` sets this property to `true` by default.
*
* @type {boolean}
* @default true
*/
this.global = true;
if ( value && value.isBufferAttribute === true && value.itemSize <= 4 ) {
this.attribute = value;
this.usage = value.usage;
this.instanced = value.isInstancedBufferAttribute;
} else if ( value && value.isInterleavedBufferAttribute === true ) {
// e.g. a column of a buffer attribute with an item size larger than four
this.attribute = value;
this.usage = value.data.usage;
}
}
/**
* This method is overwritten since the attribute data might be shared
* and thus the hash should be shared as well.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The hash.
*/
getHash( builder ) {
let id;
if ( this.bufferStride === 0 && this.bufferOffset === 0 ) {
let bufferData = builder.globalCache.getData( this.value );
if ( bufferData === undefined ) {
bufferData = {
node: this
};
builder.globalCache.setData( this.value, bufferData );
}
id = bufferData.node.id;
} else {
id = this.id;
}
return String( id );
}
/**
* This method is overwritten since the node type is inferred from
* the buffer attribute.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.bufferType === null ) {
this.bufferType = builder.getTypeFromAttribute( this.attribute );
}
return this.bufferType;
}
/**
* Depending on which value was passed to the node, `setup()` behaves
* differently. If no instance of `BufferAttribute` was passed, the method
* creates an internal attribute and configures it respectively.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup( builder ) {
if ( this.attribute !== null ) return;
//
const type = this.getNodeType( builder );
const itemSize = builder.getTypeLength( type );
const value = this.value;
const stride = this.bufferStride || itemSize;
const offset = this.bufferOffset;
let buffer;
if ( value.isInterleavedBuffer === true ) {
buffer = value;
} else if ( value.isBufferAttribute === true ) {
buffer = _getBufferAttribute( value.array, stride );
} else {
buffer = _getBufferAttribute( value, stride );
}
const bufferAttribute = new InterleavedBufferAttribute( buffer, itemSize, offset );
buffer.setUsage( this.usage );
this.attribute = bufferAttribute;
this.attribute.isInstancedBufferAttribute = this.instanced; // @TODO: Add a possible: InstancedInterleavedBufferAttribute
}
/**
* Generates the code snippet of the buffer attribute node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated code snippet.
*/
generate( builder ) {
const nodeType = this.getNodeType( builder );
const nodeName = builder.context.nodeName;
if ( nodeName !== undefined ) delete builder.context.nodeName; // deleting when consumed
const nodeAttribute = builder.getBufferAttributeFromNode( this, nodeType, nodeName );
const propertyName = builder.getPropertyName( nodeAttribute );
let output = null;
if ( builder.shaderStage === 'vertex' || builder.shaderStage === 'compute' ) {
this.name = propertyName;
output = propertyName;
} else {
let varyingName;
if ( nodeName ) {
varyingName = nodeName + 'Varying';
}
const nodeVarying = varying( this, varyingName );
output = nodeVarying.build( builder, nodeType );
}
return output;
}
/**
* Overwrites the default implementation to return a fixed value `'bufferAttribute'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return 'bufferAttribute';
}
/**
* Sets the `usage` property to the given value.
*
* @param {number} value - The usage to set.
* @return {BufferAttributeNode} A reference to this node.
*/
setUsage( value ) {
this.usage = value;
if ( this.attribute && this.attribute.isBufferAttribute === true ) {
this.attribute.usage = value;
}
return this;
}
/**
* Sets the `instanced` property to the given value.
*
* @param {boolean} value - The value to set.
* @return {BufferAttributeNode} A reference to this node.
*/
setInstanced( value ) {
this.instanced = value;
return this;
}
/**
* Returns the buffer attribute to bind when rendering the given object. Nodes that
* bind attributes of the rendered object, like {@link ReferenceAttributeNode}, overwrite it.
*
* @param {Object3D} object - The rendered object.
* @return {?(BufferAttribute|InterleavedBufferAttribute)} The buffer attribute.
*/
getObjectAttribute( /*object*/ ) {
return this.attribute;
}
}
/**
* Internal method for creating buffer attribute nodes.
*
* @private
* @param {BufferAttribute|InterleavedBuffer|TypedArray} array - The attribute data.
* @param {?string} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [stride=0] - The buffer stride.
* @param {number} [offset=0] - The buffer offset.
* @param {number} [usage=StaticDrawUsage] - The buffer usage.
* @param {boolean} [instanced=false] - Whether the buffer is instanced.
* @returns {BufferAttributeNode|Node} The buffer attribute node.
*/
function createBufferAttribute( array, type = null, stride = 0, offset = 0, usage = StaticDrawUsage, instanced = false ) {
if ( type === 'mat3' || ( type === null && array.itemSize === 9 ) ) {
return mat3(
new BufferAttributeNode( array, 'vec3', 9, 0 ).setUsage( usage ).setInstanced( instanced ),
new BufferAttributeNode( array, 'vec3', 9, 3 ).setUsage( usage ).setInstanced( instanced ),
new BufferAttributeNode( array, 'vec3', 9, 6 ).setUsage( usage ).setInstanced( instanced )
);
} else if ( type === 'mat4' || ( type === null && array.itemSize === 16 ) ) {
return mat4(
new BufferAttributeNode( array, 'vec4', 16, 0 ).setUsage( usage ).setInstanced( instanced ),
new BufferAttributeNode( array, 'vec4', 16, 4 ).setUsage( usage ).setInstanced( instanced ),
new BufferAttributeNode( array, 'vec4', 16, 8 ).setUsage( usage ).setInstanced( instanced ),
new BufferAttributeNode( array, 'vec4', 16, 12 ).setUsage( usage ).setInstanced( instanced )
);
}
return new BufferAttributeNode( array, type, stride, offset ).setUsage( usage );
}
/**
* TSL function for creating a buffer attribute node.
*
* @tsl
* @function
* @param {BufferAttribute|InterleavedBuffer|TypedArray} array - The attribute data.
* @param {?string} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [stride=0] - The buffer stride.
* @param {number} [offset=0] - The buffer offset.
* @returns {BufferAttributeNode|Node}
*/
const bufferAttribute = ( array, type = null, stride = 0, offset = 0 ) => createBufferAttribute( array, type, stride, offset );
/**
* TSL function for creating a buffer attribute node but with dynamic draw usage.
* Use this function if attribute data are updated per frame.
*
* @tsl
* @function
* @param {BufferAttribute|InterleavedBuffer|TypedArray} array - The attribute data.
* @param {?string} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [stride=0] - The buffer stride.
* @param {number} [offset=0] - The buffer offset.
* @returns {BufferAttributeNode|Node}
*/
const dynamicBufferAttribute = ( array, type = null, stride = 0, offset = 0 ) => createBufferAttribute( array, type, stride, offset, DynamicDrawUsage );
/**
* TSL function for creating a buffer attribute node but with enabled instancing
*
* @tsl
* @function
* @param {BufferAttribute|InterleavedBuffer|TypedArray} array - The attribute data.
* @param {?string} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [stride=0] - The buffer stride.
* @param {number} [offset=0] - The buffer offset.
* @returns {BufferAttributeNode|Node}
*/
const instancedBufferAttribute = ( array, type = null, stride = 0, offset = 0 ) => createBufferAttribute( array, type, stride, offset, StaticDrawUsage, true );
/**
* TSL function for creating a buffer attribute node but with dynamic draw usage and enabled instancing
*
* @tsl
* @function
* @param {BufferAttribute|InterleavedBuffer|TypedArray} array - The attribute data.
* @param {?string} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [stride=0] - The buffer stride.
* @param {number} [offset=0] - The buffer offset.
* @returns {BufferAttributeNode|Node}
*/
const instancedDynamicBufferAttribute = ( array, type = null, stride = 0, offset = 0 ) => createBufferAttribute( array, type, stride, offset, DynamicDrawUsage, true );
addMethodChaining( 'toAttribute', ( bufferNode ) => bufferAttribute( bufferNode.value, bufferNode.bufferType ) );
/**
* This class represents shader indices of different types. The following predefined node
* objects cover frequent use cases:
*
* - `vertexIndex`: The index of a vertex within a mesh.
* - `instanceIndex`: The index of either a mesh instance or an invocation of a compute shader.
* - `drawIndex`: The index of a draw call.
* - `invocationLocalIndex`: The index of a compute invocation within the scope of a workgroup.
* - `invocationSubgroupIndex`: The index of a compute invocation within the scope of a subgroup.
* - `subgroupIndex`: The index of a compute invocation's subgroup within its workgroup.
*
* @augments Node
*/
class IndexNode extends Node {
static get type() {
return 'IndexNode';
}
/**
* Constructs a new index node.
*
* @param {('vertex'|'instance'|'subgroup'|'invocationLocal'|'invocationSubgroup'|'draw')} scope - The scope of the index node.
*/
constructor( scope ) {
super( 'uint' );
/**
* The scope of the index node.
*
* @type {string}
*/
this.scope = scope;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isIndexNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( builder ) {
const nodeType = this.getNodeType( builder );
const scope = this.scope;
let propertyName;
if ( scope === IndexNode.VERTEX ) {
propertyName = builder.getVertexIndex();
} else if ( scope === IndexNode.INSTANCE ) {
propertyName = builder.getInstanceIndex();
} else if ( scope === IndexNode.DRAW ) {
propertyName = builder.getDrawIndex();
} else if ( scope === IndexNode.INVOCATION_LOCAL ) {
propertyName = builder.getInvocationLocalIndex();
} else if ( scope === IndexNode.INVOCATION_SUBGROUP ) {
propertyName = builder.getInvocationSubgroupIndex();
} else if ( scope === IndexNode.SUBGROUP ) {
propertyName = builder.getSubgroupIndex();
} else {
throw new Error( 'THREE.IndexNode: Unknown scope: ' + scope );
}
let output;
if ( builder.shaderStage === 'vertex' || builder.shaderStage === 'compute' ) {
output = propertyName;
} else {
const nodeVarying = varying( this );
output = nodeVarying.build( builder, nodeType );
}
return output;
}
}
IndexNode.VERTEX = 'vertex';
IndexNode.INSTANCE = 'instance';
IndexNode.SUBGROUP = 'subgroup';
IndexNode.INVOCATION_LOCAL = 'invocationLocal';
IndexNode.INVOCATION_SUBGROUP = 'invocationSubgroup';
IndexNode.DRAW = 'draw';
/**
* TSL object that represents the index of a vertex within a mesh.
*
* @tsl
* @type {IndexNode}
*/
const vertexIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.VERTEX );
/**
* TSL object that contextually represents specific index data depending on the shader stage.
*
* Within the vertex and fragment stages, `instanceIndex` will represent the index of the current mesh instance being evaluated by the shader.
* In these stages, use `instanceIndex` to modify a mesh based on its instance or to select per-instance data.
*
* ```js
* // instanceIndex will equal the current mesh's instance index between 0-500
* const material = new THREE.BasicNodeMaterial();
* material.positionNode = vec3( instanceIndex.mod( 10 ), instanceIndex.div( 10 ), 0 );
* const mesh = new THREE.InstancedMesh( geometry, material, 500 );
* ```
*
* Within the compute stage, `instanceIndex` will represent the global index of a compute invocation within the 3-dimensional compute workgroup load.
* In this stage, use `instanceIndex` to modify or select data at a given index within a buffer, or derive values from the index itself.
*
* ```js
* // instanceIndex will equal value between 0 - 255
* const computeFn = Fn() => {
*
* storageBuffer.element( instanceIndex ).assign( instanceIndex );
*
* } )().compute( 255 )
* ```
*
* @tsl
* @type {IndexNode}
*/
const instanceIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.INSTANCE );
/**
* TSL object that represents the index of the subgroup the current compute invocation belongs to.
* Subgroup indices are local to the workgroups to which they belong.
*
* ```js
* // Execute 12 compute threads with a workgroup size of 9. Example assumes a subgroup size of 3.
* const computeFn = Fn( () => {
*
* storageBufferOne.element( instanceIndex ).assign( subgroupIndex );
* storageBufferTwo.element( instanceIndex ).assign( workgroupId.x );
*
* } )().compute( 12, [ 9 ] );
*
* // instanceIndex = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
* // Buffer One ( Subgroup Index ) = [ 0, 0, 0, 1, 1, 1, 2, 2, 2, 0, 0, 0 ];
* // Buffer Two ( Workgroup ID ) = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 ];
* ```
*
* @tsl
* @type {IndexNode}
*/
const subgroupIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.SUBGROUP );
/**
* TSL object that represents the index of a compute invocation within the scope of a subgroup.
*
* ```js
* // Execute 12 compute threads with a workgroup size of 12. Example assumes a subgroup size of 3.
* const computeFn = Fn( () => {
*
* storageBufferOne.element( instanceIndex ).assign( invocationSubgroupIndex );
* storageBufferTwo.element( instanceIndex ).assign( subgroupIndex );
*
* } )().compute( 12, [ 12 ] );
*
* // instanceIndex = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
* // Buffer One ( Invocation Subgroup Index ) = [ 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2 ];
* // Buffer Two ( Subgroup Index ) = [ 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3 ];
* ```
*
* @tsl
* @type {IndexNode}
*/
const invocationSubgroupIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.INVOCATION_SUBGROUP );
/**
* TSL object that represents the index of a compute invocation within the scope of a workgroup.
*
* ```js
* // Execute 12 compute threads with a workgroup size of 4.
* const computeFn = Fn( () => {
*
* storageBufferOne.element( instanceIndex ).assign( invocationLocalIndex );
* storageBufferTwo.element( instanceIndex ).assign( workgroupId.x );
*
* } )().compute( 12, [ 4 ] );
*
* // instanceIndex = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
* // Buffer One ( Invocation Local Index ) = [ 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 ];
* // Buffer Two ( Workgroup ID ) = [ 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2 ];
* ```
*
* @tsl
* @type {IndexNode}
*/
const invocationLocalIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.INVOCATION_LOCAL );
/**
* TSL object that represents the index of a draw call.
*
* @tsl
* @type {IndexNode}
*/
const drawIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.DRAW );
/**
* Represents a compute shader node.
*
* @augments Node
*/
class ComputeNode extends Node {
static get type() {
return 'ComputeNode';
}
/**
* Constructs a new compute node.
*
* @param {Node} computeNode - The node that defines the compute shader logic.
* @param {Array<number>} workgroupSize - An array defining the X, Y, and Z dimensions of the workgroup for compute shader execution.
*/
constructor( computeNode, workgroupSize ) {
super( 'void' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isComputeNode = true;
/**
* The node that defines the compute shader logic.
*
* @type {Node}
*/
this.computeNode = computeNode;
/**
* An array defining the X, Y, and Z dimensions of the workgroup for compute shader execution.
*
* @type {Array<number>}
* @default [ 64 ]
*/
this.workgroupSize = workgroupSize;
/**
* The total number of threads (invocations) to execute. If it is a number, it will be used
* to automatically generate bounds checking against `instanceIndex`.
*
* @type {number|Array<number>}
*/
this.count = null;
/**
* The dispatch size for workgroups on X, Y, and Z axes.
* Used directly if `count` is not provided.
*
* @type {number|Array<number>}
*/
this.dispatchSize = null;
/**
* The version of the node.
*
* @type {number}
*/
this.version = 1;
/**
* The name or label of the uniform.
*
* @type {string}
* @default ''
*/
this.name = '';
/**
* A callback executed when the compute node finishes initialization.
*
* @type {?Function}
*/
this.onInitFunction = null;
/**
* A uniform node holding the dispatch count for bounds checking.
* Created automatically when `count` is a number.
*
* @type {?UniformNode}
*/
this.countNode = null;
/**
* The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node computes
* once per frame.
*
* @type {string}
* @default 'frame'
*/
this.updateBeforeType = NodeUpdateType.FRAME;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the {@link ComputeNode#name} property.
*
* @param {string} name - The name of the uniform.
* @return {ComputeNode} A reference to this node.
*/
setName( name ) {
this.name = name;
return this;
}
/**
* Sets the {@link ComputeNode#name} property.
*
* @deprecated
* @param {string} name - The name of the uniform.
* @return {ComputeNode} A reference to this node.
*/
label( name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() ); // @deprecated r179
return this.setName( name );
}
/**
* Sets the callback to run during initialization.
*
* @param {Function} callback - The callback function.
* @return {ComputeNode} A reference to this node.
*/
onInit( callback ) {
this.onInitFunction = callback;
return this;
}
/**
* Returns the update type of {@link Node#updateBefore}.
*
* @param {NodeFrame} [frame] - The current node frame.
* @return {NodeUpdateType} The update type.
*/
getUpdateBeforeType( frame ) {
return frame.compute !== this ? this.updateBeforeType : NodeUpdateType.NONE;
}
/**
* The method execute the compute for this node.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
updateBefore( { renderer } ) {
renderer.compute( this );
}
setup( builder ) {
if ( this.count !== null && this.countNode === null ) {
this.countNode = uniform( this.count, 'uint' ).onObjectUpdate( () => this.count );
}
const result = this.computeNode.build( builder );
if ( result ) {
const properties = builder.getNodeProperties( this );
properties.outputComputeNode = result.outputNode;
result.outputNode = null;
}
return result;
}
generate( builder, output ) {
const { shaderStage } = builder;
if ( shaderStage === 'compute' ) {
const snippet = this.computeNode.build( builder, 'void' );
if ( snippet !== '' ) {
builder.addLineFlowCode( snippet, this );
}
if ( this.count !== null && builder.allowEarlyReturns === true ) {
const countSnippet = this.countNode.build( builder, 'uint' );
const indexSnippet = instanceIndex.build( builder, 'uint' );
builder.flow.code = `${ builder.tab }if ( ${ indexSnippet } >= ${ countSnippet } ) { return; }\n\n${ builder.flow.code }`;
}
} else {
const properties = builder.getNodeProperties( this );
const outputComputeNode = properties.outputComputeNode;
if ( outputComputeNode ) {
return outputComputeNode.build( builder, output );
}
}
}
}
/**
* TSL function for creating a compute kernel node.
*
* @tsl
* @function
* @param {Node} node - The TSL logic for the compute shader.
* @param {Array<number>} [workgroupSize=[64]] - The workgroup size.
* @returns {ComputeNode}
*/
const computeKernel = ( node, workgroupSize = [ 64 ] ) => {
if ( workgroupSize.length === 0 || workgroupSize.length > 3 ) {
error( 'TSL: compute() workgroupSize must have 1, 2, or 3 elements', new StackTrace() );
}
for ( let i = 0; i < workgroupSize.length; i ++ ) {
const val = workgroupSize[ i ];
if ( typeof val !== 'number' || val <= 0 || ! Number.isInteger( val ) ) {
error( `TSL: compute() workgroupSize element at index [ ${ i } ] must be a positive integer`, new StackTrace() );
}
}
// Implicit fill-up to [ x, y, z ] with 1s, just like WGSL treats @workgroup_size when fewer dimensions are specified
while ( workgroupSize.length < 3 ) workgroupSize.push( 1 );
//
return new ComputeNode( nodeObject( node ), workgroupSize );
};
/**
* TSL function for creating a compute node.
*
* @tsl
* @function
* @param {Node} node - The TSL logic for the compute shader.
* @param {number|Array<number>} count - The compute count or dispatch size.
* @param {Array<number>} [workgroupSize=[64]] - The workgroup size.
* @returns {ComputeNode}
, */
const compute = ( node, count, workgroupSize ) => {
const computeNode = computeKernel( node, workgroupSize );
if ( typeof count === 'number' ) {
computeNode.count = count;
} else {
computeNode.dispatchSize = count;
}
return computeNode;
};
addMethodChaining( 'compute', compute );
addMethodChaining( 'computeKernel', computeKernel );
/**
* This node can be used as a cache management component for another node.
* Caching is in general used by default in {@link NodeBuilder} but this node
* allows the usage of a shared parent cache during the build process.
*
* @augments Node
*/
class IsolateNode extends Node {
static get type() {
return 'IsolateNode';
}
/**
* Constructs a new cache node.
*
* @param {Node} node - The node that should be cached.
* @param {boolean} [parent=true] - Whether this node refers to a shared parent cache or not.
*/
constructor( node, parent = true ) {
super();
/**
* The node that should be cached.
*
* @type {Node}
*/
this.node = node;
/**
* Whether this node refers to a shared parent cache or not.
*
* @type {boolean}
* @default true
*/
this.parent = parent;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isIsolateNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( builder ) {
const previousCache = builder.getCache();
const cache = builder.getCacheFromNode( this, this.parent );
builder.setCache( cache );
const nodeType = this.node.getNodeType( builder );
builder.setCache( previousCache );
return nodeType;
}
build( builder, ...params ) {
this._setupInspector( builder );
const previousCache = builder.getCache();
const cache = builder.getCacheFromNode( this, this.parent );
builder.setCache( cache );
const data = this.node.build( builder, ...params );
builder.setCache( previousCache );
return data;
}
setParent( parent ) {
this.parent = parent;
return this;
}
getParent() {
return this.parent;
}
}
/**
* TSL function for creating a cache node.
*
* @tsl
* @function
* @param {Node} node - The node that should be cached.
* @returns {IsolateNode}
*/
const isolate = ( node ) => new IsolateNode( nodeObject( node ) );
/**
* TSL function for creating a cache node.
*
* @tsl
* @function
* @deprecated
* @param {Node} node - The node that should be cached.
* @param {boolean} [parent=true] - Whether this node refers to a shared parent cache or not.
* @returns {IsolateNode}
*/
function cache( node, parent = true ) {
warn( 'TSL: "cache()" has been deprecated. Use "isolate()" instead.' ); // @deprecated r181
return isolate( node ).setParent( parent );
}
addMethodChaining( 'cache', cache );
addMethodChaining( 'isolate', isolate );
/**
* The class generates the code of a given node but returns another node in the output.
* This can be used to call a method or node that does not return a value, i.e.
* type `void` on an input where returning a value is required. Example:
*
* ```js
* material.colorNode = myColor.bypass( runVoidFn() )
*```
*
* @augments Node
*/
class BypassNode extends Node {
static get type() {
return 'BypassNode';
}
/**
* Constructs a new bypass node.
*
* @param {Node} outputNode - The output node.
* @param {Node} callNode - The call node.
*/
constructor( outputNode, callNode ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBypassNode = true;
/**
* The output node.
*
* @type {Node}
*/
this.outputNode = outputNode;
/**
* The call node.
*
* @type {Node}
*/
this.callNode = callNode;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( builder ) {
return this.outputNode.getNodeType( builder );
}
generate( builder ) {
const snippet = this.callNode.build( builder, 'void' );
if ( snippet !== '' ) {
builder.addLineFlowCode( snippet, this );
}
return this.outputNode.build( builder );
}
}
/**
* TSL function for creating a bypass node.
*
* @tsl
* @function
* @param {Node} outputNode - The output node.
* @param {Node} callNode - The call node.
* @returns {BypassNode}
*/
const bypass = /*@__PURE__*/ nodeProxy( BypassNode ).setParameterLength( 2 );
addMethodChaining( 'bypass', bypass );
/**
* This node allows to remap a node value from one range into another. E.g a value of
* `0.4` in the range `[ 0.3, 0.5 ]` should be remapped into the normalized range `[ 0, 1 ]`.
* `remap` takes care of that and converts the original value of `0.4` to `0.5`.
*
* @tsl
* @function
* @param {Node} node - The node that should be remapped.
* @param {Node} inLowNode - The source or current lower bound of the range.
* @param {Node} inHighNode - The source or current upper bound of the range.
* @param {?Node} [outLowNode=float(0)] - The target lower bound of the range.
* @param {?Node} [outHighNode=float(1)] - The target upper bound of the range.
* @returns {Node}
*/
const remap = /*@__PURE__*/ Fn( ( [ node, inLowNode, inHighNode, outLowNode = float( 0 ), outHighNode = float( 1 ), doClamp = bool( false ) ] ) => {
let t = node.sub( inLowNode ).div( inHighNode.sub( inLowNode ) );
if ( defined( doClamp ) ) t = t.clamp();
return t.mul( outHighNode.sub( outLowNode ) ).add( outLowNode );
} );
/**
* This node allows to remap a node value from one range into another but with enabled clamping. E.g a value of
* `0.4` in the range `[ 0.3, 0.5 ]` should be remapped into the normalized range `[ 0, 1 ]`.
* `remapClamp` takes care of that and converts the original value of `0.4` to `0.5`.
*
* @tsl
* @function
* @param {Node} node - The node that should be remapped.
* @param {Node} inLowNode - The source or current lower bound of the range.
* @param {Node} inHighNode - The source or current upper bound of the range.
* @param {?Node} [outLowNode=float(0)] - The target lower bound of the range.
* @param {?Node} [outHighNode=float(1)] - The target upper bound of the range.
* @returns {Node}
*/
function remapClamp( node, inLowNode, inHighNode, outLowNode = float( 0 ), outHighNode = float( 1 ) ) {
return remap( node, inLowNode, inHighNode, outLowNode, outHighNode, true );
}
addMethodChaining( 'remap', remap );
addMethodChaining( 'remapClamp', remapClamp );
/**
* This class can be used to implement basic expressions in shader code.
* Basic examples for that are `return`, `continue` or `discard` statements.
*
* @augments Node
*/
class ExpressionNode extends Node {
static get type() {
return 'ExpressionNode';
}
/**
* Constructs a new expression node.
*
* @param {string} [snippet=''] - The native code snippet.
* @param {string} [nodeType='void'] - The node type.
*/
constructor( snippet = '', nodeType = 'void' ) {
super( nodeType );
/**
* The native code snippet.
*
* @type {string}
* @default ''
*/
this.snippet = snippet;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( builder, output ) {
const type = this.getNodeType( builder );
const snippet = this.snippet;
if ( type === 'void' ) {
builder.addLineFlowCode( snippet, this );
} else {
return builder.format( snippet, type, output );
}
}
}
/**
* TSL function for creating an expression node.
*
* @tsl
* @function
* @param {string} [snippet] - The native code snippet.
* @param {?string} [nodeType='void'] - The node type.
* @returns {ExpressionNode}
*/
const expression = /*@__PURE__*/ nodeProxy( ExpressionNode ).setParameterLength( 1, 2 );
/**
* Represents a `discard` shader operation in TSL.
*
* @tsl
* @function
* @param {?ConditionalNode} conditional - An optional conditional node. It allows to decide whether the discard should be executed or not.
* @return {Node} The `discard` expression.
*/
const Discard = ( conditional ) => ( conditional ? select( conditional, expression( 'discard' ) ) : expression( 'discard' ) ).toStack();
/**
* Represents a `return` shader operation in TSL.
*
* @tsl
* @function
* @return {ExpressionNode} The `return` expression.
*/
const Return = () => expression( 'return' ).toStack();
addMethodChaining( 'discard', Discard );
/**
* Premultiplies the RGB channels of a color by its alpha channel.
*
* This function is useful for converting a non-premultiplied alpha color
* into a premultiplied alpha format, where the RGB values are scaled
* by the alpha value. Premultiplied alpha is often used in graphics
* rendering for certain operations, such as compositing and image processing.
*
* @tsl
* @function
* @param {Node<vec4>} color - The input color with non-premultiplied alpha.
* @return {Node<vec4>} The color with premultiplied alpha.
*/
const premultiplyAlpha = /*@__PURE__*/ Fn( ( [ color ] ) => {
return vec4( color.rgb.mul( color.a ), color.a );
}, { color: 'vec4', return: 'vec4' } );
/**
* Unpremultiplies the RGB channels of a color by its alpha channel.
*
* This function is useful for converting a premultiplied alpha color
* back into a non-premultiplied alpha format, where the RGB values are
* divided by the alpha value. Unpremultiplied alpha is often used in graphics
* rendering for certain operations, such as compositing and image processing.
*
* @tsl
* @function
* @param {Node<vec4>} color - The input color with premultiplied alpha.
* @return {Node<vec4>} The color with non-premultiplied alpha.
*/
const unpremultiplyAlpha = /*@__PURE__*/ Fn( ( [ color ] ) => {
return color.a.equal( 0 ).select( vec4( 0 ), vec4( color.rgb.div( color.a ), color.a ) );
}, { color: 'vec4', return: 'vec4' } );
/**
* Normally, tone mapping and color conversion happens automatically just
* before outputting a pixel to the default (screen) framebuffer. In certain
* post processing setups this is too late because some effects such as FXAA
* require e.g. sRGB input. For such scenarios, `RenderOutputNode` can be used
* to apply tone mapping and color space conversion at an arbitrary point
* in the effect chain.
*
* When applying tone mapping and color space conversion manually with this node,
* you have to set {@link RenderPipeline#outputColorTransform} to `false`.
*
* ```js
* const postProcessing = new RenderPipeline( renderer );
* postProcessing.outputColorTransform = false;
*
* const scenePass = pass( scene, camera );
* const outputPass = renderOutput( scenePass );
*
* postProcessing.outputNode = outputPass;
* ```
*
* @augments Node
*/
class RenderOutputNode extends Node {
static get type() {
return 'RenderOutputNode';
}
/**
* Constructs a new render output node.
*
* @param {Node} colorNode - The color node to process.
* @param {?number} toneMapping - The tone mapping type.
* @param {?string} outputColorSpace - The output color space.
*/
constructor( colorNode, toneMapping, outputColorSpace ) {
super( 'vec4' );
/**
* The color node to process.
*
* @type {Node}
*/
this.colorNode = colorNode;
/**
* The tone mapping type.
*
* @private
* @type {?number}
*/
this._toneMapping = toneMapping;
/**
* The output color space.
*
* @type {?string}
*/
this.outputColorSpace = outputColorSpace;
/**
* Whether the color node uses premultiplied alpha. The output
* uses the same alpha mode.
*
* @type {boolean}
* @default true
*/
this.premultipliedAlpha = true;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderOutputNode = true;
}
/**
* Sets the tone mapping type.
*
* @param {number} value - The tone mapping type.
* @return {ToneMappingNode} A reference to this node.
*/
setToneMapping( value ) {
this._toneMapping = value;
return this;
}
/**
* Gets the tone mapping type.
*
* @returns {number} The tone mapping type.
*/
getToneMapping() {
return this._toneMapping;
}
setup( { context } ) {
let outputNode = this.colorNode || context.color;
// clamp alpha
outputNode = vec4( outputNode.rgb, outputNode.a.clamp( 0.0, 1.0 ) );
// unpremultiply
if ( this.premultipliedAlpha === true ) {
outputNode = unpremultiplyAlpha( outputNode );
}
// tone mapping
const toneMapping = ( this._toneMapping !== null ? this._toneMapping : context.toneMapping ) || NoToneMapping;
const outputColorSpace = ( this.outputColorSpace !== null ? this.outputColorSpace : context.outputColorSpace ) || NoColorSpace;
if ( toneMapping !== NoToneMapping ) {
outputNode = outputNode.toneMapping( toneMapping );
}
// working to output color space
if ( outputColorSpace !== NoColorSpace && outputColorSpace !== ColorManagement.workingColorSpace ) {
outputNode = outputNode.workingToColorSpace( outputColorSpace );
}
// premultiply in output color space
if ( this.premultipliedAlpha === true ) {
outputNode = premultiplyAlpha( outputNode );
}
return outputNode;
}
}
/**
* TSL function for creating a render output node.
*
* @tsl
* @function
* @param {Node} color - The color node to process.
* @param {?number} [toneMapping=null] - The tone mapping type.
* @param {?string} [outputColorSpace=null] - The output color space.
* @returns {RenderOutputNode}
*/
const renderOutput = ( color, toneMapping = null, outputColorSpace = null ) => new RenderOutputNode( nodeObject( color ), toneMapping, outputColorSpace );
addMethodChaining( 'renderOutput', renderOutput );
class DebugNode extends Node {
static get type() {
return 'DebugNode';
}
constructor( node, callback = null ) {
super();
this.node = node;
this.callback = callback;
}
generateNodeType( builder ) {
return this.node.getNodeType( builder );
}
setup( builder ) {
return this.node.build( builder );
}
analyze( builder ) {
return this.node.build( builder );
}
generate( builder ) {
const callback = this.callback;
const snippet = this.node.build( builder );
if ( callback !== null ) {
callback( builder, snippet );
} else {
const title = '--- TSL debug - ' + builder.shaderStage + ' shader ---';
const border = '-'.repeat( title.length );
let code = '';
code += '// #' + title + '#\n';
code += builder.flow.code.replace( /^\t/mg, '' ) + '\n';
code += '/* ... */ ' + snippet + ' /* ... */\n';
code += '// #' + border + '#\n';
log$1( code );
}
return snippet;
}
}
/**
* TSL function for creating a debug node.
*
* @tsl
* @function
* @param {Node} node - The node to debug.
* @param {?Function} [callback=null] - Optional callback function to handle the debug output.
* @returns {DebugNode}
*/
const debug = ( node, callback = null ) => new DebugNode( nodeObject( node ), callback ).toStack();
addMethodChaining( 'debug', debug );
/**
* InspectorBase is the base class for all inspectors. Can only be used with {@link WebGPURenderer}.
*
* @augments EventDispatcher
*/
class InspectorBase extends EventDispatcher {
/**
* Creates a new InspectorBase.
*/
constructor() {
super();
/**
* The renderer associated with this inspector.
*
* @type {Renderer}
* @private
*/
this._renderer = null;
/**
* The current frame being processed.
*
* @type {Object}
*/
this.currentFrame = null;
/**
* Indicates whether the inspector is running.
*
* @type {boolean}
* @default false
*/
this.isRunning = false;
/**
* Indicates whether the inspector is enabled.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
}
/**
* Returns the node frame for the current renderer.
*
* @return {Object} The node frame.
*/
get nodeFrame() {
return this._renderer._nodes.nodeFrame;
}
/**
* Sets the renderer for this inspector.
*
* @param {Renderer} renderer - The renderer to associate with this inspector.
* @return {InspectorBase} This inspector instance.
*/
setRenderer( renderer ) {
this._renderer = renderer;
return this;
}
/**
* Returns the renderer associated with this inspector.
*
* @return {Renderer} The associated renderer.
*/
getRenderer() {
return this._renderer;
}
/**
* Called when a frame begins.
*/
begin() {
this.isRunning = true;
}
/**
* Called when a frame ends.
*/
finish() {
this.isRunning = false;
}
/**
* Inspects a node.
*
* @param {Node} node - The node to inspect.
*/
inspect( /*node*/ ) { }
/**
* When a compute operation is performed.
*
* @param {ComputeNode} computeNode - The compute node being executed.
* @param {number|Array<number>} dispatchSizeOrCount - The dispatch size or count.
*/
computeAsync( /*computeNode, dispatchSizeOrCount*/ ) { }
/**
* Called when a compute operation begins.
*
* @param {string} uid - A unique identifier for the render context.
* @param {ComputeNode} computeNode - The compute node being executed.
*/
beginCompute( /*uid, computeNode*/ ) { }
/**
* Called when a compute operation ends.
*
* @param {string} uid - A unique identifier for the render context.
* @param {ComputeNode} computeNode - The compute node being executed.
*/
finishCompute( /*uid*/ ) { }
/**
* Called when a render operation begins.
*
* @param {string} uid - A unique identifier for the render context.
* @param {Scene} scene - The scene being rendered.
* @param {Camera} camera - The camera being used for rendering.
* @param {?RenderTarget} renderTarget - The render target, if any.
*/
beginRender( /*uid, scene, camera, renderTarget*/ ) { }
/**
* Called when an animation loop ends.
*
* @param {string} uid - A unique identifier for the render context.
*/
finishRender( /*uid*/ ) { }
/**
* Called when a texture copy operation is performed.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
*/
copyTextureToTexture( /*srcTexture, dstTexture*/ ) { }
/**
* Called when a framebuffer copy operation is performed.
*
* @param {Texture} framebufferTexture - The texture associated with the framebuffer.
*/
copyFramebufferToTexture( /*framebufferTexture*/ ) { }
/**
* Frees all internal resources of the inspector.
*/
dispose() {
if ( this.isRunning === true ) this.finish();
this.dispatchEvent( { type: 'dispose' } );
}
}
/**
* InspectorNode is a wrapper node that allows inspection of node values during rendering.
* It can be used to debug or analyze node outputs in the rendering pipeline.
*
* The inspector node is registered by the inspected node when it is built, see {@link Node#_setupInspector}.
*
* @augments Node
*/
class InspectorNode extends Node {
/**
* Returns the type of the node.
*
* @returns {string}
*/
static get type() {
return 'InspectorNode';
}
/**
* Creates an InspectorNode.
*
* @param {Node} node - The node to inspect.
* @param {string} [name=''] - Optional name for the inspector node.
* @param {Function|null} [callback=null] - Optional callback to modify the node presented by the inspector.
*/
constructor( node, name = '', callback = null ) {
super();
this.node = node;
this.name = name;
this.callback = callback;
this.updateType = NodeUpdateType.FRAME;
this.isInspectorNode = true;
}
/**
* Returns the name of the inspector node.
*
* @returns {string}
*/
getName() {
return this.name || this.node.name;
}
/**
* Updates the inspector node, allowing inspection of the wrapped node.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
const { renderer } = frame;
if ( renderer.backend.isWebGPUBackend !== true && renderer.inspector.constructor !== InspectorBase ) {
warnOnce( 'TSL: ".toInspector()" is only available with WebGPU.' );
}
renderer.inspector.inspect( this );
}
/**
* Returns the type of the wrapped node.
*
* @param {NodeBuilder} builder - The node builder.
* @returns {string}
*/
generateNodeType( builder ) {
return this.node.getNodeType( builder );
}
}
/**
* Creates an inspector node to wrap around a given node for inspection purposes.
*
* @tsl
* @param {Node} node - The node to inspect.
* @param {string} [name=''] - Optional name for the inspector node.
* @param {Function|null} [callback=null] - Optional callback to modify the node presented by the inspector.
* @returns {Node} The inspected node.
*/
function inspect( node, name = '', callback = null ) {
node = nodeObject( node );
node._inspectorNode = new InspectorNode( node, name, callback );
return node;
}
addMethodChaining( 'toInspector', inspect );
function addNodeElement( name/*, nodeElement*/ ) {
warn( 'TSL: AddNodeElement has been removed in favor of tree-shaking. Trying add', name );
}
/**
* Base class for representing shader attributes as nodes.
*
* @augments Node
*/
class AttributeNode extends Node {
static get type() {
return 'AttributeNode';
}
/**
* Constructs a new attribute node.
*
* @param {string} attributeName - The name of the attribute.
* @param {?string} nodeType - The node type.
*/
constructor( attributeName, nodeType = null ) {
super( nodeType );
/**
* `AttributeNode` sets this property to `true` by default.
*
* @type {boolean}
* @default true
*/
this.global = true;
this._attributeName = attributeName;
}
isCacheable( /*builder*/ ) {
return false;
}
getHash( builder ) {
return this.getAttributeName( builder );
}
/**
* Returns the node which provides the value of this attribute in the current context, if any.
* The context can provide attributes with a `getAttribute( name, builder )` function, e.g. if the
* vertices are pulled from a storage buffer or the surface is reconstructed per fragment. The
* function should return the same node for the same attribute.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?Node} The node providing the value of the attribute.
*/
getContextAttribute( builder ) {
const getAttribute = builder.context.getAttribute;
return getAttribute ? getAttribute( this.getAttributeName( builder ), builder ) : null;
}
/**
* An attribute provided by the context is replaced by the provided node, like an override.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The shared node if possible. Otherwise `this` is returned.
*/
getShared( builder ) {
return this.getContextAttribute( builder ) || super.getShared( builder );
}
generateNodeType( builder ) {
const contextAttribute = this.getContextAttribute( builder );
if ( contextAttribute ) return contextAttribute.getNodeType( builder );
let nodeType = this.nodeType;
if ( nodeType === null ) {
const attributeName = this.getAttributeName( builder );
if ( builder.hasGeometryAttribute( attributeName ) ) {
const attribute = builder.geometry.getAttribute( attributeName );
nodeType = builder.getTypeFromAttribute( attribute );
} else {
nodeType = 'float';
}
}
return nodeType;
}
/**
* Sets the attribute name to the given value. The method can be
* overwritten in derived classes if the final name must be computed
* analytically.
*
* @param {string} attributeName - The name of the attribute.
* @return {AttributeNode} A reference to this node.
*/
setAttributeName( attributeName ) {
this._attributeName = attributeName;
return this;
}
/**
* Returns the attribute name of this node. The method can be
* overwritten in derived classes if the final name must be computed
* analytically.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The attribute name.
*/
getAttributeName( /*builder*/ ) {
return this._attributeName;
}
generate( builder ) {
const attributeName = this.getAttributeName( builder );
const nodeType = this.getNodeType( builder );
const geometryAttribute = builder.hasGeometryAttribute( attributeName );
if ( geometryAttribute === true ) {
const attribute = builder.geometry.getAttribute( attributeName );
const attributeType = builder.getTypeFromAttribute( attribute );
const nodeAttribute = builder.getAttribute( attributeName, attributeType );
if ( builder.shaderStage === 'vertex' ) {
return builder.format( nodeAttribute.name, attributeType, nodeType );
} else {
const nodeVarying = varying( this );
return nodeVarying.build( builder, nodeType );
}
} else {
warn( `AttributeNode: Vertex attribute "${ attributeName }" not found on geometry.` );
return builder.generateConst( nodeType );
}
}
serialize( data ) {
super.serialize( data );
data.global = this.global;
data._attributeName = this._attributeName;
}
deserialize( data ) {
super.deserialize( data );
this.global = data.global;
this._attributeName = data._attributeName;
}
}
/**
* TSL function for creating an attribute node.
*
* @tsl
* @function
* @param {string} name - The name of the attribute.
* @param {?string} [nodeType=null] - The node type.
* @returns {AttributeNode}
*/
const attribute = ( name, nodeType = null ) => new AttributeNode( name, nodeType );
/**
* A specialized context node designed to override specific target nodes within a
* node sub-graph or flow. This allows replacing specific inputs (e.g., normal
* and position vectors) dynamically during compilation for a specific flow node,
* without having to reconstruct or duplicate the source nodes.
*
* ```js
* // Method chaining example:
* node.overrideNode( positionLocal, () => positionLocal.add( vec3( 1, 0, 0 ) ) );
*
* // Context assignment example:
* material.contextNode = overrideNode( positionLocal, () => positionLocal.add( vec3( 1, 0, 0 ) ) );
* ```
*
* @augments ContextNode
*/
class OverrideContextNode extends ContextNode {
/**
* Returns the type of the node.
*
* @type {string}
* @readonly
* @static
*/
static get type() {
return 'OverrideContextNode';
}
/**
* Constructs a new override context node.
*
* @param {Map<Node, Function>} overrideNodes - A map mapping target nodes to their respective override callback functions.
* @param {Node|null} [flowNode=null] - The node whose context should be modified.
* @param {?string} [shaderStage=null] - The shader stage the overrides apply to, all stages by default.
*/
constructor( overrideNodes, flowNode = null, shaderStage = null ) {
const value = { overrideNodes };
// the surface is replaced per fragment, the varyings are evaluated in the fragment stage as well
if ( shaderStage === 'fragment' ) value.inlineVaryings = true;
super( flowNode, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOverrideContextNode = true;
}
}
/**
* TSL function for creating an `OverrideContextNode` to override a single target node.
*
* ```js
* material.contextNode = overrideNode( positionLocal, ( builder ) => positionLocal.add( vec3( 1, 0, 0 ) ) );
* ```
*
* @tsl
* @function
* @param {Node} targetNode - The target node that should be overridden.
* @param {Function|Node|null} [callback=null] - A callback function returning the overriding node (which receives the builder as its argument), or the overriding node itself.
* @param {Node|null} [flowNode=null] - The node whose context should be modified.
* @return {OverrideContextNode} The created override context node.
*/
function overrideNode( targetNode, callback = null, flowNode = null ) {
if ( callback && callback.isNode ) {
const node = callback;
callback = () => node;
}
return new OverrideContextNode( new Map( [[ targetNode, callback ]] ), flowNode );
}
addMethodChaining( 'overrideNode', ( flowNode, node, callback ) => overrideNode( node, callback, flowNode ) );
/**
* TSL function for creating an `OverrideContextNode` to override multiple target nodes.
*
* ```js
* material.contextNode = overrideNodes( [
* [ positionView, customPositionView ],
* [ positionViewDirection, ( builder ) => customPositionViewDirection ]
* ] );
* ```
*
* @tsl
* @function
* @param {Map<Node, (Function|Node)>|Array<Array<Node|Function|Node>>} overrides - The overrides mapping target nodes to callback functions or overriding nodes.
* @param {Node|null} [flowNode=null] - The node whose context should be modified.
* @param {?string} [shaderStage=null] - The shader stage the overrides apply to, all stages by default. With fragment overrides,
* all varyings of the context are evaluated in the fragment stage, e.g. if the surface is reconstructed per fragment.
* @return {OverrideContextNode} The created override context node.
*/
function overrideNodes( overrides, flowNode = null, shaderStage = null ) {
const overrideNodesMap = new Map();
for ( const [ node, value ] of overrides ) {
let callback = null;
if ( value !== null ) {
callback = typeof value === 'function' ? value : () => value;
if ( shaderStage !== null ) callback = getStageCallback( node, callback, shaderStage );
}
overrideNodesMap.set( node, callback );
}
return new OverrideContextNode( overrideNodesMap, flowNode, shaderStage );
}
addMethodChaining( 'overrideNodes', ( flowNode, overrides, shaderStage ) => overrideNodes( overrides, flowNode, shaderStage ) );
/**
* Returns an override callback which only overrides the node in the given shader stage.
*
* @private
* @param {Node} node - The target node.
* @param {Function} callback - The override callback.
* @param {string} shaderStage - The shader stage.
* @return {Function} The override callback of the shader stage.
*/
function getStageCallback( node, callback, shaderStage ) {
return ( builder ) => builder.shaderStage === shaderStage ? callback( builder ) : node;
}
/**
* Special version of {@link PropertyNode} which is used for parameters.
*
* @augments PropertyNode
*/
class ParameterNode extends PropertyNode {
static get type() {
return 'ParameterNode';
}
/**
* Constructs a new parameter node.
*
* @param {string} nodeType - The type of the node.
* @param {?string} [name=null] - The name of the parameter in the shader.
*/
constructor( nodeType, name = null ) {
super( nodeType, name );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isParameterNode = true;
}
/**
* Gets the type of a member variable in the parameter node.
*
* @param {NodeBuilder} builder - The node builder.
* @param {string} name - The name of the member variable.
* @returns {string}
*/
getMemberType( builder, name ) {
const type = this.getNodeType( builder );
const struct = builder.getStructTypeNode( type );
let memberType;
if ( struct !== null ) {
memberType = struct.getMemberType( builder, name );
} else {
error( `TSL: Member "${ name }" not found in struct "${ type }".`, new StackTrace() );
memberType = 'float';
}
return memberType;
}
getHash() {
return String( this.id );
}
generate() {
return this.name;
}
}
/**
* TSL function for creating a parameter node.
*
* @tsl
* @function
* @param {string} type - The type of the node.
* @param {?string} name - The name of the parameter in the shader.
* @returns {ParameterNode}
*/
const parameter = ( type, name ) => new ParameterNode( type, name );
/**
* Whether the given node is assignable but has not been assigned. It is just
* an expression, which is generated where it is used instead of in the stack.
*
* @private
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} node - The node to check.
* @return {boolean} Whether the assignable node can be generated as an expression.
*/
const isAssignableExpression = ( builder, node ) => {
if ( node.assignable !== true ) return false;
const data = builder.getDataFromNode( node );
return data.forceDeclaration !== true && data.assign !== true;
};
/**
* Stack is a helper for Nodes that need to produce stack-based code instead of continuous flow.
* They are usually needed in cases like `If`, `Else`.
*
* @augments Node
*/
class StackNode extends Node {
static get type() {
return 'StackNode';
}
/**
* Constructs a new stack node.
*
* @param {?StackNode} [parent=null] - The parent stack node.
*/
constructor( parent = null ) {
super();
/**
* List of nodes.
*
* @type {Array<Node>}
*/
this.nodes = [];
/**
* The output node.
*
* @type {?Node}
* @default null
*/
this.outputNode = null;
/**
* The parent stack node.
*
* @type {?StackNode}
* @default null
*/
this.parent = parent;
/**
* The current conditional node.
*
* @private
* @type {ConditionalNode}
* @default null
*/
this._currentCond = null;
/**
* The expression node. Only
* relevant for Switch/Case.
*
* @private
* @type {Node}
* @default null
*/
this._expressionNode = null;
/**
* The current node being processed.
*
* @private
* @type {Node}
* @default null
*/
this._currentNode = null;
/**
* Stores additional data for nodes that are added to the stack.
*
* @private
* @type {Map<Node, {delta: number}>}
*/
this._nodeDataLibrary = new Map();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStackNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
getElementType( builder ) {
return this.outputNode ? this.outputNode.getElementType( builder ) : 'void';
}
generateNodeType( builder ) {
return this.outputNode ? this.outputNode.getNodeType( builder ) : 'void';
}
getMemberType( builder, name ) {
return this.outputNode ? this.outputNode.getMemberType( builder, name ) : 'void';
}
/**
* Adds a node to this stack.
*
* @param {Node} node - The node to add.
* @param {number} [index=-1] - The index of the node. If not specified, the node will be added to the end of the stack.
* @return {StackNode} A reference to this stack node.
*/
addToStack( node, index = -1 ) {
if ( node.isNode !== true ) {
error( 'TSL: Invalid node added to stack.', new StackTrace() );
return this;
}
if ( index === -1 ) {
if ( this._currentNode ) {
let nodeData = this._nodeDataLibrary.get( this._currentNode );
if ( nodeData === undefined ) {
nodeData = {
delta: 0
};
this._nodeDataLibrary.set( this._currentNode, nodeData );
}
nodeData.delta ++;
index = this.nodes.indexOf( this._currentNode ) + nodeData.delta;
} else {
index = this.nodes.length;
}
}
this.nodes.splice( index, 0, node );
return this;
}
/**
* Adds a node to the stack before the given node, by default the current node.
*
* @param {Node} node - The node to add.
* @param {?Node} [beforeNode=this._currentNode] - The node of the stack that the node is added before.
* @return {StackNode} A reference to this stack node.
*/
addToStackBefore( node, beforeNode = this._currentNode ) {
const index = beforeNode ? this.nodes.indexOf( beforeNode ) : 0;
return this.addToStack( node, index );
}
/**
* Represent an `if` statement in TSL.
*
* @param {Node} boolNode - Represents the condition.
* @param {Function} method - TSL code which is executed if the condition evaluates to `true`.
* @return {StackNode} A reference to this stack node.
*/
If( boolNode, method ) {
const methodNode = new ShaderNode( method );
this._currentCond = select( boolNode, methodNode );
return this.addToStack( this._currentCond );
}
/**
* Represent an `elseif` statement in TSL.
*
* @param {Node} boolNode - Represents the condition.
* @param {Function} method - TSL code which is executed if the condition evaluates to `true`.
* @return {StackNode} A reference to this stack node.
*/
ElseIf( boolNode, method ) {
const methodNode = new ShaderNode( method );
const ifNode = select( boolNode, methodNode );
this._currentCond.elseNode = ifNode;
this._currentCond = ifNode;
return this;
}
/**
* Represent an `else` statement in TSL.
*
* @param {Function} method - TSL code which is executed in the `else` case.
* @return {StackNode} A reference to this stack node.
*/
Else( method ) {
this._currentCond.elseNode = new ShaderNode( method );
return this;
}
/**
* Represents a `switch` statement in TSL.
*
* @param {any} expression - Represents the expression.
* @return {StackNode} A reference to this stack node.
*/
Switch( expression ) {
this._expressionNode = nodeObject( expression );
return this;
}
/**
* Represents a `case` statement in TSL. The TSL version accepts an arbitrary numbers of values.
* The last parameter must be the callback method that should be executed in the `true` case.
*
* @param {...any} params - The values of the `Case()` statement as well as the callback method.
* @return {StackNode} A reference to this stack node.
*/
Case( ...params ) {
const caseNodes = [];
// extract case nodes from the parameter list
if ( params.length >= 2 ) {
for ( let i = 0; i < params.length - 1; i ++ ) {
caseNodes.push( this._expressionNode.equal( nodeObject( params[ i ] ) ) );
}
} else {
error( 'TSL: Invalid parameter length. Case() requires at least two parameters.', new StackTrace() );
}
// extract method
const method = params[ params.length - 1 ];
const methodNode = new ShaderNode( method );
// chain multiple cases when using Case( 1, 2, 3, () => {} )
let caseNode = caseNodes[ 0 ];
for ( let i = 1; i < caseNodes.length; i ++ ) {
caseNode = caseNode.or( caseNodes[ i ] );
}
// build condition
const condNode = select( caseNode, methodNode );
if ( this._currentCond === null ) {
this._currentCond = condNode;
return this.addToStack( this._currentCond );
} else {
this._currentCond.elseNode = condNode;
this._currentCond = condNode;
return this;
}
}
/**
* Represents the default code block of a Switch/Case statement.
*
* @param {Function} method - TSL code which is executed in the `else` case.
* @return {StackNode} A reference to this stack node.
*/
Default( method ) {
this.Else( method );
return this;
}
setup( builder ) {
const nodeProperties = builder.getNodeProperties( this );
let index = 0;
for ( const childNode of this.getChildren() ) {
if ( isAssignableExpression( builder, childNode ) ) continue;
nodeProperties[ 'node' + index ++ ] = childNode;
}
// return a outputNode if exists or null
return nodeProperties.outputNode || null;
}
build( builder, ...params ) {
this._setupInspector( builder );
const previousStack = getCurrentStack();
const buildStage = builder.buildStage;
setCurrentStack( this );
builder.setActiveStack( this );
//
for ( let i = 0; i < this.nodes.length; i ++ ) {
const node = this.nodes[ i ];
const previousNode = this._currentNode;
this._currentNode = node;
if ( isAssignableExpression( builder, node ) ) continue;
if ( buildStage === 'setup' ) {
node.build( builder );
} else if ( buildStage === 'analyze' ) {
node.build( builder, this );
} else if ( buildStage === 'generate' ) {
const stages = builder.getDataFromNode( node, 'any' ).stages;
const parents = stages && stages[ builder.shaderStage ];
if ( node.isVarNode && parents && parents.length === 1 && parents[ 0 ] && parents[ 0 ].isStackNode ) {
continue; // skip var nodes that are only used in .toVarying()
}
node.build( builder, 'void' );
}
this._currentNode = previousNode;
}
//
let result;
if ( this.outputNode ) {
const buildResult = this.outputNode.build( builder, ...params );
if ( builder.buildStage !== 'generate' || this.outputNode.getNodeType( builder ) !== 'void' ) {
result = buildResult;
}
} else {
result = super.build( builder, ...params );
}
setCurrentStack( previousStack );
builder.removeActiveStack( this );
return result;
}
}
/**
* TSL function for creating a stack node.
*
* @tsl
* @function
* @param {?StackNode} [parent=null] - The parent stack node.
* @returns {StackNode}
*/
const stack = /*@__PURE__*/ nodeProxy( StackNode ).setParameterLength( 0, 1 );
/**
* Generates a layout for struct members.
* This function takes an object representing struct members and returns an array of member layouts.
* Each member layout includes the member's name, type, and whether it is atomic.
*
* @param {Object.<string, string|Object>} members - An object where keys are member names and values are either types (as strings) or objects with type and atomic properties.
* @returns {Array.<{name: string, type: string, atomic: boolean}>} An array of member layouts.
*/
function getMembersLayout( members ) {
return Object.entries( members ).map( ( [ name, value ] ) => {
if ( typeof value === 'string' ) {
return { name, type: value, atomic: false };
}
return { name, type: value.type, atomic: value.atomic || false };
} );
}
/**
* Represents a struct type node in the node-based system.
* This class is used to define and manage the layout and types of struct members.
* It extends the base Node class and provides methods to get the length of the struct,
* retrieve member types, and generate the struct type for a builder.
*
* @augments Node
*/
class StructTypeNode extends Node {
static get type() {
return 'StructTypeNode';
}
/**
* Creates an instance of StructTypeNode.
*
* @param {Object} membersLayout - The layout of the members for the struct.
* @param {?string} [name=null] - The optional name of the struct.
*/
constructor( membersLayout, name = null ) {
super( 'struct' );
/**
* The layout of the members for the struct
*
* @type {Array.<{name: string, type: string, atomic: boolean}>}
*/
this.membersLayout = getMembersLayout( membersLayout );
/**
* The name of the struct.
*
* @type {?string}
* @default null
*/
this.name = name;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStructTypeNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns the length of the struct in 4-byte elements (e.g. float or int components).
* The length is calculated by summing the lengths of the struct's members, accounting for memory alignment.
* To get the size in bytes, multiply the returned value by 4.
*
* @returns {number} The length of the struct in 4-byte elements.
*/
getLength() {
let maxAlignment = 1; // maximum alignment value in this struct
let offset = 0; // global buffer offset in 4 byte elements
for ( const member of this.membersLayout ) {
const type = member.type;
const itemSize = getMemoryLengthFromType( type );
const alignment = getAlignmentFromType( type );
maxAlignment = Math.max( maxAlignment, alignment );
const chunkOffset = offset % maxAlignment; // offset in the current chunk of maxAlignment elements
const overhang = chunkOffset % alignment; // distance from the last aligned offset
if ( overhang !== 0 ) {
offset += alignment - overhang; // move to next aligned offset
}
offset += itemSize;
}
return ( Math.ceil( offset / maxAlignment ) * maxAlignment ); // ensure length is a multiple of maxAlignment
}
getMemberType( builder, name ) {
const member = this.membersLayout.find( m => m.name === name );
return member ? member.type : 'void';
}
generateNodeType( builder ) {
const structType = builder.getStructTypeFromNode( this, this.membersLayout, this.name );
return structType.name;
}
setup( builder ) {
builder.getStructTypeFromNode( this, this.membersLayout, this.name );
builder.addInclude( this );
}
generate( builder ) {
return this.getNodeType( builder );
}
}
/**
* StructNode allows to create custom structures with multiple members.
* This can also be used to define structures in attribute and uniform data.
*
* ```js
* // Define a custom struct
* const BoundingBox = struct( { min: 'vec3', max: 'vec3' } );
*
* // Create a new instance of the struct
* const bb = BoundingBox( vec3( 0 ), vec3( 1 ) ); // style 1
* const bb = BoundingBox( { min: vec3( 0 ), max: vec3( 1 ) } ); // style 2
*
* // Access the struct members
* const min = bb.get( 'min' );
*
* // Assign a new value to a member
* min.assign( vec3() );
* ```
* @augments Node
*/
class StructNode extends Node {
static get type() {
return 'StructNode';
}
constructor( structTypeNode, values ) {
super( 'vec3' );
this.structTypeNode = structTypeNode;
this.values = values;
this.isStructNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( builder ) {
return this.structTypeNode.getNodeType( builder );
}
getMemberType( builder, name ) {
return this.structTypeNode.getMemberType( builder, name );
}
_getChildren() {
// Ensure struct type is the last child for correct code generation order
const children = super._getChildren();
const structTypeProperty = children.find( child => child.childNode === this.structTypeNode );
children.splice( children.indexOf( structTypeProperty ), 1 );
children.push( structTypeProperty );
return children;
}
generate( builder ) {
const nodeVar = builder.getVarFromNode( this );
const structType = nodeVar.type;
const propertyName = builder.getPropertyName( nodeVar );
builder.addLineFlowCode( `${ propertyName } = ${ builder.generateStruct( structType, this.structTypeNode.membersLayout, this.values ) }`, this );
return nodeVar.name;
}
}
/**
* TSL function for creating a struct node.
*
* @tsl
* @function
* @param {Object} membersLayout - The layout of the struct members.
* @param {?string} [name=null] - The name of the struct.
* @returns {Function} The struct function.
*/
const struct = ( membersLayout, name = null ) => {
const structType = new StructTypeNode( membersLayout, name );
const struct = ( ...params ) => {
let values = null;
if ( params.length > 0 ) {
if ( isArrayAsParameter( params ) ) {
values = {};
const names = Object.keys( membersLayout );
for ( let i = 0; i < params.length; i ++ ) {
values[ names[ i ] ] = nodeObject( params[ i ] );
}
} else {
values = params[ 0 ];
}
}
return new StructNode( structType, values );
};
return nodeProxyConstructor( struct, structType );
};
/**
* This node can be used to define multiple outputs in a shader programs.
*
* @augments Node
*/
class OutputStructNode extends Node {
static get type() {
return 'OutputStructNode';
}
/**
* Constructs a new output struct node. The constructor can be invoked with an
* arbitrary number of nodes representing the members.
*
* @param {...Node} members - A parameter list of nodes.
*/
constructor( ...members ) {
super();
/**
* An array of nodes which defines the output.
*
* @type {Array<Node>}
*/
this.members = members;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOutputStructNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generateNodeType( /*builder*/ ) {
return 'OutputType';
}
generate( builder ) {
const nodeData = builder.getDataFromNode( this );
if ( nodeData.membersLayout === undefined ) {
const members = this.members;
const membersLayout = [];
for ( let i = 0; i < members.length; i ++ ) {
const name = 'm' + i;
const type = members[ i ].getNodeType( builder );
membersLayout.push( { name, type, index: i } );
}
nodeData.membersLayout = membersLayout;
nodeData.structType = builder.getOutputStructTypeFromNode( this, nodeData.membersLayout );
}
//
const propertyName = builder.getOutputStructName();
const members = this.members;
const structPrefix = propertyName !== '' ? propertyName + '.' : '';
for ( let i = 0; i < members.length; i ++ ) {
const snippet = members[ i ].build( builder, nodeData.membersLayout[ i ].type );
builder.addLineFlowCode( `${ structPrefix }m${ i } = ${ snippet }`, this );
}
return propertyName;
}
}
/**
* TSL function for creating an output struct node.
*
* @tsl
* @function
* @param {...Node} members - A parameter list of nodes.
* @returns {OutputStructNode}
*/
const outputStruct = /*@__PURE__*/ nodeProxy( OutputStructNode );
/**
* Represents blending configuration.
*
* This class encapsulates all blending-related properties that control how
* a material's colors are combined with the colors already in the frame buffer.
*/
class BlendMode {
/**
* Constructs a new blending configuration.
*
* @param {(NoBlending|NormalBlending|AdditiveBlending|SubtractiveBlending|MultiplyBlending|CustomBlending|MaterialBlending)} [blending=NormalBlending] - The blending mode.
*/
constructor( blending = NormalBlending ) {
/**
* Defines the blending type.
*
* It must be set to `CustomBlending` if custom blending properties like
* {@link BlendMode#blendSrc}, {@link BlendMode#blendDst} or {@link BlendMode#blendEquation}
* should have any effect.
*
* @type {(NoBlending|NormalBlending|AdditiveBlending|SubtractiveBlending|MultiplyBlending|CustomBlending|MaterialBlending)}
* @default NormalBlending
*/
this.blending = blending;
/**
* Defines the blending source factor.
*
* This determines how the source (incoming) fragment color is factored before being added
* to the destination (existing) fragment color in the frame buffer.
*
* @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
* @default SrcAlphaFactor
*/
this.blendSrc = SrcAlphaFactor;
/**
* Defines the blending destination factor.
*
* This determines how the destination (existing) fragment color in the frame buffer
* is factored before being combined with the source (incoming) fragment color.
*
* @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
* @default OneMinusSrcAlphaFactor
*/
this.blendDst = OneMinusSrcAlphaFactor;
/**
* Defines the blending equation.
*
* This determines how the source and destination colors are combined.
*
* @type {(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
* @default AddEquation
*/
this.blendEquation = AddEquation;
/**
* Defines the blending source alpha factor.
*
* When set, this allows separate control of the alpha channel's source blending factor.
* If `null`, {@link BlendMode#blendSrc} is used for the alpha channel as well.
*
* @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
* @default null
*/
this.blendSrcAlpha = null;
/**
* Defines the blending destination alpha factor.
*
* When set, this allows separate control of the alpha channel's destination blending factor.
* If `null`, {@link BlendMode#blendDst} is used for the alpha channel as well.
*
* @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
* @default null
*/
this.blendDstAlpha = null;
/**
* Defines the blending equation of the alpha channel.
*
* When set, this allows separate control of the alpha channel's blending equation.
* If `null`, {@link BlendMode#blendEquation} is used for the alpha channel as well.
*
* @type {?(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
* @default null
*/
this.blendEquationAlpha = null;
/**
* Defines whether to premultiply the alpha (transparency) value.
*
* If `true`, the RGB color of the texture or material is multiplied by its alpha value.
* This is useful for transparent textures/materials where the color data
* should already include the transparency information.
*
* @type {boolean}
* @default false
*/
this.premultiplyAlpha = false;
}
/**
* Copies the blending properties from the given source to this instance.
*
* @param {BlendMode} source - The blending configuration to copy from.
* @return {BlendMode} A reference to this instance.
*/
copy( source ) {
this.blending = source.blending;
this.blendSrc = source.blendSrc;
this.blendDst = source.blendDst;
this.blendEquation = source.blendEquation;
this.blendSrcAlpha = source.blendSrcAlpha;
this.blendDstAlpha = source.blendDstAlpha;
this.blendEquationAlpha = source.blendEquationAlpha;
this.premultiplyAlpha = source.premultiplyAlpha;
return this;
}
/**
* Returns a clone of this blending configuration.
*
* @return {BlendMode} A new Blending instance with the same properties.
*/
clone() {
return new this.constructor().copy( this );
}
}
/**
* A four-component version of {@link Color} which is internally
* used by the renderer to represents clear color with alpha as
* one object.
*
* @private
* @augments Color
*/
class Color4 extends Color {
/**
* Constructs a new four-component color.
* You can also pass a single THREE.Color, hex or
* string argument to this constructor.
*
* @param {number|string} [r=1] - The red value.
* @param {number} [g=1] - The green value.
* @param {number} [b=1] - The blue value.
* @param {number} [a=1] - The alpha value.
*/
constructor( r, g, b, a = 1 ) {
super( r, g, b );
this.a = a;
}
/**
* Overwrites the default to honor alpha.
* You can also pass a single THREE.Color, hex or
* string argument to this method.
*
* @param {number|string|Color} r - The red value.
* @param {number} [g] - The green value.
* @param {number} [b] - The blue value.
* @param {number} [a=1] - The alpha value.
* @return {Color4} A reference to this object.
*/
set( r, g, b, a = 1 ) {
this.a = a;
return super.set( r, g, b );
}
/**
* Overwrites the default to honor alpha.
*
* @param {Color4} color - The color to copy.
* @return {Color4} A reference to this object.
*/
copy( color ) {
if ( color.a !== undefined ) this.a = color.a;
return super.copy( color );
}
/**
* Overwrites the default to honor alpha.
*
* @return {Color4} The cloned color.
*/
clone() {
return new this.constructor( this.r, this.g, this.b, this.a );
}
*[ Symbol.iterator ]() {
yield this.r;
yield this.g;
yield this.b;
yield this.a;
}
}
// Predefined blend modes for MRT nodes.
const _noBlending = /**@__PURE__*/ new BlendMode( NoBlending );
const _materialBlending = /**@__PURE__*/ new BlendMode( MaterialBlending );
/**
* Returns the MRT texture index for the given name.
*
* @param {Array<Texture>} textures - The textures of a MRT-configured render target.
* @param {string} name - The name of the MRT texture which index is requested.
* @return {number} The texture index.
*/
function getTextureIndex( textures, name ) {
for ( let i = 0; i < textures.length; i ++ ) {
if ( textures[ i ].name === name ) {
return i;
}
}
return -1;
}
/**
* This node can be used setup a MRT context for rendering. A typical MRT setup for
* post-processing is shown below:
* ```js
* const mrtNode = mrt( {
* output: output,
* normal: normalView
* } ) ;
* ```
* The MRT output is defined as a dictionary.
*
* @augments OutputStructNode
*/
class MRTNode extends OutputStructNode {
static get type() {
return 'MRTNode';
}
/**
* Constructs a new output struct node.
*
* @param {Object<string, Node>} outputNodes - The MRT outputs.
*/
constructor( outputNodes ) {
super();
/**
* A dictionary representing the MRT outputs. The key
* is the name of the output, the value the node which produces
* the output result.
*
* @type {Object<string, Node>}
*/
this.outputNodes = outputNodes;
/**
* A dictionary storing the blend modes for each output.
*
* @type {Object<string, BlendMode>}
*/
this.blendModes = {
output: _materialBlending
};
/**
* A dictionary storing the clear colors for each output.
*
* @type {Object<string, Color4>}
*/
this.clearColors = {};
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMRTNode = true;
}
/**
* Sets the blend mode for the given output name.
*
* @param {string} name - The name of the output.
* @param {BlendMode} blend - The blending mode.
* @return {MRTNode} The current MRT node.
*/
setBlendMode( name, blend ) {
this.blendModes[ name ] = blend;
return this;
}
/**
* Returns the blend mode for the given output name.
*
* @param {string} name - The name of the output.
* @return {BlendMode} The blend mode.
*/
getBlendMode( name ) {
return this.blendModes[ name ] || _noBlending;
}
/**
* Sets the clear color for the given output name.
*
* @param {string} name - The name of the output.
* @param {number|string|Color} color - The clear color.
* @param {number} [alpha=1] - The clear alpha.
* @return {MRTNode} The current MRT node.
*/
setClearColor( name, color, alpha = 1 ) {
const clearColor = this.clearColors[ name ] || ( this.clearColors[ name ] = new Color4() );
clearColor.set( color );
clearColor.a = alpha;
return this;
}
/**
* Returns the clear color for the given output name.
*
* @param {string} name - The name of the output.
* @return {?Color4} The clear color. Returns `null` if no clear color is defined
* which means the renderer's default clear policy is applied.
*/
getClearColor( name ) {
return this.clearColors[ name ] || null;
}
/**
* Returns `true` if the MRT node has an output with the given name.
*
* @param {string} name - The name of the output.
* @return {NodeBuilder} Whether the MRT node has an output for the given name or not.
*/
has( name ) {
return this.outputNodes[ name ] !== undefined;
}
/**
* Returns the output node for the given name.
*
* @param {string} name - The name of the output.
* @return {Node} The output node.
*/
get( name ) {
return this.outputNodes[ name ];
}
/**
* Merges the outputs of the given MRT node with the outputs of this node.
*
* @param {MRTNode} mrtNode - The MRT to merge.
* @return {MRTNode} A new MRT node with merged outputs..
*/
merge( mrtNode ) {
const outputs = { ...this.outputNodes, ...mrtNode.outputNodes };
const blendModes = { ...this.blendModes, ...mrtNode.blendModes };
const clearColors = { ...this.clearColors, ...mrtNode.clearColors };
const mrtTarget = mrt( outputs );
mrtTarget.blendModes = blendModes;
mrtTarget.clearColors = clearColors;
return mrtTarget;
}
setup( builder ) {
const outputNodes = this.outputNodes;
const mrt = builder.renderer.getRenderTarget();
const members = [];
const textures = mrt.textures;
for ( const name in outputNodes ) {
const index = getTextureIndex( textures, name );
// Ignore if the output exists in the MRT but has never been used.
if ( index === -1 ) continue;
const type = builder.getOutputType( index );
members[ index ] = outputNodes[ name ].convert( type );
}
this.members = members;
return super.setup( builder );
}
}
/**
* TSL function for creating a MRT node.
*
* @tsl
* @function
* @param {Object<string, Node>} outputNodes - The MRT outputs.
* @returns {MRTNode}
*/
const mrt = /*@__PURE__*/ nodeProxy( MRTNode );
/**
* This node represents an operation that reinterprets the bit representation of a value
* in one type as a value in another type.
*
* @augments Node
*/
class BitcastNode extends Node {
static get type() {
return 'BitcastNode';
}
/**
* Constructs a new bitcast node.
*
* @param {Node} valueNode - The value to convert.
* @param {string} conversionType - The type to convert to.
* @param {?string} [inputType = null] - The expected input data type of the bitcast operation.
*/
constructor( valueNode, conversionType, inputType = null ) {
super();
/**
* The data to bitcast to a new type.
*
* @type {Node}
*/
this.valueNode = valueNode;
/**
* The type the value will be converted to.
*
* @type {string}
*/
this.conversionType = conversionType;
/**
* The expected input data type of the bitcast operation.
*
*
* @type {string}
* @default null
*/
this.inputType = inputType;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBitcastNode = true;
}
generateNodeType( builder ) {
// GLSL aliasing
if ( this.inputType !== null ) {
const valueType = this.valueNode.getNodeType( builder );
const valueLength = builder.getTypeLength( valueType );
return builder.getTypeFromLength( valueLength, this.conversionType );
}
return this.conversionType;
}
generate( builder ) {
const type = this.getNodeType( builder );
let inputType = '';
if ( this.inputType !== null ) {
const valueType = this.valueNode.getNodeType( builder );
const valueTypeLength = builder.getTypeLength( valueType );
inputType = valueTypeLength === 1 ? this.inputType : builder.changeComponentType( valueType, this.inputType );
} else {
inputType = this.valueNode.getNodeType( builder );
}
return `${ builder.getBitcastMethod( type, inputType ) }( ${ this.valueNode.build( builder, inputType ) } )`;
}
}
/**
* Reinterpret the bit representation of a value in one type as a value in another type.
*
* @tsl
* @function
* @param {Node | number} x - The parameter.
* @param {string} y - The new type.
* @returns {Node}
*/
const bitcast = /*@__PURE__*/ nodeProxyAssignable( BitcastNode ).setParameterLength( 2 );
/**
* Bitcasts a float or a vector of floats to a corresponding integer type with the same element size.
*
* @tsl
* @function
* @param {Node<float>} value - The float or vector of floats to bitcast.
* @returns {BitcastNode}
*/
const floatBitsToInt = ( value ) => new BitcastNode( value, 'int', 'float' );
/**
* Bitcasts a float or a vector of floats to a corresponding unsigned integer type with the same element size.
*
* @tsl
* @function
* @param {Node<float>} value - The float or vector of floats to bitcast.
* @returns {BitcastNode}
*/
const floatBitsToUint = ( value ) => new BitcastNode( value, 'uint', 'float' );
/**
* Bitcasts an integer or a vector of integers to a corresponding float type with the same element size.
*
* @tsl
* @function
* @param {Node<int>} value - The integer or vector of integers to bitcast.
* @returns {BitcastNode}
*/
const intBitsToFloat = ( value ) => new BitcastNode( value, 'float', 'int' );
/**
* Bitcast an unsigned integer or a vector of unsigned integers to a corresponding float type with the same element size.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer or vector of unsigned integers to bitcast.
* @returns {BitcastNode}
*/
const uintBitsToFloat = ( value ) => new BitcastNode( value, 'float', 'uint' );
const registeredBitcountFunctions = {};
/**
* This node represents an operation that counts the bits of a piece of shader data.
*
* @augments MathNode
*/
class BitcountNode extends MathNode {
static get type() {
return 'BitcountNode';
}
/**
* Constructs a new math node.
*
* @param {'countTrailingZeros'|'countLeadingZeros'|'countOneBits'} method - The method name.
* @param {Node} aNode - The first input.
*/
constructor( method, aNode ) {
super( method, aNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBitcountNode = true;
}
/**
* Casts the input value of the function to an integer if necessary.
*
* @private
* @param {Node<uint>|Node<int>} inputNode - The input value.
* @param {Node<uint>} outputNode - The output value.
* @param {string} elementType - The type of the input value.
*/
_resolveElementType( inputNode, outputNode, elementType ) {
if ( elementType === 'int' ) {
outputNode.assign( bitcast( inputNode, 'uint' ) );
} else {
outputNode.assign( inputNode );
}
}
_returnDataNode( inputType ) {
switch ( inputType ) {
case 'uint': {
return uint;
}
case 'int': {
return int;
}
case 'uvec2': {
return uvec2;
}
case 'uvec3': {
return uvec3;
}
case 'uvec4': {
return uvec4;
}
case 'ivec2': {
return ivec2;
}
case 'ivec3': {
return ivec3;
}
case 'ivec4': {
return ivec4;
}
}
}
/**
* Creates and registers a reusable GLSL function that emulates the behavior of countTrailingZeros.
*
* @private
* @param {string} elementType - The type of the input value.
* @returns {Function} - The generated function
*/
_createTrailingZerosBaseLayout( elementType ) {
const outputConvertNode = this._returnDataNode( elementType );
const fnDef = Fn( ( [ value ] ) => {
If( value.equal( uint( 0 ) ), () => {
return outputConvertNode( 32 );
} );
const v = uint( 0.0 );
this._resolveElementType( value, v, elementType );
const f = float( v.bitAnd( negate( v ) ) );
const uintBits = floatBitsToUint( f );
const numTrailingZeros = ( uintBits.shiftRight( 23 ) ).sub( 127 );
return outputConvertNode( numTrailingZeros );
}, { value: elementType, return: elementType } );
return fnDef;
}
/**
* Creates and registers a reusable GLSL function that emulates the behavior of countLeadingZeros.
*
* @private
* @param {string} elementType - The type of the input value.
* @returns {Function} - The generated function
*/
_createLeadingZerosBaseLayout( elementType ) {
const outputConvertNode = this._returnDataNode( elementType );
const fnDef = Fn( ( [ value ] ) => {
If( value.equal( uint( 0 ) ), () => {
return outputConvertNode( 32 );
} );
const v = uint( 0 );
const n = uint( 0 );
this._resolveElementType( value, v, elementType );
If( v.shiftRight( 16 ).equal( 0 ), () => {
n.addAssign( 16 );
v.shiftLeftAssign( 16 );
} );
If( v.shiftRight( 24 ).equal( 0 ), () => {
n.addAssign( 8 );
v.shiftLeftAssign( 8 );
} );
If( v.shiftRight( 28 ).equal( 0 ), () => {
n.addAssign( 4 );
v.shiftLeftAssign( 4 );
} );
If( v.shiftRight( 30 ).equal( 0 ), () => {
n.addAssign( 2 );
v.shiftLeftAssign( 2 );
} );
If( v.shiftRight( 31 ).equal( 0 ), () => {
n.addAssign( 1 );
} );
return outputConvertNode( n );
}, { value: elementType, return: elementType } );
return fnDef;
}
/**
* Creates and registers a reusable GLSL function that emulates the behavior of countOneBits.
*
* @private
* @param {string} elementType - The type of the input value.
* @returns {Function} - The generated function
*/
_createOneBitsBaseLayout( elementType ) {
const outputConvertNode = this._returnDataNode( elementType );
const fnDef = Fn( ( [ value ] ) => {
const v = uint( 0.0 );
this._resolveElementType( value, v, elementType );
v.assign( v.sub( v.shiftRight( uint( 1 ) ).bitAnd( uint( 0x55555555 ) ) ) );
v.assign( v.bitAnd( uint( 0x33333333 ) ).add( v.shiftRight( uint( 2 ) ).bitAnd( uint( 0x33333333 ) ) ) );
const numBits = v.add( v.shiftRight( uint( 4 ) ) ).bitAnd( uint( 0xF0F0F0F ) ).mul( uint( 0x1010101 ) ).shiftRight( uint( 24 ) );
return outputConvertNode( numBits );
}, { value: elementType, return: elementType } );
return fnDef;
}
/**
* Creates and registers a reusable GLSL function that emulates the behavior of the specified bitcount function.
* including considerations for component-wise bitcounts on vector type inputs.
*
* @private
* @param {string} inputType - The type of the input value.
* @param {number} typeLength - The vec length of the input value.
* @param {Function} baseFn - The base function that operates on an individual component of the vector.
* @returns {Function} - The alias function for the specified bitcount method.
*/
_createMainLayout( inputType, typeLength, baseFn ) {
const outputConvertNode = this._returnDataNode( inputType );
const fnDef = Fn( ( [ value ] ) => {
if ( typeLength === 1 ) {
return outputConvertNode( baseFn( value ) );
} else {
const vec = outputConvertNode( 0 );
const components = [ 'x', 'y', 'z', 'w' ];
for ( let i = 0; i < typeLength; i ++ ) {
const component = components[ i ];
vec[ component ].assign( baseFn( value[ component ] ) );
}
return vec;
}
}, { value: inputType, return: inputType } );
return fnDef;
}
setup( builder ) {
const { method, aNode } = this;
const { renderer } = builder;
if ( renderer.backend.isWebGPUBackend ) {
// use built-in WGSL functions for WebGPU
return super.setup( builder );
}
const inputType = this.getInputType( builder );
const elementType = builder.getElementType( inputType );
const typeLength = builder.getTypeLength( inputType );
const baseMethod = `${method}_base_${elementType}`;
const newMethod = `${method}_${inputType}`;
let baseFn = registeredBitcountFunctions[ baseMethod ];
if ( baseFn === undefined ) {
switch ( method ) {
case BitcountNode.COUNT_LEADING_ZEROS: {
baseFn = this._createLeadingZerosBaseLayout( elementType );
break;
}
case BitcountNode.COUNT_TRAILING_ZEROS: {
baseFn = this._createTrailingZerosBaseLayout( elementType );
break;
}
case BitcountNode.COUNT_ONE_BITS: {
baseFn = this._createOneBitsBaseLayout( elementType );
break;
}
}
registeredBitcountFunctions[ baseMethod ] = baseFn;
}
let fn = registeredBitcountFunctions[ newMethod ];
if ( fn === undefined ) {
fn = this._createMainLayout( inputType, typeLength, baseFn );
registeredBitcountFunctions[ newMethod ] = fn;
}
const output = Fn( () => {
return fn(
aNode,
);
} );
return output();
}
static get COUNT_TRAILING_ZEROS() {
return 'countTrailingZeros';
}
static get COUNT_LEADING_ZEROS() {
return 'countLeadingZeros';
}
static get COUNT_ONE_BITS() {
return 'countOneBits';
}
}
/**
* Finds the number of consecutive 0 bits from the least significant bit of the input value,
* which is also the index of the least significant bit of the input value.
*
* Can only be used with {@link WebGPURenderer} and a WebGPU backend.
*
* @tsl
* @function
* @param {Node | number} x - The input value.
* @returns {Node}
*/
const countTrailingZeros = /*@__PURE__*/ nodeProxyAssignable( BitcountNode, BitcountNode.COUNT_TRAILING_ZEROS ).setParameterLength( 1 );
/**
* Finds the number of consecutive 0 bits starting from the most significant bit of the input value.
*
* Can only be used with {@link WebGPURenderer} and a WebGPU backend.
*
* @tsl
* @function
* @param {Node | number} x - The input value.
* @returns {Node}
*/
const countLeadingZeros = /*@__PURE__*/ nodeProxyAssignable( BitcountNode, BitcountNode.COUNT_LEADING_ZEROS ).setParameterLength( 1 );
/**
* Finds the number of '1' bits set in the input value
*
* Can only be used with {@link WebGPURenderer} and a WebGPU backend.
*
* @tsl
* @function
* @returns {Node}
*/
const countOneBits = /*@__PURE__*/ nodeProxyAssignable( BitcountNode, BitcountNode.COUNT_ONE_BITS ).setParameterLength( 1 );
/**
* Generates a hash value in the range `[0, 1]` from the given seed.
*
* @tsl
* @function
* @param {Node<float>} seed - The seed.
* @return {Node<float>} The hash value.
*/
const hash = /*@__PURE__*/ Fn( ( [ seed ] ) => {
// Taken from https://www.shadertoy.com/view/XlGcRh, originally from pcg-random.org
const state = seed.toUint().mul( 747796405 ).add( 2891336453 );
const word = state.shiftRight( state.shiftRight( 28 ).add( 4 ) ).bitXor( state ).mul( 277803737 );
const result = word.shiftRight( 22 ).bitXor( word );
return result.toFloat().mul( 1 / 2 ** 32 ); // Convert to range [0, 1)
}, { seed: 'uint', return: 'float' } );
/**
* A function that remaps the `[0,1]` interval into the `[0,1]` interval.
* The corners are mapped to `0` and the center to `1`.
* Reference: {@link https://iquilezles.org/articles/functions/}.
*
* @tsl
* @function
* @param {Node<float>} x - The value to remap.
* @param {Node<float>} k - Allows to control the remapping functions shape by rising the parabola to a power `k`.
* @return {Node<float>} The remapped value.
*/
const parabola = ( x, k ) => pow( mul( 4.0, x.mul( sub( 1.0, x ) ) ), k );
/**
* A function that remaps the `[0,1]` interval into the `[0,1]` interval.
* Expands the sides and compresses the center, and keeps `0.5` mapped to `0.5`.
* Reference: {@link https://iquilezles.org/articles/functions/}.
*
* @tsl
* @function
* @param {Node<float>} x - The value to remap.
* @param {Node<float>} k - `k=1` is the identity curve,`k<1` produces the classic `gain()` shape, and `k>1` produces "s" shaped curves.
* @return {Node<float>} The remapped value.
*/
const gain = ( x, k ) => select(
x.lessThan( 0.5 ),
pow( mul( 2.0, x ), k ).mul( 0.5 ),
sub( 1.0, pow( mul( 2.0, sub( 1.0, x ) ), k ).mul( 0.5 ) )
);
/**
* A function that remaps the `[0,1]` interval into the `[0,1]` interval.
* A generalization of the `parabola()`. Keeps the corners mapped to 0 but allows the control of the shape one either side of the curve.
* Reference: {@link https://iquilezles.org/articles/functions/}.
*
* @tsl
* @function
* @param {Node<float>} x - The value to remap.
* @param {Node<float>} a - First control parameter.
* @param {Node<float>} b - Second control parameter.
* @return {Node<float>} The remapped value.
*/
const pcurve = ( x, a, b ) => pow( div( pow( x, a ), add( pow( x, a ), pow( sub( 1.0, x ), b ) ) ), div( 1.0, a ) );
/**
* A phase shifted sinus curve that starts at zero and ends at zero, with bouncing behavior.
* Reference: {@link https://iquilezles.org/articles/functions/}.
*
* @tsl
* @function
* @param {Node<float>} x - The value to compute the sin for.
* @param {Node<float>} k - Controls the amount of bounces.
* @return {Node<float>} The result value.
*/
const sinc = ( x, k ) => {
const arg = abs( PI.mul( k.mul( x ).sub( 1.0 ) ) ).max( EPSILON ).toConst();
return sin( arg ).div( arg );
};
/**
* This node represents an operation that packs floating-point values of a vector into an unsigned 32-bit integer
*
* @augments Node
*/
class PackFloatNode extends Node {
static get type() {
return 'PackFloatNode';
}
/**
*
* @param {'snorm' | 'unorm' | 'float16'} encoding - The numeric encoding that describes how the float values are mapped to the integer range.
* @param {Node} vectorNode - The vector node to be packed
*/
constructor( encoding, vectorNode ) {
super();
/**
* The vector to be packed.
*
* @type {Node}
*/
this.vectorNode = vectorNode;
/**
* The numeric encoding.
*
* @type {string}
*/
this.encoding = encoding;
/**
* The component layout of the packed integer.
*
* @type {'2x16' | '4x8'}
* @default '2x16'
*/
this.layout = '2x16';
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPackFloatNode = true;
}
generateNodeType() {
return 'uint';
}
generate( builder ) {
const inputType = this.vectorNode.getNodeType( builder );
return `${ builder.getFloatPackingMethod( this.encoding, this.layout ) }(${ this.vectorNode.build( builder, inputType )})`;
}
}
/**
* Converts each component of the normalized float to 16-bit integer values. The results are packed into a single unsigned integer.
* round(clamp(c, -1, +1) * 32767.0)
*
* @tsl
* @function
* @param {Node<vec2>} value - The 2-component vector to be packed
* @returns {Node}
*/
const packSnorm2x16 = /*@__PURE__*/ nodeProxyAssignable( PackFloatNode, 'snorm' ).setParameterLength( 1 );
/**
* Converts each component of the normalized float to 16-bit integer values. The results are packed into a single unsigned integer.
* round(clamp(c, 0, +1) * 65535.0)
*
* @tsl
* @function
* @param {Node<vec2>} value - The 2-component vector to be packed
* @returns {Node}
*/
const packUnorm2x16 = /*@__PURE__*/ nodeProxyAssignable( PackFloatNode, 'unorm' ).setParameterLength( 1 );
/**
* Converts each component of the vec2 to 16-bit floating-point values. The results are packed into a single unsigned integer.
*
* @tsl
* @function
* @param {Node<vec2>} value - The 2-component vector to be packed
* @returns {Node}
*/
const packHalf2x16 = /*@__PURE__*/ nodeProxyAssignable( PackFloatNode, 'float16' ).setParameterLength( 1 );
/**
* Converts each component of the normalized float to 8-bit integer values. The results are packed into a single unsigned integer.
*
* @tsl
* @function
* @param {Node<vec4>} value - The 4-component vector to be packed
* @returns {Node}
*/
const packSnorm4x8 = /*@__PURE__*/ nodeProxyAssignable( PackFloatNode, 'snorm', null, { layout: '4x8' } ).setParameterLength( 1 );
/**
* Converts each component of the normalized float to 8-bit integer values. The results are packed into a single unsigned integer.
*
* @tsl
* @function
* @param {Node<vec4>} value - The 4-component vector to be packed
* @returns {Node}
*/
const packUnorm4x8 = /*@__PURE__*/ nodeProxyAssignable( PackFloatNode, 'unorm', null, { layout: '4x8' } ).setParameterLength( 1 );
/**
* Represents one of the built-in functions of WGSL's `packed_4x8_integer_dot_product`
* language extension. If the extension is not available, the node falls back to an
* emulation with plain integer bit operations.
*
* @augments Node
*/
class Packed4x8IntegerNode extends Node {
static get type() {
return 'Packed4x8IntegerNode';
}
/**
* Constructs a packed 4x8 integer function node.
*
* @param {string} method - The WGSL built-in function name.
* @param {Node} aNode - The first argument.
* @param {?Node} [bNode=null] - The optional second argument.
*/
constructor( method, aNode, bNode = null ) {
super();
/**
* The WGSL built-in function name.
*
* @type {string}
*/
this.method = method;
/**
* The first argument.
*
* @type {Node}
*/
this.aNode = aNode;
/**
* The optional second argument.
*
* @type {?Node}
*/
this.bNode = bNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPacked4x8IntegerNode = true;
}
getInputType() {
const method = this.method;
if ( method === Packed4x8IntegerNode.PACK4X_I8 || method === Packed4x8IntegerNode.PACK4X_I8_CLAMP ) {
return 'ivec4';
} else if ( method === Packed4x8IntegerNode.PACK4X_U8 || method === Packed4x8IntegerNode.PACK4X_U8_CLAMP ) {
return 'uvec4';
}
return 'uint';
}
generateNodeType() {
const method = this.method;
if ( method === Packed4x8IntegerNode.DOT4_I8_PACKED ) {
return 'int';
} else if ( method === Packed4x8IntegerNode.UNPACK4X_I8 ) {
return 'ivec4';
} else if ( method === Packed4x8IntegerNode.UNPACK4X_U8 ) {
return 'uvec4';
}
return 'uint';
}
/**
* Returns the reusable `Fn()` definition that emulates this node's method.
*
* @private
* @returns {Function} The emulation function.
*/
_getEmulatedFn() {
switch ( this.method ) {
case Packed4x8IntegerNode.DOT4_U8_PACKED: return emulatedDot4U8Packed;
case Packed4x8IntegerNode.DOT4_I8_PACKED: return emulatedDot4I8Packed;
case Packed4x8IntegerNode.PACK4X_I8: return emulatedPack4xI8;
case Packed4x8IntegerNode.PACK4X_U8: return emulatedPack4xU8;
case Packed4x8IntegerNode.PACK4X_I8_CLAMP: return emulatedPack4xI8Clamp;
case Packed4x8IntegerNode.PACK4X_U8_CLAMP: return emulatedPack4xU8Clamp;
case Packed4x8IntegerNode.UNPACK4X_I8: return emulatedUnpack4xI8;
case Packed4x8IntegerNode.UNPACK4X_U8: return emulatedUnpack4xU8;
}
}
setup( builder ) {
// check for native language support
if ( builder.renderer.backend.isWebGPUBackend === true &&
typeof navigator !== 'undefined' && navigator.gpu !== undefined &&
navigator.gpu.wgslLanguageFeatures !== undefined && navigator.gpu.wgslLanguageFeatures.has( 'packed_4x8_integer_dot_product' )
) {
return super.setup( builder );
}
// emulation
const { aNode, bNode } = this;
const fn = this._getEmulatedFn();
return bNode !== null ? fn( aNode, bNode ) : fn( aNode );
}
generate( builder, output ) {
const properties = builder.getNodeProperties( this );
if ( properties.outputNode ) {
return super.generate( builder, output );
}
// generate native WGSL call
const type = this.getNodeType( builder );
const inputType = this.getInputType();
const params = [ this.aNode.build( builder, inputType ) ];
if ( this.bNode !== null ) params.push( this.bNode.build( builder, inputType ) );
return builder.format( `${this.method}( ${params.join( ', ' )} )`, type, output );
}
serialize( data ) {
super.serialize( data );
data.method = this.method;
}
deserialize( data ) {
super.deserialize( data );
this.method = data.method;
}
static get DOT4_U8_PACKED() {
return 'dot4U8Packed';
}
static get DOT4_I8_PACKED() {
return 'dot4I8Packed';
}
static get PACK4X_I8() {
return 'pack4xI8';
}
static get PACK4X_U8() {
return 'pack4xU8';
}
static get PACK4X_I8_CLAMP() {
return 'pack4xI8Clamp';
}
static get PACK4X_U8_CLAMP() {
return 'pack4xU8Clamp';
}
static get UNPACK4X_I8() {
return 'unpack4xI8';
}
static get UNPACK4X_U8() {
return 'unpack4xU8';
}
}
// emulations
const emulatedPack4xU8 = /*@__PURE__*/ Fn( ( [ v ] ) => {
const x = v.x.bitAnd( uint( 0xff ) );
const y = v.y.bitAnd( uint( 0xff ) );
const z = v.z.bitAnd( uint( 0xff ) );
const w = v.w.bitAnd( uint( 0xff ) );
return x.bitOr( y.shiftLeft( uint( 8 ) ) ).bitOr( z.shiftLeft( uint( 16 ) ) ).bitOr( w.shiftLeft( uint( 24 ) ) );
}, { v: 'uvec4', return: 'uint' } );
const emulatedPack4xI8 = /*@__PURE__*/ Fn( ( [ v ] ) => {
return emulatedPack4xU8( uvec4( v ) );
}, { v: 'ivec4', return: 'uint' } );
const emulatedPack4xU8Clamp = /*@__PURE__*/ Fn( ( [ v ] ) => {
return emulatedPack4xU8( clamp( v, uvec4( 0 ), uvec4( 255 ) ) );
}, { v: 'uvec4', return: 'uint' } );
const emulatedPack4xI8Clamp = /*@__PURE__*/ Fn( ( [ v ] ) => {
return emulatedPack4xI8( clamp( v, ivec4( -128 ), ivec4( 127 ) ) );
}, { v: 'ivec4', return: 'uint' } );
const emulatedUnpack4xU8 = /*@__PURE__*/ Fn( ( [ v ] ) => {
return uvec4(
v.bitAnd( uint( 0xff ) ),
v.shiftRight( uint( 8 ) ).bitAnd( uint( 0xff ) ),
v.shiftRight( uint( 16 ) ).bitAnd( uint( 0xff ) ),
v.shiftRight( uint( 24 ) ).bitAnd( uint( 0xff ) )
);
}, { v: 'uint', return: 'uvec4' } );
function signExtendByte( v, byteShift ) {
return int( v.shiftLeft( uint( 24 - byteShift ) ) ).shiftRight( int( 24 ) );
}
const emulatedUnpack4xI8 = /*@__PURE__*/ Fn( ( [ v ] ) => {
return ivec4(
signExtendByte( v, 0 ),
signExtendByte( v, 8 ),
signExtendByte( v, 16 ),
signExtendByte( v, 24 )
);
}, { v: 'uint', return: 'ivec4' } );
const emulatedDot4U8Packed = /*@__PURE__*/ Fn( ( [ a, b ] ) => {
const ua = emulatedUnpack4xU8( a );
const ub = emulatedUnpack4xU8( b );
return ua.x.mul( ub.x ).add( ua.y.mul( ub.y ) ).add( ua.z.mul( ub.z ) ).add( ua.w.mul( ub.w ) );
}, { a: 'uint', b: 'uint', return: 'uint' } );
const emulatedDot4I8Packed = /*@__PURE__*/ Fn( ( [ a, b ] ) => {
const ia = emulatedUnpack4xI8( a );
const ib = emulatedUnpack4xI8( b );
return ia.x.mul( ib.x ).add( ia.y.mul( ib.y ) ).add( ia.z.mul( ib.z ) ).add( ia.w.mul( ib.w ) );
}, { a: 'uint', b: 'uint', return: 'int' } );
/**
* Computes the dot product of four unsigned 8-bit integer components packed
* into each input.
*
* @tsl
* @function
* @param {Node<uint>} a - The first packed unsigned integer vector.
* @param {Node<uint>} b - The second packed unsigned integer vector.
* @returns {Node<uint>} The dot product.
*/
const dot4U8Packed = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.DOT4_U8_PACKED ).setParameterLength( 2 );
/**
* Computes the dot product of four signed 8-bit integer components packed
* into each input.
*
* @tsl
* @function
* @param {Node<uint>} a - The first packed signed integer vector.
* @param {Node<uint>} b - The second packed signed integer vector.
* @returns {Node<int>} The dot product.
*/
const dot4I8Packed = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.DOT4_I8_PACKED ).setParameterLength( 2 );
/**
* Packs the least significant 8 bits of four signed integers into a `uint`.
*
* @tsl
* @function
* @param {Node<ivec4>} value - The signed integer vector to pack.
* @returns {Node<uint>} The packed value.
*/
const pack4xI8 = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.PACK4X_I8 ).setParameterLength( 1 );
/**
* Packs the least significant 8 bits of four unsigned integers into a `uint`.
*
* @tsl
* @function
* @param {Node<uvec4>} value - The unsigned integer vector to pack.
* @returns {Node<uint>} The packed value.
*/
const pack4xU8 = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.PACK4X_U8 ).setParameterLength( 1 );
/**
* Clamps four signed integers to the signed 8-bit range and packs them into a
* `uint`.
*
* @tsl
* @function
* @param {Node<ivec4>} value - The signed integer vector to clamp and pack.
* @returns {Node<uint>} The packed value.
*/
const pack4xI8Clamp = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.PACK4X_I8_CLAMP ).setParameterLength( 1 );
/**
* Clamps four unsigned integers to the unsigned 8-bit range and packs them
* into a `uint`.
*
* @tsl
* @function
* @param {Node<uvec4>} value - The unsigned integer vector to clamp and pack.
* @returns {Node<uint>} The packed value.
*/
const pack4xU8Clamp = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.PACK4X_U8_CLAMP ).setParameterLength( 1 );
/**
* Unpacks a `uint` into four sign-extended signed 8-bit integer components.
*
* @tsl
* @function
* @param {Node<uint>} value - The packed value.
* @returns {Node<ivec4>} The unpacked signed integer vector.
*/
const unpack4xI8 = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.UNPACK4X_I8 ).setParameterLength( 1 );
/**
* Unpacks a `uint` into four zero-extended unsigned 8-bit integer components.
*
* @tsl
* @function
* @param {Node<uint>} value - The packed value.
* @returns {Node<uvec4>} The unpacked unsigned integer vector.
*/
const unpack4xU8 = /*@__PURE__*/ nodeProxyAssignable( Packed4x8IntegerNode, Packed4x8IntegerNode.UNPACK4X_U8 ).setParameterLength( 1 );
/**
* This node represents an operation that unpacks values from a 32-bit unsigned integer, reinterpreting the results as a floating-point vector
*
* @augments Node
*/
class UnpackFloatNode extends Node {
static get type() {
return 'UnpackFloatNode';
}
/**
*
* @param {'snorm' | 'unorm' | 'float16'} encoding - The numeric encoding that describes how the integer values are mapped to the float range
* @param {Node} uintNode - The uint node to be unpacked
*/
constructor( encoding, uintNode ) {
super();
/**
* The unsigned integer to be unpacked.
*
* @type {Node}
*/
this.uintNode = uintNode;
/**
* The numeric encoding.
*
* @type {string}
*/
this.encoding = encoding;
/**
* The component layout of the packed integer.
*
* @type {'2x16' | '4x8'}
* @default '2x16'
*/
this.layout = '2x16';
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isUnpackFloatNode = true;
}
generateNodeType() {
return this.layout === '4x8' ? 'vec4' : 'vec2';
}
generate( builder ) {
const inputType = this.uintNode.getNodeType( builder );
return `${ builder.getFloatUnpackingMethod( this.encoding, this.layout ) }(${ this.uintNode.build( builder, inputType )})`;
}
}
/**
* Unpacks a 32-bit unsigned integer into two 16-bit values, interpreted as normalized signed integers. Returns a vec2 with both values.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer to be unpacked
* @returns {Node}
*/
const unpackSnorm2x16 = /*@__PURE__*/ nodeProxyAssignable( UnpackFloatNode, 'snorm' ).setParameterLength( 1 );
/**
* Unpacks a 32-bit unsigned integer into two 16-bit values, interpreted as normalized unsigned integers. Returns a vec2 with both values.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer to be unpacked
* @returns {Node}
*/
const unpackUnorm2x16 = /*@__PURE__*/ nodeProxyAssignable( UnpackFloatNode, 'unorm' ).setParameterLength( 1 );
/**
* Unpacks a 32-bit unsigned integer into two 16-bit values, interpreted as 16-bit floating-point numbers. Returns a vec2 with both values.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer to be unpacked
* @returns {Node}
*/
const unpackHalf2x16 = /*@__PURE__*/ nodeProxyAssignable( UnpackFloatNode, 'float16' ).setParameterLength( 1 );
/**
* Unpacks a 32-bit unsigned integer into four 8-bit values, interpreted as normalized signed integers. Returns a vec4 with all values.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer to be unpacked
* @returns {Node}
*/
const unpackSnorm4x8 = /*@__PURE__*/ nodeProxyAssignable( UnpackFloatNode, 'snorm', null, { layout: '4x8' } ).setParameterLength( 1 );
/**
* Unpacks a 32-bit unsigned integer into four 8-bit values, interpreted as normalized unsigned integers. Returns a vec4 with all values.
*
* @tsl
* @function
* @param {Node<uint>} value - The unsigned integer to be unpacked
* @returns {Node}
*/
const unpackUnorm4x8 = /*@__PURE__*/ nodeProxyAssignable( UnpackFloatNode, 'unorm', null, { layout: '4x8' } ).setParameterLength( 1 );
/**
* This module offers a variety of ways to implement loops in TSL. In it's basic form it's:
* ```js
* Loop( count, ( { i } ) => {
*
* } );
* ```
* However, it is also possible to define a start and end ranges, data types and loop conditions:
* ```js
* Loop( { start: int( 0 ), end: int( 10 ), type: 'int', condition: '<' }, ( { i } ) => {
*
* } );
*```
* Nested loops can be defined in a compacted form:
* ```js
* Loop( 10, 5, ( { i, j } ) => {
*
* } );
* ```
* Loops that should run backwards can be defined like so:
* ```js
* Loop( { start: 10 }, () => {} );
* ```
* It is possible to execute with boolean values, similar to the `while` syntax.
* ```js
* const value = float( 0 ).toVar();
*
* Loop( value.lessThan( 10 ), () => {
*
* value.addAssign( 1 );
*
* } );
* ```
* The module also provides `Break()` and `Continue()` TSL expressions for loop control.
* @augments Node
*/
class LoopNode extends Node {
static get type() {
return 'LoopNode';
}
/**
* Constructs a new loop node.
*
* @param {Array<LoopNode~Params|loopBodyCallback>} params - Any number of loop parameters followed by the loop body.
*/
constructor( params = [] ) {
super( 'void' );
this.params = params;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns a loop variable name based on an index. The pattern is
* `0` = `i`, `1`= `j`, `2`= `k` and so on.
*
* @param {number} index - The index.
* @return {string} The loop variable name.
*/
getVarName( index ) {
return String.fromCharCode( 'i'.charCodeAt( 0 ) + index );
}
/**
* Returns properties about this node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Object} The node properties.
*/
getProperties( builder ) {
const properties = builder.getNodeProperties( this );
if ( properties.stackNode !== undefined ) return properties;
//
const inputs = {};
const params = this._getInternalParams();
for ( let i = 0, l = params.length - 1; i < l; i ++ ) {
const param = params[ i ];
const name = ( param.isNode !== true && param.name ) || this.getVarName( i );
const type = ( param.isNode !== true && param.type ) || 'int';
inputs[ name ] = expression( name, type );
}
const stack = builder.addStack();
const fnCall = params[ params.length - 1 ]( inputs );
properties.returnsNode = fnCall.context( { nodeLoop: this, nodeBlock: builder.createBlock() } );
properties.stackNode = stack;
const baseParam = params[ 0 ];
if ( baseParam.isNode !== true && typeof baseParam.update === 'function' ) {
const fnUpdateCall = Fn( baseParam.update )( inputs );
properties.updateNode = fnUpdateCall.context( { nodeLoop: this } );
}
builder.removeStack();
return properties;
}
_getInternalParams() {
const params = this.params;
if ( typeof params[ 0 ] === 'function' ) {
return [ bool( true ), params[ 0 ] ];
}
return params;
}
setup( builder ) {
// setup properties
this.getProperties( builder );
}
generate( builder ) {
const properties = this.getProperties( builder );
const params = this._getInternalParams();
const stackNode = properties.stackNode;
for ( let i = 0, l = params.length - 1; i < l; i ++ ) {
const param = params[ i ];
let isWhile = false, start = null, end = null, name = null, type = null, condition = null, update = null;
if ( param.isNode ) {
if ( param.getNodeType( builder ) === 'bool' ) {
isWhile = true;
type = 'bool';
end = param.build( builder, type );
} else {
type = 'int';
name = this.getVarName( i );
start = '0';
end = param.build( builder, type );
condition = '<';
}
} else {
type = param.type || 'int';
name = param.name || this.getVarName( i );
start = param.start;
end = param.end;
condition = param.condition;
update = param.update;
if ( typeof start === 'number' ) start = builder.generateConst( type, start );
else if ( start && start.isNode ) start = start.build( builder, type );
if ( typeof end === 'number' ) end = builder.generateConst( type, end );
else if ( end && end.isNode ) end = end.build( builder, type );
if ( start !== undefined && end === undefined ) {
start = start + ' - 1';
end = '0';
condition = '>=';
} else if ( end !== undefined && start === undefined ) {
start = '0';
condition = '<';
}
if ( condition === undefined ) {
if ( Number( start ) > Number( end ) ) {
condition = '>=';
} else {
condition = '<';
}
}
}
let loopSnippet;
if ( isWhile ) {
loopSnippet = `while ( ${ end } )`;
} else {
const internalParam = { start, end};
//
const startSnippet = internalParam.start;
const endSnippet = internalParam.end;
let updateSnippet;
const deltaOperator = () => condition.includes( '<' ) ? '+=' : '-=';
if ( update !== undefined && update !== null ) {
switch ( typeof update ) {
case 'function':
const flow = builder.flowStagesNode( properties.updateNode, 'void' );
const snippet = flow.code.replace( /\t|;/g, '' );
updateSnippet = snippet;
break;
case 'number':
updateSnippet = name + ' ' + deltaOperator() + ' ' + builder.generateConst( type, update );
break;
case 'string':
updateSnippet = name + ' ' + update;
break;
default:
if ( update.isNode ) {
updateSnippet = name + ' ' + deltaOperator() + ' ' + update.build( builder );
} else {
error( 'TSL: \'Loop( { update: ... } )\' is not a function, string or number.', this.stackTrace );
updateSnippet = 'break /* invalid update */';
}
}
} else {
if ( type === 'int' || type === 'uint' ) {
update = condition.includes( '<' ) ? '++' : '--';
} else {
update = deltaOperator() + ' 1.';
}
updateSnippet = name + ' ' + update;
}
const declarationSnippet = builder.getVar( type, name ) + ' = ' + startSnippet;
const conditionalSnippet = name + ' ' + condition + ' ' + endSnippet;
loopSnippet = `for ( ${ declarationSnippet }; ${ conditionalSnippet }; ${ updateSnippet } )`;
}
builder.addFlowCode( ( i === 0 ? '\n' : '' ) + builder.tab + loopSnippet + ' {\n\n' ).addFlowTab();
}
const flowBlock = builder.flowBlock;
builder.flowBlock = { parent: flowBlock };
const stackSnippet = stackNode.build( builder, 'void' );
properties.returnsNode.build( builder, 'void' );
builder.flowBlock = flowBlock;
builder.removeFlowTab().addFlowCode( '\n' + builder.tab + stackSnippet );
for ( let i = 0, l = params.length - 1; i < l; i ++ ) {
builder.addFlowCode( ( i === 0 ? '' : builder.tab ) + '}\n\n' ).removeFlowTab();
}
builder.addFlowTab();
}
}
/**
* TSL function for creating a loop node.
*
* @tsl
* @function
* @param {...(LoopNode~Params|loopBodyCallback)} params - Any number of loop parameters followed by the loop body.
* @returns {LoopNode}
*/
const Loop = ( ...params ) => new LoopNode( nodeArray( params, 'int' ) ).toStack();
/**
* TSL function for inserting a `continue` expression into the shader.
*
* @tsl
* @function
* @returns {ExpressionNode}
*/
const Continue = () => expression( 'continue' ).toStack();
/**
* TSL function for inserting a `break` expression into the shader.
*
* @tsl
* @function
* @returns {ExpressionNode}
*/
const Break = () => expression( 'break' ).toStack();
/**
* The parameters of a loop. A number or int/uint node defines the loop's end value,
* a bool node defines a `while` loop and an object allows a more detailed configuration.
*
* @typedef {number|Node<int>|Node<uint>|Node<bool>|LoopNode~ObjectParams} LoopNode~Params
*/
/**
* A detailed loop configuration.
*
* @typedef {Object} LoopNode~ObjectParams
* @property {number|Node<int>|Node<uint>} [start=0] - The initial value of the loop variable.
* @property {number|Node<int>|Node<uint>} [end] - The value the loop variable is compared against. If omitted, the loop counts down from `start - 1` to `0`.
* @property {string} [name] - The name of the loop variable. Defaults to `i`, `j`, `k` and so on.
* @property {string} [type='int'] - The data type of the loop variable.
* @property {('<'|'<='|'>'|'>=')} [condition] - The comparison operator. The loop runs as long as the comparison is true. Inferred from `start` and `end` if not set.
* @property {string|number|Function|Node} [update] - Defines how the loop variable is updated after each iteration. Inferred from `condition` and `type` if not set.
*/
/**
* The loop body.
*
* @callback loopBodyCallback
* @param {Object<string, Node>} inputs - The loop variables of the current `Loop()` call, keyed by their name.
*/
// https://github.com/cabbibo/glsl-tri-noise-3d
const tri = /*@__PURE__*/ Fn( ( [ x ] ) => {
return x.fract().sub( .5 ).abs();
}, { x: 'float', return: 'float' } );
const tri3 = /*@__PURE__*/ Fn( ( [ p ] ) => {
return vec3( tri( p.z.add( tri( p.y.mul( 1. ) ) ) ), tri( p.z.add( tri( p.x.mul( 1. ) ) ) ), tri( p.y.add( tri( p.x.mul( 1. ) ) ) ) );
}, { p: 'vec3', return: 'vec3' } );
/**
* Generates a noise value from the given position, speed and time parameters.
*
* @tsl
* @function
* @param {Node<vec3>} position - The position.
* @param {Node<float>} speed - The speed.
* @param {Node<float>} time - The time.
* @return {Node<float>} The generated noise.
*/
const triNoise3D = /*@__PURE__*/ Fn( ( [ position, speed, time ] ) => {
const p = vec3( position ).toVar();
const z = float( 1.4 ).toVar();
const rz = float( 0.0 ).toVar();
const bp = vec3( p ).toVar();
Loop( { start: float( 0.0 ), end: float( 3.0 ), type: 'float', condition: '<=' }, () => {
const dg = vec3( tri3( bp.mul( 2.0 ) ) ).toVar();
p.addAssign( dg.add( time.mul( float( 0.1 ).mul( speed ) ) ) );
bp.mulAssign( 1.8 );
z.mulAssign( 1.5 );
p.mulAssign( 1.2 );
const t = float( tri( p.z.add( tri( p.x.add( tri( p.y ) ) ) ) ) ).toVar();
rz.addAssign( t.div( z ) );
bp.addAssign( 0.14 );
} );
return rz;
}, { position: 'vec3', speed: 'float', time: 'float', return: 'float' } );
const _sphere = /*@__PURE__*/ new Sphere();
/**
* This node can be used to access transformation related metrics of 3D objects.
* Depending on the selected scope, a different metric is represented as a uniform
* in the shader. The following scopes are supported:
*
* - `POSITION`: The object's position in world space.
* - `VIEW_POSITION`: The object's position in view/camera space.
* - `DIRECTION`: The object's direction in world space.
* - `SCALE`: The object's scale in world space.
* - `WORLD_MATRIX`: The object's matrix in world space.
*
* @augments Node
*/
class Object3DNode extends Node {
static get type() {
return 'Object3DNode';
}
/**
* Constructs a new object 3D node.
*
* @param {('position'|'viewPosition'|'direction'|'scale'|'worldMatrix')} scope - The node represents a different type of transformation depending on the scope.
* @param {?Object3D} [object3d=null] - The 3D object.
*/
constructor( scope, object3d = null ) {
super();
/**
* The node reports a different type of transformation depending on the scope.
*
* @type {('position'|'viewPosition'|'direction'|'scale'|'worldMatrix')}
*/
this.scope = scope;
/**
* The 3D object.
*
* @type {?Object3D}
* @default null
*/
this.object3d = object3d;
/**
* Overwritten since this type of node is updated per object.
*
* @type {string}
* @default 'object'
*/
this.updateType = NodeUpdateType.OBJECT;
/**
* Holds the value of the node as a uniform.
*
* @type {UniformNode}
*/
this.uniformNode = new UniformNode( null );
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Overwritten since the node type is inferred from the scope.
*
* @return {('mat4'|'vec3'|'float')} The node type.
*/
generateNodeType() {
const scope = this.scope;
if ( scope === Object3DNode.WORLD_MATRIX ) {
return 'mat4';
} else if ( scope === Object3DNode.POSITION || scope === Object3DNode.VIEW_POSITION || scope === Object3DNode.DIRECTION || scope === Object3DNode.SCALE ) {
return 'vec3';
} else if ( scope === Object3DNode.RADIUS ) {
return 'float';
}
}
/**
* Updates the uniform value depending on the scope.
*
* @param {NodeFrame} frame - The current node frame.
*/
update( frame ) {
const object = this.object3d;
const uniformNode = this.uniformNode;
const scope = this.scope;
if ( scope === Object3DNode.WORLD_MATRIX ) {
uniformNode.value = object.matrixWorld;
} else if ( scope === Object3DNode.POSITION ) {
uniformNode.value = uniformNode.value || new Vector3();
uniformNode.value.setFromMatrixPosition( object.matrixWorld );
} else if ( scope === Object3DNode.SCALE ) {
uniformNode.value = uniformNode.value || new Vector3();
uniformNode.value.setFromMatrixScale( object.matrixWorld );
} else if ( scope === Object3DNode.DIRECTION ) {
uniformNode.value = uniformNode.value || new Vector3();
object.getWorldDirection( uniformNode.value );
} else if ( scope === Object3DNode.VIEW_POSITION ) {
const camera = frame.camera;
uniformNode.value = uniformNode.value || new Vector3();
uniformNode.value.setFromMatrixPosition( object.matrixWorld );
uniformNode.value.applyMatrix4( camera.matrixWorldInverse );
} else if ( scope === Object3DNode.RADIUS ) {
const geometry = frame.object.geometry;
if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
_sphere.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );
uniformNode.value = _sphere.radius;
}
}
/**
* Generates the code snippet of the uniform node. The node type of the uniform
* node also depends on the selected scope.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated code snippet.
*/
generate( builder ) {
const scope = this.scope;
if ( scope === Object3DNode.WORLD_MATRIX ) {
this.uniformNode.nodeType = 'mat4';
} else if ( scope === Object3DNode.POSITION || scope === Object3DNode.VIEW_POSITION || scope === Object3DNode.DIRECTION || scope === Object3DNode.SCALE ) {
this.uniformNode.nodeType = 'vec3';
} else if ( scope === Object3DNode.RADIUS ) {
this.uniformNode.nodeType = 'float';
}
return this.uniformNode.build( builder );
}
serialize( data ) {
super.serialize( data );
data.scope = this.scope;
}
deserialize( data ) {
super.deserialize( data );
this.scope = data.scope;
}
}
Object3DNode.WORLD_MATRIX = 'worldMatrix';
Object3DNode.POSITION = 'position';
Object3DNode.SCALE = 'scale';
Object3DNode.VIEW_POSITION = 'viewPosition';
Object3DNode.DIRECTION = 'direction';
Object3DNode.RADIUS = 'radius';
/**
* TSL function for creating an object 3D node that represents the object's direction in world space.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<vec3>}
*/
const objectDirection = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.DIRECTION ).setParameterLength( 1 );
/**
* TSL function for creating an object 3D node that represents the object's world matrix.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<mat4>}
*/
const objectWorldMatrix = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.WORLD_MATRIX ).setParameterLength( 1 );
/**
* TSL function for creating an object 3D node that represents the object's position in world space.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<vec3>}
*/
const objectPosition = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.POSITION ).setParameterLength( 1 );
/**
* TSL function for creating an object 3D node that represents the object's scale in world space.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<vec3>}
*/
const objectScale = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.SCALE ).setParameterLength( 1 );
/**
* TSL function for creating an object 3D node that represents the object's position in view/camera space.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<vec3>}
*/
const objectViewPosition = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.VIEW_POSITION ).setParameterLength( 1 );
/**
* TSL function for creating an object 3D node that represents the object's radius.
*
* @tsl
* @function
* @param {?Object3D} [object3d] - The 3D object.
* @returns {Object3DNode<float>}
*/
const objectRadius = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.RADIUS ).setParameterLength( 1 );
/**
* The node allows to set values for built-in shader variables. That is
* required for features like hardware-accelerated vertex clipping.
*
* @augments Node
*/
class BuiltinNode extends Node {
/**
* Constructs a new builtin node.
*
* @param {string} name - The name of the built-in shader variable.
*/
constructor( name ) {
super( 'float' );
/**
* The name of the built-in shader variable.
*
* @type {string}
*/
this.name = name;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBuiltinNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Generates the code snippet of the builtin node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated code snippet.
*/
generate( /* builder */ ) {
return this.name;
}
}
/**
* TSL function for creating a builtin node.
*
* @tsl
* @function
* @param {string} name - The name of the built-in shader variable.
* @returns {BuiltinNode}
*/
const builtin = /*@__PURE__*/ nodeProxy( BuiltinNode ).setParameterLength( 1 );
/**
* A special type of uniform node which represents array-like data
* as uniform buffers. The access usually happens via `element()`
* which returns an instance of {@link ArrayElementNode}. For example:
*
* ```js
* const bufferNode = buffer( array, 'mat4', count );
* const matrixNode = bufferNode.element( index ); // access a matrix from the buffer
* ```
* In general, it is recommended to use the more managed {@link UniformArrayNode}
* since it handles more input types and automatically cares about buffer paddings.
*
* @augments UniformNode
*/
class BufferNode extends UniformNode {
static get type() {
return 'BufferNode';
}
/**
* Constructs a new buffer node.
*
* @param {Array<number>} value - Array-like buffer data.
* @param {string} bufferType - The data type of the buffer.
* @param {number} [bufferCount=0] - The count of buffer elements.
*/
constructor( value, bufferType, bufferCount = 0 ) {
super( value, bufferType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBufferNode = true;
/**
* The data type of the buffer.
*
* @type {string}
*/
this.bufferType = bufferType;
/**
* The uniform node that holds the value of the reference node.
*
* @type {number}
* @default 0
*/
this.bufferCount = bufferCount;
/**
* An array of update ranges.
*
* @type {Array<{start: number, count: number}>}
*/
this.updateRanges = [];
}
/**
* Adds a range of data in the data array to be updated on the GPU.
*
* @param {number} start - Position at which to start update.
* @param {number} count - The number of components to update.
*/
addUpdateRange( start, count ) {
this.updateRanges.push( { start, count } );
}
/**
* Clears the update ranges.
*/
clearUpdateRanges() {
this.updateRanges.length = 0;
}
/**
* The data type of the buffer elements.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The element type.
*/
getElementType( builder ) {
return this.getNodeType( builder );
}
/**
* Overwrites the default implementation to return a fixed value `'buffer'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return 'buffer';
}
}
/**
* TSL function for creating a buffer node.
*
* @tsl
* @function
* @param {Array<number>} value - Array-like buffer data.
* @param {string} type - The data type of a buffer element.
* @param {number} count - The count of buffer elements.
* @returns {BufferNode}
*/
const buffer = ( value, type, count ) => new BufferNode( value, type, count );
/**
* Represents the element access on uniform array nodes.
*
* @augments ArrayElementNode
*/
class UniformArrayElementNode extends ArrayElementNode {
static get type() {
return 'UniformArrayElementNode';
}
/**
* Constructs a new buffer node.
*
* @param {UniformArrayNode} uniformArrayNode - The uniform array node to access.
* @param {IndexNode} indexNode - The index data that define the position of the accessed element in the array.
*/
constructor( uniformArrayNode, indexNode ) {
super( uniformArrayNode, indexNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isArrayBufferElementNode = true;
}
generate( builder ) {
const snippet = super.generate( builder );
const type = this.getNodeType( builder );
const paddedType = this.node.getPaddedType( builder );
return builder.format( snippet, paddedType, type );
}
}
/**
* Similar to {@link BufferNode} this module represents array-like data as
* uniform buffers. Unlike {@link BufferNode}, it can handle more common
* data types in the array (e.g `three.js` primitives) and automatically
* manage buffer padding. It should be the first choice when working with
* uniforms buffers.
* ```js
* const tintColors = uniformArray( [
* new Color( 1, 0, 0 ),
* new Color( 0, 1, 0 ),
* new Color( 0, 0, 1 )
* ], 'color' );
*
* const redColor = tintColors.element( 0 );
*
* @augments BufferNode
*/
class UniformArrayNode extends BufferNode {
static get type() {
return 'UniformArrayNode';
}
/**
* Constructs a new uniform array node.
*
* @param {Array<any>} value - Array holding the buffer data.
* @param {?string} [elementType=null] - The data type of a buffer element.
*/
constructor( value, elementType = null ) {
super( null );
/**
* Array holding the buffer data. Unlike {@link BufferNode}, the array can
* hold number primitives as well as three.js objects like vectors, matrices
* or colors.
*
* @type {Array<any>}
*/
this.array = value;
/**
* The data type of an array element.
*
* @type {string}
*/
this.elementType = elementType === null ? getValueType( value[ 0 ] ) : elementType;
/**
* Overwritten since uniform array nodes are updated per render.
*
* @type {string}
* @default 'render'
*/
this.updateType = NodeUpdateType.RENDER;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isArrayBufferNode = true;
}
/**
* This method is overwritten since the node type is inferred from the
* padded type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.getPaddedType( builder );
}
/**
* The data type of the array elements.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The element type.
*/
getElementType() {
return this.elementType;
}
/**
* Returns the padded type based on the element type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The padded type.
*/
getPaddedType( builder ) {
const elementType = this.elementType;
let paddedType = 'vec4';
if ( elementType === 'mat2' ) {
paddedType = builder.renderer.backend.isWebGLBackend === true ? 'vec4' : 'mat2';
} else if ( /mat/.test( elementType ) === true ) {
paddedType = 'mat4';
} else if ( elementType.charAt( 0 ) === 'i' ) {
paddedType = 'ivec4';
} else if ( elementType.charAt( 0 ) === 'u' ) {
paddedType = 'uvec4';
}
return paddedType;
}
update( /*frame*/ ) {
this.updateBuffer();
}
/**
* Composes a user-defined update with the buffer transfer.
*
* @param {Function} callback - The update function.
* @param {string} updateType - The update type.
* @return {UniformArrayNode} A reference to this node.
*/
onUpdate( callback, updateType ) {
callback = callback.bind( this );
return super.onUpdate( ( frame, self ) => {
callback( frame, self );
this.updateBuffer();
}, updateType );
}
/**
* The method makes sure to correctly transfer the data from the (complex) objects
* in the array to the internal, correctly padded value buffer.
*/
updateBuffer() {
const { array, value } = this;
const elementType = this.elementType;
if ( elementType === 'float' || elementType === 'int' || elementType === 'uint' ) {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 4;
value[ index ] = array[ i ];
}
} else if ( elementType === 'color' ) {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 4;
const vector = array[ i ];
value[ index ] = vector.r;
value[ index + 1 ] = vector.g;
value[ index + 2 ] = vector.b || 0;
//value[ index + 3 ] = vector.a || 0;
}
} else if ( elementType === 'mat2' ) {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 4;
const matrix = array[ i ];
value[ index ] = matrix.elements[ 0 ];
value[ index + 1 ] = matrix.elements[ 1 ];
value[ index + 2 ] = matrix.elements[ 2 ];
value[ index + 3 ] = matrix.elements[ 3 ];
}
} else if ( elementType === 'mat3' ) {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 16;
const matrix = array[ i ];
value[ index ] = matrix.elements[ 0 ];
value[ index + 1 ] = matrix.elements[ 1 ];
value[ index + 2 ] = matrix.elements[ 2 ];
value[ index + 4 ] = matrix.elements[ 3 ];
value[ index + 5 ] = matrix.elements[ 4 ];
value[ index + 6 ] = matrix.elements[ 5 ];
value[ index + 8 ] = matrix.elements[ 6 ];
value[ index + 9 ] = matrix.elements[ 7 ];
value[ index + 10 ] = matrix.elements[ 8 ];
value[ index + 15 ] = 1;
}
} else if ( elementType === 'mat4' ) {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 16;
const matrix = array[ i ];
for ( let i = 0; i < matrix.elements.length; i ++ ) {
value[ index + i ] = matrix.elements[ i ];
}
}
} else {
for ( let i = 0; i < array.length; i ++ ) {
const index = i * 4;
const vector = array[ i ];
value[ index ] = vector.x;
value[ index + 1 ] = vector.y;
value[ index + 2 ] = vector.z || 0;
value[ index + 3 ] = vector.w || 0;
}
}
}
/**
* Implement the value buffer creation based on the array data.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {null}
*/
setup( builder ) {
const length = this.array.length;
const elementType = this.elementType;
let arrayType = Float32Array;
const paddedType = this.getPaddedType( builder );
const paddedElementLength = builder.getTypeLength( paddedType );
if ( elementType.charAt( 0 ) === 'i' ) arrayType = Int32Array;
if ( elementType.charAt( 0 ) === 'u' ) arrayType = Uint32Array;
this.value = new arrayType( length * paddedElementLength );
this.bufferCount = length;
this.bufferType = paddedType;
this.updateBuffer(); // initialize the buffer values
return super.setup( builder );
}
/**
* Overwrites the default `element()` method to provide element access
* based on {@link UniformArrayNode}.
*
* @param {IndexNode} indexNode - The index node.
* @return {UniformArrayElementNode}
*/
element( indexNode ) {
return new UniformArrayElementNode( this, nodeObject( indexNode ) );
}
}
/**
* TSL function for creating an uniform array node.
*
* @tsl
* @function
* @param {Array<any>} values - Array-like data.
* @param {?string} [nodeType] - The data type of the array elements.
* @returns {UniformArrayNode}
*/
const uniformArray = ( values, nodeType ) => new UniformArrayNode( values, nodeType );
let _screenSizeVec, _viewportVec;
/**
* This node provides a collection of screen related metrics.
* Depending on {@link ScreenNode#scope}, the nodes can represent
* resolution or viewport data as well as fragment or uv coordinates.
*
* @augments Node
*/
class ScreenNode extends Node {
static get type() {
return 'ScreenNode';
}
/**
* Constructs a new screen node.
*
* @param {('coordinate'|'viewport'|'size'|'uv')} scope - The node's scope.
*/
constructor( scope ) {
super();
/**
* The node represents different metric depending on which scope is selected.
*
* - `ScreenNode.COORDINATE`: Window-relative coordinates of the current fragment according to WebGPU standards.
* - `ScreenNode.VIEWPORT`: The current viewport defined as a four-dimensional vector.
* - `ScreenNode.SIZE`: The dimensions of the current bound framebuffer.
* - `ScreenNode.UV`: Normalized coordinates.
*
* @type {('coordinate'|'viewport'|'size'|'uv')}
*/
this.scope = scope;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isViewportNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* This method is overwritten since the node type depends on the selected scope.
*
* @return {('vec2'|'vec4')} The node type.
*/
generateNodeType() {
if ( this.scope === ScreenNode.VIEWPORT ) return 'vec4';
else return 'vec2';
}
/**
* `ScreenNode` implements {@link Node#update} to retrieve viewport and size information
* from the current renderer.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( { renderer } ) {
const renderTarget = renderer.getRenderTarget();
if ( this.scope === ScreenNode.VIEWPORT ) {
if ( renderTarget !== null ) {
_viewportVec.copy( renderTarget.viewport );
} else {
renderer.getViewport( _viewportVec );
_viewportVec.multiplyScalar( renderer.getPixelRatio() );
}
} else {
if ( renderTarget !== null ) {
_screenSizeVec.width = renderTarget.width;
_screenSizeVec.height = renderTarget.height;
} else {
renderer.getDrawingBufferSize( _screenSizeVec );
}
}
}
setup( /*builder*/ ) {
const scope = this.scope;
let output = null;
if ( scope === ScreenNode.SIZE ) {
output = uniform( _screenSizeVec || ( _screenSizeVec = new Vector2() ) ).setGroup( renderGroup );
} else if ( scope === ScreenNode.VIEWPORT ) {
output = uniform( _viewportVec || ( _viewportVec = new Vector4() ) ).setGroup( renderGroup );
} else {
output = vec2( screenCoordinate.div( screenSize ) );
}
//
let updateType = NodeUpdateType.NONE;
if ( this.scope === ScreenNode.SIZE || this.scope === ScreenNode.VIEWPORT ) {
updateType = NodeUpdateType.RENDER;
}
this.updateType = updateType;
//
return output;
}
generate( builder ) {
if ( this.scope === ScreenNode.COORDINATE ) {
let coord = builder.getFragCoord();
if ( builder.isFlipY() ) {
// follow webgpu standards
const size = builder.getNodeProperties( screenSize ).outputNode.build( builder );
coord = `${ builder.getType( 'vec2' ) }( ${ coord }.x, ${ size }.y - ${ coord }.y )`;
}
return coord;
}
return super.generate( builder );
}
}
ScreenNode.COORDINATE = 'coordinate';
ScreenNode.VIEWPORT = 'viewport';
ScreenNode.SIZE = 'size';
ScreenNode.UV = 'uv';
// Screen
/**
* TSL object that represents the current DPR.
*
* @tsl
* @type {UniformNode<float>}
*/
const screenDPR = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( ( { renderer } ) => renderer.getPixelRatio() );
/**
* TSL object that represents normalized screen coordinates, unitless in `[0, 1]`.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const screenUV = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.UV );
/**
* TSL object that represents the screen resolution in physical pixel units.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const screenSize = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.SIZE );
/**
* TSL object that represents the current `x`/`y` pixel position on the screen in physical pixel units.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const screenCoordinate = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.COORDINATE );
// Viewport
/**
* TSL object that represents the viewport rectangle as `x`, `y`, `width` and `height` in physical pixel units.
*
* @tsl
* @type {ScreenNode<vec4>}
*/
const viewport = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.VIEWPORT );
/**
* TSL object that represents the viewport resolution in physical pixel units.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const viewportSize = viewport.zw;
/**
* TSL object that represents the current `x`/`y` pixel position on the viewport in physical pixel units.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const viewportCoordinate = /*@__PURE__*/ screenCoordinate.sub( viewport.xy );
/**
* TSL object that represents normalized viewport coordinates, unitless in `[0, 1]`.
*
* @tsl
* @type {ScreenNode<vec2>}
*/
const viewportUV = /*@__PURE__*/ viewportCoordinate.div( viewportSize );
// Cache single-camera uniforms globally. Array uniforms are cached per renderer and
// camera count so cameras with different array sizes cannot overwrite each other's
// buffers, while builders with matching counts still share them.
const _cameraArrays = /*@__PURE__*/ new WeakMap();
function getCameraArray( renderer, camera, name, create ) {
let arrays = _cameraArrays.get( renderer );
if ( arrays === undefined ) {
arrays = new Map();
_cameraArrays.set( renderer, arrays );
}
const key = name + ':' + camera.cameras.length;
let array = arrays.get( key );
if ( array === undefined ) {
array = create( camera );
arrays.set( key, array );
}
return array;
}
let _cameraProjectionMatrixBase = null;
let _cameraProjectionMatrixInverseBase = null;
let _cameraViewMatrixBase = null;
let _cameraWorldMatrixBase = null;
let _cameraNormalMatrixBase = null;
let _cameraPositionBase = null;
let _cameraViewportBase = null;
/**
* TSL object that represents the current `index` value of the camera if used ArrayCamera.
*
* @tsl
* @type {UniformNode<uint>}
*/
const cameraIndex = /*@__PURE__*/ uniform( 0, 'uint' ).setName( 'u_cameraIndex' ).setGroup( sharedUniformGroup( 'cameraIndex' ) ).toVarying( 'v_cameraIndex' );
/**
* TSL object that represents the `near` value of the camera used for the current render.
*
* @tsl
* @type {UniformNode<float>}
*/
const cameraNear = /*@__PURE__*/ uniform( 'float' ).setName( 'cameraNear' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.near );
/**
* TSL object that represents the `far` value of the camera used for the current render.
*
* @tsl
* @type {UniformNode<float>}
*/
const cameraFar = /*@__PURE__*/ uniform( 'float' ).setName( 'cameraFar' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.far );
/**
* TSL object that represents the projection matrix of the camera used for the current render.
*
* @tsl
* @type {UniformNode<mat4>}
*/
const cameraProjectionMatrix = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraProjectionMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraProjectionMatrices', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.projectionMatrix );
return uniformArray( values ).setGroup( renderGroup ).setName( 'cameraProjectionMatrices' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].projectionMatrix;
}
} );
} );
cameraProjectionMatrix = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraProjectionMatrixBase === null ) {
_cameraProjectionMatrixBase = uniform( camera.projectionMatrix ).setName( 'cameraProjectionMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrix );
}
cameraProjectionMatrix = _cameraProjectionMatrixBase;
}
return cameraProjectionMatrix;
} ).once() )();
/**
* TSL object that represents the inverse projection matrix of the camera used for the current render.
*
* @tsl
* @type {UniformNode<mat4>}
*/
const cameraProjectionMatrixInverse = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraProjectionMatrixInverse;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraProjectionMatricesInverse', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.projectionMatrixInverse );
return uniformArray( values ).setGroup( renderGroup ).setName( 'cameraProjectionMatricesInverse' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].projectionMatrixInverse;
}
} );
} );
cameraProjectionMatrixInverse = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraProjectionMatrixInverseBase === null ) {
_cameraProjectionMatrixInverseBase = uniform( camera.projectionMatrixInverse ).setName( 'cameraProjectionMatrixInverse' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrixInverse );
}
cameraProjectionMatrixInverse = _cameraProjectionMatrixInverseBase;
}
return cameraProjectionMatrixInverse;
} ).once() )();
/**
* TSL object that represents the view matrix of the camera used for the current render.
*
* @tsl
* @type {UniformNode<mat4>}
*/
const cameraViewMatrix = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraViewMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraViewMatrices', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.matrixWorldInverse );
return uniformArray( values ).setGroup( renderGroup ).setName( 'cameraViewMatrices' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].matrixWorldInverse;
}
} );
} );
cameraViewMatrix = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraViewMatrixBase === null ) {
_cameraViewMatrixBase = uniform( camera.matrixWorldInverse ).setName( 'cameraViewMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.matrixWorldInverse );
}
cameraViewMatrix = _cameraViewMatrixBase;
}
return cameraViewMatrix;
} ).once() )();
/**
* TSL object that represents the world matrix of the camera used for the current render.
*
* @tsl
* @type {UniformNode<mat4>}
*/
const cameraWorldMatrix = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraWorldMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraWorldMatrices', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.matrixWorld );
return uniformArray( values ).setGroup( renderGroup ).setName( 'cameraWorldMatrices' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].matrixWorld;
}
} );
} );
cameraWorldMatrix = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraWorldMatrixBase === null ) {
_cameraWorldMatrixBase = uniform( camera.matrixWorld ).setName( 'cameraWorldMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.matrixWorld );
}
cameraWorldMatrix = _cameraWorldMatrixBase;
}
return cameraWorldMatrix;
} ).once() )();
/**
* TSL object that represents the normal matrix of the camera used for the current render.
*
* @tsl
* @type {UniformNode<mat3>}
*/
const cameraNormalMatrix = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraNormalMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraNormalMatrices', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.normalMatrix );
return uniformArray( values ).setGroup( renderGroup ).setName( 'cameraNormalMatrices' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].normalMatrix;
}
} );
} );
cameraNormalMatrix = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraNormalMatrixBase === null ) {
_cameraNormalMatrixBase = uniform( camera.normalMatrix ).setName( 'cameraNormalMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.normalMatrix );
}
cameraNormalMatrix = _cameraNormalMatrixBase;
}
return cameraNormalMatrix;
} ).once() )();
/**
* TSL object that represents the position in world space of the camera used for the current render.
*
* @tsl
* @type {UniformNode<vec3>}
*/
const cameraPosition = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraPosition;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraPositions', ( camera ) => {
const positions = [];
for ( let i = 0, l = camera.cameras.length; i < l; i ++ ) {
positions.push( new Vector3() );
}
return uniformArray( positions ).setGroup( renderGroup ).setName( 'cameraPositions' ).onRenderUpdate( ( { camera }, self ) => {
const subCameras = camera.cameras;
const array = self.array;
for ( let i = 0, l = subCameras.length; i < l; i ++ ) {
array[ i ].setFromMatrixPosition( subCameras[ i ].matrixWorld );
}
} );
} );
cameraPosition = cameraArray.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
if ( _cameraPositionBase === null ) {
_cameraPositionBase = uniform( new Vector3() ).setName( 'cameraPosition' ).setGroup( renderGroup ).onRenderUpdate( ( { camera }, self ) => self.value.setFromMatrixPosition( camera.matrixWorld ) );
}
cameraPosition = _cameraPositionBase;
}
return cameraPosition;
} ).once() )();
/**
* TSL object that represents the viewport of the camera used for the current render.
*
* @tsl
* @type {UniformNode<vec4>}
*/
const cameraViewport = /*@__PURE__*/ ( Fn( ( { camera, renderer } ) => {
let cameraViewport;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const cameraArray = getCameraArray( renderer, camera, 'cameraViewports', ( camera ) => {
const values = camera.cameras.map( ( subCamera ) => subCamera.viewport );
return uniformArray( values, 'vec4' ).setGroup( renderGroup ).setName( 'cameraViewports' ).onRenderUpdate( ( { camera }, self ) => {
for ( let i = 0; i < camera.cameras.length; i ++ ) {
self.array[ i ] = camera.cameras[ i ].viewport;
}
} );
} );
cameraViewport = cameraArray.element( cameraIndex );
} else {
if ( _cameraViewportBase === null ) {
// Fallback for single camera
_cameraViewportBase = vec4( 0, 0, screenSize.x, screenSize.y ).toConst( 'cameraViewport' );
}
cameraViewport = _cameraViewportBase;
}
return cameraViewport;
} ).once() )();
const _modelViewMatrix = /*@__PURE__*/ new Matrix4();
/**
* This type of node is a specialized version of `Object3DNode`
* with larger set of model related metrics. Unlike `Object3DNode`,
* `ModelNode` extracts the reference to the 3D object from the
* current node frame state.
*
* @augments Object3DNode
*/
class ModelNode extends Object3DNode {
static get type() {
return 'ModelNode';
}
/**
* Constructs a new object model node.
*
* @param {('position'|'viewPosition'|'direction'|'scale'|'worldMatrix')} scope - The node represents a different type of transformation depending on the scope.
*/
constructor( scope ) {
super( scope );
}
/**
* Extracts the model reference from the frame state and then
* updates the uniform value depending on the scope.
*
* @param {NodeFrame} frame - The current node frame.
*/
update( frame ) {
this.object3d = frame.object;
super.update( frame );
}
}
/**
* TSL object that represents the object's direction in world space.
*
* @tsl
* @type {ModelNode<vec3>}
*/
const modelDirection = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.DIRECTION );
/**
* TSL object that represents the object's world matrix.
*
* @tsl
* @type {ModelNode<mat4>}
*/
const modelWorldMatrix = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.WORLD_MATRIX );
/**
* TSL object that represents the object's position in world space.
*
* @tsl
* @type {ModelNode<vec3>}
*/
const modelPosition = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.POSITION );
/**
* TSL object that represents the object's scale in world space.
*
* @tsl
* @type {ModelNode<vec3>}
*/
const modelScale = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.SCALE );
/**
* TSL object that represents the object's position in view/camera space.
*
* @tsl
* @type {ModelNode<vec3>}
*/
const modelViewPosition = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.VIEW_POSITION );
/**
* TSL object that represents the object's radius.
*
* @tsl
* @type {ModelNode<float>}
*/
const modelRadius = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.RADIUS );
/**
* TSL object that represents the object's normal matrix.
*
* @tsl
* @type {UniformNode<mat3>}
*/
const modelNormalMatrix = /*@__PURE__*/ uniform( new Matrix3() ).onObjectUpdate( ( { object }, self ) => self.value.getNormalMatrix( object.matrixWorld ) );
/**
* TSL object that represents the object's inverse world matrix.
*
* @tsl
* @type {UniformNode<mat4>}
*/
const modelWorldMatrixInverse = /*@__PURE__*/ uniform( new Matrix4() ).onObjectUpdate( ( { object }, self ) => self.value.copy( object.matrixWorld ).invert() );
/**
* TSL object that represents the object's model view matrix.
*
* @tsl
* @type {Node<mat4>}
*/
const modelViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => {
return builder.context.modelViewMatrix || mediumpModelViewMatrix;
} ).once() )().toVar( 'modelViewMatrix' );
// GPU Precision
/**
* TSL object that represents the object's model view in `mediump` precision.
*
* @tsl
* @type {Node<mat4>}
*/
const mediumpModelViewMatrix = /*@__PURE__*/ cameraViewMatrix.mul( modelWorldMatrix );
// CPU Precision
/**
* TSL object that represents the object's model view in `highp` precision
* which is achieved by computing the matrix in JS and not in the shader.
*
* @tsl
* @type {Node<mat4>}
*/
const highpModelViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => {
builder.context.isHighPrecisionModelViewMatrix = true;
const camera = builder.camera;
let highpModelViewMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const matrices = [];
for ( let i = 0; i < camera.cameras.length; i ++ ) {
matrices.push( new Matrix4() );
}
const modelViewMatrices = uniformArray( matrices ).onObjectUpdate( ( { object, camera }, self ) => {
const subCameras = camera.cameras;
const array = self.array;
for ( let i = 0, l = subCameras.length; i < l; i ++ ) {
array[ i ].multiplyMatrices( subCameras[ i ].matrixWorldInverse, object.matrixWorld );
}
} );
highpModelViewMatrix = modelViewMatrices.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
highpModelViewMatrix = uniform( 'mat4' ).onObjectUpdate( ( { object, camera } ) => {
return object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
} );
}
return highpModelViewMatrix;
} ).once() )().toVar( 'highpModelViewMatrix' );
/**
* TSL object that represents the object's model normal view in `highp` precision
* which is achieved by computing the matrix in JS and not in the shader.
*
* @tsl
* @type {Node<mat3>}
*/
const highpModelNormalViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => {
const isHighPrecisionModelViewMatrix = builder.context.isHighPrecisionModelViewMatrix;
const camera = builder.camera;
let highpModelNormalViewMatrix;
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
const matrices = [];
for ( let i = 0; i < camera.cameras.length; i ++ ) {
matrices.push( new Matrix3() );
}
const normalViewMatrices = uniformArray( matrices ).onObjectUpdate( ( { object, camera }, self ) => {
const subCameras = camera.cameras;
const array = self.array;
for ( let i = 0, l = subCameras.length; i < l; i ++ ) {
_modelViewMatrix.multiplyMatrices( subCameras[ i ].matrixWorldInverse, object.matrixWorld );
array[ i ].getNormalMatrix( _modelViewMatrix );
}
} );
highpModelNormalViewMatrix = normalViewMatrices.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex );
} else {
highpModelNormalViewMatrix = uniform( 'mat3' ).onObjectUpdate( ( { object, camera } ) => {
if ( isHighPrecisionModelViewMatrix !== true ) {
object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
}
return object.normalMatrix.getNormalMatrix( object.modelViewMatrix );
} );
}
return highpModelNormalViewMatrix;
} ).once() )().toVar( 'highpModelNormalViewMatrix' );
/**
* TSL object that represents the clip space position of the current rendered object.
*
* @tsl
* @type {VaryingNode<vec4>}
*/
const clipSpace = /*@__PURE__*/ ( Fn( ( builder ) => {
if ( builder.shaderStage !== 'fragment' ) {
warnOnce( 'TSL: `clipSpace` is only available in fragment stage.' );
return vec4();
}
return builder.context.clipSpace.toVarying( 'v_clipSpace' );
} ).once() )();
/**
* TSL object that represents the position attribute of the current rendered object.
*
* @tsl
* @type {AttributeNode<vec3>}
*/
const positionGeometry = /*@__PURE__*/ attribute( 'position', 'vec3' );
/**
* TSL object that represents the transformed vertex position in local space of the current rendered object.
*
* The term "transformed" indicates that an object or material's properties, such as skinning, batch,
* instancing, or displacement mapping, will change the vertex position of the node when present.
* To use the pre-transformed local space position of the object, use {@link positionGeometry}.
*
* @tsl
* @type {AttributeNode<vec3>}
*/
const positionLocal = /*@__PURE__*/ positionGeometry.toVarying( 'positionLocal' );
/**
* TSL object that represents the previous vertex position in local space of the current rendered object.
* Used in context of {@link VelocityNode} for rendering motion vectors.
*
* @tsl
* @type {AttributeNode<vec3>}
*/
const positionPrevious = /*@__PURE__*/ positionGeometry.toVarying( 'positionPrevious' );
/**
* TSL object that represents the vertex position in world space of the current rendered object.
*
* @tsl
* @type {VaryingNode<vec3>}
*/
const positionWorld = /*@__PURE__*/ ( Fn( ( builder ) => {
return modelWorldMatrix.mul( positionLocal ).xyz.toVarying( builder.getSubBuildProperty( 'v_positionWorld' ) );
}, 'vec3' ).once( [ 'POSITION' ] ) )();
/**
* TSL object that represents the position world direction of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const positionWorldDirection = /*@__PURE__*/ ( Fn( () => {
const vertexPWD = positionLocal.transformDirection( modelWorldMatrix ).toVarying( 'v_positionWorldDirection' );
return vertexPWD.normalize().toVar( 'positionWorldDirection' );
}, 'vec3' ).once( [ 'POSITION' ] ) )();
/**
* TSL object that represents the vertex position in view space of the current rendered object.
*
* @tsl
* @type {VaryingNode<vec3>}
*/
const positionView = /*@__PURE__*/ ( Fn( ( builder ) => {
if ( builder.shaderStage === 'fragment' && builder.material.vertexNode ) {
// reconstruct view position from clip space
const viewPos = cameraProjectionMatrixInverse.mul( clipSpace );
return viewPos.xyz.div( viewPos.w ).toVar( 'positionView' );
}
return builder.context.setupPositionView().toVarying( 'v_positionView' );
}, 'vec3' ).once( [ 'POSITION', 'VERTEX' ] ) )();
/**
* TSL object that represents the position view direction of the current rendered object.
*
* @tsl
* @type {VaryingNode<vec3>}
*/
const positionViewDirection = /*@__PURE__*/ ( Fn( ( builder ) => {
let output;
if ( builder.camera.isOrthographicCamera ) {
output = vec3( 0, 0, 1 );
} else {
output = positionView.negate().toVarying( 'v_positionViewDirection' ).normalize();
}
return output.toVar( 'positionViewDirection' );
}, 'vec3' ).once( [ 'POSITION' ] ) )();
/**
* TSL function for creating an uv attribute node with the given index.
*
* @tsl
* @function
* @param {number} [index=0] - The uv index.
* @return {AttributeNode<vec2>} The uv attribute node.
*/
const uv$1 = ( index = 0 ) => attribute( 'uv' + ( index > 0 ? index : '' ), 'vec2' );
/**
* TSL function for creating an equirect uv node.
*
* Can be used to compute texture coordinates for projecting an
* equirectangular texture onto a mesh for using it as the scene's
* background.
*
* ```js
* scene.backgroundNode = texture( equirectTexture, equirectUV() );
* ```
*
* @tsl
* @function
* @param {?Node<vec3>} [direction=positionWorldDirection] - A direction vector for sampling which is by default `positionWorldDirection`.
* @returns {Node<vec2>}
*/
const equirectUV = /*@__PURE__*/ Fn( ( [ direction = positionWorldDirection ] ) => {
const u = direction.z.atan( direction.x ).mul( 1 / ( Math.PI * 2 ) ).add( 0.5 );
const v = direction.y.clamp( -1, 1.0 ).asin().mul( 1 / Math.PI ).add( 0.5 );
return vec2( u, v );
} );
/**
* TSL function for creating an equirect direction node.
*
* Can be used to compute a direction vector from the given equirectangular
* UV coordinates.
*
* @tsl
* @function
* @param {?Node<vec2>} [uv=UV()] - The equirectangular UV coordinates.
* @returns {Node<vec3>} The computed direction vector.
*/
const equirectDirection = /*@__PURE__*/ Fn( ( [ uv = uv$1() ] ) => {
const theta = uv.x.sub( 0.5 ).mul( Math.PI * 2 );
const phi = uv.y.sub( 0.5 ).mul( Math.PI );
const cosPhi = phi.cos();
const x = cosPhi.mul( theta.cos() );
const y = phi.sin();
const z = cosPhi.mul( theta.sin() );
return vec3( x, y, z );
} );
/**
* This class allows to define multiple overloaded versions
* of the same function. Depending on the parameters of the function
* call, the node picks the best-fit overloaded version.
*
* @augments Node
*/
class FunctionOverloadingNode extends Node {
static get type() {
return 'FunctionOverloadingNode';
}
/**
* Constructs a new function overloading node.
*
* @param {Array<Function>} functionNodes - Array of `Fn` function definitions.
* @param {...Node} parametersNodes - A list of parameter nodes.
*/
constructor( functionNodes = [], ...parametersNodes ) {
super();
/**
* Array of `Fn` function definitions.
*
* @type {Array<Function>}
*/
this.functionNodes = functionNodes;
/**
* A list of parameter nodes.
*
* @type {Array<Node>}
*/
this.parametersNodes = parametersNodes;
/**
* The selected overloaded function call.
*
* @private
* @type {ShaderCallNodeInternal}
*/
this._candidateFn = null;
/**
* This node is marked as global.
*
* @type {boolean}
* @default true
*/
this.global = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* This method is overwritten since the node type is inferred from
* the function's return type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
const candidateFn = this.getCandidateFn( builder );
return candidateFn.shaderNode.layout.type;
}
/**
* Returns the candidate function for the current parameters.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {FunctionNode} The candidate function.
*/
getCandidateFn( builder ) {
const params = this.parametersNodes;
let candidateFn = this._candidateFn;
if ( candidateFn === null ) {
let bestCandidateFn = null;
let bestScore = -1;
for ( const functionNode of this.functionNodes ) {
const shaderNode = functionNode.shaderNode;
const layout = shaderNode.layout;
if ( layout === null ) {
throw new Error( 'THREE.FunctionOverloadingNode: FunctionNode must be a layout.' );
}
const inputs = layout.inputs;
if ( params.length === inputs.length ) {
let currentScore = 0;
for ( let i = 0; i < params.length; i ++ ) {
const param = params[ i ];
const input = inputs[ i ];
if ( builder.getVectorType( param.getNodeType( builder ) ) === input.type ) {
currentScore ++;
}
}
if ( currentScore > bestScore ) {
bestCandidateFn = functionNode;
bestScore = currentScore;
}
}
}
this._candidateFn = candidateFn = bestCandidateFn;
}
return candidateFn;
}
/**
* Sets up the node for the current parameters.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The setup node.
*/
setup( builder ) {
const candidateFn = this.getCandidateFn( builder );
return candidateFn( ...this.parametersNodes );
}
}
const overloadingBaseFn = /*@__PURE__*/ nodeProxy( FunctionOverloadingNode );
/**
* TSL function for creating a function overloading node.
*
* @tsl
* @function
* @param {Array<Function>} functionNodes - Array of `Fn` function definitions.
* @returns {FunctionOverloadingNode}
*/
const overloadingFn = ( functionNodes ) => ( ...params ) => overloadingBaseFn( functionNodes, ...params );
/**
* This node can be used to evaluate whether a primitive is front or back facing.
*
* @augments Node
*/
class FrontFacingNode extends Node {
static get type() {
return 'FrontFacingNode';
}
/**
* Constructs a new front facing node.
*/
constructor() {
super( 'bool' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isFrontFacingNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( builder ) {
if ( builder.shaderStage !== 'fragment' ) return 'true';
//
const { material } = builder;
if ( material.side === BackSide ) {
return 'false';
}
return builder.getFrontFacing();
}
}
/**
* TSL object that represents whether a primitive is front or back facing
*
* @tsl
* @type {FrontFacingNode<bool>}
*/
const frontFacing = /*@__PURE__*/ nodeImmutable( FrontFacingNode );
/**
* TSL object that represents the front facing status as a number instead of a bool.
* `1` means front facing, `-1` means back facing.
*
* @tsl
* @type {Node<float>}
*/
const faceDirection = /*@__PURE__*/ float( frontFacing ).mul( 2.0 ).sub( 1.0 );
/**
* Negates a vector if the rendering occurs on the back side of a face,
* based on the material's side configuration.
*
* - If the material's side is `BackSide`, the vector is inverted (negated).
* - If the material's side is `DoubleSide`, the vector is multiplied by `faceDirection`
* (negated only for back-facing fragments).
* - If the material's side is `FrontSide` (default), the vector remains unchanged.
*
* @tsl
* @function
* @param {Node<vec3>} vector - The vector to process.
* @returns {Node<vec3>} The processed vector.
*/
const negateOnBackSide = /*@__PURE__*/ Fn( ( [ vector ], { material } ) => {
const side = material.side;
if ( side === BackSide ) {
vector = vector.mul( -1 );
} else if ( side === DoubleSide ) {
vector = vector.mul( faceDirection );
}
return vector;
} );
/**
* Negates a vector if the rendering occurs on the back side of a face,
* based on the material's side configuration.
*
* - If the material's side is `BackSide`, the vector is inverted (negated).
* - If the material's side is `DoubleSide`, the vector is multiplied by `faceDirection`
* (negated only for back-facing fragments).
* - If the material's side is `FrontSide` (default), the vector remains unchanged.
*
* @tsl
* @function
* @deprecated since r185. Use {@link negateOnBackSide} instead.
* @param {Node<vec3>} vector - The vector to convert.
* @returns {Node<vec3>} The converted vector.
*/
const directionToFaceDirection = ( vector ) => {
warnOnce( 'TSL: "directionToFaceDirection()" has been renamed to "negateOnBackSide()".' ); // @deprecated r185
return negateOnBackSide( vector );
};
/**
* TSL object that represents the normal attribute of the current rendered object in local space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalGeometry = /*@__PURE__*/ attribute( 'normal', 'vec3' );
/**
* TSL object that represents the vertex normal of the current rendered object in local space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalLocal = /*@__PURE__*/ ( Fn( ( builder ) => {
if ( builder.geometry.hasAttribute( 'normal' ) === false ) {
warn( 'TSL: Vertex attribute "normal" not found on geometry.' );
return vec3( 0, 1, 0 );
}
return normalGeometry;
}, 'vec3' ).once() )().toVar( 'normalLocal' );
/**
* TSL object that represents the flat vertex normal of the current rendered object in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalFlat = /*@__PURE__*/ positionView.dFdx().cross( positionView.dFdy() ).normalize().toVar( 'normalFlat' );
/**
* TSL object that represents the vertex normal of the current rendered object in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalViewGeometry = /*@__PURE__*/ ( Fn( ( builder ) => {
let node;
if ( builder.isFlatShading() ) {
node = normalFlat;
} else {
node = transformNormalToView( normalLocal ).toVarying( 'v_normalViewGeometry' ).normalize();
}
return node;
}, 'vec3' ).once() )().toVar( 'normalViewGeometry' );
/**
* TSL object that represents the vertex normal of the current rendered object in world space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalWorldGeometry = /*@__PURE__*/ ( Fn( ( builder ) => {
let normal = normalViewGeometry.transformNormalByInverseViewMatrix( cameraViewMatrix );
if ( builder.isFlatShading() !== true ) {
normal = normal.toVarying( 'v_normalWorldGeometry' );
}
return normal.normalize().toVar( 'normalWorldGeometry' );
}, 'vec3' ).once() )();
/**
* TSL object that represents the vertex normal of the current rendered object in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalView = /*@__PURE__*/ ( Fn( ( builder ) => {
let node;
if ( builder.subBuildFn === 'NORMAL' || builder.subBuildFn === 'VERTEX' ) {
node = normalViewGeometry;
if ( builder.isFlatShading() !== true ) {
node = negateOnBackSide( node );
}
} else {
// Use custom context to avoid side effects from nodes overwriting getUV, getTextureLevel in the context (e.g. EnvironmentNode)
node = builder.context.setupNormal().context( { getUV: null, getTextureLevel: null } );
}
return node;
}, 'vec3' ).once( [ 'NORMAL', 'VERTEX' ] ) )().toVar( 'normalView' );
/**
* TSL object that represents the vertex normal of the current rendered object in world space.
*
* @tsl
* @type {Node<vec3>}
*/
const normalWorld = /*@__PURE__*/ normalView.transformNormalByInverseViewMatrix( cameraViewMatrix ).toVar( 'normalWorld' );
/**
* TSL object that represents the clearcoat vertex normal of the current rendered object in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const clearcoatNormalView = /*@__PURE__*/ ( Fn( ( { subBuildFn, context } ) => {
let node;
if ( subBuildFn === 'NORMAL' || subBuildFn === 'VERTEX' ) {
node = normalView;
} else {
// Use custom context to avoid side effects from nodes overwriting getUV, getTextureLevel in the context (e.g. EnvironmentNode)
node = context.setupClearcoatNormal().context( { getUV: null, getTextureLevel: null } );
}
return node;
}, 'vec3' ).once( [ 'NORMAL', 'VERTEX' ] ) )().toVar( 'clearcoatNormalView' );
/**
* Transforms the normal by the normal matrix of the given matrix and then normalizes the result.
*
* @tsl
* @function
* @param {Node<vec3>} normal - The normal.
* @param {Node<mat3|mat4>} [matrix=modelWorldMatrix] - The matrix.
* @return {Node<vec3>} The transformed normal.
*/
const transformNormal = /*@__PURE__*/ Fn( ( [ normal, matrix = modelWorldMatrix ] ) => {
const normalMatrix = mat3( matrix ).inverse().transpose();
return normalMatrix.mul( normal ).normalize();
} );
addMethodChaining( 'transformNormal', transformNormal );
/**
* Transforms the given normal from local to view space.
*
* @tsl
* @function
* @param {Node<vec3>} normal - The normal.
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The transformed normal.
*/
const transformNormalToView = /*@__PURE__*/ Fn( ( [ normal ], builder ) => {
const modelNormalViewMatrix = builder.context.modelNormalViewMatrix;
if ( modelNormalViewMatrix ) {
return normal.transformNormalByViewMatrix( modelNormalViewMatrix );
}
//
const transformedNormal = modelNormalMatrix.mul( normal );
return transformedNormal.transformNormalByViewMatrix( cameraViewMatrix );
} );
/**
* TSL function for creating a matcap uv node.
*
* Can be used to compute texture coordinates for projecting a
* matcap onto a mesh. Used by {@link MeshMatcapNodeMaterial}.
*
* @tsl
* @function
* @returns {Node<vec2>} The matcap UV coordinates.
*/
const matcapUV = /*@__PURE__*/ Fn( () => {
const x = vec3( positionViewDirection.z, 0, positionViewDirection.x.negate() ).normalize();
const y = positionViewDirection.cross( x );
return vec2( x.dot( normalView ), y.dot( normalView ) ).mul( 0.495 ).add( 0.5 ); // 0.495 to remove artifacts caused by undersized matcap disks
} ).once( [ 'NORMAL', 'VERTEX' ] )().toVar( 'matcapUV' );
/**
* A special type of uniform node that computes the
* maximum mipmap level for a given texture node.
*
* ```js
* const level = maxMipLevel( textureNode );
* ```
*
* @augments UniformNode
*/
class MaxMipLevelNode extends UniformNode {
static get type() {
return 'MaxMipLevelNode';
}
/**
* Constructs a new max mip level node.
*
* @param {TextureNode} textureNode - The texture node to compute the max mip level for.
*/
constructor( textureNode ) {
super( 0 );
/**
* The texture node to compute the max mip level for.
*
* @private
* @type {TextureNode}
*/
this._textureNode = textureNode;
/**
* The `updateType` is set to `NodeUpdateType.FRAME` since the node updates
* the texture once per frame in its {@link MaxMipLevelNode#update} method.
*
* @type {string}
* @default 'frame'
*/
this.updateType = NodeUpdateType.FRAME;
}
/**
* The texture node to compute the max mip level for.
*
* @readonly
* @type {TextureNode}
*/
get textureNode() {
return this._textureNode;
}
/**
* The texture.
*
* @readonly
* @type {Texture}
*/
get texture() {
return this._textureNode.value;
}
update() {
const texture = this.texture;
const images = texture.images;
const image = ( images && images.length > 0 ) ? ( ( images[ 0 ] && images[ 0 ].image ) || images[ 0 ] ) : texture.image;
if ( image && image.width !== undefined ) {
const { width, height } = image;
this.value = Math.log2( Math.max( width, height ) );
}
}
}
/**
* TSL function for creating a max mip level node.
*
* @tsl
* @function
* @param {TextureNode} textureNode - The texture node to compute the max mip level for.
* @returns {MaxMipLevelNode}
*/
const maxMipLevel = /*@__PURE__*/ nodeProxy( MaxMipLevelNode ).setParameterLength( 1 );
/**
* Represents the elapsed time in seconds.
*
* @tsl
* @type {UniformNode<float>}
*/
const time = /*@__PURE__*/ uniform( 0 ).setGroup( renderGroup ).onRenderUpdate( ( frame ) => frame.time );
/**
* Represents the delta time in seconds.
*
* @tsl
* @type {UniformNode<float>}
*/
const deltaTime = /*@__PURE__*/ uniform( 0 ).setGroup( renderGroup ).onRenderUpdate( ( frame ) => frame.deltaTime );
/**
* Represents the current frame ID.
*
* @tsl
* @type {UniformNode<uint>}
*/
const frameId = /*@__PURE__*/ uniform( 0, 'uint' ).setGroup( renderGroup ).onRenderUpdate( ( frame ) => frame.frameId );
/**
* Generates a sine wave oscillation based on a timer.
*
* @tsl
* @function
* @param {Node<float>} t - The timer to generate the oscillation with.
* @return {Node<float>} The oscillation node.
*/
const oscSine = ( t = time ) => t.add( 0.75 ).mul( Math.PI * 2 ).sin().mul( 0.5 ).add( 0.5 );
/**
* Generates a square wave oscillation based on a timer.
*
* @tsl
* @function
* @param {Node<float>} t - The timer to generate the oscillation with.
* @return {Node<float>} The oscillation node.
*/
const oscSquare = ( t = time ) => t.fract().round();
/**
* Generates a triangle wave oscillation based on a timer.
*
* @tsl
* @function
* @param {Node<float>} t - The timer to generate the oscillation with.
* @return {Node<float>} The oscillation node.
*/
const oscTriangle = ( t = time ) => t.add( 0.5 ).fract().mul( 2 ).sub( 1 ).abs();
/**
* Generates a sawtooth wave oscillation based on a timer.
*
* @tsl
* @function
* @param {Node<float>} t - The timer to generate the oscillation with.
* @return {Node<float>} The oscillation node.
*/
const oscSawtooth = ( t = time ) => t.fract();
/**
* Packs a normal vector into a color value.
*
* @tsl
* @function
* @param {Node<vec3>} node - The direction to pack.
* @return {Node<vec3>} The color.
*/
const packNormalToRGB = ( node ) => nodeObject( node ).mul( 0.5 ).add( 0.5 );
/**
* Unpacks a color value into a normal vector.
*
* @tsl
* @function
* @param {Node<vec3>} node - The color to unpack.
* @return {Node<vec3>} The direction.
*/
const unpackRGBToNormal = ( node ) => nodeObject( node ).mul( 2.0 ).sub( 1 );
/**
* Unpacks a tangent space normal, reconstructing the Z component by projecting the X,Y coordinates onto the hemisphere.
* The X,Y coordinates are expected to be in the [-1, 1] range.
*
* @tsl
* @function
* @param {Node<vec2>} xy - The X,Y coordinates of the normal.
* @return {Node<vec3>} The resulting normal.
*/
const unpackNormal = ( xy ) => vec3( xy, sqrt( saturate( float( 1.0 ).sub( dot( xy, xy ) ) ) ) );
/**
* @tsl
* @function
* @deprecated since r185. Use {@link packNormalToRGB} instead.
* @param {Node<vec3>} node - The direction to pack.
* @returns {Node<vec3>}
*/
const directionToColor = ( node ) => {
warnOnce( 'TSL: "directionToColor()" has been renamed to "packNormalToRGB()".' ); // @deprecated r185
return packNormalToRGB( node );
};
/**
* @tsl
* @function
* @deprecated since r185. Use {@link unpackRGBToNormal} instead.
* @param {Node<vec3>} node - The color to unpack.
* @returns {Node<vec3>}
*/
const colorToDirection = ( node ) => {
warnOnce( 'TSL: "colorToDirection()" has been renamed to "unpackRGBToNormal()".' ); // @deprecated r185
return unpackRGBToNormal( node );
};
/**
* Applies a rotation to the given position node.
*
* @augments Node
*/
class RotateNode extends Node {
static get type() {
return 'RotateNode';
}
/**
* Constructs a new rotate node.
*
* @param {Node} positionNode - The position node.
* @param {Node} rotationNode - Represents the rotation that is applied to the position node. Depending
* on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value.
* @param {string} [order='XYZ'] - The Euler rotation order. Only used for 3D rotation.
*/
constructor( positionNode, rotationNode, order = 'XYZ' ) {
super();
/**
* The position node.
*
* @type {Node}
*/
this.positionNode = positionNode;
/**
* Represents the rotation that is applied to the position node.
* Depending on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value.
*
* @type {Node}
*/
this.rotationNode = rotationNode;
/**
* The Euler rotation order.
*
* @private
* @type {string}
* @default 'XYZ'
*/
this._order = order;
}
/**
* Overwrites the default `customCacheKey()` implementation by including the
* Euler order into the cache key.
*
* @return {number} The hash.
*/
customCacheKey() {
return hashString( this._order );
}
/**
* Sets the Euler rotation order.
*
* @param {string} value - The Euler rotation order.
* @return {RotateNode} A reference to this node.
*/
setOrder( value ) {
this._order = value;
return this;
}
/**
* Gets the Euler rotation order.
*
* @return {string} The Euler rotation order.
*/
getOrder() {
return this._order;
}
/**
* The type of the {@link RotateNode#positionNode} defines the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node's type.
*/
generateNodeType( builder ) {
return this.positionNode.getNodeType( builder );
}
setup( builder ) {
const { rotationNode, positionNode } = this;
const nodeType = this.getNodeType( builder );
if ( nodeType === 'vec2' ) {
const cosAngle = rotationNode.cos();
const sinAngle = rotationNode.sin();
const rotationMatrix = mat2(
cosAngle, sinAngle,
sinAngle.negate(), cosAngle
);
return rotationMatrix.mul( positionNode );
} else {
const rotation = rotationNode;
const order = this._order;
const rotationXMatrix = mat4( vec4( 1.0, 0.0, 0.0, 0.0 ), vec4( 0.0, cos( rotation.x ), sin( rotation.x ), 0.0 ), vec4( 0.0, sin( rotation.x ).negate(), cos( rotation.x ), 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) );
const rotationYMatrix = mat4( vec4( cos( rotation.y ), 0.0, sin( rotation.y ).negate(), 0.0 ), vec4( 0.0, 1.0, 0.0, 0.0 ), vec4( sin( rotation.y ), 0.0, cos( rotation.y ), 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) );
const rotationZMatrix = mat4( vec4( cos( rotation.z ), sin( rotation.z ), 0.0, 0.0 ), vec4( sin( rotation.z ).negate(), cos( rotation.z ), 0.0, 0.0 ), vec4( 0.0, 0.0, 1.0, 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) );
const matrixMap = {
'X': rotationXMatrix,
'Y': rotationYMatrix,
'Z': rotationZMatrix
};
const matrixChain = matrixMap[ order.charAt( 0 ) ]
.mul( matrixMap[ order.charAt( 1 ) ] )
.mul( matrixMap[ order.charAt( 2 ) ] );
return matrixChain.mul( vec4( positionNode, 1.0 ) ).xyz;
}
}
serialize( data ) {
super.serialize( data );
data.order = this._order;
}
deserialize( data ) {
super.deserialize( data );
this._order = data.order;
}
}
/**
* TSL function for creating a rotate node.
*
* @tsl
* @function
* @param {Node} positionNode - The position node.
* @param {Node} rotationNode - Represents the rotation that is applied to the position node. Depending
* on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value.
* @param {string} [order='XYZ'] - The Euler rotation order. Only used for 3D rotation.
* @returns {RotateNode}
*/
const rotate = /*@__PURE__*/ nodeProxy( RotateNode ).setParameterLength( 2, 3 );
/**
* Replaces the default UV coordinates used in texture lookups.
*
* ```js
*material.contextNode = replaceDefaultUV( ( textureNode ) => {
*
* // ...
* return customUVCoordinates;
*
*} );
*```
*
* @tsl
* @function
* @param {function(Node):Node<vec2>|Node<vec2>} callback - A callback that receives the texture node
* and must return the new uv coordinates.
* @param {Node} [node=null] - An optional node to which the context will be applied.
* @return {ContextNode} A context node that replaces the default UV coordinates.
*/
function replaceDefaultUV( callback, node = null ) {
const getUV = typeof callback === 'function' ? callback : () => callback;
return context( node, { getUV } );
}
/**
* Rotates the given uv coordinates around a center point
*
* @tsl
* @function
* @param {Node<vec2>} uv - The uv coordinates.
* @param {Node<float>} rotation - The rotation defined in radians.
* @param {Node<vec2>} center - The center of rotation
* @return {Node<vec2>} The rotated uv coordinates.
*/
const rotateUV = /*@__PURE__*/ Fn( ( [ uv, rotation, center = vec2( 0.5 ) ] ) => {
return rotate( uv.sub( center ), rotation ).add( center );
}, { uv: 'vec2', rotation: 'float', center: 'vec2', return: 'vec2' } );
/**
* Applies a spherical warping effect to the given uv coordinates.
*
* @tsl
* @function
* @param {Node<vec2>} uv - The uv coordinates.
* @param {Node<float>} strength - The strength of the effect.
* @param {Node<vec2>} center - The center point
* @return {Node<vec2>} The updated uv coordinates.
*/
const spherizeUV = /*@__PURE__*/ Fn( ( [ uv, strength, center = vec2( 0.5 ) ] ) => {
const delta = uv.sub( center );
const delta2 = delta.dot( delta );
const delta4 = delta2.mul( delta2 );
const deltaOffset = delta4.mul( strength );
return uv.add( delta.mul( deltaOffset ) );
}, { uv: 'vec2', strength: 'float', center: 'vec2', return: 'vec2' } );
/**
* This can be used to achieve a billboarding behavior for flat meshes. That means they are
* oriented always towards the camera.
*
* ```js
* material.vertexNode = billboarding();
* ```
*
* @tsl
* @function
* @param {Object} config - The configuration object.
* @param {?Node<vec3>} [config.position=null] - Can be used to define the billboard center position directly.
* When null, the center is derived automatically from `positionWorld`.
* @param {boolean} [config.horizontal=true] - Whether to follow the camera rotation horizontally or not.
* @param {boolean} [config.vertical=false] - Whether to follow the camera rotation vertically or not.
* @param {boolean} [config.horizontalRotation=false] - Whether to rotate around the Y axis to face the camera.
* @return {Node<vec3>} The updated vertex position in clip space.
*/
const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false, horizontalRotation = false } ) => {
let center;
if ( position !== null ) {
center = nodeObject( position );
} else {
center = positionWorld.sub( modelWorldMatrix.mul( vec4( positionGeometry, 0 ) ).xyz );
}
const worldMatrix = modelWorldMatrix.toVar();
worldMatrix[ 3 ][ 0 ] = center.x;
worldMatrix[ 3 ][ 1 ] = center.y;
worldMatrix[ 3 ][ 2 ] = center.z;
const modelViewMatrix = cameraViewMatrix.mul( worldMatrix );
const scaleX = modelWorldMatrix[ 0 ].length();
const scaleY = modelWorldMatrix[ 1 ].length();
const scaleZ = modelWorldMatrix[ 2 ].length();
let right, up, forward;
if ( defined( horizontalRotation ) ) {
const worldPosition = worldMatrix[ 3 ].xyz;
const look = cameraPosition.sub( worldPosition );
const lookXZ = vec3( look.x, 0, look.z ).normalize();
const right_w = vec3( lookXZ.z, 0, lookXZ.x.negate() );
right = cameraViewMatrix.mul( vec4( right_w, 0 ) ).xyz.mul( scaleX );
up = cameraViewMatrix[ 1 ].xyz.mul( scaleY );
forward = cameraViewMatrix.mul( vec4( lookXZ, 0 ) ).xyz.mul( scaleZ );
} else {
if ( defined( horizontal ) ) right = vec3( scaleX, 0, 0 );
if ( defined( vertical ) ) up = vec3( 0, scaleY, 0 );
forward = vec3( 0, 0, 1 );
}
if ( right ) {
modelViewMatrix[ 0 ][ 0 ] = right.x;
modelViewMatrix[ 0 ][ 1 ] = right.y;
modelViewMatrix[ 0 ][ 2 ] = right.z;
}
if ( up ) {
modelViewMatrix[ 1 ][ 0 ] = up.x;
modelViewMatrix[ 1 ][ 1 ] = up.y;
modelViewMatrix[ 1 ][ 2 ] = up.z;
}
if ( forward ) {
modelViewMatrix[ 2 ][ 0 ] = forward.x;
modelViewMatrix[ 2 ][ 1 ] = forward.y;
modelViewMatrix[ 2 ][ 2 ] = forward.z;
}
return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionGeometry );
} );
/**
* A node that represents the dimensions of a texture. The texture size is
* retrieved in the shader via built-in shader functions like `textureDimensions()`
* or `textureSize()`.
*
* @augments Node
*/
class TextureSizeNode extends Node {
static get type() {
return 'TextureSizeNode';
}
/**
* Constructs a new texture size node.
*
* @param {TextureNode} textureNode - A texture node which size should be retrieved.
* @param {?Node<int>} [levelNode=null] - A level node which defines the requested mip.
*/
constructor( textureNode, levelNode = null ) {
super( 'uvec2' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isTextureSizeNode = true;
/**
* A texture node which size should be retrieved.
*
* @type {TextureNode}
*/
this.textureNode = textureNode;
/**
* A level node which defines the requested mip.
*
* @type {Node<int>}
* @default null
*/
this.levelNode = levelNode;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( builder ) {
const textureProperty = this.textureNode.build( builder, 'property' );
const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' );
const snippet = builder.generateTextureSize( this.textureNode.value, textureProperty, level );
return builder.format( snippet, this.getNodeType( builder ) );
}
}
/**
* TSL function for creating a texture size node.
*
* @tsl
* @function
* @param {TextureNode} textureNode - A texture node which size should be retrieved.
* @param {?Node<int>} [levelNode=null] - A level node which defines the requested mip.
* @returns {TextureSizeNode}
*/
const textureSize = /*@__PURE__*/ nodeProxy( TextureSizeNode ).setParameterLength( 1, 2 );
const EmptyTexture$1 = /*@__PURE__*/ new Texture();
/**
* This type of uniform node represents a 2D texture.
*
* @augments UniformNode
*/
class TextureNode extends UniformNode {
static get type() {
return 'TextureNode';
}
/**
* Constructs a new texture node.
*
* @param {Texture} [value=EmptyTexture] - The texture.
* @param {?Node<vec2|vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
*/
constructor( value = EmptyTexture$1, uvNode = null, levelNode = null, biasNode = null ) {
super( value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isTextureNode = true;
/**
* Represents the texture coordinates.
*
* @type {?Node<vec2|vec3>}
* @default null
*/
this.uvNode = uvNode;
/**
* Represents the mip level that should be selected.
*
* @type {?Node<int>}
* @default null
*/
this.levelNode = levelNode;
/**
* Represents the bias to be applied during level-of-detail computation.
*
* @type {?Node<float>}
* @default null
*/
this.biasNode = biasNode;
/**
* Represents a reference value a texture sample is compared to.
*
* @type {?Node<float>}
* @default null
*/
this.compareNode = null;
/**
* When using texture arrays, the depth node defines the layer to select.
*
* @type {?Node<int>}
* @default null
*/
this.depthNode = null;
/**
* When defined, a texture is sampled using explicit gradients.
*
* @type {?Array<Node<vec2>>}
* @default null
*/
this.gradNode = null;
/**
* Represents the optional index constant of the channel to gather.
* This must be in range [0, 3] and a compile-time constant.
*
* @type {?Node<int>}
* @default null
*/
this.gatherNode = null;
/**
* Represents the optional texel offset applied to the unnormalized texture
* coordinate before sampling the texture.
*
* @type {?Node<ivec2|ivec3>}
* @default null
*/
this.offsetNode = null;
/**
* Whether texture values should be sampled or fetched.
*
* @type {boolean}
* @default true
*/
this.sampler = true;
/**
* Whether the uv transformation matrix should be
* automatically updated or not. Use `setUpdateMatrix()`
* if you want to change the value of the property.
*
* @type {boolean}
* @default false
*/
this.updateMatrix = false;
/**
* The reference node.
*
* @type {?Node}
* @default null
*/
this.referenceNode = null;
/**
* The mergeable value is stored in a private property.
*
* @private
* @type {boolean}
* @default true
*/
this._mergeable = true;
/**
* The texture value is stored in a private property.
*
* @private
* @type {Texture}
*/
this._value = value;
/**
* The uniform node that represents the uv transformation matrix.
*
* @private
* @type {?UniformNode<mat3>}
* @default null
*/
this._matrixUniform = null;
/**
* The uniform node that represents the y-flip. Only required for WebGL.
*
* @private
* @type {?UniformNode<bool>}
* @default null
*/
this._flipYUniform = null;
/**
* Whether the node is used as a comparison sampler, e.g. via `samplerComparison()`.
*
* @private
* @type {boolean}
* @default false
*/
this._samplerComparison = false;
this.setUpdateMatrix( uvNode === null );
}
set mergeable( value ) {
if ( this.referenceNode ) {
this.referenceNode.mergeable = value;
} else {
this._mergeable = value;
}
}
/**
* Whether the uniform may be merged with other uniforms at compile-time.
*
* @type {boolean}
* @default true
*/
get mergeable() {
return this.referenceNode ? this.referenceNode.mergeable : this._mergeable;
}
set value( value ) {
if ( this.referenceNode ) {
this.referenceNode.value = value;
} else {
this._value = value;
}
}
/**
* The texture value.
*
* @type {Texture}
*/
get value() {
return this.referenceNode ? this.referenceNode.value : this._value;
}
/**
* Overwritten since the uniform hash is defined by the texture's UUID - if mergeable - and the hash of its reference node otherwise.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The uniform hash.
*/
getUniformHash( builder ) {
if ( this.referenceNode && ! this.referenceNode.mergeable ) {
return this.referenceNode.getHash( builder );
}
if ( ! this._mergeable ) {
return this.getHash( builder );
}
return this.value.uuid;
}
/**
* Overwritten since the texture's type and filters influence the generated shader code.
*
* @return {number} The custom cache key.
*/
customCacheKey() {
const { type, minFilter, magFilter } = this.value;
return hash$1( this.id, type, minFilter, magFilter );
}
/**
* Overwritten since the node type is inferred from the texture type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( /*builder*/ ) {
if ( this.gatherNode !== null ) {
return 'vec4';
}
return getTextureType( this.value );
}
/**
* Overwrites the default implementation to return a fixed value `'texture'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return 'texture';
}
/**
* Returns a default uvs based on the current texture's channel.
*
* @return {AttributeNode<vec2>} The default uvs.
*/
getDefaultUV() {
return uv$1( this.value.channel );
}
/**
* Overwritten to always return the texture reference of the node.
*
* @param {any} state - This method can be invocated in different contexts so `state` can refer to any object type.
* @return {Texture} The texture reference.
*/
updateReference( /*state*/ ) {
return this.value;
}
/**
* Transforms the given uv node with the texture transformation matrix.
*
* @param {Node} uvNode - The uv node to transform.
* @return {Node} The transformed uv node.
*/
getTransformedUV( uvNode ) {
return this._getMatrixUniform().mul( vec3( uvNode, 1 ) ).xy;
}
/**
* Transforms the given uv gradient with the linear part of the texture matrix.
*
* @private
* @param {Node<vec2>} gradientNode - The uv gradient.
* @return {Node<vec2>} The transformed gradient.
*/
_getTransformedGradient( gradientNode ) {
return this._getMatrixUniform().mul( vec3( gradientNode, 0 ) ).xy;
}
/**
* Returns the uniform of the texture matrix shared by all nodes of the same base texture node.
*
* @private
* @return {UniformNode<mat3>} The texture matrix uniform.
*/
_getMatrixUniform() {
const baseNode = this.getBase();
if ( baseNode._matrixUniform === null ) {
baseNode._matrixUniform = uniform( baseNode.value.matrix ).onObjectUpdate( () => {
const texture = baseNode.value;
if ( texture.matrixAutoUpdate === true ) texture.updateMatrix();
return texture.matrix;
} );
}
return baseNode._matrixUniform;
}
/**
* Defines whether the uv transformation matrix should automatically be updated or not.
*
* @param {boolean} value - The update toggle.
* @return {TextureNode} A reference to this node.
*/
setUpdateMatrix( value ) {
this.updateMatrix = value;
return this;
}
/**
* Setups the uv node. Depending on the backend as well as texture's image and type, it might be necessary
* to modify the uv node for correct sampling.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} uvNode - The uv node to setup.
* @return {Node} The updated uv node.
*/
setupUV( builder, uvNode ) {
if ( builder.isFlipY() ) {
const baseNode = this.getBase();
if ( baseNode._flipYUniform === null ) {
baseNode._flipYUniform = uniform( false ).onObjectUpdate( () => {
const texture = baseNode.value;
return ( texture.image instanceof ImageBitmap && texture.flipY === true ) || texture.isRenderTargetTexture === true || texture.isFramebufferTexture === true || texture.isDepthTexture === true;
} );
}
uvNode = uvNode.toVar();
if ( this.sampler ) {
uvNode = baseNode._flipYUniform.select( uvNode.flipY(), uvNode );
} else {
uvNode = baseNode._flipYUniform.select( uvNode.setY( int( textureSize( this, this.levelNode ).y ).sub( int( uvNode.y ) ).sub( 1 ) ), uvNode );
}
}
return uvNode;
}
/**
* Setups texture node by preparing the internal nodes for code generation.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup( builder ) {
const properties = builder.getNodeProperties( this );
properties.referenceNode = this.referenceNode;
//
const texture = this.value;
if ( ! texture || texture.isTexture !== true ) {
throw new NodeError( 'THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().', this.stackTrace );
}
//
const uvNode = Fn( () => {
let uvNode = this.uvNode;
if ( ( uvNode === null || builder.context.forceUVContext === true ) && builder.context.getUV ) {
uvNode = builder.context.getUV( this, builder );
}
if ( ! uvNode ) uvNode = this.getDefaultUV();
if ( this.updateMatrix === true ) {
uvNode = this.getTransformedUV( uvNode );
}
uvNode = this.setupUV( builder, uvNode );
return uvNode;
} )();
//
let levelNode = this.levelNode;
if ( levelNode === null && builder.context.getTextureLevel ) {
levelNode = builder.context.getTextureLevel( this );
}
//
let compareNode = null;
let compareStepNode = null;
if ( this.compareNode !== null ) {
if ( builder.renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) {
compareNode = this.compareNode;
} else {
const compareFunction = texture.compareFunction;
if ( compareFunction === null || compareFunction === LessCompare || compareFunction === LessEqualCompare || compareFunction === GreaterCompare || compareFunction === GreaterEqualCompare ) {
compareStepNode = this.compareNode;
} else {
compareNode = this.compareNode;
warnOnce( 'TSL: Only "LessCompare", "LessEqualCompare", "GreaterCompare" and "GreaterEqualCompare" are supported for depth texture comparison fallback.' );
}
}
}
// the context can provide analytic gradients of the default uv, e.g. if the uv is reconstructed per fragment
let gradNode = this.gradNode;
if ( gradNode === null && this.uvNode === null && levelNode === null && this.biasNode === null && this.sampler === true && builder.context.getTextureGradient ) {
const gradient = builder.context.getTextureGradient( this, builder );
if ( gradient ) {
gradNode = this.updateMatrix === true ? gradient.map( ( node ) => this._getTransformedGradient( node ) ) : gradient;
}
}
properties.uvNode = uvNode;
properties.levelNode = levelNode;
properties.biasNode = this.biasNode;
properties.compareNode = compareNode;
properties.compareStepNode = compareStepNode;
properties.gradNode = gradNode;
properties.gatherNode = this.gatherNode;
properties.depthNode = this.depthNode;
properties.offsetNode = this.offsetNode;
}
/**
* Generates the uv code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} uvNode - The uv node to generate code for.
* @return {string} The generated code snippet.
*/
generateUV( builder, uvNode ) {
return uvNode.build( builder, this.sampler === true ? 'vec2' : 'ivec2' );
}
/**
* Generates the offset code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} offsetNode - The offset node to generate code for.
* @return {string} The generated code snippet.
*/
generateOffset( builder, offsetNode ) {
return offsetNode.build( builder, 'ivec2' );
}
/**
* Generates the snippet for the texture sampling.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} textureProperty - The texture property.
* @param {string} uvSnippet - The uv snippet.
* @param {?string} levelSnippet - The level snippet.
* @param {?string} biasSnippet - The bias snippet.
* @param {?string} depthSnippet - The depth snippet.
* @param {?string} compareSnippet - The compare snippet.
* @param {?Array<string>} gradSnippet - The grad snippet.
* @param {?string} gatherSnippet - The gather snippet.
* @param {?string} offsetSnippet - The offset snippet.
* @param {?string} flipYSnippet - The y-flip snippet. Only used for WebGL.
* @return {string} The generated code snippet.
*/
generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, depthSnippet, compareSnippet, gradSnippet, gatherSnippet, offsetSnippet, flipYSnippet ) {
const texture = this.value;
let snippet;
if ( biasSnippet ) {
snippet = builder.generateTextureBias( texture, textureProperty, uvSnippet, biasSnippet, depthSnippet, offsetSnippet );
} else if ( gradSnippet ) {
snippet = builder.generateTextureGrad( texture, textureProperty, uvSnippet, gradSnippet, depthSnippet, offsetSnippet );
} else if ( gatherSnippet ) {
if ( compareSnippet ) {
snippet = builder.generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, flipYSnippet );
} else {
snippet = builder.generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet, flipYSnippet );
}
} else if ( compareSnippet ) {
snippet = builder.generateTextureCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet );
} else if ( this.sampler === false ) {
snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet );
} else if ( levelSnippet ) {
snippet = builder.generateTextureLevel( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet );
} else {
snippet = builder.generateTexture( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet );
}
return snippet;
}
/**
* Generates the code snippet of the texture node.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} output - The current output.
* @return {string} The generated code snippet.
*/
generate( builder, output ) {
const texture = this.value;
const properties = builder.getNodeProperties( this );
const textureProperty = super.generate( builder, 'property' );
if ( output === 'samplerComparison' || properties.compareNode !== null ) {
// make sure the texture node creates a binding with a comparison sampler if necessary
this.getSharedNode( builder )._samplerComparison = true;
}
if ( /^sampler/.test( output ) ) {
return textureProperty + '_sampler';
} else if ( builder.isReference( output ) ) {
return textureProperty;
} else {
const nodeData = builder.getDataFromNode( this );
const nodeType = this.getNodeType( builder );
let propertyName = nodeData.propertyName;
if ( propertyName === undefined ) {
const { uvNode, levelNode, biasNode, compareNode, compareStepNode, depthNode, gradNode, gatherNode, offsetNode } = properties;
const uvSnippet = this.generateUV( builder, uvNode );
const levelSnippet = levelNode ? levelNode.build( builder, 'float' ) : null;
const biasSnippet = biasNode ? biasNode.build( builder, 'float' ) : null;
const depthSnippet = depthNode ? depthNode.build( builder, 'int' ) : null;
const compareSnippet = compareNode ? compareNode.build( builder, 'float' ) : null;
const compareStepSnippet = compareStepNode ? compareStepNode.build( builder, 'float' ) : null;
const gradSnippet = gradNode ? [ gradNode[ 0 ].build( builder, 'vec2' ), gradNode[ 1 ].build( builder, 'vec2' ) ] : null;
const gatherSnippet = gatherNode ? gatherNode.build( builder, 'int' ) : null;
const offsetSnippet = offsetNode ? this.generateOffset( builder, offsetNode ) : null;
const flipYUniform = this.getBase()._flipYUniform;
const flipYSnippet = flipYUniform ? flipYUniform.build( builder, 'bool' ) : null;
let finalDepthSnippet = depthSnippet;
if ( finalDepthSnippet === null && texture.isArrayTexture && this.isTexture3DNode !== true ) {
finalDepthSnippet = '0';
}
const nodeVar = builder.getVarFromNode( this );
propertyName = builder.getPropertyName( nodeVar );
let snippet = this.generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, finalDepthSnippet, compareSnippet, gradSnippet, gatherSnippet, offsetSnippet, flipYSnippet );
let snippetType;
if ( texture.isDepthTexture === true && gatherSnippet === null ) {
snippetType = 'float';
} else {
snippetType = texture.type === UnsignedIntType ? 'uvec4' : ( texture.type === IntType ? 'ivec4' : 'vec4' );
}
snippet = builder.format( snippet, snippetType, nodeType );
if ( compareStepSnippet !== null ) {
const compareFunction = texture.compareFunction;
if ( compareFunction === GreaterCompare || compareFunction === GreaterEqualCompare ) {
snippet = step( expression( snippet, nodeType ), expression( compareStepSnippet, 'float' ) ).build( builder, nodeType );
} else {
snippet = step( expression( compareStepSnippet, 'float' ), expression( snippet, nodeType ) ).build( builder, nodeType );
}
}
builder.addLineFlowCode( `${propertyName} = ${snippet}`, this );
nodeData.snippet = snippet;
nodeData.propertyName = propertyName;
}
let snippet = propertyName;
if ( builder.needsToWorkingColorSpace( texture ) ) {
snippet = colorSpaceToWorking( expression( snippet, nodeType ), texture.colorSpace ).setup( builder ).build( builder, nodeType );
}
return builder.format( snippet, nodeType, output );
}
}
/**
* Sets the sampler value.
*
* @param {boolean} value - The sampler value to set.
* @return {TextureNode} A reference to this texture node.
*/
setSampler( value ) {
this.sampler = value;
return this;
}
/**
* Returns the sampler value.
*
* @return {boolean} The sampler value.
*/
getSampler() {
return this.sampler;
}
// @TODO: Move to TSL
/**
* Samples the texture with the given uv node.
*
* @param {Node} uvNode - The uv node.
* @return {TextureNode} A texture node representing the texture sample.
*/
sample( uvNode ) {
const textureNode = this.clone();
textureNode.uvNode = nodeObject( uvNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* TSL function for creating a texture node that fetches/loads texels without interpolation.
*
* @param {Node<uvec2>} uvNode - The uv node.
* @returns {TextureNode} A texture node representing the texture load.
*/
load( uvNode ) {
return this.sample( uvNode ).setSampler( false );
}
/**
* Samples a blurred version of the texture by defining an internal bias.
*
* @param {Node<float>} amountNode - How blurred the texture should be.
* @return {TextureNode} A texture node representing the texture sample.
*/
blur( amountNode ) {
const textureNode = this.clone();
textureNode.biasNode = nodeObject( amountNode ).mul( maxMipLevel( textureNode ) );
textureNode.referenceNode = this.getBase();
const map = textureNode.value;
if ( textureNode.generateMipmaps === false && ( map && map.generateMipmaps === false || map.minFilter === NearestFilter || map.magFilter === NearestFilter ) ) {
warn( 'TSL: texture().blur() requires mipmaps and sampling. Use .generateMipmaps=true and .minFilter/.magFilter=THREE.LinearFilter in the Texture.' );
textureNode.biasNode = null;
}
return textureNode;
}
/**
* Samples a specific mip of the texture.
*
* @param {Node<int>} levelNode - The mip level to sample.
* @return {TextureNode} A texture node representing the texture sample.
*/
level( levelNode ) {
const textureNode = this.clone();
textureNode.levelNode = nodeObject( levelNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Returns the texture size of the requested level.
*
* @param {Node<int>} levelNode - The level to compute the size for.
* @return {TextureSizeNode} The texture size.
*/
size( levelNode ) {
return textureSize( this, levelNode );
}
/**
* Samples the texture with the given bias.
*
* @param {Node<float>} biasNode - The bias node.
* @return {TextureNode} A texture node representing the texture sample.
*/
bias( biasNode ) {
const textureNode = this.clone();
textureNode.biasNode = nodeObject( biasNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Returns the base texture of this node.
* @return {TextureNode} The base texture node.
*/
getBase() {
return this.referenceNode ? this.referenceNode.getBase() : this;
}
/**
* Samples the texture by executing a compare operation.
*
* @param {Node<float>} compareNode - The node that defines the compare value.
* @return {TextureNode} A texture node representing the texture sample.
*/
compare( compareNode ) {
const textureNode = this.clone();
textureNode.compareNode = nodeObject( compareNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Samples the texture using an explicit gradient.
*
* @param {Node<vec2>} gradNodeX - The gradX node.
* @param {Node<vec2>} gradNodeY - The gradY node.
* @return {TextureNode} A texture node representing the texture sample.
*/
grad( gradNodeX, gradNodeY ) {
const textureNode = this.clone();
textureNode.gradNode = [ nodeObject( gradNodeX ), nodeObject( gradNodeY ) ];
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Gathers four texels from the texture.
*
* @param {Node<int>} gatherNode - The index of the channel to read. This must be in range [0, 3] and a compile-time constant.
* @return {TextureNode} A texture node representing the texture sample.
*/
gather( gatherNode = 0 ) {
const textureNode = this.clone();
textureNode.gatherNode = nodeObject( gatherNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Returns `true` if the texture is sampled with a depth comparison,
* meaning the node must be bound with a comparison sampler.
*
* @return {boolean} Whether comparison sampling is used or not.
*/
isSampleCompare() {
return this.compareNode !== null || this._samplerComparison === true;
}
/**
* Samples the texture by defining a depth node.
*
* @param {Node<int>} depthNode - The depth node.
* @return {TextureNode} A texture node representing the texture sample.
*/
depth( depthNode ) {
const textureNode = this.clone();
textureNode.depthNode = nodeObject( depthNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
/**
* Samples the texture by defining an offset node.
*
* @param {Node<ivec2>} offsetNode - The offset node.
* @return {TextureNode} A texture node representing the texture sample.
*/
offset( offsetNode ) {
const textureNode = this.clone();
textureNode.offsetNode = nodeObject( offsetNode );
textureNode.referenceNode = this.getBase();
return textureNode;
}
// --
serialize( data ) {
super.serialize( data );
data.value = this.value.toJSON( data.meta ).uuid;
data.sampler = this.sampler;
data.updateMatrix = this.updateMatrix;
}
deserialize( data ) {
super.deserialize( data );
this.value = data.meta.textures[ data.value ];
this.sampler = data.sampler;
this.updateMatrix = data.updateMatrix;
}
/**
* Clones the texture node.
*
* @return {TextureNode} The cloned texture node.
*/
clone() {
const newNode = new this.constructor( this.value, this.uvNode, this.levelNode, this.biasNode );
newNode.sampler = this.sampler;
newNode.depthNode = this.depthNode;
newNode.compareNode = this.compareNode;
newNode.gradNode = this.gradNode;
newNode.gatherNode = this.gatherNode;
newNode.offsetNode = this.offsetNode;
return newNode;
}
}
/**
* TSL function for creating a texture node.
*
* @tsl
* @function
* @param {?Texture} value - The texture.
* @param {?Node<vec2|vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {TextureNode}
*/
const textureBase = /*@__PURE__*/ nodeProxy( TextureNode ).setParameterLength( 1, 4 ).setName( 'texture' );
/**
* TSL function for creating a texture node or sample a texture node already existing.
*
* @tsl
* @function
* @param {?(Texture|TextureNode)} [value=EmptyTexture] - The texture.
* @param {?Node<vec2|vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {TextureNode}
*/
const texture = ( value = EmptyTexture$1, uvNode = null, levelNode = null, biasNode = null ) => {
let textureNode;
if ( value && value.isTextureNode === true ) {
textureNode = value.clone();
textureNode.referenceNode = value.getBase(); // Ensure the reference is set to the original node
if ( uvNode !== null ) textureNode.uvNode = nodeObject( uvNode );
if ( levelNode !== null ) textureNode.levelNode = nodeObject( levelNode );
if ( biasNode !== null ) textureNode.biasNode = nodeObject( biasNode );
} else {
textureNode = textureBase( value, uvNode, levelNode, biasNode );
}
return textureNode;
};
/**
* TSL function for creating a non-mergeable uniform texture node.
*
* @tsl
* @function
* @param {?Texture} value - The texture.
* @returns {TextureNode}
*/
const uniformTexture = ( value = EmptyTexture$1 ) => {
const textureNode = texture( value );
textureNode.mergeable = false;
return textureNode;
};
/**
* TSL function for creating a texture node that fetches/loads texels without interpolation.
*
* @tsl
* @function
* @param {?(Texture|TextureNode)} [value=EmptyTexture] - The texture.
* @param {?Node<vec2|vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {TextureNode}
*/
const textureLoad = ( ...params ) => texture( ...params ).setSampler( false );
const textureLevel = ( value, uv, level ) => texture( value, uv ).level( level );
/**
* Converts a texture or texture node to a sampler.
*
* @tsl
* @function
* @param {TextureNode|Texture} value - The texture or texture node to convert.
* @returns {Node}
*/
const sampler = ( value ) => ( value.isNode === true ? value : texture( value ) ).convert( 'sampler' );
/**
* Converts a texture or texture node to a sampler comparison.
*
* @tsl
* @function
* @param {TextureNode|Texture} value - The texture or texture node to convert.
* @returns {Node}
*/
const samplerComparison = ( value ) => ( value.isNode === true ? value : texture( value ) ).convert( 'samplerComparison' );
const _size$3 = /*@__PURE__*/ new Vector2();
/**
* A special type of texture node which represents the data of the current viewport
* as a texture. The module extracts data from the current bound framebuffer with
* a copy operation so no extra render pass is required to produce the texture data
* (which is good for performance). `ViewportTextureNode` can be used as an input for a
* variety of effects like refractive or transmissive materials.
*
* @augments TextureNode
*/
class ViewportTextureNode extends TextureNode {
static get type() {
return 'ViewportTextureNode';
}
/**
* Constructs a new viewport texture node.
*
* @param {Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @param {?Texture} [framebufferTexture=null] - A framebuffer texture holding the viewport data. If not provided, a framebuffer texture is created automatically.
*/
constructor( uvNode = screenUV, levelNode = null, framebufferTexture = null ) {
let defaultFramebuffer = null;
if ( framebufferTexture === null ) {
defaultFramebuffer = new FramebufferTexture();
defaultFramebuffer.minFilter = LinearMipmapLinearFilter;
framebufferTexture = defaultFramebuffer;
} else {
defaultFramebuffer = framebufferTexture;
}
super( framebufferTexture, uvNode, levelNode );
/**
* Whether to generate mipmaps or not.
*
* @type {boolean}
* @default false
*/
this.generateMipmaps = false;
/**
* The reference framebuffer texture. This is used to store the framebuffer texture
* for the current render target. If the render target changes, a new framebuffer texture
* is created automatically.
*
* @type {FramebufferTexture}
* @default null
*/
this.defaultFramebuffer = defaultFramebuffer;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOutputTextureNode = true;
/**
* The `updateBeforeType` is set to `NodeUpdateType.RENDER` since the node should extract
* the current contents of the bound framebuffer for each render call.
*
* @type {string}
* @default 'render'
*/
this.updateBeforeType = NodeUpdateType.RENDER;
/**
* The framebuffer texture for the current renderer context.
*
* @type {WeakMap<RenderTarget, FramebufferTexture>}
* @private
*/
this._cacheTextures = new WeakMap();
}
/**
* This methods returns a texture for the given render target or canvas target reference.
*
* To avoid rendering errors, `ViewportTextureNode` must use unique framebuffer textures
* for different render contexts.
*
* @param {?(RenderTarget|CanvasTarget)} [reference=null] - The render target or canvas target reference.
* @return {Texture} The framebuffer texture.
*/
getTextureForReference( reference = null ) {
let defaultFramebuffer;
let cacheTextures;
if ( this.referenceNode ) {
defaultFramebuffer = this.referenceNode.defaultFramebuffer;
cacheTextures = this.referenceNode._cacheTextures;
} else {
defaultFramebuffer = this.defaultFramebuffer;
cacheTextures = this._cacheTextures;
}
if ( reference === null ) {
return defaultFramebuffer;
}
if ( cacheTextures.has( reference ) === false ) {
const framebufferTexture = defaultFramebuffer.clone();
cacheTextures.set( reference, framebufferTexture );
}
return cacheTextures.get( reference );
}
updateReference( frame ) {
const renderer = frame.renderer;
const renderTarget = renderer.getRenderTarget();
const canvasTarget = renderer.getCanvasTarget();
const reference = renderTarget ? renderTarget : canvasTarget;
this.value = this.getTextureForReference( reference );
return this.value;
}
updateBefore( frame ) {
const renderer = frame.renderer;
const renderTarget = renderer.getRenderTarget();
const canvasTarget = renderer.getCanvasTarget();
const reference = renderTarget ? renderTarget : canvasTarget;
if ( reference === null ) {
renderer.getDrawingBufferSize( _size$3 );
} else if ( reference.getDrawingBufferSize ) {
reference.getDrawingBufferSize( _size$3 );
} else {
_size$3.set( reference.width, reference.height );
}
//
const framebufferTexture = this.getTextureForReference( reference );
if ( framebufferTexture.image.width !== _size$3.width || framebufferTexture.image.height !== _size$3.height ) {
framebufferTexture.image.width = _size$3.width;
framebufferTexture.image.height = _size$3.height;
framebufferTexture.needsUpdate = true;
}
//
const currentGenerateMipmaps = framebufferTexture.generateMipmaps;
framebufferTexture.generateMipmaps = this.generateMipmaps;
renderer.copyFramebufferToTexture( framebufferTexture );
framebufferTexture.generateMipmaps = currentGenerateMipmaps;
}
clone() {
const viewportTextureNode = new this.constructor( this.uvNode, this.levelNode, this.value );
viewportTextureNode.generateMipmaps = this.generateMipmaps;
return viewportTextureNode;
}
}
/**
* TSL function for creating a viewport texture node.
*
* @tsl
* @function
* @param {?Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @param {?Texture} [framebufferTexture=null] - A framebuffer texture holding the viewport data. If not provided, a framebuffer texture is created automatically.
* @returns {ViewportTextureNode}
*/
const viewportTexture = /*@__PURE__*/ nodeProxy( ViewportTextureNode ).setParameterLength( 0, 3 );
/**
* TSL function for creating a viewport texture node with enabled mipmap generation.
*
* @tsl
* @function
* @param {?Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @param {?Texture} [framebufferTexture=null] - A framebuffer texture holding the viewport data. If not provided, a framebuffer texture is created automatically.
* @returns {ViewportTextureNode}
*/
const viewportMipTexture = /*@__PURE__*/ nodeProxy( ViewportTextureNode, null, null, { generateMipmaps: true } ).setParameterLength( 0, 3 );
// Singleton instances for common usage
const _singletonOpaqueViewportTextureNode = /*@__PURE__*/ viewportMipTexture();
/**
* TSL function for creating a viewport texture node with enabled mipmap generation.
* The texture should only contain the opaque rendering objects.
*
* This should be used just in transparent or transmissive materials.
*
* @tsl
* @function
* @param {?Node} [uv=screenUV] - The uv node.
* @param {?Node} [level=null] - The level node.
* @returns {ViewportTextureNode}
*/
const viewportOpaqueMipTexture = ( uv = screenUV, level = null ) => _singletonOpaqueViewportTextureNode.sample( uv, level ); // TODO: Use once() when sample() supports it
let _sharedDepthbuffer = null;
/**
* Represents the depth of the current viewport as a texture. This module
* can be used in combination with viewport texture to achieve effects
* that require depth evaluation.
*
* @augments ViewportTextureNode
*/
class ViewportDepthTextureNode extends ViewportTextureNode {
static get type() {
return 'ViewportDepthTextureNode';
}
/**
* Constructs a new viewport depth texture node.
*
* @param {Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @param {?DepthTexture} [depthTexture=null] - A depth texture. If not provided, uses a shared depth texture.
*/
constructor( uvNode = screenUV, levelNode = null, depthTexture = null ) {
if ( depthTexture === null ) {
if ( _sharedDepthbuffer === null ) {
_sharedDepthbuffer = new DepthTexture();
}
depthTexture = _sharedDepthbuffer;
}
super( uvNode, levelNode, depthTexture );
}
}
/**
* TSL function for a viewport depth texture node.
*
* @tsl
* @function
* @param {?Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @param {?DepthTexture} [depthTexture=null] - A depth texture. If not provided, a depth texture is created automatically.
* @returns {ViewportDepthTextureNode}
*/
const viewportDepthTexture = /*@__PURE__*/ nodeProxy( ViewportDepthTextureNode ).setParameterLength( 0, 3 );
/**
* This node offers a collection of features in context of the depth logic in the fragment shader.
* Depending on {@link ViewportDepthNode#scope}, it can be used to define a depth value for the current
* fragment or for depth evaluation purposes.
*
* @augments Node
*/
class ViewportDepthNode extends Node {
static get type() {
return 'ViewportDepthNode';
}
/**
* Constructs a new viewport depth node.
*
* @param {('depth'|'depthBase'|'linearDepth')} scope - The node's scope.
* @param {?Node} [valueNode=null] - The value node.
*/
constructor( scope, valueNode = null ) {
super( 'float' );
/**
* The node behaves differently depending on which scope is selected.
*
* - `ViewportDepthNode.DEPTH_BASE`: Allows to define a value for the current fragment's depth.
* - `ViewportDepthNode.DEPTH`: Represents the depth value for the current fragment (`valueNode` is ignored).
* - `ViewportDepthNode.LINEAR_DEPTH`: Represents the linear (orthographic) depth value of the current fragment.
* If a `valueNode` is set, the scope can be used to convert perspective depth data to linear data.
*
* @type {('depth'|'depthBase'|'linearDepth')}
*/
this.scope = scope;
/**
* Can be used to define a custom depth value.
* The property is ignored in the `ViewportDepthNode.DEPTH` scope.
*
* @type {?Node}
* @default null
*/
this.valueNode = valueNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isViewportDepthNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( builder ) {
const { scope } = this;
if ( scope === ViewportDepthNode.DEPTH_BASE ) {
return builder.getFragDepth();
}
return super.generate( builder );
}
setup( { camera } ) {
const { scope } = this;
const value = this.valueNode;
let node = null;
if ( scope === ViewportDepthNode.DEPTH_BASE ) {
if ( value !== null ) {
node = depthBase().assign( value );
}
} else if ( scope === ViewportDepthNode.DEPTH ) {
if ( camera.isOrthographicCamera !== true ) {
node = viewZToPerspectiveDepth( positionView.z, cameraNear, cameraFar );
} else {
node = viewZToOrthographicDepth( positionView.z, cameraNear, cameraFar );
}
} else if ( scope === ViewportDepthNode.LINEAR_DEPTH ) {
if ( value !== null ) {
if ( camera.isOrthographicCamera !== true ) {
const viewZ = perspectiveDepthToViewZ( value, cameraNear, cameraFar );
node = viewZToOrthographicDepth( viewZ, cameraNear, cameraFar );
} else {
node = value;
}
} else {
node = viewZToOrthographicDepth( positionView.z, cameraNear, cameraFar );
}
}
return node;
}
}
ViewportDepthNode.DEPTH_BASE = 'depthBase';
ViewportDepthNode.DEPTH = 'depth';
ViewportDepthNode.LINEAR_DEPTH = 'linearDepth';
// NOTE: viewZ, the z-coordinate in camera space, is negative for points in front of the camera
/**
* TSL function for converting a viewZ value to an orthographic depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const viewZToOrthographicDepth = ( viewZ, near, far ) => viewZ.add( near ).div( near.sub( far ) );
/**
* TSL function for converting a viewZ value to a reversed orthographic depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const viewZToReversedOrthographicDepth = ( viewZ, near, far ) => viewZ.add( far ).div( far.sub( near ) );
/**
* TSL function for converting an orthographic depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The orthographic depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const orthographicDepthToViewZ = /*@__PURE__*/ Fn( ( [ depth, near, far ], builder ) => {
if ( builder.renderer.reversedDepthBuffer === true ) {
return far.sub( near ).mul( depth ).sub( far );
} else {
return near.sub( far ).mul( depth ).sub( near );
}
} );
/**
* TSL function for converting a viewZ value to a perspective depth value.
*
* Note: {link https://twitter.com/gonnavis/status/1377183786949959682}.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const viewZToPerspectiveDepth = ( viewZ, near, far ) => near.add( viewZ ).mul( far ).div( far.sub( near ).mul( viewZ ) );
/**
* TSL function for converting a viewZ value to a reversed perspective depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const viewZToReversedPerspectiveDepth = ( viewZ, near, far ) => near.mul( viewZ.add( far ) ).div( viewZ.mul( near.sub( far ) ) );
/**
* TSL function for converting a perspective depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The perspective depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const perspectiveDepthToViewZ = /*@__PURE__*/ Fn( ( [ depth, near, far ], builder ) => {
if ( builder.renderer.reversedDepthBuffer === true ) {
return near.mul( far ).div( near.sub( far ).mul( depth ).sub( near ) );
} else {
return near.mul( far ).div( far.sub( near ).mul( depth ).sub( far ) );
}
} );
/**
* TSL function for converting a viewZ value to a logarithmic depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const viewZToLogarithmicDepth = ( viewZ, near, far ) => {
// NOTE: viewZ must be negative--see explanation at the end of this comment block.
// The final logarithmic depth formula used here is adapted from one described in an
// article by Thatcher Ulrich (see http://tulrich.com/geekstuff/log_depth_buffer.txt),
// which was an improvement upon an earlier formula one described in an
// Outerra article (https://outerra.blogspot.com/2009/08/logarithmic-z-buffer.html).
// Ulrich's formula is the following:
// z = K * log( w / cameraNear ) / log( cameraFar / cameraNear )
// where K = 2^k - 1, and k is the number of bits in the depth buffer.
// The Outerra variant ignored the camera near plane (it assumed it was 0) and instead
// opted for a "C-constant" for resolution adjustment of objects near the camera.
// Outerra states: "Notice that the 'C' variant doesn’t use a near plane distance, it has it
// set at 0" (quote from https://outerra.blogspot.com/2012/11/maximizing-depth-buffer-range-and.html).
// Ulrich's variant has the benefit of constant relative precision over the whole near-far range.
// It was debated here whether Outerra's "C-constant" or Ulrich's "near plane" variant should
// be used, and ultimately Ulrich's "near plane" version was chosen.
// Outerra eventually made another improvement to their original "C-constant" variant,
// but it still does not incorporate the camera near plane (for this version,
// see https://outerra.blogspot.com/2013/07/logarithmic-depth-buffer-optimizations.html).
// Here we make 4 changes to Ulrich's formula:
// 1. Clamp the camera near plane so we don't divide by 0.
// 2. Use log2 instead of log to avoid an extra multiply (shaders implement log using log2).
// 3. Assume K is 1 (K = maximum value in depth buffer; see Ulrich's formula above).
// 4. To maintain consistency with the functions "viewZToOrthographicDepth" and "viewZToPerspectiveDepth",
// we modify the formula here to use 'viewZ' instead of 'w'. The other functions expect a negative viewZ,
// so we do the same here, hence the 'viewZ.negate()' call.
// For visual representation of this depth curve, see https://www.desmos.com/calculator/uyqk0vex1u
near = near.max( EPSILON ).toVar();
const numerator = log2( viewZ.negate().div( near ) );
const denominator = log2( far.div( near ) );
return numerator.div( denominator );
};
/**
* TSL function for converting a logarithmic depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The logarithmic depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const logarithmicDepthToViewZ = ( depth, near, far ) => {
// NOTE: we add a 'negate()' call to the return value here to maintain consistency with
// the functions "orthographicDepthToViewZ" and "perspectiveDepthToViewZ" (they return
// a negative viewZ).
const exponent = depth.mul( log( far.div( near ) ) );
return float( Math.E ).pow( exponent ).mul( near ).negate();
};
/**
* TSL function for retrieving the depth value as produced by the camera's projection matrix.
*
* @tsl
* @function
* @param {Node<float>} depth - The depth value.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
const projectionDepth = /*@__PURE__*/ Fn( ( [ depth, near, far ], builder ) => {
if ( builder.renderer.logarithmicDepthBuffer === true ) {
const viewZ = logarithmicDepthToViewZ( depth, near, far );
return viewZToPerspectiveDepth( viewZ, near, far );
}
return depth;
} );
/**
* TSL function for defining a value for the current fragment's depth.
*
* @tsl
* @function
* @param {Node<float>} value - The depth value to set.
* @returns {ViewportDepthNode<float>}
*/
const depthBase = /*@__PURE__*/ nodeProxy( ViewportDepthNode, ViewportDepthNode.DEPTH_BASE );
/**
* TSL object that represents the depth value for the current fragment.
*
* @tsl
* @type {ViewportDepthNode}
*/
const depth = /*@__PURE__*/ nodeImmutable( ViewportDepthNode, ViewportDepthNode.DEPTH );
/**
* TSL function for converting a perspective depth value to linear depth.
*
* @tsl
* @function
* @param {?Node<float>} [value=null] - The perspective depth. If `null` is provided, the current fragment's depth is used.
* @returns {ViewportDepthNode<float>}
*/
const linearDepth = /*@__PURE__*/ nodeProxy( ViewportDepthNode, ViewportDepthNode.LINEAR_DEPTH ).setParameterLength( 0, 1 );
/**
* TSL object that represents the linear (orthographic) depth value of the current fragment
*
* @tsl
* @type {ViewportDepthNode}
*/
const viewportLinearDepth = /*@__PURE__*/ linearDepth( viewportDepthTexture() );
depth.assign = ( value ) => depthBase( value );
/**
* A special version of a screen uv function that involves a depth comparison
* when computing the final uvs. The function mitigates visual errors when
* using viewport texture nodes for refraction purposes. Without this function
* objects in front of a refractive surface might appear on the refractive surface
* which is incorrect.
*
* @tsl
* @function
* @param {?Node<vec2>} uv - Optional uv coordinates. By default `screenUV` is used.
* @return {Node<vec2>} The update uv coordinates.
*/
const viewportSafeUV = /*@__PURE__*/ Fn( ( [ uv = null ] ) => {
const depth = linearDepth();
const depthDiff = linearDepth( viewportDepthTexture( uv ) ).sub( depth );
const finalUV = depthDiff.lessThan( 0 ).select( screenUV, uv );
return finalUV;
} );
/**
* TSL function for computing texture coordinates for animated sprite sheets.
*
* ```js
* const uvNode = spritesheetUV( vec2( 6, 6 ), uv(), time.mul( animationSpeed ) );
*
* material.colorNode = texture( spriteSheet, uvNode );
* ```
*
* @tsl
* @function
* @param {Node<vec2>} countNode - The node that defines the number of sprites in the x and y direction (e.g 6x6).
* @param {?Node<vec2>} [uvNode=uv()] - The uv node.
* @param {?Node<float>} [frameNode=float(0)] - The node that defines the current frame/sprite.
* @returns {Node<vec2>}
*/
const spritesheetUV = /*@__PURE__*/ Fn( ( [ countNode, uvNode = uv$1(), frameNode = float( 0 ) ] ) => {
const width = countNode.x;
const height = countNode.y;
const frameNum = frameNode.mod( width.mul( height ) ).floor();
const column = frameNum.mod( width );
const row = height.sub( frameNum.add( 1 ).div( width ).ceil() );
const scale = countNode.reciprocal();
const uvFrameOffset = vec2( column, row );
return uvNode.add( uvFrameOffset ).mul( scale );
} );
/**
* TSL function for creating a triplanar textures node.
*
* Can be used for triplanar texture mapping.
*
* ```js
* material.colorNode = triplanarTexture( texture( diffuseMap ) );
* ```
*
* @tsl
* @function
* @param {Node} textureXNode - First texture node.
* @param {?Node} [textureYNode=null] - Second texture node. When not set, the shader will sample from `textureXNode` instead.
* @param {?Node} [textureZNode=null] - Third texture node. When not set, the shader will sample from `textureXNode` instead.
* @param {?Node<float>} [scaleNode=float(1)] - The scale node.
* @param {?Node<vec3>} [positionNode=positionLocal] - Vertex positions in local space.
* @param {?Node<vec3>} [normalNode=normalLocal] - Normals in local space.
* @returns {Node<vec4>}
*/
const triplanarTextures = /*@__PURE__*/ Fn( ( [ textureXNode, textureYNode = null, textureZNode = null, scaleNode = float( 1 ), positionNode = positionLocal, normalNode = normalLocal ] ) => {
// Reference: https://github.com/keijiro/StandardTriplanar
// Blending factor of triplanar mapping
let bf = normalNode.abs().normalize();
bf = bf.div( bf.dot( vec3( 1.0 ) ) );
// Triplanar mapping
const tx = positionNode.yz.mul( scaleNode );
const ty = positionNode.zx.mul( scaleNode );
const tz = positionNode.xy.mul( scaleNode );
// Base color
const textureX = textureXNode.value;
const textureY = textureYNode !== null ? textureYNode.value : textureX;
const textureZ = textureZNode !== null ? textureZNode.value : textureX;
const cx = texture( textureX, tx ).mul( bf.x );
const cy = texture( textureY, ty ).mul( bf.y );
const cz = texture( textureZ, tz ).mul( bf.z );
return add( cx, cy, cz );
} );
/**
* TSL function for creating a triplanar textures node.
*
* @tsl
* @function
* @param {Node} textureXNode - First texture node.
* @param {?Node} [textureYNode=null] - Second texture node. When not set, the shader will sample from `textureXNode` instead.
* @param {?Node} [textureZNode=null] - Third texture node. When not set, the shader will sample from `textureXNode` instead.
* @param {?Node<float>} [scaleNode=float(1)] - The scale node.
* @param {?Node<vec3>} [positionNode=positionLocal] - Vertex positions in local space.
* @param {?Node<vec3>} [normalNode=normalLocal] - Normals in local space.
* @returns {Node<vec4>}
*/
const triplanarTexture = ( ...params ) => triplanarTextures( ...params );
const _reflectorPlane = new Plane();
const _normal = new Vector3();
const _reflectorWorldPosition = new Vector3();
const _cameraWorldPosition = new Vector3();
const _rotationMatrix = new Matrix4();
const _lookAtPosition = new Vector3( 0, 0, -1 );
const clipPlane = new Vector4();
const _view = new Vector3();
const _target = new Vector3();
const _q = new Vector4();
const _size$2 = new Vector2();
const _defaultRT = new RenderTarget();
const _defaultUV = screenUV.flipX();
_defaultRT.depthTexture = new DepthTexture( 1, 1 );
let _inReflector = false;
/**
* This node can be used to implement mirror-like flat reflective surfaces.
*
* ```js
* const groundReflector = reflector();
* material.colorNode = groundReflector;
*
* const plane = new Mesh( geometry, material );
* plane.add( groundReflector.target );
* ```
*
* @augments TextureNode
*/
class ReflectorNode extends TextureNode {
static get type() {
return 'ReflectorNode';
}
/**
* Constructs a new reflector node.
*
* @param {Object} [parameters={}] - An object holding configuration parameters.
* @param {Object3D} [parameters.target=new Object3D()] - The 3D object the reflector is linked to.
* @param {number} [parameters.resolutionScale=1] - The resolution scale.
* @param {boolean} [parameters.generateMipmaps=false] - Whether mipmaps should be generated or not.
* @param {boolean} [parameters.bounces=true] - Whether reflectors can render other reflector nodes or not.
* @param {boolean} [parameters.depth=false] - Whether depth data should be generated or not.
* @param {number} [parameters.samples] - Anti-Aliasing samples of the internal render-target.
* @param {TextureNode} [parameters.defaultTexture] - The default texture node.
* @param {ReflectorBaseNode} [parameters.reflector] - The reflector base node.
*/
constructor( parameters = {} ) {
super( parameters.defaultTexture || _defaultRT.texture, _defaultUV );
/**
* A reference to the internal reflector base node which holds the actual implementation.
*
* @private
* @type {ReflectorBaseNode}
* @default ReflectorBaseNode
*/
this._reflectorBaseNode = parameters.reflector || new ReflectorBaseNode( this, parameters );
/**
* A reference to the internal depth node.
*
* @private
* @type {?Node}
* @default null
*/
this._depthNode = null;
this.setUpdateMatrix( false );
}
/**
* A reference to the internal reflector node.
*
* @type {ReflectorBaseNode}
*/
get reflector() {
return this._reflectorBaseNode;
}
/**
* A reference to 3D object the reflector is linked to.
*
* @type {Object3D}
*/
get target() {
return this._reflectorBaseNode.target;
}
/**
* Returns a node representing the mirror's depth. That can be used
* to implement more advanced reflection effects like distance attenuation.
*
* @return {Node} The depth node.
*/
getDepthNode() {
if ( this._depthNode === null ) {
if ( this._reflectorBaseNode.depth !== true ) {
throw new Error( 'THREE.ReflectorNode: Depth node can only be requested when the reflector is created with { depth: true }. ' );
}
this._depthNode = new ReflectorNode( {
defaultTexture: _defaultRT.depthTexture,
reflector: this._reflectorBaseNode
} );
}
return this._depthNode;
}
setup( builder ) {
// ignore if used in post-processing
if ( ! builder.object.isQuadMesh ) this._reflectorBaseNode.build( builder );
return super.setup( builder );
}
clone() {
const newNode = new this.constructor( this.reflectorNode );
newNode.uvNode = this.uvNode;
newNode.levelNode = this.levelNode;
newNode.biasNode = this.biasNode;
newNode.sampler = this.sampler;
newNode.depthNode = this.depthNode;
newNode.compareNode = this.compareNode;
newNode.gradNode = this.gradNode;
newNode.gatherNode = this.gatherNode;
newNode.offsetNode = this.offsetNode;
newNode._reflectorBaseNode = this._reflectorBaseNode;
return newNode;
}
/**
* Frees internal resources. Should be called when the node is no longer in use.
*/
dispose() {
super.dispose();
this._reflectorBaseNode.dispose();
}
}
/**
* Holds the actual implementation of the reflector (virtual cameras, render
* targets and the reflection rendering).
*
* This logic is kept separate from {@link ReflectorNode} because the latter is
* a {@link TextureNode} representing a single texture view. A reflector can be
* sampled by multiple texture nodes at once - e.g. one for color and one for
* depth (see {@link ReflectorNode#getDepthNode}) - which all reference the same
* base node so the reflection is rendered only once.
*
* @private
* @augments Node
*/
class ReflectorBaseNode extends Node {
static get type() {
return 'ReflectorBaseNode';
}
/**
* Constructs a new reflector base node.
*
* @param {TextureNode} textureNode - Represents the rendered reflections as a texture node.
* @param {Object} [parameters={}] - An object holding configuration parameters.
* @param {Object3D} [parameters.target=new Object3D()] - The 3D object the reflector is linked to.
* @param {number} [parameters.resolutionScale=1] - The resolution scale.
* @param {boolean} [parameters.generateMipmaps=false] - Whether mipmaps should be generated or not.
* @param {boolean} [parameters.bounces=true] - Whether reflectors can render other reflector nodes or not.
* @param {boolean} [parameters.depth=false] - Whether depth data should be generated or not.
* @param {number} [parameters.samples] - Anti-Aliasing samples of the internal render-target.
*/
constructor( textureNode, parameters = {} ) {
super();
const {
target = new Object3D(),
resolutionScale = 1,
generateMipmaps = false,
bounces = true,
depth = false,
samples = 0
} = parameters;
/**
* Represents the rendered reflections as a texture node.
*
* @type {TextureNode}
*/
this.textureNode = textureNode;
/**
* The 3D object the reflector is linked to.
*
* @type {Object3D}
* @default {new Object3D()}
*/
this.target = target;
/**
* The resolution scale.
*
* @type {number}
* @default {1}
*/
this.resolutionScale = resolutionScale;
if ( parameters.resolution !== undefined ) {
warnOnce( 'ReflectorNode: The "resolution" parameter has been renamed to "resolutionScale".' ); // @deprecated r180
this.resolutionScale = parameters.resolution;
}
/**
* Whether mipmaps should be generated or not.
*
* @type {boolean}
* @default {false}
*/
this.generateMipmaps = generateMipmaps;
/**
* Whether reflectors can render other reflector nodes or not.
*
* @type {boolean}
* @default {true}
*/
this.bounces = bounces;
/**
* Whether depth data should be generated or not.
*
* @type {boolean}
* @default {false}
*/
this.depth = depth;
/**
* The number of anti-aliasing samples for the render-target
*
* @type {number}
* @default {0}
*/
this.samples = samples;
/**
* The `updateBeforeType` is set to `NodeUpdateType.RENDER` when {@link ReflectorBaseNode#bounces}
* is `true`. Otherwise it's `NodeUpdateType.FRAME`.
*
* @type {string}
* @default 'render'
*/
this.updateBeforeType = bounces ? NodeUpdateType.RENDER : NodeUpdateType.FRAME;
/**
* Weak map for managing virtual cameras.
*
* @type {WeakMap<Camera, Camera>}
*/
this.virtualCameras = new WeakMap();
/**
* Weak map for managing render targets.
*
* @type {Map<Camera, RenderTarget>}
*/
this.renderTargets = new Map();
/**
* Force render even if reflector is facing away from camera.
*
* @type {boolean}
* @default {false}
*/
this.forceUpdate = false;
/**
* Whether the reflector has been rendered or not.
*
* When the reflector is facing away from the camera,
* this flag is set to `false` and the texture will be empty(black).
*
* @type {boolean}
* @default {false}
*/
this.hasOutput = false;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Updates the resolution of the internal render target.
*
* @private
* @param {RenderTarget} renderTarget - The render target to resize.
* @param {Renderer} renderer - The renderer that is used to determine the new size.
*/
_updateResolution( renderTarget, renderer ) {
const resolution = this.resolutionScale;
renderer.getDrawingBufferSize( _size$2 );
renderTarget.setSize( Math.round( _size$2.width * resolution ), Math.round( _size$2.height * resolution ) );
}
setup( builder ) {
this._updateResolution( _defaultRT, builder.renderer );
return super.setup( builder );
}
/**
* Frees internal resources. Should be called when the node is no longer in use.
*/
dispose() {
super.dispose();
for ( const renderTarget of this.renderTargets.values() ) {
renderTarget.dispose();
}
}
/**
* Returns a virtual camera for the given camera. The virtual camera is used to
* render the scene from the reflector's view so correct reflections can be produced.
*
* @param {Camera} camera - The scene's camera.
* @return {Camera} The corresponding virtual camera.
*/
getVirtualCamera( camera ) {
let virtualCamera = this.virtualCameras.get( camera );
if ( virtualCamera === undefined ) {
virtualCamera = camera.clone();
this.virtualCameras.set( camera, virtualCamera );
}
return virtualCamera;
}
/**
* Returns a render target for the given camera. The reflections are rendered
* into this render target.
*
* @param {Camera} camera - The scene's camera.
* @return {RenderTarget} The render target.
*/
getRenderTarget( camera ) {
let renderTarget = this.renderTargets.get( camera );
if ( renderTarget === undefined ) {
renderTarget = new RenderTarget( 1, 1, { type: HalfFloatType, samples: this.samples } );
if ( this.generateMipmaps === true ) {
renderTarget.texture.minFilter = LinearMipMapLinearFilter;
renderTarget.texture.generateMipmaps = true;
}
if ( this.depth === true ) {
renderTarget.depthTexture = new DepthTexture();
}
this.renderTargets.set( camera, renderTarget );
}
return renderTarget;
}
/**
* Uses the virtual camera of the current camera as the update reference so
* the reflection is updated for each camera that renders the reflector.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Camera} The virtual camera.
*/
updateReference( state ) {
return this.getVirtualCamera( state.camera );
}
updateBefore( frame ) {
if ( this.bounces === false && _inReflector ) return false;
_inReflector = true;
const { scene, camera, renderer, material } = frame;
const { target } = this;
const virtualCamera = this.getVirtualCamera( camera );
const renderTarget = this.getRenderTarget( virtualCamera );
renderer.getDrawingBufferSize( _size$2 );
this._updateResolution( renderTarget, renderer );
//
_reflectorWorldPosition.setFromMatrixPosition( target.matrixWorld );
_cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
_rotationMatrix.extractRotation( target.matrixWorld );
_normal.set( 0, 0, 1 );
_normal.applyMatrix4( _rotationMatrix );
_view.subVectors( _reflectorWorldPosition, _cameraWorldPosition );
// Avoid rendering when reflector is facing away unless forcing an update
const isFacingAway = _view.dot( _normal ) > 0;
let needsClear = false;
if ( isFacingAway === true && this.forceUpdate === false ) {
if ( this.hasOutput === false ) {
_inReflector = false;
return;
}
needsClear = true;
}
_view.reflect( _normal ).negate();
_view.add( _reflectorWorldPosition );
_rotationMatrix.extractRotation( camera.matrixWorld );
_lookAtPosition.set( 0, 0, -1 );
_lookAtPosition.applyMatrix4( _rotationMatrix );
_lookAtPosition.add( _cameraWorldPosition );
_target.subVectors( _reflectorWorldPosition, _lookAtPosition );
_target.reflect( _normal ).negate();
_target.add( _reflectorWorldPosition );
//
virtualCamera.coordinateSystem = camera.coordinateSystem;
virtualCamera.position.copy( _view );
virtualCamera.up.set( 0, 1, 0 );
virtualCamera.up.applyMatrix4( _rotationMatrix );
virtualCamera.up.reflect( _normal );
virtualCamera.lookAt( _target );
virtualCamera.near = camera.near;
virtualCamera.far = camera.far;
virtualCamera.updateMatrixWorld();
virtualCamera.projectionMatrix.copy( camera.projectionMatrix );
// The reflection is mirrored horizontally, so the view offset must be mirrored as well.
virtualCamera.projectionMatrix.elements[ 8 ] *= -1;
virtualCamera.projectionMatrix.elements[ 12 ] *= -1;
// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
_reflectorPlane.setFromNormalAndCoplanarPoint( _normal, _reflectorWorldPosition );
_reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse );
clipPlane.set( _reflectorPlane.normal.x, _reflectorPlane.normal.y, _reflectorPlane.normal.z, _reflectorPlane.constant );
const projectionMatrix = virtualCamera.projectionMatrix;
_q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];
_q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];
_q.z = -1;
_q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];
// Calculate the scaled plane vector
clipPlane.multiplyScalar( 1.0 / clipPlane.dot( _q ) );
const clipBias = 0;
// Replacing the third row of the projection matrix
projectionMatrix.elements[ 2 ] = clipPlane.x;
projectionMatrix.elements[ 6 ] = clipPlane.y;
projectionMatrix.elements[ 10 ] = ( renderer.coordinateSystem === WebGPUCoordinateSystem ) ? ( clipPlane.z - clipBias ) : ( clipPlane.z + 1.0 - clipBias );
projectionMatrix.elements[ 14 ] = clipPlane.w;
//
this.textureNode.value = renderTarget.texture;
if ( this.depth === true ) {
this.textureNode.getDepthNode().value = renderTarget.depthTexture;
}
material.visible = false;
const currentRenderTarget = renderer.getRenderTarget();
const currentMRT = renderer.getMRT();
const currentAutoClear = renderer.autoClear;
renderer.setMRT( null );
renderer.setRenderTarget( renderTarget );
renderer.autoClear = true;
const previousName = scene.name;
scene.name = ( scene.name || 'Scene' ) + ' [ Reflector ]'; // TODO: Add bounce index
if ( needsClear ) {
renderer.clear();
this.hasOutput = false;
} else {
renderer.render( scene, virtualCamera );
this.hasOutput = true;
}
scene.name = previousName;
renderer.setMRT( currentMRT );
renderer.setRenderTarget( currentRenderTarget );
renderer.autoClear = currentAutoClear;
material.visible = true;
_inReflector = false;
this.forceUpdate = false;
}
/**
* The resolution scale.
*
* @deprecated
* @type {number}
* @default {1}
*/
get resolution() {
warnOnce( 'ReflectorNode: The "resolution" property has been renamed to "resolutionScale".' ); // @deprecated r180
return this.resolutionScale;
}
set resolution( value ) {
warnOnce( 'ReflectorNode: The "resolution" property has been renamed to "resolutionScale".' ); // @deprecated r180
this.resolutionScale = value;
}
}
/**
* TSL function for creating a reflector node.
*
* @tsl
* @function
* @param {Object} [parameters={}] - An object holding configuration parameters.
* @param {Object3D} [parameters.target=new Object3D()] - The 3D object the reflector is linked to.
* @param {number} [parameters.resolution=1] - The resolution scale.
* @param {boolean} [parameters.generateMipmaps=false] - Whether mipmaps should be generated or not.
* @param {boolean} [parameters.bounces=true] - Whether reflectors can render other reflector nodes or not.
* @param {boolean} [parameters.depth=false] - Whether depth data should be generated or not.
* @param {number} [parameters.samples] - Anti-Aliasing samples of the internal render-target.
* @param {TextureNode} [parameters.defaultTexture] - The default texture node.
* @param {ReflectorBaseNode} [parameters.reflector] - The reflector base node.
* @returns {ReflectorNode}
*/
const reflector = ( parameters ) => new ReflectorNode( parameters );
const _m1$1 = /*@__PURE__*/ new Matrix4();
/**
* TSL object that represents the refraction ratio of the material used for rendering the current object.
*
* @tsl
* @type {UniformNode<float>}
*/
const materialRefractionRatio = /*@__PURE__*/ uniform( 0 ).onReference( ( { material } ) => material ).onObjectUpdate( ( { material } ) => material.refractionRatio );
/**
* TSL object that represents the intensity of environment maps of PBR materials.
* When `material.envMap` is set, the value is `material.envMapIntensity` otherwise `scene.environmentIntensity`.
*
* @tsl
* @type {Node<float>}
*/
const materialEnvIntensity = /*@__PURE__*/ uniform( 1 ).onReference( ( { material } ) => material ).onObjectUpdate( function ( { material, scene } ) {
return material.envMap ? material.envMapIntensity : scene.environmentIntensity;
} );
/**
* TSL object that represents the rotation of environment maps.
* When `material.envMap` is set, the value is `material.envMapRotation`.
* `scene.environmentRotation` controls the rotation of `scene.environment` or `scene.environmentNode` instead.
*
* @tsl
* @type {Node<mat4>}
*/
const materialEnvRotation = /*@__PURE__*/ uniform( new Matrix4() ).onReference( function ( frame ) {
return frame.material;
} ).onObjectUpdate( function ( { material, scene } ) {
const hasSceneEnvironment = ( scene.environment !== null ) || ( scene.environmentNode && scene.environmentNode.isNode );
const rotation = ( hasSceneEnvironment && material.envMap === null ) ? scene.environmentRotation : material.envMapRotation;
if ( rotation ) {
// note: since the matrix is orthonormal, we can use the more-efficient transpose() in lieu of invert()
_m1$1.makeRotationFromEuler( rotation ).transpose();
} else {
_m1$1.identity();
}
return _m1$1;
} );
/**
* The reflect vector in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const reflectView = /*@__PURE__*/ positionViewDirection.negate().reflect( normalView );
/**
* The refract vector in view space.
*
* @tsl
* @type {Node<vec3>}
*/
const refractView = /*@__PURE__*/ positionViewDirection.negate().refract( normalView, materialRefractionRatio );
/**
* Used for sampling cube maps when using cube reflection mapping.
*
* @tsl
* @type {Node<vec3>}
*/
const reflectVector = /*@__PURE__*/ reflectView.transformDirection( cameraWorldMatrix ).toVar( 'reflectVector' );
/**
* Used for sampling cube maps when using cube refraction mapping.
*
* @tsl
* @type {Node<vec3>}
*/
const refractVector = /*@__PURE__*/ refractView.transformDirection( cameraWorldMatrix ).toVar( 'refractVector' );
const EmptyTexture = /*@__PURE__*/ new CubeTexture();
/**
* This type of uniform node represents a cube texture.
*
* @augments TextureNode
*/
class CubeTextureNode extends TextureNode {
static get type() {
return 'CubeTextureNode';
}
/**
* Constructs a new cube texture node.
*
* @param {CubeTexture} value - The cube texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
*/
constructor( value, uvNode = null, levelNode = null, biasNode = null ) {
super( value, uvNode, levelNode, biasNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isCubeTextureNode = true;
}
/**
* Overwrites the default implementation to return the appropriate cube texture type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
if ( this.value.isDepthTexture === true ) {
return 'cubeDepthTexture';
}
return 'cubeTexture';
}
/**
* Returns a default uvs based on the mapping type of the cube texture.
*
* @return {Node<vec3>} The default uv attribute.
*/
getDefaultUV() {
const texture = this.value;
if ( texture.mapping === CubeReflectionMapping ) {
return reflectVector;
} else if ( texture.mapping === CubeRefractionMapping ) {
return refractVector;
} else {
error( 'CubeTextureNode: Mapping "%s" not supported.', texture.mapping );
return vec3( 0, 0, 0 );
}
}
/**
* Overwritten with an empty implementation since the `updateMatrix` flag is ignored
* for cube textures. The uv transformation matrix is not applied to cube textures.
*
* @param {boolean} value - The update toggle.
*/
setUpdateMatrix( /*updateMatrix*/ ) { } // Ignore .updateMatrix for CubeTextureNode
/**
* Setups the uv node. Depending on the backend as well as the texture type, it might be necessary
* to modify the uv node for correct sampling.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} uvNode - The uv node to setup.
* @return {Node} The updated uv node.
*/
setupUV( builder, uvNode ) {
const texture = this.value;
if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem || ( ! texture.isRenderTargetTexture && ! texture.isDepthTexture ) ) {
uvNode = vec3( uvNode.x.negate(), uvNode.yz );
}
return uvNode;
}
/**
* Generates the uv code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} cubeUV - The uv node to generate code for.
* @return {string} The generated code snippet.
*/
generateUV( builder, cubeUV ) {
return cubeUV.build( builder, this.sampler === true ? 'vec3' : 'ivec3' );
}
}
/**
* TSL function for creating a cube texture node.
*
* @tsl
* @function
* @param {CubeTexture} value - The cube texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {CubeTextureNode}
*/
const cubeTextureBase = /*@__PURE__*/ nodeProxy( CubeTextureNode ).setParameterLength( 1, 4 ).setName( 'cubeTexture' );
/**
* TSL function for creating a cube texture uniform node.
*
* @tsl
* @function
* @param {?(CubeTexture|CubeTextureNode)} [value=EmptyTexture] - The cube texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {CubeTextureNode}
*/
const cubeTexture = ( value = EmptyTexture, uvNode = null, levelNode = null, biasNode = null ) => {
let textureNode;
if ( value && value.isCubeTextureNode === true ) {
textureNode = value.clone();
textureNode.referenceNode = value; // Ensure the reference is set to the original node
if ( uvNode !== null ) textureNode.uvNode = nodeObject( uvNode );
if ( levelNode !== null ) textureNode.levelNode = nodeObject( levelNode );
if ( biasNode !== null ) textureNode.biasNode = nodeObject( biasNode );
} else {
textureNode = cubeTextureBase( value, uvNode, levelNode, biasNode );
}
return textureNode;
};
/**
* TSL function for creating a non-mergeable uniform cube texture node.
*
* @tsl
* @function
* @param {?CubeTexture} [value=EmptyTexture] - The cube texture.
* @returns {CubeTextureNode}
*/
const uniformCubeTexture = ( value = EmptyTexture ) => {
const textureNode = cubeTextureBase( value );
textureNode.mergeable = false;
return textureNode;
};
// TODO: Avoid duplicated code and use only ReferenceBaseNode or ReferenceNode
/**
* This type of node establishes a reference to a property of another object.
* In this way, the value of the node is automatically linked to the value of
* referenced object. Reference nodes internally represent the linked value
* as a uniform.
*
* @augments Node
*/
class ReferenceNode extends Node {
static get type() {
return 'ReferenceNode';
}
/**
* Constructs a new reference node.
*
* @param {string} property - The name of the property the node refers to.
* @param {string} uniformType - The uniform type that should be used to represent the property value.
* @param {?Object} [object=null] - The object the property belongs to.
* @param {?number} [count=null] - When the linked property is an array-like, this parameter defines its length.
*/
constructor( property, uniformType, object = null, count = null ) {
super();
/**
* The name of the property the node refers to.
*
* @type {string}
*/
this.property = property;
/**
* The uniform type that should be used to represent the property value.
*
* @type {string}
*/
this.uniformType = uniformType;
/**
* The object the property belongs to.
*
* @type {?Object}
* @default null
*/
this.object = object;
/**
* When the linked property is an array, this parameter defines its length.
*
* @type {?number}
* @default null
*/
this.count = count;
/**
* The property name might have dots so nested properties can be referred.
* The hierarchy of the names is stored inside this array.
*
* @type {Array<string>}
*/
this.properties = property.split( '.' );
/**
* Points to the current referred object. This property exists next to {@link ReferenceNode#object}
* since the final reference might be updated from calling code.
*
* @type {?Object}
* @default null
*/
this.reference = object;
/**
* The uniform node that holds the value of the reference node.
*
* @type {UniformNode}
* @default null
*/
this.node = null;
/**
* The uniform group of the internal uniform.
*
* @type {UniformGroupNode}
* @default null
*/
this.group = null;
/**
* An optional label of the internal uniform node.
*
* @type {?string}
* @default null
*/
this.name = null;
/**
* Overwritten since reference nodes are updated per object.
*
* @type {string}
* @default 'object'
*/
this.updateType = NodeUpdateType.OBJECT;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* When the referred property is array-like, this method can be used
* to access elements via an index node.
*
* @param {IndexNode} indexNode - indexNode.
* @return {ReferenceElementNode} A reference to an element.
*/
element( indexNode ) {
return new ReferenceElementNode( this, nodeObject( indexNode ) );
}
/**
* Sets the uniform group for this reference node.
*
* @param {UniformGroupNode} group - The uniform group to set.
* @return {ReferenceNode} A reference to this node.
*/
setGroup( group ) {
this.group = group;
return this;
}
/**
* Sets the name for the internal uniform.
*
* @param {string} name - The label to set.
* @return {ReferenceNode} A reference to this node.
*/
setName( name ) {
this.name = name;
return this;
}
/**
* Sets the label for the internal uniform.
*
* @deprecated
* @param {string} name - The label to set.
* @return {ReferenceNode} A reference to this node.
*/
label( name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.' ); // @deprecated r179
return this.setName( name );
}
/**
* Sets the node type which automatically defines the internal
* uniform type.
*
* @param {string} uniformType - The type to set.
*/
setNodeType( uniformType ) {
let node = null;
if ( this.count !== null ) {
node = buffer( null, uniformType, this.count );
} else if ( Array.isArray( this.getValueFromReference() ) ) {
node = uniformArray( null, uniformType );
node.updateType = NodeUpdateType.OBJECT;
} else if ( uniformType === 'texture' ) {
node = texture( null );
} else if ( uniformType === 'cubeTexture' ) {
node = cubeTexture( null );
} else {
node = uniform( null, uniformType );
}
if ( this.group !== null ) {
node.setGroup( this.group );
}
if ( this.name !== null ) node.setName( this.name );
this.node = node;
}
/**
* This method is overwritten since the node type is inferred from
* the type of the reference node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.node === null ) {
this.updateReference( builder );
this.updateValue();
}
return this.node.getNodeType( builder );
}
/**
* Returns the property value from the given referred object.
*
* @param {Object} [object=this.reference] - The object to retrieve the property value from.
* @return {any} The value.
*/
getValueFromReference( object = this.reference ) {
const { properties } = this;
let value = object[ properties[ 0 ] ];
for ( let i = 1; i < properties.length; i ++ ) {
value = value[ properties[ i ] ];
}
return value;
}
/**
* Allows to update the reference based on the given state. The state is only
* evaluated {@link ReferenceNode#object} is not set.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Object} The updated reference.
*/
updateReference( state ) {
this.reference = this.object !== null ? this.object : state.object;
return this.reference;
}
/**
* The output of the reference node is the internal uniform node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {UniformNode} The output node.
*/
setup( /* builder */ ) {
this.updateValue();
return this.node;
}
/**
* Overwritten to update the internal uniform value.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( /*frame*/ ) {
this.updateValue();
}
/**
* Retrieves the value from the referred object property and uses it
* to updated the internal uniform.
*/
updateValue() {
if ( this.node === null ) this.setNodeType( this.uniformType );
const value = this.getValueFromReference();
if ( Array.isArray( value ) ) {
this.node.array = value;
} else {
this.node.value = value;
}
}
}
/**
* TSL function for creating a reference node.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {string} type - The uniform type that should be used to represent the property value.
* @param {?Object} [object] - The object the property belongs to.
* @returns {ReferenceNode}
*/
const reference = ( name, type, object ) => new ReferenceNode( name, type, object );
/**
* TSL function for creating a reference node. Use this function if you want need a reference
* to an array-like property that should be represented as a uniform buffer.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {string} type - The uniform type that should be used to represent the property value.
* @param {number} count - The number of value inside the array-like object.
* @param {Object} object - An array-like object the property belongs to.
* @returns {ReferenceNode}
*/
const referenceBuffer = ( name, type, count, object ) => new ReferenceNode( name, type, object, count );
/**
* This node is a special type of reference node which is intended
* for linking material properties with node values.
* ```js
* const opacityNode = materialReference( 'opacity', 'float', material );
* ```
* When changing `material.opacity`, the node value of `opacityNode` will
* automatically be updated.
*
* @augments ReferenceNode
*/
class MaterialReferenceNode extends ReferenceNode {
static get type() {
return 'MaterialReferenceNode';
}
/**
* Constructs a new material reference node.
*
* @param {string} property - The name of the property the node refers to.
* @param {string} inputType - The uniform type that should be used to represent the property value.
* @param {?Material} [material=null] - The material the property belongs to. When no material is set,
* the node refers to the material of the current rendered object.
*/
constructor( property, inputType, material = null ) {
super( property, inputType, material );
/**
* The material the property belongs to. When no material is set,
* the node refers to the material of the current rendered object.
*
* @type {?Material}
* @default null
*/
this.material = material;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMaterialReferenceNode = true;
}
/**
* Updates the reference based on the given state. The state is only evaluated
* {@link MaterialReferenceNode#material} is not set.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Object} The updated reference.
*/
updateReference( state ) {
this.reference = this.material !== null ? this.material : state.material;
return this.reference;
}
}
/**
* TSL function for creating a material reference node.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {string} type - The uniform type that should be used to represent the property value.
* @param {?Material} [material=null] - The material the property belongs to.
* When no material is set, the node refers to the material of the current rendered object.
* @returns {MaterialReferenceNode}
*/
const materialReference = ( name, type, material = null ) => new MaterialReferenceNode( name, type, material );
// Normal Mapping Without Precomputed Tangents
// http://www.thetenthplanet.de/archives/1180
const uv = uv$1();
const q0 = positionView.dFdx();
const q1 = positionView.dFdy();
const st0 = uv.dFdx();
const st1 = uv.dFdy();
const N = normalView;
const q1perp = q1.cross( N );
const q0perp = N.cross( q0 );
const T = q1perp.mul( st0.x ).add( q0perp.mul( st1.x ) );
const B = q1perp.mul( st0.y ).add( q0perp.mul( st1.y ) );
const det = T.dot( T ).max( B.dot( B ) );
const scale = det.equal( 0.0 ).select( 0.0, det.inverseSqrt() );
/**
* Tangent vector in view space, computed dynamically from geometry and UV derivatives.
* Useful for normal mapping without precomputed tangents.
*
* Reference: http://www.thetenthplanet.de/archives/1180
*
* @tsl
* @type {Node<vec3>}
*/
const tangentViewFrame = /*@__PURE__*/ T.mul( scale ).toVar( 'tangentViewFrame' );
/**
* Bitangent vector in view space, computed dynamically from geometry and UV derivatives.
* Complements the tangentViewFrame for constructing the tangent space basis.
*
* Reference: http://www.thetenthplanet.de/archives/1180
*
* @tsl
* @type {Node<vec3>}
*/
const bitangentViewFrame = /*@__PURE__*/ B.mul( scale ).toVar( 'bitangentViewFrame' );
/**
* TSL object that represents the tangent attribute of the current rendered object.
*
* @tsl
* @type {Node<vec4>}
*/
const tangentGeometry = /*@__PURE__*/ attribute( 'tangent', 'vec4' );
/**
* TSL object that represents the vertex tangent in local space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const tangentLocal = /*@__PURE__*/ tangentGeometry.xyz.toVar( 'tangentLocal' );
/**
* TSL object that represents the vertex tangent in view space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const tangentView = /*@__PURE__*/ ( Fn( ( builder ) => {
let node;
if ( builder.subBuildFn === 'VERTEX' || builder.geometry.hasAttribute( 'tangent' ) ) {
node = modelViewMatrix.mul( vec4( tangentLocal, 0 ) ).xyz.toVarying( 'v_tangentView' ).normalize();
} else {
node = tangentViewFrame;
}
if ( builder.isFlatShading() !== true ) {
node = negateOnBackSide( node );
}
return node;
}, 'vec3' ).once( [ 'NORMAL', 'VERTEX' ] ) )().toVar( 'tangentView' );
/**
* TSL object that represents the vertex tangent in world space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const tangentWorld = /*@__PURE__*/ tangentView.transformDirection( cameraWorldMatrix ).toVarying( 'v_tangentWorld' ).normalize().toVar( 'tangentWorld' );
/**
* Returns the bitangent node and assigns it to a varying if the material is not flat shaded.
*
* @tsl
* @private
* @param {Node<vec3>} crossNormalTangent - The cross product of the normal and tangent vectors.
* @param {string} varyingName - The name of the varying to assign the bitangent to.
* @returns {Node<vec3>} The bitangent node.
*/
const getBitangent = /*@__PURE__*/ Fn( ( [ crossNormalTangent, varyingName ], builder ) => {
let bitangent = crossNormalTangent.mul( tangentGeometry.w ).xyz;
if ( builder.subBuildFn === 'NORMAL' && builder.isFlatShading() !== true ) {
bitangent = bitangent.toVarying( varyingName );
}
return bitangent;
} ).once( [ 'NORMAL' ] );
/**
* TSL object that represents the bitangent attribute of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const bitangentGeometry = /*@__PURE__*/ getBitangent( normalGeometry.cross( tangentGeometry ), 'v_bitangentGeometry' ).normalize().toVar( 'bitangentGeometry' );
/**
* TSL object that represents the vertex bitangent in local space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const bitangentLocal = /*@__PURE__*/ getBitangent( normalLocal.cross( tangentLocal ), 'v_bitangentLocal' ).normalize().toVar( 'bitangentLocal' );
/**
* TSL object that represents the vertex bitangent in view space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const bitangentView = /*@__PURE__*/ ( Fn( ( builder ) => {
let node;
if ( builder.subBuildFn === 'VERTEX' || builder.geometry.hasAttribute( 'tangent' ) ) {
node = getBitangent( normalView.cross( tangentView ), 'v_bitangentView' ).normalize();
} else {
node = bitangentViewFrame;
}
if ( builder.isFlatShading() !== true ) {
node = negateOnBackSide( node );
}
return node;
}, 'vec3' ).once( [ 'NORMAL', 'VERTEX' ] ) )().toVar( 'bitangentView' );
/**
* TSL object that represents the vertex bitangent in world space of the current rendered object.
*
* @tsl
* @type {Node<vec3>}
*/
const bitangentWorld = /*@__PURE__*/ getBitangent( normalWorld.cross( tangentWorld ), 'v_bitangentWorld' ).normalize().toVar( 'bitangentWorld' );
/**
* TSL object that represents the TBN matrix in view space.
*
* @tsl
* @type {Node<mat3>}
*/
const TBNViewMatrix = /*@__PURE__*/ mat3( tangentView, bitangentView, normalView ).toVar( 'TBNViewMatrix' );
/**
* TSL object that represents the parallax direction.
*
* @tsl
* @type {Node<mat3>}
*/
const parallaxDirection = /*@__PURE__*/ positionViewDirection.mul( TBNViewMatrix )/*.normalize()*/;
/**
* TSL function for computing parallax uv coordinates.
*
* @tsl
* @function
* @param {Node<vec2>} uv - A uv node.
* @param {Node<vec2>} scale - A scale node.
* @returns {Node<vec2>} Parallax uv coordinates.
*/
const parallaxUV = ( uv, scale ) => uv.sub( parallaxDirection.mul( scale ) );
/**
* TSL function for computing bent normals.
*
* @tsl
* @function
* @returns {Node<vec3>} Bent normals.
*/
const bentNormalView = /*@__PURE__*/ ( Fn( () => {
// https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/anisotropy
let bentNormal = anisotropyB.cross( positionViewDirection );
bentNormal = bentNormal.cross( anisotropyB ).normalize();
bentNormal = mix( bentNormal, normalView, anisotropy.mul( roughness.oneMinus() ).oneMinus().pow2().pow2() ).normalize();
return bentNormal;
} ).once() )();
/**
* This class can be used for applying normals maps to materials.
*
* ```js
* material.normalNode = normalMap( texture( normalTex ) );
* ```
*
* @augments Node
*/
class NormalMapNode extends Node {
static get type() {
return 'NormalMapNode';
}
/**
* Constructs a new normal map node.
*
* @param {Node<vec3>} node - Represents the normal map data.
* @param {?Node<vec2>} [scaleNode=null] - Controls the intensity of the effect.
*/
constructor( node, scaleNode = null ) {
super( 'vec3' );
/**
* Represents the normal map data.
*
* @type {Node<vec3>}
*/
this.node = node;
/**
* Controls the intensity of the effect.
*
* @type {?Node<vec2>}
* @default null
*/
this.scaleNode = scaleNode;
/**
* The normal map type.
*
* @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
* @default TangentSpaceNormalMap
*/
this.normalMapType = TangentSpaceNormalMap;
/**
* Controls how to unpack the sampled normal map values.
*
* @type {string}
* @default NoNormalPacking
*/
this.unpackNormalMode = NoNormalPacking;
}
setup( builder ) {
const { normalMapType, scaleNode, unpackNormalMode } = this;
let normalMap = this.node.mul( 2.0 ).sub( 1.0 );
if ( normalMapType === TangentSpaceNormalMap ) {
if ( unpackNormalMode === NormalRGPacking ) {
normalMap = unpackNormal( normalMap.xy );
} else if ( unpackNormalMode === NormalGAPacking ) {
normalMap = unpackNormal( normalMap.yw );
} else if ( unpackNormalMode !== NoNormalPacking ) {
error( `THREE.NodeMaterial: Unexpected unpack normal mode: ${ unpackNormalMode }` );
}
} else {
if ( unpackNormalMode !== NoNormalPacking ) {
error( `THREE.NodeMaterial: Normal map type '${ normalMapType }' is not compatible with unpack normal mode '${ unpackNormalMode }'` );
}
}
if ( scaleNode !== null ) {
let scale = scaleNode;
if ( builder.isFlatShading() === true ) {
scale = negateOnBackSide( scale );
}
normalMap = vec3( normalMap.xy.mul( scale ), normalMap.z );
}
let output = null;
if ( normalMapType === ObjectSpaceNormalMap ) {
output = transformNormalToView( normalMap );
} else if ( normalMapType === TangentSpaceNormalMap ) {
output = TBNViewMatrix.mul( normalMap ).normalize();
} else {
error( `NodeMaterial: Unsupported normal map type: ${ normalMapType }` );
output = normalView; // Fallback to default normal view
}
return output;
}
}
/**
* TSL function for creating a normal map node.
*
* @tsl
* @function
* @param {Node<vec3>} node - Represents the normal map data.
* @param {?Node<vec2>} [scaleNode=null] - Controls the intensity of the effect.
* @returns {NormalMapNode}
*/
const normalMap = /*@__PURE__*/ nodeProxy( NormalMapNode ).setParameterLength( 1, 2 );
// Bump Mapping Unparametrized Surfaces on the GPU by Morten S. Mikkelsen
// https://mmikk.github.io/papers3d/mm_sfgrad_bump.pdf
const dHdxy_fwd = /*@__PURE__*/ Fn( ( { textureNode, bumpScale } ) => {
// It's used to preserve the same TextureNode instance
const sampleTexture = ( callback ) => textureNode.isolate().context( { getUV: ( texNode ) => callback( texNode.uvNode || uv$1() ), forceUVContext: true } );
const Hll = float( sampleTexture( ( uvNode ) => uvNode ) );
return vec2(
float( sampleTexture( ( uvNode ) => uvNode.add( uvNode.dFdx() ) ) ).sub( Hll ),
float( sampleTexture( ( uvNode ) => uvNode.add( uvNode.dFdy() ) ) ).sub( Hll )
).mul( bumpScale );
} );
// Evaluate the derivative of the height w.r.t. screen-space using forward differencing (listing 2)
const perturbNormalArb = /*@__PURE__*/ Fn( ( inputs ) => {
const { surf_pos, surf_norm, dHdxy } = inputs;
// normalize is done to ensure that the bump map looks the same regardless of the texture's scale
const vSigmaX = surf_pos.dFdx().normalize();
const vSigmaY = surf_pos.dFdy().normalize();
const vN = surf_norm; // normalized
const R1 = vSigmaY.cross( vN );
const R2 = vN.cross( vSigmaX );
const fDet = vSigmaX.dot( R1 ).mul( faceDirection );
const vGrad = fDet.sign().mul( dHdxy.x.mul( R1 ).add( dHdxy.y.mul( R2 ) ) );
return fDet.abs().mul( surf_norm ).sub( vGrad ).normalize();
} );
/**
* This class can be used for applying bump maps to materials.
*
* ```js
* material.normalNode = bumpMap( texture( bumpTex ) );
* ```
*
* @augments Node
*/
class BumpMapNode extends Node {
static get type() {
return 'BumpMapNode';
}
/**
* Constructs a new bump map node.
*
* @param {Node<float>} textureNode - Represents the bump map data.
* @param {?Node<float>} [scaleNode=null] - Controls the intensity of the bump effect.
* @param {Node<vec3>} [normalNode=normalView] - The view-space normal to perturb.
*/
constructor( textureNode, scaleNode = null, normalNode = normalView ) {
super( 'vec3' );
/**
* Represents the bump map data.
*
* @type {Node<float>}
*/
this.textureNode = textureNode;
/**
* Controls the intensity of the bump effect.
*
* @type {?Node<float>}
* @default null
*/
this.scaleNode = scaleNode;
/**
* The view-space normal to perturb.
*
* @type {Node<vec3>}
* @default normalView
*/
this.normalNode = normalNode;
}
setup( builder ) {
// Screen-space derivatives are unreliable on thin lines, so the bump
// effect is disabled for wireframe rendering.
if ( builder.material.wireframe === true ) return this.normalNode;
const bumpScale = this.scaleNode !== null ? this.scaleNode : 1;
const dHdxy = dHdxy_fwd( { textureNode: this.textureNode, bumpScale } );
return perturbNormalArb( {
surf_pos: positionView,
surf_norm: this.normalNode,
dHdxy
} );
}
}
/**
* TSL function for creating a bump map node.
*
* @tsl
* @function
* @param {Node<float>} textureNode - Represents the bump map data.
* @param {?Node<float>} [scaleNode=null] - Controls the intensity of the bump effect.
* @param {Node<vec3>} [normalNode=normalView] - The view-space normal to perturb.
* @returns {BumpMapNode}
*/
const bumpMap = /*@__PURE__*/ nodeProxy( BumpMapNode ).setParameterLength( 1, 3 );
const _propertyCache = new Map();
/**
* This class should simplify the node access to material properties.
* It internal uses reference nodes to make sure changes to material
* properties are automatically reflected to predefined TSL objects
* like e.g. `materialColor`.
*
* @augments Node
*/
class MaterialNode extends Node {
static get type() {
return 'MaterialNode';
}
/**
* Constructs a new material node.
*
* @param {string} scope - The scope defines what kind of material property is referred by the node.
*/
constructor( scope ) {
super();
/**
* The scope defines what material property is referred by the node.
*
* @type {string}
*/
this.scope = scope;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns a cached reference node for the given property and type.
*
* @param {string} property - The name of the material property.
* @param {string} type - The uniform type of the property.
* @return {MaterialReferenceNode} A material reference node representing the property access.
*/
getCache( property, type ) {
let node = _propertyCache.get( property );
if ( node === undefined ) {
node = materialReference( property, type );
_propertyCache.set( property, node );
}
return node;
}
/**
* Returns a float-typed material reference node for the given property name.
*
* @param {string} property - The name of the material property.
* @return {MaterialReferenceNode<float>} A material reference node representing the property access.
*/
getFloat( property ) {
return this.getCache( property, 'float' );
}
/**
* Returns a color-typed material reference node for the given property name.
*
* @param {string} property - The name of the material property.
* @return {MaterialReferenceNode<color>} A material reference node representing the property access.
*/
getColor( property ) {
return this.getCache( property, 'color' );
}
/**
* Returns a texture-typed material reference node for the given property name.
*
* @param {string} property - The name of the material property.
* @return {MaterialReferenceNode} A material reference node representing the property access.
*/
getTexture( property ) {
return this.getCache( property === 'map' ? 'map' : property + 'Map', 'texture' );
}
/**
* The node setup is done depending on the selected scope. Multiple material properties
* might be grouped into a single node composition if they logically belong together.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The node representing the selected scope.
*/
setup( builder ) {
const material = builder.context.material;
const scope = this.scope;
let node = null;
if ( scope === MaterialNode.COLOR ) {
const colorNode = material.color !== undefined ? this.getColor( scope ) : vec3();
if ( material.map && material.map.isTexture === true ) {
node = colorNode.mul( this.getTexture( 'map' ) );
} else {
node = colorNode;
}
} else if ( scope === MaterialNode.OPACITY ) {
const opacityNode = this.getFloat( scope );
if ( material.alphaMap && material.alphaMap.isTexture === true ) {
node = opacityNode.mul( this.getTexture( 'alpha' ) );
} else {
node = opacityNode;
}
} else if ( scope === MaterialNode.SPECULAR_STRENGTH ) {
if ( material.specularMap && material.specularMap.isTexture === true ) {
node = this.getTexture( 'specular' ).r;
} else {
node = float( 1 );
}
} else if ( scope === MaterialNode.SPECULAR_INTENSITY ) {
const specularIntensityNode = this.getFloat( scope );
if ( material.specularIntensityMap && material.specularIntensityMap.isTexture === true ) {
node = specularIntensityNode.mul( this.getTexture( scope ).a );
} else {
node = specularIntensityNode;
}
} else if ( scope === MaterialNode.SPECULAR_COLOR ) {
const specularColorNode = this.getColor( scope );
if ( material.specularColorMap && material.specularColorMap.isTexture === true ) {
node = specularColorNode.mul( this.getTexture( scope ).rgb );
} else {
node = specularColorNode;
}
} else if ( scope === MaterialNode.ROUGHNESS ) { // TODO: cleanup similar branches
const roughnessNode = this.getFloat( scope );
if ( material.roughnessMap && material.roughnessMap.isTexture === true ) {
node = roughnessNode.mul( this.getTexture( scope ).g );
} else {
node = roughnessNode;
}
} else if ( scope === MaterialNode.DIFFUSE_ROUGHNESS ) {
const diffuseRoughnessNode = this.getFloat( scope );
if ( material.diffuseRoughnessMap && material.diffuseRoughnessMap.isTexture === true ) {
node = diffuseRoughnessNode.mul( this.getTexture( scope ).r );
} else {
node = diffuseRoughnessNode;
}
} else if ( scope === MaterialNode.METALNESS ) {
const metalnessNode = this.getFloat( scope );
if ( material.metalnessMap && material.metalnessMap.isTexture === true ) {
node = metalnessNode.mul( this.getTexture( scope ).b );
} else {
node = metalnessNode;
}
} else if ( scope === MaterialNode.EMISSIVE ) {
const emissiveIntensityNode = this.getFloat( 'emissiveIntensity' );
const emissiveNode = this.getColor( scope ).mul( emissiveIntensityNode );
if ( material.emissiveMap && material.emissiveMap.isTexture === true ) {
node = emissiveNode.mul( this.getTexture( scope ) );
} else {
node = emissiveNode;
}
} else if ( scope === MaterialNode.NORMAL ) {
if ( material.normalMap ) {
node = normalMap( this.getTexture( 'normal' ), this.getCache( 'normalScale', 'vec2' ) );
node.normalMapType = material.normalMapType;
if ( material.normalMap.format == RGFormat || material.normalMap.format == RED_GREEN_RGTC2_Format || material.normalMap.format == RG11_EAC_Format ) {
node.unpackNormalMode = NormalRGPacking;
}
} else if ( material.bumpMap ) {
node = bumpMap( this.getTexture( 'bump' ).r, this.getFloat( 'bumpScale' ) );
} else {
node = normalView;
}
} else if ( scope === MaterialNode.CLEARCOAT ) {
const clearcoatNode = this.getFloat( scope );
if ( material.clearcoatMap && material.clearcoatMap.isTexture === true ) {
node = clearcoatNode.mul( this.getTexture( scope ).r );
} else {
node = clearcoatNode;
}
} else if ( scope === MaterialNode.CLEARCOAT_ROUGHNESS ) {
const clearcoatRoughnessNode = this.getFloat( scope );
if ( material.clearcoatRoughnessMap && material.clearcoatRoughnessMap.isTexture === true ) {
node = clearcoatRoughnessNode.mul( this.getTexture( scope ).g );
} else {
node = clearcoatRoughnessNode;
}
} else if ( scope === MaterialNode.CLEARCOAT_NORMAL ) {
if ( material.clearcoatNormalMap ) {
node = normalMap( this.getTexture( scope ), this.getCache( scope + 'Scale', 'vec2' ) );
} else {
node = normalView;
}
} else if ( scope === MaterialNode.SHEEN ) {
const sheenNode = this.getColor( 'sheenColor' ).mul( this.getFloat( 'sheen' ) ); // Move this mul() to CPU
if ( material.sheenColorMap && material.sheenColorMap.isTexture === true ) {
node = sheenNode.mul( this.getTexture( 'sheenColor' ).rgb );
} else {
node = sheenNode;
}
} else if ( scope === MaterialNode.SHEEN_ROUGHNESS ) {
const sheenRoughnessNode = this.getFloat( scope );
if ( material.sheenRoughnessMap && material.sheenRoughnessMap.isTexture === true ) {
node = sheenRoughnessNode.mul( this.getTexture( scope ).a );
} else {
node = sheenRoughnessNode;
}
node = node.clamp( 0.0001, 1.0 );
} else if ( scope === MaterialNode.ANISOTROPY ) {
if ( material.anisotropyMap && material.anisotropyMap.isTexture === true ) {
const anisotropyPolar = this.getTexture( scope );
const anisotropyMat = mat2( materialAnisotropyVector.x, materialAnisotropyVector.y, materialAnisotropyVector.y.negate(), materialAnisotropyVector.x );
node = anisotropyMat.mul( anisotropyPolar.rg.mul( 2.0 ).sub( vec2( 1.0 ) ).normalize().mul( anisotropyPolar.b ) );
} else {
node = materialAnisotropyVector;
}
} else if ( scope === MaterialNode.IRIDESCENCE_THICKNESS ) {
const iridescenceThicknessMaximum = this.getFloat( 'iridescenceThicknessMaximum' );
if ( material.iridescenceThicknessMap ) {
const iridescenceThicknessMinimum = this.getFloat( 'iridescenceThicknessMinimum' );
node = iridescenceThicknessMaximum.sub( iridescenceThicknessMinimum ).mul( this.getTexture( scope ).g ).add( iridescenceThicknessMinimum );
} else {
node = iridescenceThicknessMaximum;
}
} else if ( scope === MaterialNode.TRANSMISSION ) {
const transmissionNode = this.getFloat( scope );
if ( material.transmissionMap ) {
node = transmissionNode.mul( this.getTexture( scope ).r );
} else {
node = transmissionNode;
}
} else if ( scope === MaterialNode.THICKNESS ) {
const thicknessNode = this.getFloat( scope );
if ( material.thicknessMap ) {
node = thicknessNode.mul( this.getTexture( scope ).g );
} else {
node = thicknessNode;
}
} else if ( scope === MaterialNode.IOR ) {
node = this.getFloat( scope );
} else if ( scope === MaterialNode.LIGHT_MAP ) {
if ( material.lightMap ) {
node = this.getTexture( scope ).rgb.mul( this.getFloat( 'lightMapIntensity' ) );
} else {
node = vec3( 0.0 );
}
} else if ( scope === MaterialNode.AO ) {
if ( material.aoMap ) {
node = this.getTexture( scope ).r.sub( 1.0 ).mul( this.getFloat( 'aoMapIntensity' ) ).add( 1.0 );
} else {
node = float( 1.0 );
}
} else if ( scope === MaterialNode.LINE_DASH_OFFSET ) {
node = ( material.dashOffset ) ? this.getFloat( scope ) : float( 0 );
} else {
const outputType = this.getNodeType( builder );
node = this.getCache( scope, outputType );
}
return node;
}
}
MaterialNode.ALPHA_TEST = 'alphaTest';
MaterialNode.COLOR = 'color';
MaterialNode.OPACITY = 'opacity';
MaterialNode.SHININESS = 'shininess';
MaterialNode.SPECULAR = 'specular';
MaterialNode.SPECULAR_STRENGTH = 'specularStrength';
MaterialNode.SPECULAR_INTENSITY = 'specularIntensity';
MaterialNode.SPECULAR_COLOR = 'specularColor';
MaterialNode.REFLECTIVITY = 'reflectivity';
MaterialNode.ROUGHNESS = 'roughness';
MaterialNode.DIFFUSE_ROUGHNESS = 'diffuseRoughness';
MaterialNode.METALNESS = 'metalness';
MaterialNode.NORMAL = 'normal';
MaterialNode.CLEARCOAT = 'clearcoat';
MaterialNode.CLEARCOAT_ROUGHNESS = 'clearcoatRoughness';
MaterialNode.CLEARCOAT_NORMAL = 'clearcoatNormal';
MaterialNode.EMISSIVE = 'emissive';
MaterialNode.ROTATION = 'rotation';
MaterialNode.SHEEN = 'sheen';
MaterialNode.SHEEN_ROUGHNESS = 'sheenRoughness';
MaterialNode.ANISOTROPY = 'anisotropy';
MaterialNode.IRIDESCENCE = 'iridescence';
MaterialNode.IRIDESCENCE_IOR = 'iridescenceIOR';
MaterialNode.IRIDESCENCE_THICKNESS = 'iridescenceThickness';
MaterialNode.IOR = 'ior';
MaterialNode.TRANSMISSION = 'transmission';
MaterialNode.THICKNESS = 'thickness';
MaterialNode.ATTENUATION_DISTANCE = 'attenuationDistance';
MaterialNode.ATTENUATION_COLOR = 'attenuationColor';
MaterialNode.LINE_SCALE = 'scale';
MaterialNode.LINE_DASH_SIZE = 'dashSize';
MaterialNode.LINE_GAP_SIZE = 'gapSize';
MaterialNode.LINE_WIDTH = 'linewidth';
MaterialNode.LINE_DASH_OFFSET = 'dashOffset';
MaterialNode.POINT_SIZE = 'size';
MaterialNode.DISPERSION = 'dispersion';
MaterialNode.RETROREFLECTIVITY = 'retroreflectivity';
MaterialNode.LIGHT_MAP = 'light';
MaterialNode.AO = 'ao';
/**
* TSL object that represents alpha test of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialAlphaTest = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ALPHA_TEST );
/**
* TSL object that represents the diffuse color of the current material.
* The value is composed via `color` * `map`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialColor = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.COLOR );
/**
* TSL object that represents the shininess of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialShininess = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SHININESS );
/**
* TSL object that represents the emissive color of the current material.
* The value is composed via `emissive` * `emissiveIntensity` * `emissiveMap`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialEmissive = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.EMISSIVE );
/**
* TSL object that represents the opacity of the current material.
* The value is composed via `opacity` * `alphaMap`.
*
* @tsl
* @type {Node<float>}
*/
const materialOpacity = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.OPACITY );
/**
* TSL object that represents the specular of the current material.
*
* @tsl
* @type {Node<vec3>}
*/
const materialSpecular = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SPECULAR );
/**
* TSL object that represents the specular intensity of the current material.
* The value is composed via `specularIntensity` * `specularMap.a`.
*
* @tsl
* @type {Node<float>}
*/
const materialSpecularIntensity = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SPECULAR_INTENSITY );
/**
* TSL object that represents the specular color of the current material.
* The value is composed via `specularColor` * `specularMap.rgb`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialSpecularColor = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SPECULAR_COLOR );
/**
* TSL object that represents the specular strength of the current material.
* The value is composed via `specularMap.r`.
*
* @tsl
* @type {Node<float>}
*/
const materialSpecularStrength = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SPECULAR_STRENGTH );
/**
* TSL object that represents the reflectivity of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialReflectivity = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.REFLECTIVITY );
/**
* TSL object that represents the roughness of the current material.
* The value is composed via `roughness` * `roughnessMap.g`.
*
* @tsl
* @type {Node<float>}
*/
const materialRoughness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ROUGHNESS );
/**
* TSL object that represents the diffuse roughness of the current material.
* The value is composed via `diffuseRoughness` * `diffuseRoughnessMap.r`.
*
* @tsl
* @type {Node<float>}
*/
const materialDiffuseRoughness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.DIFFUSE_ROUGHNESS );
/**
* TSL object that represents the metalness of the current material.
* The value is composed via `metalness` * `metalnessMap.b`.
*
* @tsl
* @type {Node<float>}
*/
const materialMetalness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.METALNESS );
/**
* TSL object that represents the normal of the current material.
* The value will be either `normalMap` * `normalScale`, `bumpMap` * `bumpScale` or `normalView`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialNormal = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.NORMAL );
/**
* TSL object that represents the clearcoat of the current material.
* The value is composed via `clearcoat` * `clearcoatMap.r`
*
* @tsl
* @type {Node<float>}
*/
const materialClearcoat = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.CLEARCOAT );
/**
* TSL object that represents the clearcoat roughness of the current material.
* The value is composed via `clearcoatRoughness` * `clearcoatRoughnessMap.r`.
*
* @tsl
* @type {Node<float>}
*/
const materialClearcoatRoughness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.CLEARCOAT_ROUGHNESS );
/**
* TSL object that represents the clearcoat normal of the current material.
* The value will be either `clearcoatNormalMap` or `normalView`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialClearcoatNormal = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.CLEARCOAT_NORMAL );
/**
* TSL object that represents the rotation of the current sprite material.
*
* @tsl
* @type {Node<float>}
*/
const materialRotation = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ROTATION );
/**
* TSL object that represents the sheen color of the current material.
* The value is composed via `sheen` * `sheenColor` * `sheenColorMap`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialSheen = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SHEEN );
/**
* TSL object that represents the sheen roughness of the current material.
* The value is composed via `sheenRoughness` * `sheenRoughnessMap.a`.
*
* @tsl
* @type {Node<float>}
*/
const materialSheenRoughness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.SHEEN_ROUGHNESS );
/**
* TSL object that represents the anisotropy of the current material.
*
* @tsl
* @type {Node<vec2>}
*/
const materialAnisotropy = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ANISOTROPY );
/**
* TSL object that represents the iridescence of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialIridescence = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.IRIDESCENCE );
/**
* TSL object that represents the iridescence IOR of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialIridescenceIOR = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.IRIDESCENCE_IOR );
/**
* TSL object that represents the iridescence thickness of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialIridescenceThickness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.IRIDESCENCE_THICKNESS );
/**
* TSL object that represents the transmission of the current material.
* The value is composed via `transmission` * `transmissionMap.r`.
*
* @tsl
* @type {Node<float>}
*/
const materialTransmission = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.TRANSMISSION );
/**
* TSL object that represents the thickness of the current material.
* The value is composed via `thickness` * `thicknessMap.g`.
*
* @tsl
* @type {Node<float>}
*/
const materialThickness = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.THICKNESS );
/**
* TSL object that represents the IOR of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialIOR = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.IOR );
/**
* TSL object that represents the attenuation distance of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialAttenuationDistance = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ATTENUATION_DISTANCE );
/**
* TSL object that represents the attenuation color of the current material.
*
* @tsl
* @type {Node<vec3>}
*/
const materialAttenuationColor = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.ATTENUATION_COLOR );
/**
* TSL object that represents the scale of the current dashed line material.
*
* @tsl
* @type {Node<float>}
*/
const materialLineScale = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LINE_SCALE );
/**
* TSL object that represents the dash size of the current dashed line material.
*
* @tsl
* @type {Node<float>}
*/
const materialLineDashSize = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LINE_DASH_SIZE );
/**
* TSL object that represents the gap size of the current dashed line material.
*
* @tsl
* @type {Node<float>}
*/
const materialLineGapSize = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LINE_GAP_SIZE );
/**
* TSL object that represents the line width of the current line material.
*
* @tsl
* @type {Node<float>}
*/
const materialLineWidth = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LINE_WIDTH );
/**
* TSL object that represents the dash offset of the current line material.
*
* @tsl
* @type {Node<float>}
*/
const materialLineDashOffset = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LINE_DASH_OFFSET );
/**
* TSL object that represents the point size of the current points material.
*
* @tsl
* @type {Node<float>}
*/
const materialPointSize = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.POINT_SIZE );
/**
* TSL object that represents the dispersion of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialDispersion = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.DISPERSION );
/**
* TSL object that represents the retroreflective strength of the current material.
*
* @tsl
* @type {Node<float>}
*/
const materialRetroreflectivity = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.RETROREFLECTIVITY );
/**
* TSL object that represents the light map of the current material.
* The value is composed via `lightMapIntensity` * `lightMap.rgb`.
*
* @tsl
* @type {Node<vec3>}
*/
const materialLightMap = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.LIGHT_MAP );
/**
* TSL object that represents the ambient occlusion map of the current material.
* The value is composed via `aoMap.r` - 1 * `aoMapIntensity` + 1.
*
* @tsl
* @type {Node<float>}
*/
const materialAO = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.AO );
/**
* TSL object that represents the anisotropy vector of the current material.
*
* @tsl
* @type {Node<vec2>}
*/
const materialAnisotropyVector = /*@__PURE__*/ uniform( new Vector2() ).onReference( function ( frame ) {
return frame.material;
} ).onRenderUpdate( function ( { material } ) {
this.value.set( material.anisotropy * Math.cos( material.anisotropyRotation ), material.anisotropy * Math.sin( material.anisotropyRotation ) );
} );
/**
* TSL object that represents the position in clip space after the model-view-projection transform of the current rendered object.
*
* @tsl
* @type {VaryingNode<vec4>}
*/
const modelViewProjection = /*@__PURE__*/ ( Fn( ( builder ) => {
return builder.context.setupModelViewProjection();
}, 'vec4' ).once() )().toVarying( 'v_modelViewProjection' );
/**
* EventNode is a node that executes a callback during specific update phases.
*
* @augments Node
*/
class EventNode extends Node {
static get type() {
return 'EventNode';
}
/**
* Creates an EventNode.
*
* @param {string} eventType - The type of event
* @param {Function} callback - The callback to execute on update.
*/
constructor( eventType, callback ) {
super( 'void' );
this.eventType = eventType;
this.callback = callback;
if ( eventType === EventNode.OBJECT ) {
this.updateType = NodeUpdateType.OBJECT;
} else if ( eventType === EventNode.MATERIAL ) {
this.updateType = NodeUpdateType.RENDER;
} else if ( eventType === EventNode.FRAME ) {
this.updateType = NodeUpdateType.FRAME;
} else if ( eventType === EventNode.AFTER_OBJECT ) {
this.updateAfterType = NodeUpdateType.OBJECT;
} else if ( eventType === EventNode.BEFORE_OBJECT ) {
this.updateBeforeType = NodeUpdateType.OBJECT;
} else if ( eventType === EventNode.BEFORE_MATERIAL ) {
this.updateBeforeType = NodeUpdateType.RENDER;
} else if ( eventType === EventNode.BEFORE_FRAME ) {
this.updateBeforeType = NodeUpdateType.FRAME;
}
}
isCacheable( /*builder*/ ) {
return false;
}
setup( builder ) {
const { eventType, callback } = this;
if ( eventType === EventNode.BEFORE_RENDER_PIPELINE ) {
const callbacks = builder.context.onBeforePipelineCallbacks;
if ( callbacks ) {
callbacks.push( callback );
}
} else if ( eventType === EventNode.AFTER_RENDER_PIPELINE ) {
const callbacks = builder.context.onAfterPipelineCallbacks;
if ( callbacks ) {
callbacks.push( callback );
}
}
return super.setup( builder );
}
update( frame ) {
this.callback( frame );
}
updateBefore( frame ) {
this.callback( frame );
}
updateAfter( frame ) {
this.callback( frame );
}
}
EventNode.OBJECT = 'object';
EventNode.MATERIAL = 'material';
EventNode.FRAME = 'frame';
EventNode.AFTER_OBJECT = 'afterObject';
EventNode.BEFORE_OBJECT = 'beforeObject';
EventNode.BEFORE_MATERIAL = 'beforeMaterial';
EventNode.BEFORE_FRAME = 'beforeFrame';
EventNode.BEFORE_RENDER_PIPELINE = 'beforeRenderPipeline';
EventNode.AFTER_RENDER_PIPELINE = 'afterRenderPipeline';
/**
* Helper to create an EventNode and add it to the stack.
*
* @param {string} type - The event type.
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const createEvent = ( type, callback ) => new EventNode( type, callback ).toStack();
/**
* Creates an event that triggers a function every time an object (Mesh|Sprite) is rendered.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnObjectUpdate = ( callback ) => createEvent( EventNode.OBJECT, callback );
/**
* Creates an event that triggers a function when the first object that uses the material is rendered.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnMaterialUpdate = ( callback ) => createEvent( EventNode.MATERIAL, callback );
/**
* Creates an event that triggers a function every frame.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnFrameUpdate = ( callback ) => createEvent( EventNode.FRAME, callback );
/**
* Creates an event that triggers a function every time an object (Mesh|Sprite) has been rendered.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnAfterObjectUpdate = ( callback ) => createEvent( EventNode.AFTER_OBJECT, callback );
/**
* Creates an event that triggers a function before an object (Mesh|Sprite) is updated.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnBeforeObjectUpdate = ( callback ) => createEvent( EventNode.BEFORE_OBJECT, callback );
/**
* Creates an event that triggers a function before the material is updated.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnBeforeMaterialUpdate = ( callback ) => createEvent( EventNode.BEFORE_MATERIAL, callback );
/**
* Creates an event that triggers a function before every frame.
*
* The event will be bound to the declared TSL function `Fn()`; it must be declared within a `Fn()` or the JS function call must be inherited from one.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnBeforeFrameUpdate = ( callback ) => createEvent( EventNode.BEFORE_FRAME, callback );
/**
* Creates an event that triggers a function before the render pipeline is rendered.
*
* The node must be part of a node chain that is used as the output of a `RenderPipeline`,
* otherwise the event is ignored.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnBeforeRenderPipeline = ( callback ) => createEvent( EventNode.BEFORE_RENDER_PIPELINE, callback );
/**
* Creates an event that triggers a function after the render pipeline is rendered.
*
* The node must be part of a node chain that is used as the output of a `RenderPipeline`,
* otherwise the event is ignored.
*
* @param {Function} callback - The callback function.
* @returns {EventNode}
*/
const OnAfterRenderPipeline = ( callback ) => createEvent( EventNode.AFTER_RENDER_PIPELINE, callback );
/**
* This class enables element access on instances of {@link StorageBufferNode}.
* In most cases, it is indirectly used when accessing elements with the
* {@link StorageBufferNode#element} method.
*
* ```js
* const position = positionStorage.element( instanceIndex );
* ```
*
* @augments ArrayElementNode
*/
class StorageArrayElementNode extends ArrayElementNode {
static get type() {
return 'StorageArrayElementNode';
}
/**
* Constructs storage buffer element node.
*
* @param {StorageBufferNode} storageBufferNode - The storage buffer node.
* @param {Node} indexNode - The index node that defines the element access.
*/
constructor( storageBufferNode, indexNode ) {
super( storageBufferNode, indexNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageArrayElementNode = true;
}
/**
* The storage buffer node.
*
* @param {Node} value
* @type {StorageBufferNode}
*/
set storageBufferNode( value ) {
this.node = value;
}
get storageBufferNode() {
return this.node;
}
getMemberType( builder, name ) {
const structTypeNode = this.storageBufferNode.structTypeNode;
if ( structTypeNode ) {
return structTypeNode.getMemberType( builder, name );
}
return 'void';
}
setup( builder ) {
if ( builder.isAvailable( 'storageBuffer' ) === false ) {
if ( this.node.isPBO === true ) {
builder.setupPBO( this.node );
}
}
return super.setup( builder );
}
generate( builder, output ) {
let snippet;
const isAssignContext = builder.isContextAssign();
//
if ( builder.isAvailable( 'storageBuffer' ) === false ) {
if ( this.node.isPBO === true && isAssignContext !== true && ( this.node.value.isInstancedBufferAttribute || builder.shaderStage !== 'compute' ) ) {
snippet = builder.generatePBO( this );
} else {
snippet = this.node.build( builder );
}
} else {
snippet = super.generate( builder );
}
if ( isAssignContext !== true ) {
const type = this.getNodeType( builder );
snippet = builder.format( snippet, type, output );
}
return snippet;
}
}
/**
* TSL function for creating a storage element node.
*
* @tsl
* @function
* @param {StorageBufferNode} storageBufferNode - The storage buffer node.
* @param {Node} indexNode - The index node that defines the element access.
* @returns {StorageArrayElementNode}
*/
const storageElement = /*@__PURE__*/ nodeProxy( StorageArrayElementNode ).setParameterLength( 2 );
/**
* This node is used in context of compute shaders and allows to define a
* storage buffer for data. A typical workflow is to create instances of
* this node with the convenience functions `attributeArray()` or `instancedArray()`,
* setup up a compute shader that writes into the buffers and then convert
* the storage buffers to attribute nodes for rendering.
*
* ```js
* const positionBuffer = instancedArray( particleCount, 'vec3' ); // the storage buffer node
*
* const computeInit = Fn( () => { // the compute shader
*
* const position = positionBuffer.element( instanceIndex );
*
* // compute position data
*
* position.x = 1;
* position.y = 1;
* position.z = 1;
*
* } )().compute( particleCount );
*
* const particleMaterial = new THREE.SpriteNodeMaterial();
* particleMaterial.positionNode = positionBuffer.toAttribute();
*
* renderer.computeAsync( computeInit );
*
* ```
*
* @augments BufferNode
*/
class StorageBufferNode extends BufferNode {
static get type() {
return 'StorageBufferNode';
}
/**
* Constructs a new storage buffer node.
*
* @param {StorageBufferAttribute|StorageInstancedBufferAttribute|BufferAttribute} value - The buffer data.
* @param {?(string|Struct)} [bufferType=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [bufferCount=0] - The buffer count.
*/
constructor( value, bufferType = null, bufferCount = 0 ) {
let nodeType, structTypeNode = null;
if ( bufferType && bufferType.isStructTypeNode ) {
nodeType = 'struct';
structTypeNode = bufferType;
if ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) {
bufferCount = value.count;
}
} else if ( bufferType === null && ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) ) {
nodeType = getTypeFromLength( value.itemSize );
bufferCount = value.count;
} else {
nodeType = bufferType;
}
super( value, nodeType, bufferCount );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageBufferNode = true;
/**
* The buffer struct type.
*
* @type {?StructTypeNode}
* @default null
*/
this.structTypeNode = structTypeNode;
/**
* The access type of the texture node.
*
* @type {string}
* @default 'readWrite'
*/
this.access = NodeAccess.READ_WRITE;
/**
* Whether the node is atomic or not.
*
* @type {boolean}
* @default false
*/
this.isAtomic = false;
/**
* Whether the node represents a PBO or not.
* Only relevant for WebGL.
*
* @type {boolean}
* @default false
*/
this.isPBO = false;
/**
* A reference to the internal buffer attribute node.
*
* @private
* @type {?BufferAttributeNode}
* @default null
*/
this._attribute = null;
/**
* A reference to the internal varying node.
*
* @private
* @type {?VaryingNode}
* @default null
*/
this._varying = null;
/**
* `StorageBufferNode` sets this property to `true` by default.
*
* @type {boolean}
* @default true
*/
this.global = true;
if ( value.isStorageBufferAttribute !== true && value.isStorageInstancedBufferAttribute !== true ) {
// TODO: Improve it, possibly adding a new property to the BufferAttribute to identify it as a storage buffer read-only attribute in Renderer
if ( value.isInstancedBufferAttribute ) value.isStorageInstancedBufferAttribute = true;
else value.isStorageBufferAttribute = true;
}
}
/**
* This method is overwritten since the buffer data might be shared
* and thus the hash should be shared as well.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The hash.
*/
getHash( builder ) {
let id;
if ( this.bufferCount === 0 ) {
let bufferData = builder.globalCache.getData( this.value );
if ( bufferData === undefined ) {
bufferData = {
node: this
};
builder.globalCache.setData( this.value, bufferData );
}
id = bufferData.node.id;
} else {
id = this.id;
}
return String( id );
}
/**
* Overwrites the default implementation to return a fixed value `'indirectStorageBuffer'` or `'storageBuffer'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return this.value.isIndirectStorageBufferAttribute ? 'indirectStorageBuffer' : 'storageBuffer';
}
/**
* Enables element access with the given index node.
*
* @param {IndexNode} indexNode - The index node.
* @return {StorageArrayElementNode} A node representing the element access.
*/
element( indexNode ) {
return storageElement( this, indexNode );
}
/**
* Defines whether this node is a PBO or not. Only relevant for WebGL.
*
* @param {boolean} value - The value so set.
* @return {StorageBufferNode} A reference to this node.
*/
setPBO( value ) {
this.isPBO = value;
return this;
}
/**
* Returns the `isPBO` value.
*
* @return {boolean} Whether the node represents a PBO or not.
*/
getPBO() {
return this.isPBO;
}
/**
* Defines the node access.
*
* @param {string} value - The node access.
* @return {StorageBufferNode} A reference to this node.
*/
setAccess( value ) {
this.access = value;
return this;
}
/**
* Convenience method for configuring a read-only node access.
*
* @return {StorageBufferNode} A reference to this node.
*/
toReadOnly() {
return this.setAccess( NodeAccess.READ_ONLY );
}
/**
* Defines whether the node is atomic or not.
*
* @param {boolean} value - The atomic flag.
* @return {StorageBufferNode} A reference to this node.
*/
setAtomic( value ) {
this.isAtomic = value;
return this;
}
/**
* Convenience method for making this node atomic.
*
* @return {StorageBufferNode} A reference to this node.
*/
toAtomic() {
return this.setAtomic( true );
}
/**
* Returns attribute data for this storage buffer node.
*
* @return {{attribute: BufferAttributeNode, varying: VaryingNode}} The attribute data.
*/
getAttributeData() {
if ( this._attribute === null ) {
this._attribute = bufferAttribute( this.value );
this._varying = varying( this._attribute );
}
return {
attribute: this._attribute,
varying: this._varying
};
}
/**
* This method is overwritten since the node type is inferred from the struct type if present.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.structTypeNode !== null ) {
return this.structTypeNode.getNodeType( builder );
}
return super.generateNodeType( builder );
}
/**
* Returns the type of a member of the struct.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The name of the member.
* @return {string} The type of the member.
*/
getMemberType( builder, name ) {
if ( this.structTypeNode !== null ) {
return this.structTypeNode.getMemberType( builder, name );
}
return 'void';
}
/**
* Generates the code snippet of the storage buffer node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The generated code snippet.
*/
generate( builder ) {
if ( this.structTypeNode !== null ) this.structTypeNode.build( builder );
if ( builder.isAvailable( 'storageBuffer' ) || builder.isAvailable( 'indirectStorageBuffer' ) ) {
return super.generate( builder );
}
const { attribute, varying } = this.getAttributeData();
const output = varying.build( builder );
builder.registerTransform( output, attribute );
return builder.format( output, attribute.getNodeType( builder ), this.getNodeType( builder ) );
}
}
/**
* TSL function for creating a storage buffer node.
*
* @tsl
* @function
* @param {StorageBufferAttribute|StorageInstancedBufferAttribute|BufferAttribute} value - The buffer data.
* @param {?(string|Struct)} [type=null] - The buffer type (e.g. `'vec3'`).
* @param {number} [count=0] - The buffer count.
* @returns {StorageBufferNode}
*/
const storage = ( value, type = null, count = 0 ) => new StorageBufferNode( value, type, count );
const _columns = /*@__PURE__*/ new WeakMap();
/**
* Returns a column of the given buffer attribute, like a column of the matrices of an
* instanced mesh. The column views the attribute itself, so both share the same GPU buffer.
*
* @private
* @param {BufferAttribute} attribute - The buffer attribute.
* @param {string} type - The type of the column (e.g. `'vec4'`).
* @param {number} offset - The offset of the column.
* @return {InterleavedBufferAttribute} The column.
*/
function getAttributeColumn( attribute, type, offset ) {
let columns = _columns.get( attribute );
if ( columns === undefined ) {
columns = [];
_columns.set( attribute, columns );
}
return columns[ offset ] || ( columns[ offset ] = new InterleavedBufferAttribute( attribute, getLengthFromType( type ), offset ) );
}
/**
* A buffer attribute node that binds the attribute of the rendered object
* referred to by a {@link ReferenceAttributeNode}.
*
* @private
* @augments BufferAttributeNode
*/
class ReferenceBufferAttributeNode extends BufferAttributeNode {
static get type() {
return 'ReferenceBufferAttributeNode';
}
/**
* Constructs a new reference buffer attribute node.
*
* @param {ReferenceAttributeNode} referenceNode - The reference attribute node.
* @param {string} bufferType - The buffer type (e.g. `'vec3'`).
* @param {number} [bufferStride=0] - The buffer stride, when binding a column of the attribute.
* @param {number} [bufferOffset=0] - The buffer offset, when binding a column of the attribute.
*/
constructor( referenceNode, bufferType, bufferStride = 0, bufferOffset = 0 ) {
super( null, bufferType, bufferStride, bufferOffset );
/**
* The reference attribute node.
*
* @type {ReferenceAttributeNode}
*/
this.referenceNode = referenceNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isReferenceBufferAttributeNode = true;
}
/**
* Overwritten since the node has no attribute data of its own.
*
* @return {string} The hash.
*/
getHash() {
return String( this.id );
}
/**
* Returns the referred attribute of the given object, or its column.
*
* @param {Object3D} object - The rendered object.
* @return {?(BufferAttribute|InterleavedBufferAttribute)} The buffer attribute.
*/
getObjectAttribute( object ) {
const attribute = this.referenceNode.getAttribute( object );
if ( ! attribute || this.bufferStride === 0 ) return attribute;
return getAttributeColumn( attribute, this.bufferType, this.bufferOffset );
}
/**
* The attribute of the object the program is built for defines the layout.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup( builder ) {
this.attribute = this.getObjectAttribute( builder.object );
}
}
/**
* This node binds a buffer attribute that is a property of the rendered object, like the
* matrices of an instanced mesh. Unlike {@link BufferAttributeNode}, the attribute isn't part
* of the program: each object binds its own attribute, so objects can share a program.
* ```js
* const matrix = referenceAttribute( 'instanceMatrix', 'mat4' );
* const color = referenceAttribute( 'instanceColor' );
* ```
* Matrices are bound as columns. Storage buffer attributes are bound as storage buffers.
*
* @augments Node
*/
class ReferenceAttributeNode extends Node {
static get type() {
return 'ReferenceAttributeNode';
}
/**
* Constructs a new reference attribute node.
*
* @param {string} property - The name of the property the node refers to.
* @param {?string} [nodeType=null] - The node type. Inferred from the attribute if not set.
* @param {?Object} [object=null] - The object the property belongs to. The rendered object if not set.
*/
constructor( property, nodeType = null, object = null ) {
super( nodeType );
/**
* The name of the property the node refers to.
*
* @type {string}
*/
this.property = property;
/**
* The property name might have dots so nested properties can be referred.
* The hierarchy of the names is stored inside this array.
*
* @type {Array<string>}
*/
this.properties = property.split( '.' );
/**
* The object the property belongs to. If `null`, the rendered object is used.
*
* @type {?Object}
* @default null
*/
this.object = object;
}
/**
* Returns the value of the referred property of the given object.
*
* @param {Object} object - The object.
* @return {?(BufferAttribute|InterleavedBufferAttribute)} The value of the referred property.
*/
getValueFromReference( object ) {
let value = object;
for ( const property of this.properties ) {
if ( value === undefined || value === null ) break;
value = value[ property ];
}
return value;
}
/**
* Returns the attribute the node binds when rendering the given object.
*
* @param {Object3D} object - The rendered object.
* @return {?(BufferAttribute|InterleavedBufferAttribute)} The buffer attribute.
*/
getAttribute( object ) {
return this.getValueFromReference( this.object !== null ? this.object : object );
}
/**
* Returns the node type, inferred from the attribute if not set.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
if ( this.nodeType !== null ) return this.nodeType;
const attribute = this.getAttribute( builder.object );
return attribute ? builder.getTypeFromAttribute( attribute ) : 'float';
}
/**
* Sets up the attribute nodes.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The attribute node.
*/
setup( builder ) {
const attribute = this.getAttribute( builder.object );
if ( ! attribute ) throw new Error( `THREE.ReferenceAttributeNode: Attribute "${ this.property }" not found.` );
const type = this.getNodeType( builder );
if ( attribute.isStorageBufferAttribute === true || attribute.isStorageInstancedBufferAttribute === true ) {
const index = attribute.isStorageInstancedBufferAttribute === true ? instanceIndex : vertexIndex;
return storage( attribute, type, Math.max( attribute.count, 1 ) ).onObjectUpdate( ( { object } ) => this.getAttribute( object ) ).element( index );
}
if ( type === 'mat4' ) {
return mat4( ...[ 0, 4, 8, 12 ].map( ( offset ) => new ReferenceBufferAttributeNode( this, 'vec4', 16, offset ) ) );
} else if ( type === 'mat3' ) {
return mat3( ...[ 0, 3, 6 ].map( ( offset ) => new ReferenceBufferAttributeNode( this, 'vec3', 9, offset ) ) );
}
return new ReferenceBufferAttributeNode( this, type );
}
}
/**
* TSL function for creating a reference attribute node.
*
* @tsl
* @function
* @param {string} name - The name of the property the node refers to.
* @param {?string} [type=null] - The node type. Inferred from the attribute if not set.
* @param {?Object} [object=null] - The object the property belongs to. The rendered object if not set.
* @returns {ReferenceAttributeNode}
*/
const referenceAttribute = ( name, type = null, object = null ) => new ReferenceAttributeNode( name, type, object );
const _previousInstanceMatrices = /*@__PURE__*/ new WeakMap();
const _previousMatrices = /*@__PURE__*/ new WeakMap();
// The default limit of vertex buffers in WebGPU.
const MAX_VERTEX_BUFFERS = 8;
/**
* Creates the appropriate node for instanced matrix transformations.
* Depending on buffer limits and storage capability, returns either a storage, buffer, or instanced interleaved attribute node.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - The matrix buffer attribute.
* @returns {Node} The matrix node.
*/
function createInstanceMatrixNode( builder, instanceMatrix ) {
let instanceMatrixNode;
const matrixCount = Math.max( instanceMatrix.count, 1 );
const isStorageMatrix = instanceMatrix.isStorageInstancedBufferAttribute === true;
if ( isStorageMatrix ) {
instanceMatrixNode = storage( instanceMatrix, 'mat4', matrixCount ).element( instanceIndex );
} else {
const uniformBufferSize = matrixCount * 16 * 4;
if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
} else {
// the columns view the matrices, so both share the same GPU buffer which is freed via instanceMatrix.dispose()
const bufferFn = instanceMatrix.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;
const instanceBuffers = [ 0, 4, 8, 12 ].map( ( offset ) => bufferFn( new InterleavedBufferAttribute( instanceMatrix, 4, offset ) ) );
instanceMatrixNode = mat4( ...instanceBuffers );
}
}
return instanceMatrixNode;
}
/**
* Retrieves or initializes the previous frame instance matrix node for motion vectors.
* Uses a WeakMap to cache previous frame instance matrices and their TSL nodes.
*
* @param {InstancedMesh} instancedMesh - The instanced mesh object.
* @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - The current matrix buffer attribute.
* @param {NodeBuilder} builder - The current node builder.
* @returns {Node} The previous frame instance matrix node.
*/
function getPreviousInstance( instancedMesh, instanceMatrix, builder ) {
let data = _previousInstanceMatrices.get( instancedMesh );
if ( data === undefined ) {
const previousInstanceMatrix = instanceMatrix.clone();
data = {
previousInstanceMatrix,
node: createInstanceMatrixNode( builder, previousInstanceMatrix )
};
_previousInstanceMatrices.set( instancedMesh, data );
instancedMesh.addEventListener( 'dispose', onPreviousInstanceDispose );
}
return data.node;
}
/**
* Frees the GPU buffer of the previous frame instance matrices when the mesh is disposed.
* The matrices are kept since the node refers to them, rendering the mesh again re-creates the buffer.
*
* @param {Object} event - The dispose event.
*/
function onPreviousInstanceDispose( { target: object } ) {
_previousInstanceMatrices.get( object ).previousInstanceMatrix.dispose();
}
/**
* Returns the instance matrices of the previous frame of the given mesh, for motion vectors.
*
* @param {InstancedMesh} object - The instanced mesh.
* @returns {InstancedBufferAttribute} The matrices of the previous frame.
*/
function getPreviousMatrix$1( object ) {
const matrices = object.instanceMatrix;
let previous = _previousMatrices.get( object );
if ( previous === undefined || previous.count !== matrices.count ) {
if ( previous === undefined ) {
object.addEventListener( 'dispose', onPreviousMatrixDispose );
} else {
previous.dispose();
}
previous = matrices.clone();
_previousMatrices.set( object, previous );
}
return previous;
}
/**
* Disposes the instance matrices of the previous frame when the mesh is disposed.
*
* @param {Object} event - The dispose event.
*/
function onPreviousMatrixDispose( { target: object } ) {
object.removeEventListener( 'dispose', onPreviousMatrixDispose );
_previousMatrices.get( object ).dispose();
_previousMatrices.delete( object );
}
/**
* Binds the instance matrices of the previous frame of the rendered mesh.
*
* @private
* @augments ReferenceAttributeNode
*/
class PreviousInstanceMatrixNode extends ReferenceAttributeNode {
static get type() {
return 'PreviousInstanceMatrixNode';
}
/**
* Constructs a new previous instance matrix node.
*/
constructor() {
super( 'previousInstanceMatrix', 'mat4' );
}
/**
* Returns the instance matrices of the previous frame of the given mesh.
*
* @param {InstancedMesh} object - The instanced mesh.
* @return {InstancedBufferAttribute} The matrices of the previous frame.
*/
getValueFromReference( object ) {
return getPreviousMatrix$1( object );
}
}
/**
* Returns `true` if the build can bind the instance data of the rendered mesh, so it can
* be shared between instanced meshes.
*
* @param {InstancedMesh} object - The instanced mesh.
* @param {NodeBuilder} builder - The current node builder.
* @returns {boolean} Whether the build can bind the instance data of the rendered mesh.
*/
function canReferenceInstanceData( object, builder ) {
// only render objects bind attributes per object
if ( builder.renderer.isRenderer !== true ) return false;
// the instance data take up to three vertex buffers next to the ones of the geometry
const instanceBuffers = 1 + ( object.instanceColor !== null ? 1 : 0 ) + ( builder.needsPreviousData() ? 1 : 0 );
return Object.keys( object.geometry.attributes ).length + instanceBuffers <= MAX_VERTEX_BUFFERS;
}
/**
* Transforms the position, the previous position and the normal with the given instance matrices.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node<mat4>} instanceMatrixNode - The instance matrix.
* @param {?Node<mat4>} previousMatrixNode - The instance matrix of the previous frame, if motion vectors are needed.
*/
function transformInstance( builder, instanceMatrixNode, previousMatrixNode ) {
// POSITION
const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz;
positionLocal.assign( instancePosition );
if ( previousMatrixNode !== null ) {
positionPrevious.assign( previousMatrixNode.mul( positionPrevious ).xyz );
}
// NORMAL
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const instanceNormal = transformNormal( normalLocal, instanceMatrixNode );
normalLocal.assign( instanceNormal );
}
}
/**
* TSL object representing a varying property for the instanced color vector.
*
* @type {VaryingNode<vec3>}
*/
const instanceColor = /*@__PURE__*/ varyingProperty( 'vec3', 'vInstanceColor' );
/**
* TSL function representing the standard instancing vertex shader setup.
* Transforms positionLocal and normalLocal, and assigns varying color in-place.
*
* @tsl
* @function
* @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} matrices - The instanced transformation matrices.
* @param {?InstancedBufferAttribute|StorageInstancedBufferAttribute} [colors=null] - The optional instanced colors.
*/
const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
const isStorageColor = colors && colors.isStorageInstancedBufferAttribute === true;
const instanceMatrixNode = createInstanceMatrixNode( builder, matrices );
let instanceColorNode = null;
if ( colors ) {
if ( isStorageColor ) {
instanceColorNode = storage( colors, 'vec3', Math.max( colors.count, 1 ) ).element( instanceIndex );
} else {
// bound directly, so the GPU buffer is freed via instanceColor.dispose()
const bufferFn = colors.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;
instanceColorNode = vec3( bufferFn( colors ) );
}
}
let previousInstanceMatrixNode = null;
if ( builder.needsPreviousData() ) {
const instancedMesh = builder.object;
OnAfterObjectUpdate( ( { object } ) => {
const { previousInstanceMatrix } = _previousInstanceMatrices.get( object );
previousInstanceMatrix.array.set( matrices.array );
previousInstanceMatrix.version = matrices.version;
} );
previousInstanceMatrixNode = getPreviousInstance( instancedMesh, matrices, builder );
}
transformInstance( builder, instanceMatrixNode, previousInstanceMatrixNode );
// COLOR
if ( instanceColorNode !== null ) {
instanceColor.assign( instanceColorNode );
}
}, 'void' );
/**
* TSL wrapper for applying instanced mesh rendering setup.
*
* The instance data of the rendered mesh are bound per object via {@link ReferenceAttributeNode},
* so instanced meshes share builds.
*
* @tsl
* @function
* @param {InstancedMesh} instancedMesh - The instanced mesh.
*/
const instancedMesh = /*@__PURE__*/ Fn( ( [ instancedMesh ], builder ) => {
if ( canReferenceInstanceData( instancedMesh, builder ) ) {
let previousInstanceMatrixNode = null;
if ( builder.needsPreviousData() ) {
OnAfterObjectUpdate( ( { object } ) => {
const previous = getPreviousMatrix$1( object );
previous.array.set( object.instanceMatrix.array );
previous.version = object.instanceMatrix.version;
} );
previousInstanceMatrixNode = new PreviousInstanceMatrixNode();
}
transformInstance( builder, referenceAttribute( 'instanceMatrix', 'mat4' ), previousInstanceMatrixNode );
if ( instancedMesh.instanceColor !== null ) {
instanceColor.assign( referenceAttribute( 'instanceColor', 'vec3' ) );
}
} else {
builder.objectSpecific = true;
instance( instancedMesh.instanceMatrix, instancedMesh.instanceColor );
}
}, 'void' );
/**
* TSL function that retrieves the batching color for a given instance ID from a colors texture.
*
* @param {Node<texture>} colorsTexture - The colors texture.
* @param {Node<int>} id - The instance or batch ID.
* @returns {Node<vec4>} The retrieved color.
*/
const getBatchingColor = /*@__PURE__*/ Fn( ( [ colorsTexture, id ] ) => {
const size = int( textureSize( textureLoad( colorsTexture ), 0 ).x ).toConst();
const j = int( id );
const x = j.mod( size ).toConst();
const y = j.div( size ).toConst();
return textureLoad( colorsTexture, ivec2( x, y ) );
} );
/**
* TSL function that retrieves the indirect index for a given batch ID.
*
* @param {BatchedMesh} batchMesh - The batched mesh.
* @param {Node<int>} id - The draw or instance ID.
* @returns {Node<uint>} The indirect index.
*/
const getIndirectIndex = /*@__PURE__*/ Fn( ( [ indirectTexture, id ] ) => {
const size = int( textureSize( textureLoad( indirectTexture ), 0 ).x ).toConst();
const x = int( id ).mod( size ).toConst();
const y = int( id ).div( size ).toConst();
return textureLoad( indirectTexture, ivec2( x, y ) ).x;
} );
/**
* Creates the node that reads a batching matrix from the given matrices texture.
*
* @param {Texture} matricesTexture - The matrices texture.
* @param {Node<uint>} id - The indirect instance ID.
* @returns {Node<mat4>} The matrix node.
*/
function createBatchingMatrixNode( matricesTexture, id ) {
const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst();
const j = float( id ).mul( 4 ).toInt().toConst();
const x = j.mod( size ).toConst();
const y = j.div( size ).toConst();
return mat4(
textureLoad( matricesTexture, ivec2( x, y ) ),
textureLoad( matricesTexture, ivec2( x.add( 1 ), y ) ),
textureLoad( matricesTexture, ivec2( x.add( 2 ), y ) ),
textureLoad( matricesTexture, ivec2( x.add( 3 ), y ) )
);
}
/**
* Returns the batching matrices texture of the previous frame for motion vectors.
* It is created on first use and again after the batched mesh released it.
*
* @param {BatchedMesh} batchMesh - The batched mesh.
* @returns {DataTexture} The previous frame matrices texture.
*/
function getPreviousMatricesTexture( batchMesh ) {
let previousMatricesTexture = batchMesh._previousMatricesTexture;
if ( previousMatricesTexture === null ) {
const { image, format, type, version } = batchMesh._matricesTexture;
previousMatricesTexture = new DataTexture( image.data.slice(), image.width, image.height, format, type );
previousMatricesTexture.version = version;
batchMesh._previousMatricesTexture = previousMatricesTexture;
}
return previousMatricesTexture;
}
/**
* TSL object representing a varying property for the batching color vector.
*
* @type {VaryingNode<vec4>}
*/
const batchColor = /*@__PURE__*/ varyingProperty( 'vec4', 'vBatchColor' );
/**
* TSL object representing a varying property for the batch indirect index (instance ID).
*
* @type {VaryingNode<uint>}
*/
const batchIndirectIndex = /*@__PURE__*/ varyingProperty( 'uint', 'vBatchIndirectId' );
/**
* TSL function representing the vertex shader batching setup.
* Applies the batch transformation matrix to positionLocal, normalLocal, and tangentLocal.
* Also assigns the batch color if a color texture is present.
*
* @tsl
* @function
* @param {BatchedMesh} batchMesh - The batched mesh.
*/
const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
const batchingIdNode = builder.getDrawIndex() === null ? instanceIndex : drawIndex;
const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) );
batchIndirectIndex.assign( indirectId );
const batchingMatrix = createBatchingMatrixNode( batchMesh._matricesTexture, indirectId );
const colorsTexture = batchMesh._colorsTexture;
if ( colorsTexture !== null ) {
const color = getBatchingColor( colorsTexture, indirectId );
batchColor.assign( color );
}
const bm = mat3( batchingMatrix );
positionLocal.assign( batchingMatrix.mul( positionLocal ) );
if ( builder.needsPreviousData() ) {
OnAfterObjectUpdate( ( { object } ) => {
const previousMatricesTexture = getPreviousMatricesTexture( object );
const matricesTexture = object._matricesTexture;
if ( previousMatricesTexture.version !== matricesTexture.version ) {
previousMatricesTexture.image.data.set( matricesTexture.image.data );
previousMatricesTexture.version = matricesTexture.version;
}
} );
const previousBatchingMatrixNode = createBatchingMatrixNode( getPreviousMatricesTexture( batchMesh ), indirectId );
positionPrevious.assign( previousBatchingMatrixNode.mul( positionPrevious ).xyz );
}
const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
const batchingNormal = bm.mul( transformedNormal ).xyz;
normalLocal.assign( batchingNormal );
if ( builder.hasGeometryAttribute( 'tangent' ) ) {
tangentLocal.mulAssign( bm );
}
const drawFormat = builder.renderer.backend.isWebGPUBackend === true ? 'merged' : 'multiDraw';
OnObjectUpdate( ( { object, camera, material } ) => {
object._updateDrawParameters( camera, object.geometry, material, drawFormat );
} );
}, 'void' );
const _skeletonsUpdated = /*@__PURE__*/ new WeakMap();
/**
* Creates an accessor for bone matrices stored in a bone texture.
*
* @param {TextureNode} boneTexture - The bone texture node.
* @returns {Object} An accessor with the same `element()` interface as a buffer node.
*/
function getBoneTextureMatrices( boneTexture ) {
return {
element: ( i ) => {
const size = int( textureSize( boneTexture ).x ).toConst();
const j = int( i ).mul( 4 ).toConst();
const y = j.div( size ).toConst();
const x = j.sub( y.mul( size ) ).toConst();
return mat4(
boneTexture.load( ivec2( x, y ) ),
boneTexture.load( ivec2( x.add( 1 ), y ) ),
boneTexture.load( ivec2( x.add( 2 ), y ) ),
boneTexture.load( ivec2( x.add( 3 ), y ) )
);
}
};
}
/**
* Creates the bone matrices node. Skeletons that fit within the uniform buffer limit
* use a uniform buffer, larger skeletons fall back to a bone texture.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Skeleton} skeleton - The skeleton.
* @returns {Object} The bone matrices node.
*/
function getBoneMatricesNode( builder, skeleton ) {
let node;
const uniformBufferSize = skeleton.bones.length * 16 * 4;
if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
node = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skeleton.bones.length );
} else {
if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture();
const boneTexture = texture( skeleton.boneTexture );
OnObjectUpdate( ( { object } ) => {
const skeleton = object.skeleton;
if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture();
boneTexture.value = skeleton.boneTexture;
} );
node = getBoneTextureMatrices( boneTexture );
}
return node;
}
/**
* Computes the skinned position by applying bone matrices based on weights.
*
* @param {Node} boneMatrices - The bone matrices buffer or storage node.
* @param {Node<vec3>} position - The vertex position to transform.
* @param {Node<mat4>} bindMatrix - The bind matrix node.
* @param {Node<mat4>} bindMatrixInverse - The inverse bind matrix node.
* @param {Node<uvec4>} skinIndex - The skin index attribute.
* @param {Node<vec4>} skinWeight - The skin weight attribute.
* @returns {Node<vec3>} The skinned position.
*/
function getSkinnedPosition( boneMatrices, position, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) {
const boneMatX = boneMatrices.element( skinIndex.x );
const boneMatY = boneMatrices.element( skinIndex.y );
const boneMatZ = boneMatrices.element( skinIndex.z );
const boneMatW = boneMatrices.element( skinIndex.w );
// POSITION
const skinVertex = bindMatrix.mul( position );
const skinned = add(
boneMatX.mul( skinWeight.x ).mul( skinVertex ),
boneMatY.mul( skinWeight.y ).mul( skinVertex ),
boneMatZ.mul( skinWeight.z ).mul( skinVertex ),
boneMatW.mul( skinWeight.w ).mul( skinVertex )
);
return bindMatrixInverse.mul( skinned ).xyz;
}
/**
* Computes the skinned normal and tangent vectors by applying bone matrices based on weights.
*
* @param {Node} boneMatrices - The bone matrices buffer or storage node.
* @param {Node<vec3>} normal - The normal vector in local space.
* @param {Node<vec3>} tangent - The tangent vector in local space.
* @param {Node<mat4>} bindMatrix - The bind matrix node.
* @param {Node<mat4>} bindMatrixInverse - The inverse bind matrix node.
* @param {Node<uvec4>} skinIndex - The skin index attribute.
* @param {Node<vec4>} skinWeight - The skin weight attribute.
* @returns {{skinNormal: Node<vec3>, skinTangent: Node<vec3>}} The skinned normal and tangent.
*/
function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) {
const boneMatX = boneMatrices.element( skinIndex.x );
const boneMatY = boneMatrices.element( skinIndex.y );
const boneMatZ = boneMatrices.element( skinIndex.z );
const boneMatW = boneMatrices.element( skinIndex.w );
// NORMAL and TANGENT
let skinMatrix = add(
skinWeight.x.mul( boneMatX ),
skinWeight.y.mul( boneMatY ),
skinWeight.z.mul( boneMatZ ),
skinWeight.w.mul( boneMatW )
);
skinMatrix = bindMatrixInverse.mul( skinMatrix ).mul( bindMatrix );
const skinMatrix3 = mat3( skinMatrix );
const skinNormal = skinMatrix3.mul( normal );
const skinTangent = skinMatrix3.mul( tangent );
return { skinNormal, skinTangent };
}
/**
* Creates the bone data of the previous frame if they do not exist yet.
*
* @param {Skeleton} skeleton - The skeleton.
*/
function setupPreviousBoneData( skeleton ) {
if ( skeleton.previousBoneMatrices === null ) {
skeleton.update();
skeleton.previousBoneMatrices = new Float32Array( skeleton.boneMatrices );
}
if ( skeleton.previousBoneTexture === null && skeleton.boneTexture !== null ) {
const { width, height } = skeleton.boneTexture.image;
skeleton.previousBoneTexture = new DataTexture( skeleton.previousBoneMatrices, width, height, RGBAFormat, FloatType );
skeleton.previousBoneTexture.needsUpdate = true;
}
}
/**
* Creates the bone matrices node of the previous frame.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Skeleton} skeleton - The skeleton.
* @returns {Object} The previous bone matrices node.
*/
function getPreviousBoneMatricesNode( builder, skeleton ) {
let node;
setupPreviousBoneData( skeleton );
const uniformBufferSize = skeleton.bones.length * 16 * 4;
if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
node = referenceBuffer( 'skeleton.previousBoneMatrices', 'mat4', skeleton.bones.length );
} else {
const previousBoneTexture = texture( skeleton.previousBoneTexture );
OnObjectUpdate( ( { object } ) => {
previousBoneTexture.value = object.skeleton.previousBoneTexture;
} );
node = getBoneTextureMatrices( previousBoneTexture );
}
return node;
}
/**
* TSL function representing the standard skeletal animation vertex shader setup.
* Transforms positionLocal, normalLocal, and tangentLocal in-place.
*
* @tsl
* @function
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
*/
const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
const skinIndexNode = attribute( 'skinIndex', 'uvec4' );
const skinWeightNode = attribute( 'skinWeight', 'vec4' );
const bindMatrixNode = reference( 'bindMatrix', 'mat4' );
const bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' );
const boneMatricesNode = getBoneMatricesNode( builder, skinnedMesh.skeleton );
const needsPreviousData = builder.needsPreviousData();
OnObjectUpdate( ( { object, frameId } ) => {
const skeleton = object.skeleton;
if ( needsPreviousData ) setupPreviousBoneData( skeleton );
if ( _skeletonsUpdated.get( skeleton ) !== frameId ) {
_skeletonsUpdated.set( skeleton, frameId );
if ( skeleton.previousBoneMatrices !== null ) {
skeleton.previousBoneMatrices.set( skeleton.boneMatrices );
if ( skeleton.previousBoneTexture !== null ) {
skeleton.previousBoneTexture.needsUpdate = true;
}
}
skeleton.update();
}
} );
if ( needsPreviousData ) {
const previousBoneMatricesNode = getPreviousBoneMatricesNode( builder, skinnedMesh.skeleton );
const previousSkinnedPosition = getSkinnedPosition( previousBoneMatricesNode, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionPrevious.assign( previousSkinnedPosition );
}
const skinPosition = getSkinnedPosition( boneMatricesNode, positionLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionLocal.assign( skinPosition );
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
normalLocal.assign( skinNormal );
if ( builder.hasGeometryAttribute( 'tangent' ) ) {
tangentLocal.assign( skinTangent );
}
}
}, 'void' );
/**
* TSL function that computes skeletal animation for custom compute passes.
*
* @tsl
* @function
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
* @param {Node<vec3>} [toPosition=null] - The target position node to assign.
* @returns {Node<vec3>} The computed skinned position node.
*/
const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], builder ) => {
const positionNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'position' ).array, 3 ), 'vec3' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const skinIndexNode = storage( new InstancedBufferAttribute( new Uint32Array( skinnedMesh.geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const skinWeightNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' );
const bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' );
const boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length );
const skeleton = skinnedMesh.skeleton;
OnObjectUpdate( ( { frameId } ) => {
if ( _skeletonsUpdated.get( skeleton ) !== frameId ) {
_skeletonsUpdated.set( skeleton, frameId );
if ( skeleton.previousBoneMatrices !== null ) {
skeleton.previousBoneMatrices.set( skeleton.boneMatrices );
if ( skeleton.previousBoneTexture !== null ) {
skeleton.previousBoneTexture.needsUpdate = true;
}
}
skeleton.update();
}
} );
if ( builder.needsPreviousData() ) {
setupPreviousBoneData( skeleton );
const previousBoneMatricesNode = buffer( skeleton.previousBoneMatrices, 'mat4', skeleton.bones.length );
const previousSkinnedPosition = getSkinnedPosition( previousBoneMatricesNode, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionPrevious.assign( previousSkinnedPosition );
}
const skinPosition = getSkinnedPosition( boneMatricesNode, positionNode, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
if ( toPosition !== null ) {
toPosition.assign( skinPosition );
}
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
normalLocal.assign( skinNormal );
if ( builder.hasGeometryAttribute( 'tangent' ) ) {
tangentLocal.assign( skinTangent );
}
}
return skinPosition;
} );
const _morphTextures = /*@__PURE__*/ new WeakMap();
const _morphVec4 = /*@__PURE__*/ new Vector4();
const _morphInfluencesData = /*@__PURE__*/ new WeakMap();
/**
* TSL function that retrieves and scales the morphed attribute (position, normal or color) texel value.
*
* @param {Object} params - The parameter object.
* @param {Node<texture>} params.bufferMap - The morph target data array texture.
* @param {Node<float>} params.influence - The target's animation influence weight.
* @param {number} params.stride - The vertex data stride (e.g. 1 or 2).
* @param {Node<int>} params.width - The texture width limit.
* @param {Node<int>} params.depth - The target layer index (morph target index).
* @param {Node<int>} params.offset - The texture offset (e.g. 0 for position, 1 for normal, 2 for color).
* @returns {Node<vec4>} The scaled morph target value.
*/
const getMorph = /*@__PURE__*/ Fn( ( { bufferMap, influence, stride, width, depth, offset } ) => {
const texelIndex = int( vertexIndex ).mul( stride ).add( offset );
const y = texelIndex.div( width );
const x = texelIndex.sub( y.mul( width ) );
const bufferAttrib = textureLoad( bufferMap, ivec2( x, y ) ).depth( depth );
return bufferAttrib.mul( influence );
} );
/**
* The morph target base influence. It is `1` when `morphTargetsRelative`
* is `true`, otherwise `1 - sum( influences )`.
*
* @type {UniformNode<float>}
*/
const morphBase = /*@__PURE__*/ uniform( 1 ).onObjectUpdate( ( { object } ) => {
return object.geometry.morphTargetsRelative ? 1 : 1 - object.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 );
} );
/**
* The sum of the weighted color morph targets.
*
* @type {PropertyNode<vec4>}
*/
const morphColorSum = /*@__PURE__*/ property( 'vec4', 'morphColorSum' );
/**
* TSL function that applies the color morph targets to the given color.
* The result is computed in the vertex stage.
*
* @tsl
* @function
* @param {Node<vec4>} color - The color to morph. Must be available in the vertex stage.
* @returns {Node<vec4>} The morphed color.
*/
const morphColor = /*@__PURE__*/ Fn( ( [ color ], { geometry } ) => {
if ( geometry.morphAttributes.color[ 0 ].itemSize === 4 ) {
return varying( color.mul( morphBase ).add( morphColorSum ) );
}
return varying( vec4( color.rgb.mul( morphBase ).add( morphColorSum.rgb ), color.a ) );
} );
/**
* Resolves or creates a compiled DataArrayTexture containing encoded vertex morph targets data for WebGL2/WebGPU.
*
* @param {BufferGeometry} geometry - The geometry to parse.
* @returns {Object} The resolved morph targets texture data mapping entry.
*/
function getEntry( geometry ) {
const hasMorphPosition = geometry.morphAttributes.position !== undefined;
const hasMorphNormals = geometry.morphAttributes.normal !== undefined;
const hasMorphColors = geometry.morphAttributes.color !== undefined;
// instead of using attributes, the WebGL 2 code path encodes morph targets
// into an array of data textures. Each layer represents a single morph target.
const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;
let entry = _morphTextures.get( geometry );
if ( entry === undefined || entry.count !== morphTargetsCount ) {
if ( entry !== undefined ) entry.texture.dispose();
const morphTargets = geometry.morphAttributes.position || [];
const morphNormals = geometry.morphAttributes.normal || [];
const morphColors = geometry.morphAttributes.color || [];
let vertexDataCount = 0;
if ( hasMorphPosition === true ) vertexDataCount = 1;
if ( hasMorphNormals === true ) vertexDataCount = 2;
if ( hasMorphColors === true ) vertexDataCount = 3;
let width = geometry.attributes.position.count * vertexDataCount;
let height = 1;
const maxTextureSize = 4096; // @TODO: Use 'capabilities.maxTextureSize'
if ( width > maxTextureSize ) {
height = Math.ceil( width / maxTextureSize );
width = maxTextureSize;
}
const buffer = new Float32Array( width * height * 4 * morphTargetsCount );
const bufferTexture = new DataArrayTexture( buffer, width, height, morphTargetsCount );
bufferTexture.type = FloatType;
bufferTexture.needsUpdate = true;
// fill buffer
const vertexDataStride = vertexDataCount * 4;
for ( let i = 0; i < morphTargetsCount; i ++ ) {
const morphTarget = morphTargets[ i ];
const morphNormal = morphNormals[ i ];
const morphColor = morphColors[ i ];
const offset = width * height * 4 * i;
for ( let j = 0; j < morphTarget.count; j ++ ) {
const stride = j * vertexDataStride;
if ( hasMorphPosition === true ) {
_morphVec4.fromBufferAttribute( morphTarget, j );
buffer[ offset + stride + 0 ] = _morphVec4.x;
buffer[ offset + stride + 1 ] = _morphVec4.y;
buffer[ offset + stride + 2 ] = _morphVec4.z;
buffer[ offset + stride + 3 ] = 0;
}
if ( hasMorphNormals === true ) {
_morphVec4.fromBufferAttribute( morphNormal, j );
buffer[ offset + stride + 4 ] = _morphVec4.x;
buffer[ offset + stride + 5 ] = _morphVec4.y;
buffer[ offset + stride + 6 ] = _morphVec4.z;
buffer[ offset + stride + 7 ] = 0;
}
if ( hasMorphColors === true ) {
_morphVec4.fromBufferAttribute( morphColor, j );
buffer[ offset + stride + 8 ] = _morphVec4.x;
buffer[ offset + stride + 9 ] = _morphVec4.y;
buffer[ offset + stride + 10 ] = _morphVec4.z;
buffer[ offset + stride + 11 ] = ( morphColor.itemSize === 4 ) ? _morphVec4.w : 1;
}
}
}
entry = {
count: morphTargetsCount,
texture: bufferTexture,
stride: vertexDataCount,
size: new Vector2( width, height )
};
_morphTextures.set( geometry, entry );
function disposeTexture() {
bufferTexture.dispose();
_morphTextures.delete( geometry );
geometry.removeEventListener( 'dispose', disposeTexture );
}
geometry.addEventListener( 'dispose', disposeTexture );
}
return entry;
}
/**
* TSL function representing the vertex shader morph targets blend setup.
* Dynamically computes morph targets weights and updates positionLocal and normalLocal in-place.
*
* @tsl
* @function
* @param {Mesh} mesh - The mesh.
*/
const morphReference = /*@__PURE__*/ Fn( ( [ mesh ], builder ) => {
const { geometry } = mesh;
const hasMorphPosition = geometry.morphAttributes.position !== undefined;
const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined;
const hasMorphColors = builder.material.vertexColors === true && geometry.morphAttributes.color !== undefined;
const hasColorAlpha = hasMorphColors === true && geometry.morphAttributes.color[ 0 ].itemSize === 4;
const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;
if ( morphTargetsCount === 0 ) return;
if ( mesh.morphTexture ) builder.objectSpecific = true;
// Init
let morphInfluenceData = _morphInfluencesData.get( mesh );
if ( morphInfluenceData === undefined || morphInfluenceData.count !== morphTargetsCount ) {
morphInfluenceData = {
influences: mesh.morphTargetInfluences ? uniformArray( mesh.morphTargetInfluences, 'float' ) : null,
count: morphTargetsCount
};
_morphInfluencesData.set( mesh, morphInfluenceData );
}
const { influences } = morphInfluenceData;
// Shader
const { texture: bufferMap, stride, size } = getEntry( geometry );
if ( hasMorphPosition === true ) positionLocal.mulAssign( morphBase );
if ( hasMorphNormals === true ) normalLocal.mulAssign( morphBase );
if ( hasMorphColors === true ) morphColorSum.assign( 0 );
const width = int( size.width );
Loop( morphTargetsCount, ( { i } ) => {
const influence = float( 0 ).toVar();
if ( mesh.count > 1 && ( mesh.morphTexture !== null && mesh.morphTexture !== undefined ) ) {
influence.assign( textureLoad( mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r );
} else {
influence.assign( influences.element( i ).toVar() );
}
If( influence.notEqual( 0 ), () => {
if ( hasMorphPosition === true ) {
positionLocal.addAssign( getMorph( {
bufferMap,
influence,
stride,
width,
depth: i,
offset: int( 0 )
} ).xyz );
}
if ( hasMorphNormals === true ) {
normalLocal.addAssign( getMorph( {
bufferMap,
influence,
stride,
width,
depth: i,
offset: int( 1 )
} ).xyz );
}
if ( hasMorphColors === true ) {
const morph = getMorph( {
bufferMap,
influence,
stride,
width,
depth: i,
offset: int( 2 )
} );
if ( hasColorAlpha === true ) {
morphColorSum.addAssign( morph );
} else {
morphColorSum.rgb.addAssign( morph.rgb );
}
}
} );
} );
// Update
OnObjectUpdate( ( { object } ) => {
const { influences } = morphInfluenceData;
if ( influences ) {
influences.array = object.morphTargetInfluences;
influences.update();
}
} );
}, 'void' );
/**
* Base class for lighting nodes.
*
* @augments Node
*/
class LightingNode extends Node {
static get type() {
return 'LightingNode';
}
/**
* Constructs a new lighting node.
*/
constructor() {
super( 'vec3' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isLightingNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
}
/**
* A generic class that can be used by nodes which contribute
* ambient occlusion to the scene. E.g. an ambient occlusion map
* node can be used as input for this module. Used in {@link NodeMaterial}.
*
* @augments LightingNode
*/
class AONode extends LightingNode {
static get type() {
return 'AONode';
}
/**
* Constructs a new AO node.
*
* @param {?Node<float>} [aoNode=null] - The ambient occlusion node.
*/
constructor( aoNode = null ) {
super();
/**
* The ambient occlusion node.
*
* @type {?Node<float>}
* @default null
*/
this.aoNode = aoNode;
}
setup( builder ) {
builder.context.ambientOcclusion.mulAssign( this.aoNode );
}
}
/**
* `LightingContextNode` represents an extension of the {@link ContextNode} module
* by adding lighting specific context data. It represents the runtime context of
* {@link LightsNode}.
*
* @augments ContextNode
*/
class LightingContextNode extends ContextNode {
static get type() {
return 'LightingContextNode';
}
/**
* Constructs a new lighting context node.
*
* @param {LightsNode} lightsNode - The lights node.
* @param {?LightingModel} [lightingModel=null] - The current lighting model.
* @param {?Array<LightingNode>} materialLightings - The material lightings nodes.
* @param {?Node<vec3>} [backdropNode=null] - A backdrop node.
* @param {?Node<float>} [backdropAlphaNode=null] - A backdrop alpha node.
*/
constructor( lightsNode, lightingModel = null, materialLightings = [], backdropNode = null, backdropAlphaNode = null ) {
super( lightsNode );
/**
* The current lighting model.
*
* @type {?LightingModel}
* @default null
*/
this.lightingModel = lightingModel;
/**
* @type {?Array<LightingNode>}
* @default []
*/
this.materialLightings = materialLightings;
/**
* A backdrop node.
*
* @type {?Node<vec3>}
* @default null
*/
this.backdropNode = backdropNode;
/**
* A backdrop alpha node.
*
* @type {?Node<float>}
* @default null
*/
this.backdropAlphaNode = backdropAlphaNode;
this._value = null;
}
/**
* Returns a lighting context object.
*
* @return {{
* radiance: Node<vec3>,
* irradiance: Node<vec3>,
* iblIrradiance: Node<vec3>,
* ambientOcclusion: Node<float>,
* reflectedLight: {directDiffuse: Node<vec3>, directSpecular: Node<vec3>, indirectDiffuse: Node<vec3>, indirectSpecular: Node<vec3>},
* backdrop: Node<vec3>,
* backdropAlpha: Node<float>
* }} The lighting context object.
*/
getContext() {
const { materialLightings, backdropNode, backdropAlphaNode } = this;
const directDiffuse = vec3().toVar( 'directDiffuse' ),
directSpecular = vec3().toVar( 'directSpecular' ),
indirectDiffuse = vec3().toVar( 'indirectDiffuse' ),
indirectSpecular = vec3().toVar( 'indirectSpecular' );
const reflectedLight = {
directDiffuse,
directSpecular,
indirectDiffuse,
indirectSpecular
};
const context = {
radiance: vec3().toVar( 'radiance' ),
irradiance: vec3().toVar( 'irradiance' ),
iblIrradiance: vec3().toVar( 'iblIrradiance' ),
ambientOcclusion: float( 1 ).toVar( 'ambientOcclusion' ),
reflectedLight,
materialLightings,
backdrop: backdropNode,
backdropAlpha: backdropAlphaNode
};
return context;
}
setup( builder ) {
this.value = this._value || ( this._value = this.getContext() );
this.value.lightingModel = this.lightingModel || builder.context.lightingModel;
return super.setup( builder );
}
}
const lightingContext = /*@__PURE__*/ nodeProxy( LightingContextNode );
/**
* A generic class that can be used by nodes which contribute
* irradiance to the scene. E.g. a light map node can be used
* as input for this module. Used in {@link NodeMaterial}.
*
* @augments LightingNode
*/
class IrradianceNode extends LightingNode {
static get type() {
return 'IrradianceNode';
}
/**
* Constructs a new irradiance node.
*
* @param {Node<vec3>} node - A node contributing irradiance.
*/
constructor( node ) {
super();
/**
* A node contributing irradiance.
*
* @type {Node<vec3>}
*/
this.node = node;
}
setup( builder ) {
builder.context.irradiance.addAssign( this.node );
}
}
/**
* This node is used in {@link NodeMaterial} to setup the clipping
* which can happen hardware-accelerated (if supported) and optionally
* use alpha-to-coverage for anti-aliasing clipped edges.
*
* @augments Node
*/
class ClippingNode extends Node {
static get type() {
return 'ClippingNode';
}
/**
* Constructs a new clipping node.
*
* @param {('default'|'hardware'|'alphaToCoverage')} [scope='default'] - The node's scope. Similar to other nodes,
* the selected scope influences the behavior of the node and what type of code is generated.
*/
constructor( scope = ClippingNode.DEFAULT ) {
super();
/**
* The node's scope. Similar to other nodes, the selected scope influences
* the behavior of the node and what type of code is generated.
*
* @type {('default'|'hardware'|'alphaToCoverage')}
*/
this.scope = scope;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Setups the node depending on the selected scope.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The result node.
*/
setup( builder ) {
super.setup( builder );
const clippingContext = builder.clippingContext;
const { intersectionPlanes, unionPlanes } = clippingContext;
this.hardwareClipping = builder.hardwareClipping;
// with ArrayCamera, clipping planes are in world space
this.position = builder.camera.isArrayCamera === true ? cameraWorldMatrix.mul( vec4( positionView, 1 ) ).xyz : positionView;
if ( this.scope === ClippingNode.ALPHA_TO_COVERAGE ) {
return this.setupAlphaToCoverage( intersectionPlanes, unionPlanes );
} else if ( this.scope === ClippingNode.HARDWARE ) {
return this.setupHardwareClipping( unionPlanes, builder );
} else {
return this.setupDefault( intersectionPlanes, unionPlanes );
}
}
/**
* Setups alpha to coverage.
*
* @param {Array<Vector4>} intersectionPlanes - The intersection planes.
* @param {Array<Vector4>} unionPlanes - The union planes.
* @return {Node} The result node.
*/
setupAlphaToCoverage( intersectionPlanes, unionPlanes ) {
return Fn( () => {
const distanceToPlane = float().toVar( 'distanceToPlane' );
const distanceGradient = float().toVar( 'distanceToGradient' );
const clipOpacity = float( 1 ).toVar( 'clipOpacity' );
const numUnionPlanes = unionPlanes.length;
if ( this.hardwareClipping === false && numUnionPlanes > 0 ) {
const clippingPlanes = uniformArray( unionPlanes ).setGroup( renderGroup );
Loop( numUnionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
distanceToPlane.assign( this.position.dot( plane.xyz ).negate().add( plane.w ) );
distanceGradient.assign( distanceToPlane.fwidth().div( 2.0 ) );
clipOpacity.mulAssign( smoothstep( distanceGradient.negate(), distanceGradient, distanceToPlane ) );
} );
}
const numIntersectionPlanes = intersectionPlanes.length;
if ( numIntersectionPlanes > 0 ) {
const clippingPlanes = uniformArray( intersectionPlanes ).setGroup( renderGroup );
const intersectionClipOpacity = float( 1 ).toVar( 'intersectionClipOpacity' );
Loop( numIntersectionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
distanceToPlane.assign( this.position.dot( plane.xyz ).negate().add( plane.w ) );
distanceGradient.assign( distanceToPlane.fwidth().div( 2.0 ) );
intersectionClipOpacity.mulAssign( smoothstep( distanceGradient.negate(), distanceGradient, distanceToPlane ).oneMinus() );
} );
clipOpacity.mulAssign( intersectionClipOpacity.oneMinus() );
}
diffuseColor.a.mulAssign( clipOpacity );
diffuseColor.a.equal( 0.0 ).discard();
} )();
}
/**
* Setups the default clipping.
*
* @param {Array<Vector4>} intersectionPlanes - The intersection planes.
* @param {Array<Vector4>} unionPlanes - The union planes.
* @return {Node} The result node.
*/
setupDefault( intersectionPlanes, unionPlanes ) {
return Fn( () => {
const numUnionPlanes = unionPlanes.length;
if ( this.hardwareClipping === false && numUnionPlanes > 0 ) {
const clippingPlanes = uniformArray( unionPlanes ).setGroup( renderGroup );
Loop( numUnionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
this.position.dot( plane.xyz ).greaterThan( plane.w ).discard();
} );
}
const numIntersectionPlanes = intersectionPlanes.length;
if ( numIntersectionPlanes > 0 ) {
const clippingPlanes = uniformArray( intersectionPlanes ).setGroup( renderGroup );
const clipped = bool( true ).toVar( 'clipped' );
Loop( numIntersectionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
clipped.assign( this.position.dot( plane.xyz ).greaterThan( plane.w ).and( clipped ) );
} );
clipped.discard();
}
} )();
}
/**
* Setups hardware clipping.
*
* @param {Array<Vector4>} unionPlanes - The union planes.
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The result node.
*/
setupHardwareClipping( unionPlanes, builder ) {
const numUnionPlanes = unionPlanes.length;
builder.enableHardwareClipping( numUnionPlanes );
return Fn( () => {
const clippingPlanes = uniformArray( unionPlanes ).setGroup( renderGroup );
const hw_clip_distances = builtin( builder.getClipDistance() );
Loop( numUnionPlanes, ( { i } ) => {
const plane = clippingPlanes.element( i );
const distance = this.position.dot( plane.xyz ).sub( plane.w ).negate();
hw_clip_distances.element( i ).assign( distance );
} );
} )();
}
}
ClippingNode.ALPHA_TO_COVERAGE = 'alphaToCoverage';
ClippingNode.DEFAULT = 'default';
ClippingNode.HARDWARE = 'hardware';
/**
* TSL function for setting up the default clipping logic.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
const clipping = () => new ClippingNode();
/**
* TSL function for setting up alpha to coverage.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
const clippingAlpha = () => new ClippingNode( ClippingNode.ALPHA_TO_COVERAGE );
/**
* TSL function for setting up hardware-based clipping.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
const hardwareClipping = () => new ClippingNode( ClippingNode.HARDWARE );
const refreshUniforms = [
'alphaMap',
'alphaTest',
'anisotropy',
'anisotropyMap',
'anisotropyRotation',
'aoMap',
'aoMapIntensity',
'attenuationColor',
'attenuationDistance',
'bumpMap',
'bumpScale',
'clearcoat',
'clearcoatMap',
'clearcoatNormalMap',
'clearcoatNormalScale',
'clearcoatRoughness',
'clearcoatRoughnessMap',
'color',
'dashOffset',
'dashSize',
'diffuseRoughness',
'diffuseRoughnessMap',
'dispersion',
'displacementBias',
'displacementMap',
'displacementScale',
'emissive',
'emissiveIntensity',
'emissiveMap',
'envMap',
'envMapIntensity',
'envMapRotation',
'gapSize',
'gradientMap',
'ior',
'iridescence',
'iridescenceIOR',
'iridescenceMap',
'iridescenceThicknessMap',
'iridescenceThicknessMaximum',
'iridescenceThicknessMinimum',
'lightMap',
'lightMapIntensity',
'linewidth',
'map',
'matcap',
'metalness',
'metalnessMap',
'normalMap',
'normalScale',
'opacity',
'reflectivity',
'refractionRatio',
'retroreflectivity',
'rotation',
'roughness',
'roughnessMap',
'scale',
'sheen',
'sheenColor',
'sheenColorMap',
'sheenRoughness',
'sheenRoughnessMap',
'shininess',
'size',
'specular',
'specularColor',
'specularColorMap',
'specularIntensity',
'specularIntensityMap',
'specularMap',
'steps',
'thickness',
'thicknessMap',
'transmission',
'transmissionMap',
'wireframe'
];
/**
* A WeakMap to cache lights data for node materials.
* Cache lights data by render ID to avoid unnecessary recalculations.
*
* @private
* @type {WeakMap<LightsNode,Object>}
*/
const _lightsCache = new WeakMap();
/**
* Holds the material data for comparison.
*
* @private
* @type {WeakMap<Material,Object>}
*/
const _materialCache = new WeakMap();
/**
* Holds the geometry data for comparison.
*
* @private
* @type {WeakMap<BufferGeometry,Object>}
*/
const _geometryCache = new WeakMap();
/**
* Holds the texture data for comparison.
*
* @private
* @type {WeakMap<Texture,Object>}
*/
const _textureCache = new WeakMap();
/**
* Holds the output target data for comparison.
*
* @private
* @type {WeakMap<RenderTarget|CanvasTarget,Object>}
*/
const _outputTargetCache = new WeakMap();
/**
* This class is used by {@link WebGPURenderer} as management component.
* It's primary purpose is to determine whether render objects require a
* refresh right before they are going to be rendered or not.
*/
class NodeMaterialObserver {
/**
* Constructs a new node material observer.
*
* @param {NodeBuilder} builder - The node builder.
*/
constructor( builder ) {
/**
* A node material can be used by more than one render object so the
* monitor must maintain a list of render objects.
*
* @type {WeakMap<RenderObject,Object>}
*/
this.renderObjects = new WeakMap();
/**
* Whether the builder's context uses node objects or not.
*
* @type {boolean}
*/
this.hasNode = this.containsNode( builder );
/**
* Whether the node builder's 3D object is animated or not.
*
* @type {boolean}
*/
this.hasAnimation = builder.object.isSkinnedMesh === true;
/**
* A list of all possible material uniforms
*
* @type {Array<string>}
*/
this.refreshUniforms = refreshUniforms;
/**
* Holds the current render ID from the node frame.
*
* @type {number}
* @default 0
*/
this.renderId = 0;
}
/**
* Returns `true` if the given render object is verified for the first time of this observer.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the given render object is verified for the first time of this observer.
*/
firstInitialization( renderObject ) {
const hasInitialized = this.renderObjects.has( renderObject );
if ( hasInitialized === false ) {
this.getRenderObjectData( renderObject );
return true;
}
return false;
}
/**
* Returns `true` if the given 3D object uses instance buffers with dynamic draw usage.
* Such buffers must be uploaded once per render so the render object requires a full refresh.
*
* @param {Object3D} object - The 3D object.
* @return {boolean} Whether the given 3D object uses instance buffers with dynamic draw usage or not.
*/
hasDynamicInstancing( object ) {
return object.isInstancedMesh === true && ( object.instanceMatrix.usage === DynamicDrawUsage ||
( object.instanceColor !== null && object.instanceColor.usage === DynamicDrawUsage ) );
}
/**
* Returns `true` if the current rendering produces motion vectors.
*
* @param {Renderer} renderer - The renderer.
* @return {boolean} Whether the current rendering produces motion vectors or not.
*/
needsVelocity( renderer ) {
const mrt = renderer.getMRT();
return ( mrt !== null && mrt.has( 'velocity' ) );
}
/**
* Returns monitoring data for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {Object} The monitoring data.
*/
getRenderObjectData( renderObject ) {
let data = this.renderObjects.get( renderObject );
if ( data === undefined ) {
const { geometry, object } = renderObject;
data = {
geometryId: geometry.id,
geometryVersion: this.getGeometryData( geometry )._version,
materialVersion: this.getMaterialData( renderObject.material )._version,
attributesDisposeVersion: renderObject._geometries.attributes.disposeVersion,
worldMatrix: object.matrixWorld.clone()
};
if ( object.center ) {
data.center = object.center.clone();
}
if ( object.morphTargetInfluences ) {
data.morphTargetInfluences = object.morphTargetInfluences.slice();
}
if ( object.isInstancedMesh === true ) {
data.instanceMatrix = object.instanceMatrix;
data.instanceMatrixVersion = object.instanceMatrix.version;
data.instanceColor = object.instanceColor;
data.instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null;
data.morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null;
}
if ( object.isBatchedMesh === true ) {
data.matricesTextureVersion = object._matricesTexture.version;
data.colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null;
data.indirectTextureVersion = object._indirectTexture.version;
}
if ( renderObject.bundle !== null ) {
data.version = renderObject.bundle.version;
}
// global data
data.outputTarget = this.getOutputTarget( renderObject.renderer );
data.outputTargetVersion = this.getOutputTargetData( data.outputTarget, renderObject.context )._version;
const { environment, environmentIntensity, environmentRotation } = renderObject.scene;
data.environmentIntensity = environmentIntensity;
data.environmentRotation = environmentRotation.clone();
data.environmentVersion = environment !== null ? this.getTextureData( environment )._version : null;
data.lights = this.getLightsData( renderObject.lightsNode.getBuiltinLights(), [] );
this.renderObjects.set( renderObject, data );
}
return data;
}
/**
* Returns the current output target of the given renderer. That is the active
* render target or, if none is set, the output render target or the canvas target.
*
* @param {Renderer} renderer - The renderer.
* @return {RenderTarget|CanvasTarget} The output target.
*/
getOutputTarget( renderer ) {
const renderTarget = renderer.getRenderTarget();
return renderTarget !== null ? renderTarget : ( renderer.getOutputRenderTarget() || renderer.getCanvasTarget() );
}
/**
* Returns an output target data structure holding the size of the given output
* target for monitoring. Its version is incremented whenever a size change is observed.
*
* @param {RenderTarget|CanvasTarget} outputTarget - The output target.
* @param {RenderContext} renderContext - The render context.
* @return {Object} An object for monitoring the output target size.
*/
getOutputTargetData( outputTarget, renderContext ) {
const { width, height } = renderContext;
let data = _outputTargetCache.get( outputTarget );
if ( data === undefined ) {
data = { _version: 0, width, height };
_outputTargetCache.set( outputTarget, data );
} else if ( data.width !== width || data.height !== height ) {
data.width = width;
data.height = height;
data._version ++;
}
return data;
}
/**
* Returns an attribute data structure holding the attributes versions for
* monitoring.
*
* @param {Object} attributes - The geometry attributes.
* @return {Object} An object for monitoring the versions of attributes.
*/
getAttributesData( attributes ) {
const attributesData = {};
for ( const name in attributes ) {
const attribute = attributes[ name ];
attributesData[ name ] = {
id: attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id,
version: attribute.isInterleavedBufferAttribute ? attribute.data.version : attribute.version,
};
}
return attributesData;
}
/**
* Returns `true` if the node builder's context (e.g. material) uses node properties.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether the node builder's context uses node properties or not.
*/
containsNode( builder ) {
// material
const material = builder.material;
for ( const property in material ) {
if ( material[ property ] && material[ property ].isNode )
return true;
}
// context
if ( builder.context.modelViewMatrix || builder.context.modelNormalViewMatrix || builder.context.getAO || builder.context.getShadow || builder.context.getGI )
return true;
return false;
}
/**
* Returns a geometry data structure holding the geometry property values for
* monitoring.
*
* @param {BufferGeometry} geometry - The geometry.
* @return {Object} An object for monitoring geometry properties.
*/
getGeometryData( geometry ) {
let data = _geometryCache.get( geometry );
if ( data === undefined ) {
data = {
_renderId: -1,
_version: 0,
attributes: this.getAttributesData( geometry.attributes ),
indexId: geometry.index ? geometry.index.id : null,
indexVersion: geometry.index ? geometry.index.version : null,
drawRange: { start: geometry.drawRange.start, count: geometry.drawRange.count }
};
// force refresh on dispose
geometry.addEventListener( 'dispose', () => {
data._version ++;
} );
_geometryCache.set( geometry, data );
}
return data;
}
/**
* Returns a texture data structure holding the texture state for
* monitoring.
*
* @param {Texture} texture - The texture.
* @return {Object} An object for monitoring the texture.
*/
getTextureData( texture ) {
let data = _textureCache.get( texture );
if ( data === undefined ) {
data = { _version: 0 };
// force refresh on dispose
const onDispose = () => {
data._version ++;
};
if ( texture.renderTarget !== null ) {
texture.renderTarget.addEventListener( 'dispose', onDispose );
} else {
texture.addEventListener( 'dispose', onDispose );
}
_textureCache.set( texture, data );
}
return data;
}
/**
* Returns a data structure holding the uv transformation of the given
* texture for monitoring.
*
* @param {Texture} texture - The texture.
* @return {Object} An object for monitoring the uv transformation.
*/
getTextureTransformData( texture ) {
return {
offset: texture.offset.clone(),
repeat: texture.repeat.clone(),
center: texture.center.clone(),
rotation: texture.rotation,
matrix: texture.matrix.clone()
};
}
/**
* Returns `true` if the uv transformation of the given texture has not changed.
*
* @param {Object} transformData - The monitored uv transformation.
* @param {Texture} texture - The texture.
* @return {boolean} Whether the uv transformation has not changed or not.
*/
equalsTextureTransform( transformData, texture ) {
if ( texture.matrixAutoUpdate === true ) {
return transformData.rotation === texture.rotation && transformData.offset.equals( texture.offset ) && transformData.repeat.equals( texture.repeat ) && transformData.center.equals( texture.center );
}
return transformData.matrix.equals( texture.matrix );
}
/**
* Returns a material data structure holding the material property values for
* monitoring.
*
* @param {Material} material - The material.
* @return {Object} An object for monitoring material properties.
*/
getMaterialData( material ) {
let data = _materialCache.get( material );
if ( data === undefined ) {
data = { _renderId: -1, _version: 0 };
for ( const property of this.refreshUniforms ) {
const value = material[ property ];
if ( value === undefined ) continue;
if ( value === null ) {
data[ property ] = null; // track unset properties
} else if ( typeof value === 'object' && value.clone !== undefined ) {
if ( value.isTexture === true ) {
data[ property ] = { id: value.id, version: 0, cacheVersion: this.getTextureData( value )._version, transform: this.getTextureTransformData( value ) };
} else {
data[ property ] = value.clone();
}
} else {
data[ property ] = value;
}
}
_materialCache.set( material, data );
}
return data;
}
/**
* Returns `true` if the given render object has not changed its state.
*
* @param {RenderObject} renderObject - The render object.
* @param {Array<Light>} lightsData - The current material lights.
* @param {number} renderId - The current render ID.
* @return {boolean} Whether the given render object is equal to its cached state or not.
*/
equals( renderObject, lightsData, renderId ) {
const { object, material, geometry } = renderObject;
const renderObjectData = this.getRenderObjectData( renderObject );
// world matrix
if ( renderObjectData.worldMatrix.equals( object.matrixWorld ) !== true ) {
renderObjectData.worldMatrix.copy( object.matrixWorld );
return false;
}
// material
const materialData = this.getMaterialData( renderObject.material );
// check the material properties just once per render for all render objects
if ( materialData._renderId !== renderId ) {
materialData._renderId = renderId;
let changed = false;
for ( const property in materialData ) {
const value = materialData[ property ];
const mtlValue = material[ property ];
if ( property === '_renderId' ) continue;
if ( property === '_version' ) continue;
if ( value === null || mtlValue === null || mtlValue === undefined ) {
// a property was assigned or removed since the last observation so a new snapshot is required
if ( value !== ( mtlValue === undefined ? null : mtlValue ) ) {
if ( mtlValue === null || mtlValue === undefined ) {
materialData[ property ] = null;
} else if ( mtlValue.isTexture === true ) {
materialData[ property ] = { id: mtlValue.id, version: mtlValue.version, cacheVersion: this.getTextureData( mtlValue )._version, transform: this.getTextureTransformData( mtlValue ) };
} else if ( typeof mtlValue === 'object' && mtlValue.clone !== undefined ) {
materialData[ property ] = mtlValue.clone();
} else {
materialData[ property ] = mtlValue;
}
changed = true;
}
} else if ( value.equals !== undefined ) {
if ( value.equals( mtlValue ) === false ) {
value.copy( mtlValue );
changed = true;
}
} else if ( mtlValue.isTexture === true ) {
const textureData = this.getTextureData( mtlValue );
if ( value.id !== mtlValue.id || value.version !== mtlValue.version || value.cacheVersion !== textureData._version || this.equalsTextureTransform( value.transform, mtlValue ) === false ) {
value.id = mtlValue.id;
value.version = mtlValue.version;
value.cacheVersion = textureData._version;
value.transform.offset.copy( mtlValue.offset );
value.transform.repeat.copy( mtlValue.repeat );
value.transform.center.copy( mtlValue.center );
value.transform.rotation = mtlValue.rotation;
value.transform.matrix.copy( mtlValue.matrix );
changed = true;
}
} else if ( value !== mtlValue ) {
materialData[ property ] = mtlValue;
changed = true;
}
}
if ( changed === true ) materialData._version ++;
}
// a version mismatch means the material has changed since this render object was last refreshed
if ( renderObjectData.materialVersion !== materialData._version ) {
renderObjectData.materialVersion = materialData._version;
return false;
}
const outputTargetData = this.getOutputTargetData( renderObjectData.outputTarget, renderObject.context );
// a version mismatch means the output target has been resized since this render object was last refreshed
if ( renderObjectData.outputTargetVersion !== outputTargetData._version ) {
renderObjectData.outputTargetVersion = outputTargetData._version;
return false;
}
// geometry
if ( renderObjectData.geometryId !== geometry.id ) {
renderObjectData.geometryId = geometry.id;
renderObjectData.geometryVersion = this.getGeometryData( geometry )._version;
return false;
}
const geometryData = this.getGeometryData( renderObject.geometry );
// check the geometry properties just once per render for all render objects
if ( geometryData._renderId !== renderId ) {
geometryData._renderId = renderId;
let changed = false;
// attributes
const attributes = geometry.attributes;
const storedAttributes = geometryData.attributes;
let currentAttributeCount = 0;
let storedAttributeCount = 0;
for ( const _ in attributes ) currentAttributeCount ++; // eslint-disable-line no-unused-vars
for ( const name in storedAttributes ) {
storedAttributeCount ++;
const storedAttributeData = storedAttributes[ name ];
const attribute = attributes[ name ];
if ( attribute === undefined ) {
// attribute was removed
delete storedAttributes[ name ];
changed = true;
continue;
}
const id = attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id;
const version = attribute.isInterleavedBufferAttribute ? attribute.data.version : attribute.version;
if ( storedAttributeData.id !== id || storedAttributeData.version !== version ) {
storedAttributeData.id = id;
storedAttributeData.version = version;
changed = true;
}
}
if ( storedAttributeCount !== currentAttributeCount ) {
geometryData.attributes = this.getAttributesData( attributes );
changed = true;
}
// check index
const index = geometry.index;
const currentIndexId = index ? index.id : null;
const currentIndexVersion = index ? index.version : null;
if ( geometryData.indexId !== currentIndexId || geometryData.indexVersion !== currentIndexVersion ) {
geometryData.indexId = currentIndexId;
geometryData.indexVersion = currentIndexVersion;
changed = true;
}
// check drawRange
if ( geometryData.drawRange.start !== geometry.drawRange.start || geometryData.drawRange.count !== geometry.drawRange.count ) {
geometryData.drawRange.start = geometry.drawRange.start;
geometryData.drawRange.count = geometry.drawRange.count;
changed = true;
}
if ( changed === true ) geometryData._version ++;
}
// a version mismatch means the geometry has changed since this render object was last refreshed
if ( renderObjectData.geometryVersion !== geometryData._version ) {
renderObjectData.geometryVersion = geometryData._version;
return false;
}
// morph targets
if ( renderObjectData.morphTargetInfluences ) {
let morphChanged = false;
for ( let i = 0; i < renderObjectData.morphTargetInfluences.length; i ++ ) {
if ( renderObjectData.morphTargetInfluences[ i ] !== object.morphTargetInfluences[ i ] ) {
renderObjectData.morphTargetInfluences[ i ] = object.morphTargetInfluences[ i ];
morphChanged = true;
}
}
if ( morphChanged ) return false;
}
// instancing
if ( object.isInstancedMesh === true ) {
const instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null;
const morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null;
if ( renderObjectData.instanceMatrix !== object.instanceMatrix ||
renderObjectData.instanceMatrixVersion !== object.instanceMatrix.version ||
renderObjectData.instanceColor !== object.instanceColor ||
renderObjectData.instanceColorVersion !== instanceColorVersion ||
renderObjectData.morphTextureVersion !== morphTextureVersion ) {
renderObjectData.instanceMatrix = object.instanceMatrix;
renderObjectData.instanceMatrixVersion = object.instanceMatrix.version;
renderObjectData.instanceColor = object.instanceColor;
renderObjectData.instanceColorVersion = instanceColorVersion;
renderObjectData.morphTextureVersion = morphTextureVersion;
return false;
}
}
// batching
if ( object.isBatchedMesh === true ) {
const colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null;
if ( renderObjectData.matricesTextureVersion !== object._matricesTexture.version ||
renderObjectData.colorsTextureVersion !== colorsTextureVersion ||
renderObjectData.indirectTextureVersion !== object._indirectTexture.version ) {
renderObjectData.matricesTextureVersion = object._matricesTexture.version;
renderObjectData.colorsTextureVersion = colorsTextureVersion;
renderObjectData.indirectTextureVersion = object._indirectTexture.version;
return false;
}
}
// lights
if ( renderObjectData.lights ) {
for ( let i = 0; i < lightsData.length; i ++ ) {
const lightData = renderObjectData.lights[ i ];
const currentLightData = lightsData[ i ];
if ( lightData.map !== currentLightData.map || lightData.cacheVersion !== currentLightData.cacheVersion ||
lightData.shadowMapWidth !== currentLightData.shadowMapWidth || lightData.shadowMapHeight !== currentLightData.shadowMapHeight ) {
lightData.map = currentLightData.map;
lightData.cacheVersion = currentLightData.cacheVersion;
lightData.shadowMapWidth = currentLightData.shadowMapWidth;
lightData.shadowMapHeight = currentLightData.shadowMapHeight;
return false;
}
}
}
// scene
const scene = renderObject.scene;
if ( scene.environment !== null && material.envMap === null ) {
const environmentVersion = this.getTextureData( scene.environment )._version;
if ( renderObjectData.environmentIntensity !== scene.environmentIntensity ||
renderObjectData.environmentRotation.equals( scene.environmentRotation ) === false ||
renderObjectData.environmentVersion !== environmentVersion ) {
renderObjectData.environmentIntensity = scene.environmentIntensity;
renderObjectData.environmentRotation.copy( scene.environmentRotation );
renderObjectData.environmentVersion = environmentVersion;
return false;
}
}
// center
if ( renderObjectData.center ) {
if ( renderObjectData.center.equals( object.center ) === false ) {
renderObjectData.center.copy( object.center );
return false;
}
}
// bundle
if ( renderObject.bundle !== null ) {
renderObjectData.version = renderObject.bundle.version;
}
return true;
}
/**
* Returns the lights data for the given material lights.
*
* @param {Array<Light>} materialLights - The material lights.
* @return {Array<Object>} The lights data for the given material lights.
*/
getLightsData( materialLights, lights ) {
lights.length = 0;
for ( const light of materialLights ) {
let data = null;
if ( light.isSpotLight === true && light.map !== null ) {
// only add lights that have a map
data = { map: light.map.version, cacheVersion: this.getTextureData( light.map )._version };
}
if ( light.castShadow === true && light.shadow !== undefined ) {
// resizing a shadow map recreates its textures so the bindings
// of all related render objects must be updated
if ( data === null ) data = {};
data.shadowMapWidth = light.shadow.mapSize.width;
data.shadowMapHeight = light.shadow.mapSize.height;
}
if ( data !== null ) lights.push( data );
}
return lights;
}
/**
* Returns the lights for the given lights node and render ID.
*
* @param {LightsNode} lightsNode - The lights node.
* @param {number} renderId - The render ID.
* @return {Array<Object>} The lights for the given lights node and render ID.
*/
getLights( lightsNode, renderId ) {
let cached = _lightsCache.get( lightsNode );
if ( cached === undefined ) {
cached = { renderId: -1, lightsData: [] };
_lightsCache.set( lightsNode, cached );
}
if ( cached.renderId === renderId ) {
return cached.lightsData;
}
cached.renderId = renderId;
this.getLightsData( lightsNode.getBuiltinLights(), cached.lightsData );
return cached.lightsData;
}
/**
* Returns `true` if a vertex buffer or the index of the given render object
* has been disposed since the last check.
*
* @param {RenderObject} renderObject - The render object.
* @param {Object} renderObjectData - The cached render object data.
* @return {boolean} Whether attributes of the render object have been disposed or not.
*/
hasDisposedAttributes( renderObject, renderObjectData ) {
const attributes = renderObject._geometries.attributes;
for ( const vertexBuffer of renderObject.getVertexBuffers() ) {
if ( attributes.has( vertexBuffer ) === false ) return true;
}
const index = renderObject.getIndex();
if ( index !== null && attributes.has( index ) === false ) return true;
renderObjectData.attributesDisposeVersion = attributes.disposeVersion;
return false;
}
/**
* Checks if the given render object requires a refresh.
*
* @param {RenderObject} renderObject - The render object.
* @param {NodeFrame} nodeFrame - The current node frame.
* @return {number} The refresh type, see {@link RenderObjectRefreshType}.
*/
needsRefresh( renderObject, nodeFrame ) {
if ( this.hasNode || this.hasAnimation || this.hasDynamicInstancing( renderObject.object ) || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
return RenderObjectRefreshType.FULL;
const { renderId } = nodeFrame;
let refreshType = RenderObjectRefreshType.NONE;
// shared UBOs are potentially never updated when objects don't change. Below block
// make sure these UBOs are updated at least once.
if ( this.renderId !== renderId ) {
this.renderId = renderId;
// no early out here. instead, use the equals() code path below so the internal cache state gets synched
refreshType = RenderObjectRefreshType.SHARED;
}
// bindings might refer to per-target resources so a target switch needs a full refresh
const renderObjectData = this.getRenderObjectData( renderObject );
const outputTarget = this.getOutputTarget( nodeFrame.renderer );
if ( renderObjectData.outputTarget !== outputTarget ) {
renderObjectData.outputTarget = outputTarget;
refreshType = RenderObjectRefreshType.FULL;
}
const attributes = renderObject._geometries.attributes;
// re-upload disposed attributes. checked before the static early out since other geometries can destroy shared buffers
if ( renderObjectData.attributesDisposeVersion !== attributes.disposeVersion ) {
if ( this.hasDisposedAttributes( renderObject, renderObjectData ) ) refreshType = RenderObjectRefreshType.FULL;
}
const isStatic = renderObject.object.static === true;
const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && renderObjectData.version === renderObject.bundle.version;
if ( isStatic || isBundle )
return refreshType;
const lightsData = this.getLights( renderObject.lightsNode, renderId );
if ( this.equals( renderObject, lightsData, renderId ) === false ) {
refreshType = RenderObjectRefreshType.FULL;
}
return refreshType;
}
}
// See: https://casual-effects.com/research/Wyman2017Hashed/index.html
const ALPHA_HASH_SCALE = 0.05; // Derived from trials only, and may be changed.
const hash2D = /*@__PURE__*/ Fn( ( [ value ] ) => {
return fract( mul( 1.0e4, sin( mul( 17.0, value.x ).add( mul( 0.1, value.y ) ) ) ).mul( add( 0.1, abs( sin( mul( 13.0, value.y ).add( value.x ) ) ) ) ) );
}, { value: 'vec2', return: 'float' } );
const hash3D = /*@__PURE__*/ Fn( ( [ value ] ) => {
return hash2D( vec2( hash2D( value.xy ), value.z ) );
}, { value: 'vec3', return: 'float' } );
const getAlphaHashThreshold = /*@__PURE__*/ Fn( ( [ position ] ) => {
// Find the discretized derivatives of our coordinates
const maxDeriv = max(
length( dFdx( position.xyz ) ),
length( dFdy( position.xyz ) )
);
const pixScale = float( 1 ).div( float( ALPHA_HASH_SCALE ).mul( maxDeriv ) ).toVar( 'pixScale' );
// Find two nearest log-discretized noise scales
const pixScales = vec2(
exp2( floor( log2( pixScale ) ) ),
exp2( ceil( log2( pixScale ) ) )
);
// Compute alpha thresholds at our two noise scales
const alpha = vec2(
hash3D( floor( pixScales.x.mul( position.xyz ) ) ),
hash3D( floor( pixScales.y.mul( position.xyz ) ) ),
);
// Factor to interpolate lerp with
const lerpFactor = fract( log2( pixScale ) );
// Interpolate alpha threshold from noise at two scales
const x = add( mul( lerpFactor.oneMinus(), alpha.x ), mul( lerpFactor, alpha.y ) );
// Pass into CDF to compute uniformly distrib threshold
const a = min( lerpFactor, lerpFactor.oneMinus() );
const cases = vec3(
x.mul( x ).div( mul( 2.0, a ).mul( sub( 1.0, a ) ) ),
x.sub( mul( 0.5, a ) ).div( sub( 1.0, a ) ),
sub( 1.0, sub( 1.0, x ).mul( sub( 1.0, x ) ).div( mul( 2.0, a ).mul( sub( 1.0, a ) ) ) ) );
// Find our final, uniformly distributed alpha threshold (ατ)
const threshold = x.lessThan( a.oneMinus() ).select( x.lessThan( a ).select( cases.x, cases.y ), cases.z );
// Avoids ατ == 0. Could also do ατ =1-ατ
return clamp( threshold, 1.0e-6, 1.0 );
}, { position: 'vec3', return: 'float' } );
/**
* An attribute node for representing vertex colors.
*
* @augments AttributeNode
*/
class VertexColorNode extends AttributeNode {
static get type() {
return 'VertexColorNode';
}
/**
* Constructs a new vertex color node.
*
* @param {number} index - The attribute index.
*/
constructor( index ) {
super( null, 'vec4' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVertexColorNode = true;
/**
* The attribute index to enable more than one sets of vertex colors.
*
* @type {number}
* @default 0
*/
this.index = index;
}
/**
* Overwrites the default implementation by honoring the attribute index.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The attribute name.
*/
getAttributeName( /*builder*/ ) {
const index = this.index;
return 'color' + ( index > 0 ? index : '' );
}
generate( builder ) {
const attributeName = this.getAttributeName( builder );
const geometryAttribute = builder.hasGeometryAttribute( attributeName );
let result;
if ( geometryAttribute === true ) {
result = super.generate( builder );
} else {
// Vertex color fallback should be white
result = builder.generateConst( this.nodeType, new Vector4( 1, 1, 1, 1 ) );
}
return result;
}
serialize( data ) {
super.serialize( data );
data.index = this.index;
}
deserialize( data ) {
super.deserialize( data );
this.index = data.index;
}
}
/**
* TSL function for creating a reference node.
*
* @tsl
* @function
* @param {number} [index=0] - The attribute index.
* @returns {VertexColorNode}
*/
const vertexColor = ( index = 0 ) => new VertexColorNode( index );
/**
* Base class for all node materials.
*
* @augments Material
*/
class NodeMaterial extends Material {
static get type() {
return 'NodeMaterial';
}
/**
* Represents the type of the node material.
*
* @type {string}
*/
get type() {
return this.constructor.type;
}
set type( _value ) { /* */ }
/**
* Constructs a new node material.
*/
constructor() {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeMaterial = true;
/**
* Whether this material is affected by fog or not.
*
* @type {boolean}
* @default true
*/
this.fog = true;
/**
* Whether this material is affected by lights or not.
*
* @type {boolean}
* @default false
*/
this.lights = false;
/**
* Node materials which set their `lights` property to `true`
* are affected by all lights of the scene. Sometimes selective
* lighting is wanted which means only _some_ lights in the scene
* affect a material. This can be achieved by creating an instance
* of {@link LightsNode} with a list of selective
* lights and assign the node to this property.
*
* ```js
* const customLightsNode = lights( [ light1, light2 ] );
* material.lightsNode = customLightsNode;
* ```
*
* @type {?LightsNode}
* @default null
*/
this.lightsNode = null;
/**
* The environment of node materials can be defined by an environment
* map assigned to the `envMap` property or by `Scene.environment`
* if the node material is a PBR material. This node property allows to overwrite
* the default behavior and define the environment with a custom node.
*
* ```js
* material.envNode = pmremTexture( renderTarget.texture );
* ```
*
* @type {?Node<vec3>}
* @default null
*/
this.envNode = null;
/**
* The lighting of node materials might be influenced by ambient occlusion.
* The default AO is inferred from an ambient occlusion map assigned to `aoMap`
* and the respective `aoMapIntensity`. This node property allows to overwrite
* the default and define the ambient occlusion with a custom node instead.
*
* If you don't want to overwrite the diffuse color but modify the existing
* values instead, use {@link materialAO}.
*
* @type {?Node<float>}
* @default null
*/
this.aoNode = null;
/**
* The diffuse color of node materials is by default inferred from the
* `color` and `map` properties. This node property allows to overwrite the default
* and define the diffuse color with a node instead.
*
* ```js
* material.colorNode = color( 0xff0000 ); // define red color
* ```
*
* If you don't want to overwrite the diffuse color but modify the existing
* values instead, use {@link materialColor}.
*
* ```js
* material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint
* ```
*
* @type {?Node<vec3>}
* @default null
*/
this.colorNode = null;
/**
* The normals of node materials are by default inferred from the `normalMap`/`normalScale`
* or `bumpMap`/`bumpScale` properties. This node property allows to overwrite the default
* and define the normals with a node instead.
*
* If you don't want to overwrite the normals but modify the existing values instead,
* use {@link materialNormal}.
*
* @type {?Node<vec3>}
* @default null
*/
this.normalNode = null;
/**
* The opacity of node materials is by default inferred from the `opacity`
* and `alphaMap` properties. This node property allows to overwrite the default
* and define the opacity with a node instead.
*
* If you don't want to overwrite the opacity but modify the existing
* value instead, use {@link materialOpacity}.
*
* @type {?Node<float>}
* @default null
*/
this.opacityNode = null;
/**
* This node can be used to implement a variety of filter-like effects. The idea is
* to store the current rendering into a texture e.g. via `viewportSharedTexture()`, use it
* to create an arbitrary effect and then assign the node composition to this property.
* Everything behind the object using this material will now be affected by a filter.
*
* ```js
* const material = new NodeMaterial()
* material.transparent = true;
*
* // everything behind the object will be monochromatic
* material.backdropNode = saturation( viewportSharedTexture().rgb, 0 );
* ```
*
* Backdrop computations are part of the lighting so only lit materials can use this property.
*
* @type {?Node<vec3>}
* @default null
*/
this.backdropNode = null;
/**
* This node allows to modulate the influence of `backdropNode` to the outgoing light.
*
* @type {?Node<float>}
* @default null
*/
this.backdropAlphaNode = null;
/**
* The alpha test of node materials is by default inferred from the `alphaTest`
* property. This node property allows to overwrite the default and define the
* alpha test with a node instead.
*
* If you don't want to overwrite the alpha test but modify the existing
* value instead, use {@link materialAlphaTest}.
*
* @type {?Node<float>}
* @default null
*/
this.alphaTestNode = null;
/**
* Discards the fragment if the mask value is `false`.
*
* @type {?Node<bool>}
* @default null
*/
this.maskNode = null;
/**
* This node can be used to implement a shadow mask for the material.
*
* @type {?Node<bool>}
* @default null
*/
this.maskShadowNode = null;
/**
* The local vertex positions are computed based on multiple factors like the
* attribute data, morphing, skinning, instancing or batching. This node property
* allows to overwrite the default and define local vertex positions with nodes instead.
*
* If you don't want to overwrite the vertex positions but modify the existing
* values instead, use {@link positionLocal}.
*
*```js
* material.positionNode = positionLocal.add( displace );
* ```
*
* @type {?Node<vec3>}
* @default null
*/
this.positionNode = null;
/**
* This node property is intended for logic which modifies geometry data once or per animation step.
* Apps usually place such logic randomly in initialization routines or in the animation loop.
* `geometryNode` is intended as a dedicated API so there is an intended spot where geometry modifications
* can be implemented.
*
* The idea is to assign a `Fn` definition that holds the geometry modification logic. A typical example
* would be a GPU based particle system that provides a node material for usage on app level. The particle
* simulation would be implemented as compute shaders and managed inside a `Fn` function. This function is
* eventually assigned to `geometryNode`.
*
* @type {?Function}
* @default null
*/
this.geometryNode = null;
/**
* Allows to overwrite depth values in the fragment shader.
*
* @type {?Node<float>}
* @default null
*/
this.depthNode = null;
/**
* Allows to overwrite the position used for shadow map rendering which
* is by default {@link positionWorld}, the vertex position
* in world space.
*
* @type {?Node<float>}
* @default null
*/
this.receivedShadowPositionNode = null;
/**
* Allows to overwrite the geometry position used for shadow map projection which
* is by default {@link positionLocal}, the vertex position in local space.
*
* @type {?Node<float>}
* @default null
*/
this.castShadowPositionNode = null;
/**
* This node can be used to influence how an object using this node material
* receive shadows.
*
* ```js
* const totalShadows = float( 1 ).toVar();
* material.receivedShadowNode = Fn( ( [ shadow ] ) => {
* totalShadows.mulAssign( shadow );
* //return float( 1 ); // bypass received shadows
* return shadow.mix( color( 0xff0000 ), 1 ); // modify shadow color
* } );
*
* @type {?(Function|FunctionNode<vec4>)}
* @default null
*/
this.receivedShadowNode = null;
/**
* This node can be used to influence how an object using this node material
* casts shadows. To apply a color to shadows, you can simply do:
*
* ```js
* material.castShadowNode = vec4( 1, 0, 0, 1 );
* ```
*
* Which can be nice to fake colored shadows of semi-transparent objects. It
* is also common to use the property with `Fn` function so checks are performed
* per fragment.
*
* ```js
* materialCustomShadow.castShadowNode = Fn( () => {
* hash( vertexIndex ).greaterThan( 0.5 ).discard();
* return materialColor;
* } )();
* ```
*
* @type {?Node<vec4>}
* @default null
*/
this.castShadowNode = null;
/**
* This node can be used to overwrite the final output of the material.
*
* Unlike {@link NodeMaterial#fragmentNode}, the built-in material logic
* (diffuse color, lighting, etc.) is still evaluated; assigning a node
* only replaces the resulting output color.
*
* @type {?Node<vec4>}
* @default null
*/
this.outputNode = null;
/**
* MRT configuration is done on renderer or pass level. This node allows to
* overwrite what values are written into MRT targets on material level. This
* can be useful for implementing selective FX features that should only affect
* specific objects.
*
* @type {?MRTNode}
* @default null
*/
this.mrtNode = null;
/**
* This node property can be used if you need complete freedom in implementing
* the fragment shader. Assigning a node will replace the built-in material
* logic used in the fragment stage.
*
* @type {?Node<vec4>}
* @default null
*/
this.fragmentNode = null;
/**
* This node property can be used if you need complete freedom in implementing
* the vertex shader. Assigning a node will replace the built-in material logic
* used in the vertex stage.
*
* @type {?Node<vec4>}
* @default null
*/
this.vertexNode = null;
/**
* This node can be used as a global context management component for this material.
*
* @type {?ContextNode}
* @default null
*/
this.contextNode = null;
}
/**
* Returns an array of child nodes for this material.
*
* @private
* @returns {Array<{property: string, childNode: Node}>}
*/
_getNodeChildren() {
const children = [];
for ( const property of Object.getOwnPropertyNames( this ) ) {
if ( property.startsWith( '_' ) === true ) continue;
const object = this[ property ];
if ( object && object.isNode === true ) {
children.push( { property, childNode: object } );
}
}
return children;
}
/**
* Allows to define a custom cache key that influence the material key computation
* for render objects.
*
* @return {string} The custom cache key.
*/
customProgramCacheKey() {
const values = [];
for ( const { property, childNode } of this._getNodeChildren() ) {
values.push( hashString( property.slice( 0, -4 ) ), childNode.getCacheKey( true ) );
}
return this.type + hashArray( values );
}
/**
* Builds this material with the given node builder.
*
* @param {NodeBuilder} builder - The current node builder.
*/
build( builder ) {
this.setup( builder );
}
/**
* Setups a node material observer with the given builder.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {NodeMaterialObserver} The node material observer.
*/
setupObserver( builder ) {
return new NodeMaterialObserver( builder );
}
/**
* Setups the vertex and fragment stage of this node material.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup( builder ) {
builder.context.setupNormal = () => subBuild( this.setupNormal( builder ), 'NORMAL', 'vec3' );
builder.context.setupPositionView = () => this.setupPositionView( builder );
builder.context.setupModelViewProjection = () => this.setupModelViewProjection( builder );
const renderer = builder.renderer;
const renderTarget = renderer.getRenderTarget();
// < VERTEX STAGE >
builder.addStack();
const mvp = this.setupVertex( builder );
const vertexNode = subBuild( this.vertexNode || mvp, 'VERTEX' );
builder.context.clipSpace = vertexNode;
builder.stack.outputNode = vertexNode;
this.setupHardwareClipping( builder );
if ( this.geometryNode !== null ) {
builder.stack.outputNode = builder.stack.outputNode.bypass( this.geometryNode );
}
builder.addFlow( 'vertex', builder.removeStack() );
// < FRAGMENT STAGE >
builder.addStack();
let resultNode;
const clippingNode = this.setupClipping( builder );
if ( this.depthWrite === true || this.depthTest === true ) {
// only write depth if depth buffer is configured
if ( renderTarget !== null ) {
if ( renderTarget.depthBuffer === true ) this.setupDepth( builder );
} else {
if ( renderer.depth === true ) this.setupDepth( builder );
}
}
if ( this.fragmentNode === null ) {
this.setupDiffuseColor( builder );
this.setupAmbientOcclusion( builder );
this.setupVariants( builder );
const outgoingLightNode = this.setupLighting( builder );
if ( clippingNode !== null ) builder.stack.addToStack( clippingNode );
// force unsigned floats - useful for RenderTargets
const basicOutput = vec4( outgoingLightNode, diffuseColor.a ).max( 0 );
resultNode = this.setupOutput( builder, basicOutput );
// OUTPUT NODE
output.assign( resultNode );
//
const isCustomOutput = this.outputNode !== null;
if ( isCustomOutput ) resultNode = this.outputNode;
//
if ( builder.context.getOutput ) {
resultNode = builder.context.getOutput( resultNode, builder );
}
// MRT
if ( renderTarget !== null ) {
const mrt = renderer.getMRT();
const materialMRT = this.mrtNode;
if ( mrt !== null ) {
if ( isCustomOutput ) output.assign( resultNode );
resultNode = mrt;
if ( materialMRT !== null ) {
resultNode = mrt.merge( materialMRT );
}
} else if ( materialMRT !== null ) {
resultNode = materialMRT;
}
}
} else {
let fragmentNode = this.fragmentNode;
if ( fragmentNode.isOutputStructNode !== true ) {
fragmentNode = fragmentNode.convert( builder.getOutputType() );
}
resultNode = this.setupOutput( builder, fragmentNode );
}
builder.stack.outputNode = resultNode;
builder.addFlow( 'fragment', builder.removeStack() );
// < OBSERVER >
builder.observer = this.setupObserver( builder );
}
/**
* Setups the clipping node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {ClippingNode} The clipping node.
*/
setupClipping( builder ) {
if ( builder.clippingContext === null ) return null;
const { unionPlanes, intersectionPlanes } = builder.clippingContext;
let result = null;
if ( unionPlanes.length > 0 || intersectionPlanes.length > 0 ) {
const samples = builder.renderer.currentSamples;
if ( this.alphaToCoverage && samples > 1 ) {
// to be added to flow when the color/alpha value has been determined
result = clippingAlpha();
} else {
builder.stack.addToStack( clipping() );
}
}
return result;
}
/**
* Setups the hardware clipping if available on the current device.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupHardwareClipping( builder ) {
builder.hardwareClipping = false;
if ( builder.clippingContext === null ) return;
const candidateCount = builder.clippingContext.unionPlanes.length;
// 8 planes supported by WebGL ANGLE_clip_cull_distance and WebGPU clip-distances
if ( candidateCount > 0 && candidateCount <= 8 && builder.isAvailable( 'clipDistance' ) ) {
builder.stack.addToStack( hardwareClipping() );
builder.hardwareClipping = true;
}
return;
}
/**
* Setups the depth of this material.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupDepth( builder ) {
const { renderer, camera } = builder;
// Depth
let depthNode = this.depthNode;
if ( depthNode === null ) {
const mrt = renderer.getMRT();
if ( mrt && mrt.has( 'depth' ) ) {
depthNode = mrt.get( 'depth' );
} else if ( renderer.logarithmicDepthBuffer === true ) {
if ( camera.isOrthographicCamera !== true ) {
depthNode = viewZToLogarithmicDepth( positionView.z, cameraNear, cameraFar );
} else {
depthNode = viewZToOrthographicDepth( positionView.z, cameraNear, cameraFar );
}
}
}
if ( depthNode !== null ) {
depth.assign( depthNode ).toStack();
}
}
/**
* Setups the position node in view space. This method exists
* so derived node materials can modify the implementation e.g. sprite materials.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The position in view space.
*/
setupPositionView( /*builder*/ ) {
return modelViewMatrix.mul( positionLocal ).xyz;
}
/**
* Setups the position in clip space.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec4>} The position in view space.
*/
setupModelViewProjection( /*builder*/ ) {
return cameraProjectionMatrix.mul( positionView );
}
/**
* Setups the logic for the vertex stage.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec4>} The position in clip space.
*/
setupVertex( builder ) {
builder.addStack();
this.setupPosition( builder );
builder.context.position = builder.removeStack();
return modelViewProjection;
}
/**
* Setups the computation of the position in local space.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The position in local space.
*/
setupPosition( builder ) {
const { object, geometry } = builder;
if ( geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color ) {
morphReference( object );
}
if ( object.isSkinnedMesh === true ) {
skinning( object );
}
if ( this.displacementMap ) {
const displacementMap = materialReference( 'displacementMap', 'texture' );
const displacementScale = materialReference( 'displacementScale', 'float' );
const displacementBias = materialReference( 'displacementBias', 'float' );
positionLocal.addAssign( normalLocal.normalize().mul( ( displacementMap.x.mul( displacementScale ).add( displacementBias ) ) ) );
}
if ( object.isBatchedMesh ) {
batch( object );
}
if ( ( object.isInstancedMesh && object.instanceMatrix && object.instanceMatrix.isInstancedBufferAttribute === true ) ) {
instancedMesh( object );
}
if ( this.positionNode !== null ) {
positionLocal.assign( subBuild( this.positionNode, 'POSITION', 'vec3' ) );
}
return positionLocal;
}
/**
* Setups the computation of the material's diffuse color.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupDiffuseColor( builder ) {
const { object, geometry } = builder;
// MASK
if ( this.maskNode !== null ) {
// Discard if the mask is `false`
bool( this.maskNode ).not().discard();
}
// COLOR
let colorNode = this.colorNode ? vec4( this.colorNode ) : materialColor;
// VERTEX COLORS
let vertexColorNode = null;
if ( this.vertexColors === true && geometry.hasAttribute( 'color' ) ) {
vertexColorNode = vertexColor();
}
// INSTANCED COLORS
if ( object.instanceColor ) {
vertexColorNode = vertexColorNode !== null ? vertexColorNode.mul( instanceColor ) : instanceColor;
}
if ( object.isBatchedMesh && object._colorsTexture ) {
vertexColorNode = vertexColorNode !== null ? vertexColorNode.mul( batchColor ) : batchColor;
}
// MORPHED COLORS
if ( this.vertexColors === true && geometry.morphAttributes.color !== undefined ) {
vertexColorNode = morphColor( vec4( vertexColorNode !== null ? vertexColorNode : 1 ) );
}
if ( vertexColorNode !== null ) {
colorNode = colorNode.mul( vertexColorNode );
}
// DIFFUSE COLOR
diffuseColor.assign( colorNode );
// OPACITY
const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
diffuseColor.a.assign( diffuseColor.a.mul( opacityNode ) );
// ALPHA TEST
let alphaTestNode = null;
if ( this.alphaTestNode !== null || this.alphaTest > 0 ) {
alphaTestNode = this.alphaTestNode !== null ? float( this.alphaTestNode ) : materialAlphaTest;
if ( this.alphaToCoverage === true ) {
diffuseColor.a = smoothstep( alphaTestNode, alphaTestNode.add( fwidth( diffuseColor.a ) ), diffuseColor.a );
diffuseColor.a.lessThanEqual( 0 ).discard();
} else {
diffuseColor.a.lessThanEqual( alphaTestNode ).discard();
}
}
// ALPHA HASH
if ( this.alphaHash === true ) {
diffuseColor.a.lessThan( getAlphaHashThreshold( positionLocal ) ).discard();
}
// OPAQUE
if ( builder.isOpaque() ) {
diffuseColor.a.assign( 1.0 );
}
}
/**
* Abstract interface method that can be implemented by derived materials
* to setup material-specific node variables.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants( /*builder*/ ) {
// Interface function.
}
/**
* Setups the outgoing light node variable
*
* @return {Node<vec3>} The outgoing light node.
*/
setupOutgoingLight() {
return ( this.lights === true ) ? vec3( 0 ) : diffuseColor.rgb;
}
/**
* Setups the normal node from the material.
*
* @return {Node<vec3>} The normal node.
*/
setupNormal() {
return this.normalNode ? vec3( this.normalNode ) : materialNormal;
}
/**
* Setups the environment node from the material.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec4>} The environment node.
*/
setupEnvironment( /*builder*/ ) {
let node = null;
if ( this.envNode ) {
node = this.envNode;
} else if ( this.envMap ) {
node = this.envMap.isCubeTexture ? materialReference( 'envMap', 'cubeTexture' ) : materialReference( 'envMap', 'texture' );
}
return node;
}
/**
* Setups the light map node from the material.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The light map node.
*/
setupLightMap( builder ) {
let node = null;
if ( builder.material.lightMap ) {
node = new IrradianceNode( materialLightMap );
}
return node;
}
/**
* Setups the lights node based on the scene, environment and material.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {LightingNode<Array>} The lights node.
*/
setupMaterialLightings( builder ) {
const materialLightsNode = [];
const envNode = this.setupEnvironment( builder );
if ( envNode && envNode.isLightingNode ) {
materialLightsNode.push( envNode );
}
const lightMapNode = this.setupLightMap( builder );
if ( lightMapNode && lightMapNode.isLightingNode ) {
materialLightsNode.push( lightMapNode );
}
if ( builder.context.ambientOcclusion ) {
materialLightsNode.push( new AONode( builder.context.ambientOcclusion ) );
}
if ( builder.context.getGI ) {
const giNode = builder.context.getGI( null, builder );
if ( giNode !== null ) {
materialLightsNode.push( new IrradianceNode( giNode ) );
}
}
return materialLightsNode;
}
/**
* Setups the ambient occlusion node from the material.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The ambient occlusion node.
*/
setupAmbientOcclusion( builder ) {
let aoNode = this.aoNode;
if ( aoNode === null && builder.material.aoMap ) {
aoNode = materialAO;
}
if ( builder.context.getAO ) {
aoNode = builder.context.getAO( aoNode, builder );
}
if ( aoNode !== null ) {
ambientOcclusion.assign( aoNode );
builder.context.ambientOcclusion = ambientOcclusion;
}
}
/**
* This method should be implemented by most derived materials
* since it defines the material's lighting model.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
* @return {LightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
// Interface function.
}
/**
* Setups the outgoing light node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The outgoing light node.
*/
setupLighting( builder ) {
const { material } = builder;
const { backdropNode, backdropAlphaNode, emissiveNode } = this;
// OUTGOING LIGHT
const sceneLighting = this.lights === true && builder.renderer.lighting.enabled;
const lights = sceneLighting || this.lightsNode !== null;
const materialLightings = sceneLighting ? this.setupMaterialLightings( builder ) : [];
const lightsNode = lights ? ( this.lightsNode || builder.lightsNode ) : null;
let outgoingLightNode = this.setupOutgoingLight( builder );
if ( lightsNode && ( materialLightings.length > 0 || lightsNode.getScope().hasLights ) ) {
const lightingModel = this.setupLightingModel( builder ) || null;
outgoingLightNode = lightingContext( lightsNode, lightingModel, materialLightings, backdropNode, backdropAlphaNode );
} else if ( backdropNode !== null ) {
outgoingLightNode = vec3( backdropAlphaNode !== null ? mix( outgoingLightNode, backdropNode, backdropAlphaNode ) : backdropNode );
}
// EMISSIVE
if ( ( emissiveNode && emissiveNode.isNode === true ) || ( material.emissive && material.emissive.isColor === true ) ) {
emissive.assign( vec3( emissiveNode ? emissiveNode : materialEmissive ) );
outgoingLightNode = outgoingLightNode.add( emissive );
}
return outgoingLightNode;
}
/**
* Setup the fog.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node<vec4>} outputNode - The existing output node.
* @return {Node<vec4>} The output node.
*/
setupFog( builder, outputNode ) {
const fogNode = builder.fogNode;
if ( fogNode ) {
output.assign( outputNode );
outputNode = vec4( fogNode.toVar() );
}
return outputNode;
}
/**
* Setups premultiplied alpha.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node<vec4>} outputNode - The existing output node.
* @return {Node<vec4>} The output node.
*/
setupPremultipliedAlpha( builder, outputNode ) {
return premultiplyAlpha( outputNode );
}
/**
* Setups the output node.
*
* This method can be implemented by derived materials to extend the functionality
* of the material's output or replace it altogether.
*
* ```js
* class ColoredShadowMaterial extends MeshPhongNodeMaterial {
* constructor( parameters ) {
* super( parameters );
* this._shadeColor = uniform( new Color( parameters.shadeColor ?? 0xff0000 ) );
* }
*
* setupOutput( builder, outputNode ) {
* // Modify the native output of the MeshPhongNodeMaterial fragment shader
* const brightness = min( outputNode.r, 1.0 );
* const mixedColor = mix( this._shadeColor, diffuseColor.rgb, brightness );
* // Return new output back into NodeMaterial flow
* return super.setupOutput( builder, vec4( mixedColor, outputNode.a ) );
* }
* }
* ```
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node<vec4>} outputNode - The existing output node.
* @return {Node<vec4>} The output node.
*/
setupOutput( builder, outputNode ) {
// FOG
if ( this.fog === true ) {
outputNode = this.setupFog( builder, outputNode );
}
// PREMULTIPLIED ALPHA
if ( this.premultipliedAlpha === true ) {
outputNode = this.setupPremultipliedAlpha( builder, outputNode );
}
return outputNode;
}
/**
* Most classic material types have a node pendant e.g. for `MeshBasicMaterial`
* there is `MeshBasicNodeMaterial`. This utility method is intended for
* defining all material properties of the classic type in the node type.
*
* @param {Material} material - The material to copy properties with their values to this node material.
*/
setDefaultValues( material ) {
// This approach is to reuse the native refreshUniforms*
// and turn available the use of features like transmission and environment in core
for ( const property in material ) {
const value = material[ property ];
if ( this[ property ] === undefined ) {
this[ property ] = value;
if ( value && value.clone ) this[ property ] = value.clone();
}
}
const descriptors = Object.getOwnPropertyDescriptors( material.constructor.prototype );
for ( const key in descriptors ) {
if ( Object.getOwnPropertyDescriptor( this.constructor.prototype, key ) === undefined && descriptors[ key ].get !== undefined ) {
Object.defineProperty( this.constructor.prototype, key, descriptors[ key ] );
}
}
}
/**
* Serializes this material to JSON.
*
* @param {?(Object|string)} meta - The meta information for serialization.
* @return {Object} The serialized node.
*/
toJSON( meta ) {
const isRoot = ( meta === undefined || typeof meta === 'string' );
if ( isRoot ) {
meta = {
textures: {},
images: {},
nodes: {}
};
}
const data = Material.prototype.toJSON.call( this, meta );
data.inputNodes = {};
for ( const { property, childNode } of this._getNodeChildren() ) {
data.inputNodes[ property ] = childNode.toJSON( meta ).uuid;
}
// 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;
}
/**
* Copies the common properties of the given material to this instance.
*
* @param {Material} source - The material to copy.
* @return {NodeMaterial} A reference to this node material.
*/
copy( source ) {
for ( let prototype = Object.getPrototypeOf( this ); prototype !== Object.prototype; prototype = Object.getPrototypeOf( prototype ) ) {
const descriptors = Object.getOwnPropertyDescriptors( prototype );
for ( const property in descriptors ) {
if ( descriptors[ property ].set !== undefined && source[ property ] !== undefined ) {
const value = source[ property ];
if ( this[ property ] && this[ property ].copy !== undefined ) {
this[ property ].copy( value );
} else {
this[ property ] = value;
}
}
}
}
for ( const property in this ) {
// Skip internal/private properties (starting with '_'), flags (starting with 'is' + uppercase),
// and properties that are handled separately or should not be copied (id, uuid, version, type, userData, clippingPlanes).
if ( /^(?:is[A-Z]|_)|^(?:id|uuid|version|type|userData|clippingPlanes)$/.test( property ) ) continue;
if ( this[ property ] !== undefined && source[ property ] !== undefined ) {
const value = source[ property ];
if ( this[ property ] && this[ property ].copy !== undefined ) {
this[ property ].copy( value );
} else {
this[ property ] = value;
}
}
}
this.clippingPlanes = source.clippingPlanes ? source.clippingPlanes.map( ( plane ) => plane.clone() ) : null;
this.userData = JSON.parse( JSON.stringify( source.userData ) );
return this;
}
}
const _camera = /*@__PURE__*/ new OrthographicCamera( -1, 1, 1, -1, 0, 1 );
/**
* The purpose of this special geometry is to fill the entire viewport with a single triangle.
*
* Reference: {@link https://github.com/mrdoob/three.js/pull/21358}
*
* @private
* @augments BufferGeometry
*/
class QuadGeometry extends BufferGeometry {
/**
* Constructs a new quad geometry.
*
* @param {boolean} [flipY=false] - Whether the uv coordinates should be flipped along the vertical axis or not.
*/
constructor( flipY = false ) {
super();
const uv = flipY === false ? [ 0, -1, 0, 1, 2, 1 ] : [ 0, 2, 0, 0, 2, 0 ];
this.setAttribute( 'position', new Float32BufferAttribute( [ -1, 3, 0, -1, -1, 0, 3, -1, 0 ], 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( uv, 2 ) );
}
}
const _geometry = /*@__PURE__*/ new QuadGeometry();
const _vertexNode = /*@__PURE__*/ vec4(
array( [ -1, -1, 3.0 ] ).element( vertexIndex ),
array( [ 3.0, -1, -1 ] ).element( vertexIndex ),
0.0,
1.0
);
/**
* This module is a helper for passes which need to render a full
* screen effect which is quite common in context of post processing.
*
* The intended usage is to reuse a single quad mesh for rendering
* subsequent passes by just reassigning the `material` reference.
*
* Note: This module can only be used with `WebGPURenderer`.
*
* @augments Mesh
*/
class QuadMesh extends Mesh {
/**
* Constructs a new quad mesh.
*
* @param {NodeMaterial} material - The material to render the quad mesh with.
*/
constructor( material ) {
super( _geometry, material );
/**
* The camera to render the quad mesh with.
*
* @type {OrthographicCamera}
* @readonly
*/
this.camera = _camera;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isQuadMesh = true;
}
/**
* Async version of `render()`.
*
* @async
* @deprecated
* @param {Renderer} renderer - The renderer.
* @return {Promise} A Promise that resolves when the render has been finished.
*/
async renderAsync( renderer ) {
warnOnce( 'QuadMesh: "renderAsync()" has been deprecated. Use "render()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await renderer.init();
renderer.render( this, _camera );
}
/**
* Renders the quad mesh
*
* @param {Renderer} renderer - The renderer.
*/
render( renderer ) {
const previousVertexNode = this.material.vertexNode;
this.material.vertexNode = _vertexNode;
renderer.render( this, _camera );
this.material.vertexNode = previousVertexNode;
}
}
/**
* Saves the state of the given renderer and stores it into the given state object.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function saveRendererState( renderer, state = {} ) {
state.toneMapping = renderer.toneMapping;
state.toneMappingExposure = renderer.toneMappingExposure;
state.outputColorSpace = renderer.outputColorSpace;
state.renderTarget = renderer.getRenderTarget();
state.activeCubeFace = renderer.getActiveCubeFace();
state.activeMipmapLevel = renderer.getActiveMipmapLevel();
state.renderObjectFunction = renderer.getRenderObjectFunction();
state.pixelRatio = renderer.getPixelRatio();
state.mrt = renderer.getMRT();
state.clearColor = renderer.getClearColor( state.clearColor || new Color() );
state.clearAlpha = renderer.getClearAlpha();
state.autoClear = renderer.autoClear;
state.autoClearColor = renderer.autoClearColor;
state.autoClearDepth = renderer.autoClearDepth;
state.autoClearStencil = renderer.autoClearStencil;
state.opaque = renderer.opaque;
state.transparent = renderer.transparent;
state.scissorTest = renderer.getScissorTest();
return state;
}
/**
* Saves the state of the given renderer and stores it into the given state object.
* Besides, the function also resets the state of the renderer to its default values.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function resetRendererState( renderer, state ) {
state = saveRendererState( renderer, state );
renderer.setMRT( null );
renderer.setRenderObjectFunction( null );
renderer.setClearColor( 0x000000, 1 );
renderer.autoClear = true;
renderer.autoClearColor = true;
renderer.autoClearDepth = true;
renderer.autoClearStencil = true;
renderer.opaque = true;
renderer.transparent = true;
return state;
}
/**
* Restores the state of the given renderer from the given state object.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Object} state - The state to restore.
*/
function restoreRendererState( renderer, state ) {
renderer.toneMapping = state.toneMapping;
renderer.toneMappingExposure = state.toneMappingExposure;
renderer.outputColorSpace = state.outputColorSpace;
renderer.setRenderTarget( state.renderTarget, state.activeCubeFace, state.activeMipmapLevel );
renderer.setRenderObjectFunction( state.renderObjectFunction );
renderer.setPixelRatio( state.pixelRatio );
renderer.setMRT( state.mrt );
renderer.setClearColor( state.clearColor, state.clearAlpha );
renderer.autoClear = state.autoClear;
renderer.autoClearColor = state.autoClearColor;
renderer.autoClearDepth = state.autoClearDepth;
renderer.autoClearStencil = state.autoClearStencil;
renderer.opaque = state.opaque;
renderer.transparent = state.transparent;
renderer.setScissorTest( state.scissorTest );
}
/**
* Saves the state of the given scene and stores it into the given state object.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Scene} scene - The scene.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function saveSceneState( scene, state = {} ) {
state.background = scene.background;
state.backgroundNode = scene.backgroundNode;
state.overrideMaterial = scene.overrideMaterial;
return state;
}
/**
* Saves the state of the given scene and stores it into the given state object.
* Besides, the function also resets the state of the scene to its default values.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Scene} scene - The scene.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function resetSceneState( scene, state ) {
state = saveSceneState( scene, state );
scene.background = null;
scene.backgroundNode = null;
scene.overrideMaterial = null;
return state;
}
/**
* Restores the state of the given scene from the given state object.
*
* @private
* @function
* @param {Scene} scene - The scene.
* @param {Object} state - The state to restore.
*/
function restoreSceneState( scene, state ) {
scene.background = state.background;
scene.backgroundNode = state.backgroundNode;
scene.overrideMaterial = state.overrideMaterial;
}
/**
* Saves the state of the given renderer and scene and stores it into the given state object.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Scene} scene - The scene.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function saveRendererAndSceneState( renderer, scene, state = {} ) {
state = saveRendererState( renderer, state );
state = saveSceneState( scene, state );
return state;
}
/**
* Saves the state of the given renderer and scene and stores it into the given state object.
* Besides, the function also resets the state of the renderer and scene to its default values.
*
* If not state object is provided, the function creates one.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Scene} scene - The scene.
* @param {Object} [state={}] - The state.
* @return {Object} The state.
*/
function resetRendererAndSceneState( renderer, scene, state ) {
state = resetRendererState( renderer, state );
state = resetSceneState( scene, state );
return state;
}
/**
* Restores the state of the given renderer and scene from the given state object.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {Scene} scene - The scene.
* @param {Object} state - The state to restore.
*/
function restoreRendererAndSceneState( renderer, scene, state ) {
restoreRendererState( renderer, state );
restoreSceneState( scene, state );
}
var RendererUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
resetRendererAndSceneState: resetRendererAndSceneState,
resetRendererState: resetRendererState,
resetSceneState: resetSceneState,
restoreRendererAndSceneState: restoreRendererAndSceneState,
restoreRendererState: restoreRendererState,
restoreSceneState: restoreSceneState,
saveRendererAndSceneState: saveRendererAndSceneState,
saveRendererState: saveRendererState,
saveSceneState: saveSceneState
});
const _size$1 = /*@__PURE__*/ new Vector2();
/**
* `RTTNode` takes another node and uses it with a `QuadMesh` to render into a texture (RTT).
* This module is especially relevant in context of post processing where certain nodes require
* texture input for their effects. With the helper function `convertToTexture()` which is based
* on this module, the node system can automatically ensure texture input if required.
*
* @augments TextureNode
*/
class RTTNode extends TextureNode {
static get type() {
return 'RTTNode';
}
/**
* Constructs a new RTT node.
*
* @param {Node} node - The node to render a texture with.
* @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized.
* @param {?number} [height=null] - The height of the internal render target.
* @param {Object} [options={}] - The options for the internal render target.
* @param {number} [options.type=HalfFloatType] - The texture type.
* @param {boolean} [options.autoUpdate=true] - Whether the texture should automatically be updated or not.
* @param {number} [options.resolutionScale=1] - The resolution scale.
*/
constructor( node, width = null, height = null, options = {} ) {
const {
autoUpdate = true,
resolutionScale = 1
} = options;
const renderTarget = new RenderTarget( width ?? 1, height ?? 1, { type: HalfFloatType, ...options } );
super( renderTarget.texture, uv$1() );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRTTNode = true;
/**
* The node to render a texture with.
*
* @type {Node}
*/
this.node = node;
/**
* The width of the internal render target.
* If not width is applied, the render target is automatically resized.
*
* @type {?number}
* @default null
*/
this.width = width;
/**
* The height of the internal render target.
*
* @type {?number}
* @default null
*/
this.height = height;
/**
* The render target
*
* @type {RenderTarget}
*/
this.renderTarget = renderTarget;
/**
* Whether the texture requires an update or not.
*
* @type {boolean}
* @default true
*/
this.textureNeedsUpdate = true;
/**
* Whether the texture should automatically be updated or not.
*
* @type {boolean}
* @default true
*/
this.autoUpdate = autoUpdate;
/**
* The resolution scale
*
* @private
* @type {number}
* @default 1
*/
this._resolutionScale = resolutionScale;
/**
* The internal quad mesh for RTT.
*
* @private
* @type {QuadMesh}
*/
this._quadMesh = new QuadMesh( new NodeMaterial() );
/**
* The renderer state saved and restored around the RTT render.
* Kept per instance because nested RTT nodes must not share it.
*
* @private
* @type {Object}
*/
this._rendererState = {};
/**
* The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates
* the texture once per frame in its {@link RTTNode#updateBefore} method.
*
* @type {string}
* @default 'frame'
*/
this.updateBeforeType = NodeUpdateType.FRAME;
}
/**
* Whether the internal render target should automatically be resized or not.
*
* @type {boolean}
* @readonly
* @default true
*/
get autoResize() {
return this.width === null;
}
setup( builder ) {
this._quadMesh.material.contextNode = context( builder.getSharedContext() );
this._quadMesh.material.fragmentNode = this.node;
this._quadMesh.material.name = 'RTT';
this._quadMesh.material.needsUpdate = true;
return super.setup( builder );
}
/**
* Sets the size of the internal render target.
*
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( width, height ) {
const effectiveWidth = Math.floor( width * this._resolutionScale );
const effectiveHeight = Math.floor( height * this._resolutionScale );
this.renderTarget.setSize( effectiveWidth, effectiveHeight );
this.textureNeedsUpdate = true;
}
/**
* Sets the resolution scale.
* The resolution scale is a factor that is multiplied with the renderer's width and height.
*
* @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
* @returns {RTTNode} A reference to this node.
*/
setResolutionScale( resolutionScale ) {
this._resolutionScale = resolutionScale;
if ( this.autoResize === false ) {
this.setSize( this.width, this.height );
}
return this;
}
/**
* Gets the resolution scale.
*
* @returns {number} The resolution scale.
*/
getResolutionScale() {
return this._resolutionScale;
}
/**
* Overwritten since the value is defined by the internal render target.
*
* @param {Texture} value - The texture value.
*/
set value( value ) {
if ( this.renderTarget && value !== this.renderTarget.texture ) {
error( 'TSL: "rtt()" does not allow overwriting the value.' );
}
}
/**
* The texture of the internal render target.
*
* @type {Texture}
*/
get value() {
return this.renderTarget ? this.renderTarget.texture : null;
}
/**
* Renders the node's output into the internal render target before the main render pass.
* Handles automatic resizing of the render target when `autoResize` is enabled,
* and skips rendering if neither `textureNeedsUpdate` nor `autoUpdate` is true.
*
* @param {NodeFrame} frame - The current node frame, providing access to the renderer and other frame data.
*/
updateBefore( frame ) {
const { renderer } = frame;
if ( this.textureNeedsUpdate === false && this.autoUpdate === false ) return;
this.textureNeedsUpdate = false;
//
if ( this.autoResize === true ) {
const size = renderer.getDrawingBufferSize( _size$1 );
const effectiveWidth = Math.floor( size.width * this._resolutionScale );
const effectiveHeight = Math.floor( size.height * this._resolutionScale );
if ( effectiveWidth !== this.renderTarget.width || effectiveHeight !== this.renderTarget.height ) {
this.renderTarget.setSize( effectiveWidth, effectiveHeight );
this.textureNeedsUpdate = true;
}
}
//
let name = 'RTT';
const callName = this.name || this.node.name;
if ( callName ) {
name = callName + ' [ ' + name + ' ]';
}
this._quadMesh.name = name;
//
resetRendererState( renderer, this._rendererState );
renderer.setClearColor( this._rendererState.clearColor, this._rendererState.clearAlpha ); // keep clear color/alpha
renderer.setRenderTarget( this.renderTarget );
this._quadMesh.render( renderer );
restoreRendererState( renderer, this._rendererState );
}
clone() {
const newNode = new TextureNode( this.value, this.uvNode, this.levelNode );
newNode.sampler = this.sampler;
newNode.referenceNode = this;
return newNode;
}
/**
* Frees internal resources. Should be called when the node is no longer in use.
*/
dispose() {
this.renderTarget.dispose();
this._quadMesh.material.dispose();
super.dispose();
}
}
/**
* TSL function for creating a RTT node.
*
* @tsl
* @function
* @param {Node} node - The node to render a texture with.
* @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized.
* @param {?number} [height=null] - The height of the internal render target.
* @param {Object} [options={}] - The options for the internal render target.
* @param {number} [options.type=HalfFloatType] - The texture type.
* @param {boolean} [options.autoUpdate=true] - Whether the texture should automatically be updated or not.
* @param {number} [options.resolutionScale=1] - The resolution scale.
* @returns {RTTNode}
*/
const rtt = ( node, ...params ) => new RTTNode( nodeObject( node ), ...params );
/**
* TSL function for converting nodes to textures nodes.
*
* @tsl
* @function
* @param {Node} node - The node to render a texture with.
* @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized.
* @param {?number} [height=null] - The height of the internal render target.
* @param {Object} [options={}] - The options for the internal render target.
* @param {number} [options.type=HalfFloatType] - The texture type.
* @param {boolean} [options.autoUpdate=true] - Whether the texture should automatically be updated or not.
* @param {number} [options.resolutionScale=1] - The resolution scale.
* @returns {RTTNode}
*/
const convertToTexture = ( node, ...params ) => {
if ( node.isSampleNode || node.isTextureNode ) return node;
if ( node.isPassNode ) return node.getTextureNode();
return rtt( node, ...params );
};
/**
* Returns `true` if the given depth value belongs to the background, i.e. the cleared
* far plane. Takes the renderer's reversed depth buffer into account.
*
* @tsl
* @function
* @param {Node<float>} depth - The depth value.
* @return {Node<bool>} Whether the depth value belongs to the background.
*/
const isBackgroundDepth = /*@__PURE__*/ Fn( ( [ depth ], builder ) => {
return builder.renderer.reversedDepthBuffer === true ? depth.lessThanEqual( 0.0 ) : depth.greaterThanEqual( 1.0 );
} );
/**
* Computes a position in view space based on a fragment's screen position expressed as uv coordinates, the fragments
* depth value and the camera's inverse projection matrix.
*
* @tsl
* @function
* @param {Node<vec2>} screenPosition - The fragment's screen position expressed as uv coordinates.
* @param {Node<float>} depth - The fragment's depth value.
* @param {Node<mat4>} projectionMatrixInverse - The camera's inverse projection matrix.
* @return {Node<vec3>} The fragments position in view space.
*/
const getViewPosition = /*@__PURE__*/ Fn( ( [ screenPosition, depth, projectionMatrixInverse ], builder ) => {
let clipSpacePosition;
if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem || builder.renderer.reversedDepthBuffer === true ) {
screenPosition = vec2( screenPosition.x, screenPosition.y.oneMinus() ).mul( 2.0 ).sub( 1.0 );
clipSpacePosition = vec4( vec3( screenPosition, depth ), 1.0 );
} else {
clipSpacePosition = vec4( vec3( screenPosition.x, screenPosition.y.oneMinus(), depth ).mul( 2.0 ).sub( 1.0 ), 1.0 );
}
const viewSpacePosition = vec4( projectionMatrixInverse.mul( clipSpacePosition ) );
return viewSpacePosition.xyz.div( viewSpacePosition.w );
} );
/**
* Computes a screen position expressed as uv coordinates based on a fragment's position in view space
* and the camera's projection matrix
*
* @tsl
* @function
* @param {Node<vec3>} viewPosition - The fragments position in view space.
* @param {Node<mat4>} projectionMatrix - The camera's projection matrix.
* @return {Node<vec2>} The fragment's screen position expressed as uv coordinates.
*/
const getScreenPosition = /*@__PURE__*/ Fn( ( [ viewPosition, projectionMatrix ] ) => {
const sampleClipPos = projectionMatrix.mul( vec4( viewPosition, 1.0 ) );
const sampleUv = sampleClipPos.xy.div( sampleClipPos.w ).mul( 0.5 ).add( 0.5 ).toVar();
return vec2( sampleUv.x, sampleUv.y.oneMinus() );
}, { viewPosition: 'vec3', projectionMatrix: 'mat4', return: 'vec2' } );
/**
* Converts a clip-space position into a screen position expressed as uv coordinates.
*
* @tsl
* @function
* @param {Node<vec4>} clipPosition - The position in clip space.
* @return {Node<vec2>} The screen position expressed as uv coordinates.
*/
const getScreenPositionFromClip = /*@__PURE__*/ Fn( ( [ clipPosition ] ) => {
const screen = clipPosition.xy.div( clipPosition.w ).mul( 0.5 ).add( 0.5 ).toVar();
return vec2( screen.x, screen.y.oneMinus() );
}, { clipPosition: 'vec4', return: 'vec2' } );
/**
* Computes a normal vector based on depth data. Can be used as a fallback when no normal render
* target is available or if flat surface normals are required.
*
* @tsl
* @function
* @param {Node<vec2>} uv - The texture coordinate.
* @param {DepthTexture} depthTexture - The depth texture.
* @param {Node<mat4>} projectionMatrixInverse - The camera's inverse projection matrix.
* @return {Node<vec3>} The computed normal vector.
*/
const getNormalFromDepth = /*@__PURE__*/ Fn( ( [ uv, depthTexture, projectionMatrixInverse ] ) => {
const size = textureSize( textureLoad( depthTexture ) );
const p = ivec2( uv.mul( size ) ).toVar();
const c0 = textureLoad( depthTexture, p ).toVar();
const l2 = textureLoad( depthTexture, p.sub( ivec2( 2, 0 ) ) ).toVar();
const l1 = textureLoad( depthTexture, p.sub( ivec2( 1, 0 ) ) ).toVar();
const r1 = textureLoad( depthTexture, p.add( ivec2( 1, 0 ) ) ).toVar();
const r2 = textureLoad( depthTexture, p.add( ivec2( 2, 0 ) ) ).toVar();
const b2 = textureLoad( depthTexture, p.add( ivec2( 0, 2 ) ) ).toVar();
const b1 = textureLoad( depthTexture, p.add( ivec2( 0, 1 ) ) ).toVar();
const t1 = textureLoad( depthTexture, p.sub( ivec2( 0, 1 ) ) ).toVar();
const t2 = textureLoad( depthTexture, p.sub( ivec2( 0, 2 ) ) ).toVar();
const dl = abs( sub( float( 2 ).mul( l1 ).sub( l2 ), c0 ) ).toVar();
const dr = abs( sub( float( 2 ).mul( r1 ).sub( r2 ), c0 ) ).toVar();
const db = abs( sub( float( 2 ).mul( b1 ).sub( b2 ), c0 ) ).toVar();
const dt = abs( sub( float( 2 ).mul( t1 ).sub( t2 ), c0 ) ).toVar();
const ce = getViewPosition( uv, c0, projectionMatrixInverse ).toVar();
const dpdx = dl.lessThan( dr ).select( ce.sub( getViewPosition( uv.sub( vec2( float( 1 ).div( size.x ), 0 ) ), l1, projectionMatrixInverse ) ), ce.negate().add( getViewPosition( uv.add( vec2( float( 1 ).div( size.x ), 0 ) ), r1, projectionMatrixInverse ) ) );
const dpdy = db.lessThan( dt ).select( ce.sub( getViewPosition( uv.add( vec2( 0, float( 1 ).div( size.y ) ) ), b1, projectionMatrixInverse ) ), ce.negate().add( getViewPosition( uv.sub( vec2( 0, float( 1 ).div( size.y ) ) ), t1, projectionMatrixInverse ) ) );
return normalize( cross( dpdx, dpdy ) );
} );
/**
* Interleaved Gradient Noise (IGN) from Jimenez 2014.
*
* IGN has "low discrepancy" resulting in evenly distributed samples. It's superior compared to
* default white noise, blue noise or Bayer.
*
* References:
* - {@link https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/}
* - {@link https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/}
*
* @tsl
* @function
* @param {Node<vec2>} position - The input position, usually screen coordinates.
* @return {Node<float>} The noise value.
*/
const interleavedGradientNoise = /*@__PURE__*/ Fn( ( [ position ] ) => {
return fract( float( 52.9829189 ).mul( fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) ) );
}, { position: 'vec2', return: 'float' } );
/**
* Vogel disk sampling for uniform circular distribution.
*
* This function generates sample points distributed uniformly on a disk using the golden angle,
* resulting in an efficient low-discrepancy sequence for sampling. The rotation parameter (phi)
* allows randomizing the pattern per-pixel when combined with IGN.
*
* @tsl
* @function
* @param {Node<int>} sampleIndex - The index of the current sample (0-based).
* @param {Node<int>} samplesCount - The total number of samples.
* @param {Node<float>} phi - Rotation angle in radians (typically from IGN * 2π).
* @return {Node<vec2>} A 2D point on the unit disk.
*/
const vogelDiskSample = /*@__PURE__*/ Fn( ( [ sampleIndex, samplesCount, phi ] ) => {
const goldenAngle = float( 2.399963229728653 ); // 2π * (2 - φ) where φ is golden ratio
const r = sqrt( float( sampleIndex ).add( 0.5 ).div( float( samplesCount ) ) );
const theta = float( sampleIndex ).mul( goldenAngle ).add( phi );
return vec2( cos( theta ), sin( theta ) ).mul( r );
}, { sampleIndex: 'int', samplesCount: 'int', phi: 'float', return: 'vec2' } );
/**
* Class representing a node that samples a value using a provided callback function.
*
* @extends Node
*/
class SampleNode extends Node {
/**
* Returns the type of the node.
*
* @type {string}
* @readonly
* @static
*/
static get type() {
return 'SampleNode';
}
/**
* Creates an instance of SampleNode.
*
* @param {Function} callback - The function to be called when sampling. Should accept a UV node and return a value.
* @param {?Node<vec2>} [uvNode=null] - The UV node to be used in the texture sampling.
*/
constructor( callback, uvNode = null ) {
super();
this.callback = callback;
/**
* Represents the texture coordinates.
*
* @type {?Node<vec2|vec3>}
* @default null
*/
this.uvNode = uvNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampleNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets up the node by sampling with the default UV accessor.
*
* @returns {Node} The result of the callback function when called with the UV node.
*/
setup() {
return this.sample( uv$1() );
}
/**
* Calls the callback function with the provided UV node.
*
* @param {Node<vec2>} uv - The UV node or value to be passed to the callback.
* @returns {Node} The result of the callback function.
*/
sample( uv ) {
return this.callback( uv );
}
}
/**
* Helper function to create a SampleNode wrapped as a node object.
*
* @function
* @param {Function} callback - The function to be called when sampling. Should accept a UV node and return a value.
* @param {?Node<vec2>} [uv=null] - The UV node to be used in the texture sampling.
* @returns {SampleNode} The created SampleNode instance wrapped as a node object.
*/
const sample = ( callback, uv = null ) => new SampleNode( callback, nodeObject( uv ) );
/**
* This special type of instanced buffer attribute is intended for compute shaders.
* In earlier three.js versions it was only possible to update attribute data
* on the CPU via JavaScript and then upload the data to the GPU. With the
* new material system and renderer it is now possible to use compute shaders
* to compute the data for an attribute more efficiently on the GPU.
*
* The idea is to create an instance of this class and provide it as an input
* to {@link StorageBufferNode}.
*
* Note: This type of buffer attribute can only be used with `WebGPURenderer`.
*
* @augments InstancedBufferAttribute
*/
class StorageInstancedBufferAttribute extends InstancedBufferAttribute {
/**
* Constructs a new storage instanced buffer attribute.
*
* @param {number|TypedArray} count - The item count. It is also valid to pass a typed array as an argument.
* The subsequent parameters are then obsolete.
* @param {number} itemSize - The item size.
* @param {TypedArray.constructor} [typeClass=Float32Array] - A typed array constructor.
*/
constructor( count, itemSize, typeClass = Float32Array ) {
const array = ArrayBuffer.isView( count ) ? count : new typeClass( count * itemSize );
super( array, itemSize );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageInstancedBufferAttribute = true;
}
}
/**
* This special type of buffer attribute is intended for compute shaders.
* In earlier three.js versions it was only possible to update attribute data
* on the CPU via JavaScript and then upload the data to the GPU. With the
* new material system and renderer it is now possible to use compute shaders
* to compute the data for an attribute more efficiently on the GPU.
*
* The idea is to create an instance of this class and provide it as an input
* to {@link StorageBufferNode}.
*
* Note: This type of buffer attribute can only be used with `WebGPURenderer`.
*
* @augments BufferAttribute
*/
class StorageBufferAttribute extends BufferAttribute {
/**
* Constructs a new storage buffer attribute.
*
* @param {number|TypedArray} count - The item count. It is also valid to pass a typed array as an argument.
* The subsequent parameters are then obsolete.
* @param {number} itemSize - The item size.
* @param {TypedArray.constructor} [typeClass=Float32Array] - A typed array constructor.
*/
constructor( count, itemSize, typeClass = Float32Array ) {
const array = ArrayBuffer.isView( count ) ? count : new typeClass( count * itemSize );
super( array, itemSize );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageBufferAttribute = true;
}
}
/**
* TSL function for creating a storage buffer node with a configured `StorageBufferAttribute`.
*
* @tsl
* @function
* @param {number|TypedArray} count - The data count. It is also valid to pass a typed array as an argument.
* @param {string|Struct} [type='float'] - The data type.
* @returns {StorageBufferNode}
*/
const attributeArray = ( count, type = 'float' ) => {
let itemSize, typedArray;
if ( type.isStructTypeNode === true ) {
itemSize = type.getLength();
typedArray = getTypedArrayFromType( 'float' );
} else {
itemSize = getLengthFromType( type );
typedArray = getTypedArrayFromType( type );
}
const buffer = new StorageBufferAttribute( count, itemSize, typedArray );
const node = storage( buffer, type, buffer.count );
return node;
};
/**
* TSL function for creating a storage buffer node with a configured `StorageInstancedBufferAttribute`.
*
* @tsl
* @function
* @param {number|TypedArray} count - The data count. It is also valid to pass a typed array as an argument.
* @param {string|Struct} [type='float'] - The data type.
* @returns {StorageBufferNode}
*/
const instancedArray = ( count, type = 'float' ) => {
let itemSize, typedArray;
if ( type.isStructTypeNode === true ) {
itemSize = type.getLength();
typedArray = getTypedArrayFromType( 'float' );
} else {
itemSize = getLengthFromType( type );
typedArray = getTypedArrayFromType( type );
}
const buffer = new StorageInstancedBufferAttribute( count, itemSize, typedArray );
const node = storage( buffer, type, buffer.count );
return node;
};
// Mipped Bicubic Texture Filtering by N8
// https://www.shadertoy.com/view/Dl2SDW
const bC = 1.0 / 6.0;
const w0 = ( a ) => mul( bC, mul( a, mul( a, a.negate().add( 3.0 ) ).sub( 3.0 ) ).add( 1.0 ) );
const w1 = ( a ) => mul( bC, mul( a, mul( a, mul( 3.0, a ).sub( 6.0 ) ) ).add( 4.0 ) );
const w2 = ( a ) => mul( bC, mul( a, mul( a, mul( -3, a ).add( 3.0 ) ).add( 3.0 ) ).add( 1.0 ) );
const w3 = ( a ) => mul( bC, pow( a, 3 ) );
const bicubicWeights = ( a ) => {
const w0a = w0( a );
const w1a = w1( a );
const w2a = w2( a );
const w3a = w3( a );
const g0a = w0a.add( w1a );
const g1a = w2a.add( w3a );
// h0 and h1 are the two offset functions.
const h0a = add( -1, w1a.div( g0a ) );
const h1a = add( 1.0, w3a.div( g1a ) );
return { g0: g0a, g1: g1a, h0: h0a, h1: h1a };
};
const bicubic = ( textureNode, p0, p3, g0, g1, lod ) => {
const p1 = vec2( p3.x, p0.y );
const p2 = vec2( p0.x, p3.y );
const a = g0.y.mul( add( g0.x.mul( textureNode.sample( p0 ).level( lod ) ), g1.x.mul( textureNode.sample( p1 ).level( lod ) ) ) );
const b = g1.y.mul( add( g0.x.mul( textureNode.sample( p2 ).level( lod ) ), g1.x.mul( textureNode.sample( p3 ).level( lod ) ) ) );
return a.add( b );
};
/**
* Applies mipped bicubic texture filtering to the given texture node.
*
* @tsl
* @function
* @param {TextureNode} textureNode - The texture node that should be filtered.
* @param {Node<float>} lodNode - Defines the LOD to sample from.
* @return {Node} The filtered texture sample.
*/
const textureBicubicLevel = /*@__PURE__*/ Fn( ( [ textureNode, lodNode ] ) => {
const fLodSize = vec2( textureNode.size( int( lodNode ) ) );
const cLodSize = vec2( textureNode.size( int( lodNode.add( 1.0 ) ) ) );
const lodSize = vec4( fLodSize, cLodSize );
const lodSizeInv = div( 1.0, lodSize );
const uvScaled = textureNode.uvNode.xyxy.mul( lodSize ).add( 0.5 );
const iuv = floor( uvScaled );
const fuv = fract( uvScaled );
const { g0, g1, h0, h1 } = bicubicWeights( fuv );
const p0 = iuv.add( h0 ).sub( 0.5 ).mul( lodSizeInv );
const p3 = iuv.add( h1 ).sub( 0.5 ).mul( lodSizeInv );
const fSample = bicubic( textureNode, p0.xy, p3.xy, g0.xy, g1.xy, floor( lodNode ) );
const cSample = bicubic( textureNode, p0.zw, p3.zw, g0.zw, g1.zw, ceil( lodNode ) );
return fract( lodNode ).mix( fSample, cSample );
} );
/**
* Applies mipped bicubic texture filtering to the given texture node.
*
* @tsl
* @function
* @param {TextureNode} textureNode - The texture node that should be filtered.
* @param {Node<float>} [strength] - Defines the strength of the bicubic filtering.
* @return {Node} The filtered texture sample.
*/
const textureBicubic = /*@__PURE__*/ Fn( ( [ textureNode, strength ] ) => {
const lod = strength.mul( maxMipLevel( textureNode ) );
return textureBicubicLevel( textureNode, lod );
} );
/**
* A node for representing the uv coordinates of points.
*
* Can only be used with a WebGL backend. In WebGPU, point
* primitives always have the size of one pixel and can thus
* can't be used as sprite-like objects that display textures.
*
* @augments Node
*/
class PointUVNode extends Node {
static get type() {
return 'PointUVNode';
}
/**
* Constructs a new point uv node.
*/
constructor() {
super( 'vec2' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPointUVNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
generate( /*builder*/ ) {
return 'vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y )';
}
}
/**
* TSL object that represents the uv coordinates of points.
*
* @tsl
* @type {PointUVNode}
*/
const pointUV = /*@__PURE__*/ nodeImmutable( PointUVNode );
const _m1 = /*@__PURE__*/ new Matrix4();
/**
* TSL object that represents the scene's background blurriness.
*
* @tsl
* @type {Node<float>}
*/
const backgroundBlurriness = /*@__PURE__*/ uniform( 0 ).setGroup( renderGroup ).onRenderUpdate( ( { scene } ) => scene.backgroundBlurriness );
/**
* TSL object that represents the scene's background intensity.
*
* @tsl
* @type {Node<float>}
*/
const backgroundIntensity = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( ( { scene } ) => scene.backgroundIntensity );
/**
* TSL object that represents the scene's background rotation.
*
* @tsl
* @type {Node<mat4>}
*/
const backgroundRotation = /*@__PURE__*/ uniform( new Matrix4() ).setGroup( renderGroup ).onRenderUpdate( ( { scene } ) => {
const background = scene.background;
if ( ( background !== null && background.isTexture && background.mapping !== UVMapping ) || ( scene.backgroundNode && scene.backgroundNode.isNode ) ) {
// note: since the matrix is orthonormal, we can use the more-efficient transpose() in lieu of invert()
_m1.makeRotationFromEuler( scene.backgroundRotation ).transpose();
} else {
_m1.identity();
}
return _m1;
} );
/**
* This special version of a texture node can be used to
* write data into a storage texture with a compute shader.
*
* ```js
* const storageTexture = new THREE.StorageTexture( width, height );
*
* const computeTexture = Fn( ( { storageTexture } ) => {
*
* const posX = instanceIndex.mod( width );
* const posY = instanceIndex.div( width );
* const indexUV = uvec2( posX, posY );
*
* // generate RGB values
*
* const r = 1;
* const g = 1;
* const b = 1;
*
* textureStore( storageTexture, indexUV, vec4( r, g, b, 1 ) ).toWriteOnly();
*
* } );
*
* const computeNode = computeTexture( { storageTexture } ).compute( width * height );
* renderer.computeAsync( computeNode );
* ```
*
* This node can only be used with a WebGPU backend.
*
* @augments TextureNode
*/
class StorageTextureNode extends TextureNode {
static get type() {
return 'StorageTextureNode';
}
/**
* Constructs a new storage texture node.
*
* @param {StorageTexture} value - The storage texture.
* @param {Node<vec2|vec3>} uvNode - The uv node.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
*/
constructor( value, uvNode, storeNode = null ) {
super( value, uvNode );
/**
* The value node that should be stored in the texture.
*
* @type {?Node}
* @default null
*/
this.storeNode = storeNode;
/**
* The mip level to write to for storage textures.
*
* @type {number}
* @default 0
*/
this.mipLevel = 0;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageTextureNode = true;
/**
* The access type of the texture node.
*
* @type {string}
* @default 'writeOnly'
*/
this.access = NodeAccess.WRITE_ONLY;
}
/**
* Overwrites the default implementation to return a fixed value `'storageTexture'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return 'storageTexture';
}
/**
* Overwrites the default implementation since storage texture
* coordinates are texel coordinates and should not be transformed
* by the texture uv matrix.
*
* @param {Node} uvNode - The uv node.
* @return {Node} The unmodified uv node.
*/
getTransformedUV( uvNode ) {
return uvNode;
}
setup( builder ) {
super.setup( builder );
const properties = builder.getNodeProperties( this );
properties.storeNode = this.storeNode;
return properties;
}
/**
* Defines the node access.
*
* @param {string} value - The node access.
* @return {StorageTextureNode} A reference to this node.
*/
setAccess( value ) {
this.access = value;
return this;
}
/**
* Sets the mip level to write to.
*
* @param {number} level - The mip level.
* @return {StorageTextureNode} A reference to this node.
*/
setMipLevel( level ) {
this.mipLevel = level;
return this;
}
/**
* Generates the code snippet of the storage node. If no `storeNode`
* is defined, the texture node is generated as normal texture.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} output - The current output.
* @return {string} The generated code snippet.
*/
generate( builder, output ) {
if ( this.storeNode !== null ) {
this.generateStore( builder );
return '';
}
return super.generate( builder, output );
}
/**
* Generates the snippet for the storage texture.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} textureProperty - The texture property.
* @param {string} uvSnippet - The uv snippet.
* @param {?string} levelSnippet - The level snippet.
* @param {?string} biasSnippet - The bias snippet.
* @param {?string} depthSnippet - The depth snippet.
* @param {?string} compareSnippet - The compare snippet.
* @param {?Array<string>} gradSnippet - The grad snippet.
* @param {?string} offsetSnippet - The offset snippet.
* @return {string} The generated code snippet.
*/
generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, depthSnippet, compareSnippet, gradSnippet, offsetSnippet ) {
const texture = this.value;
return builder.generateStorageTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet );
}
/**
* Convenience method for configuring a read/write node access.
*
* @return {StorageTextureNode} A reference to this node.
*/
toReadWrite() {
return this.setAccess( NodeAccess.READ_WRITE );
}
/**
* Convenience method for configuring a read-only node access.
*
* @return {StorageTextureNode} A reference to this node.
*/
toReadOnly() {
return this.setAccess( NodeAccess.READ_ONLY );
}
/**
* Convenience method for configuring a write-only node access.
*
* @return {StorageTextureNode} A reference to this node.
*/
toWriteOnly() {
return this.setAccess( NodeAccess.WRITE_ONLY );
}
/**
* Stores a value in this storage texture at the given coordinates.
*
* @param {Node<vec2|vec3>} uvNode - The storage texture coordinates.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
* @return {StorageTextureNode} A storage texture node representing the store operation.
*/
store( uvNode, storeNode ) {
const node = this.clone();
node.referenceNode = this.getBase();
node.uvNode = uvNode;
node.storeNode = storeNode;
if ( storeNode !== null ) node.toStack();
return node;
}
/**
* Generates the code snippet of the storage texture node.
*
* @param {NodeBuilder} builder - The current node builder.
*/
generateStore( builder ) {
const properties = builder.getNodeProperties( this );
const { uvNode, storeNode, depthNode } = properties;
const textureProperty = super.generate( builder, 'property' );
const uvSnippet = uvNode.build( builder, this.value.is3DTexture === true ? 'uvec3' : 'uvec2' );
const storeType = builder.getTypeFromLength( 4, builder.getComponentTypeFromTexture( this.value ) );
const storeSnippet = storeNode.build( builder, storeType );
const depthSnippet = depthNode ? depthNode.build( builder, 'int' ) : null;
const snippet = builder.generateTextureStore( this.value, textureProperty, uvSnippet, depthSnippet, storeSnippet );
builder.addLineFlowCode( snippet, this );
}
clone() {
const newNode = super.clone();
newNode.storeNode = this.storeNode;
newNode.mipLevel = this.mipLevel;
newNode.access = this.access;
return newNode;
}
}
/**
* TSL function for creating a storage texture node.
*
* @tsl
* @function
* @param {StorageTexture} value - The storage texture.
* @param {?Node<vec2|vec3>} uvNode - The uv node.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
* @returns {StorageTextureNode}
*/
const storageTexture = /*@__PURE__*/ nodeProxy( StorageTextureNode ).setParameterLength( 1, 3 );
/**
* TSL function for storing a value in a storage texture.
*
* Unlike {@link storageTexture}, this function also accepts an existing storage
* texture node and is intended for performing the store operation itself.
*
* @tsl
* @function
* @param {StorageTexture|StorageTextureNode} value - The storage texture or storage texture node.
* @param {Node<vec2|vec3>} uvNode - The uv node.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
* @returns {StorageTextureNode}
*/
const textureStore = ( value, uvNode, storeNode ) => {
let node;
if ( value.isStorageTextureNode === true ) {
node = value.store( uvNode, storeNode );
} else {
node = storageTexture( value, uvNode, storeNode );
if ( storeNode !== null ) node.toStack();
}
return node;
};
/**
* This special version of a texture node can be used to
* write data into a 3D storage texture with a compute shader.
*
* @augments StorageTextureNode
*/
class StorageTexture3DNode extends StorageTextureNode {
static get type() {
return 'StorageTexture3DNode';
}
/**
* Constructs a new 3D storage texture node.
*
* @param {Storage3DTexture} value - The 3D storage texture.
* @param {Node<vec3>} uvNode - The uv node.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
*/
constructor( value, uvNode, storeNode = null ) {
super( value, uvNode, storeNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageTexture3DNode = true;
}
/**
* Returns a default uv node which is in context of 3D textures a three-dimensional
* uv node.
*
* @return {Node<vec3>} The default uv node.
*/
getDefaultUV() {
return vec3( 0.5, 0.5, 0.5 );
}
/**
* Overwritten with an empty implementation since the `updateMatrix` flag is ignored
* for 3D textures. The uv transformation matrix is not applied to 3D textures.
*
* @param {boolean} value - The update toggle.
*/
setUpdateMatrix( /*value*/ ) { } // Ignore .updateMatrix for 3d TextureNode
/**
* Generates the uv code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} uvNode - The uv node to generate code for.
* @return {string} The generated code snippet.
*/
generateUV( builder, uvNode ) {
return uvNode.build( builder, this.sampler === true ? 'vec3' : 'ivec3' );
}
/**
* Generates the offset code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} offsetNode - The offset node to generate code for.
* @return {string} The generated code snippet.
*/
generateOffset( builder, offsetNode ) {
return offsetNode.build( builder, 'ivec3' );
}
}
/**
* TSL function for creating a 3D storage texture node.
*
* @tsl
* @function
* @param {Storage3DTexture} value - The 3D storage texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node} [storeNode=null] - The value node that should be stored in the texture.
* @returns {StorageTexture3DNode}
*/
const storageTexture3D = /*@__PURE__*/ nodeProxy( StorageTexture3DNode ).setParameterLength( 1, 3 );
const normal = /*@__PURE__*/ Fn( ( { texture, uv } ) => {
const epsilon = 0.0001;
const ret = vec3().toVar();
If( uv.x.lessThan( epsilon ), () => {
ret.assign( vec3( 1, 0, 0 ) );
} ).ElseIf( uv.y.lessThan( epsilon ), () => {
ret.assign( vec3( 0, 1, 0 ) );
} ).ElseIf( uv.z.lessThan( epsilon ), () => {
ret.assign( vec3( 0, 0, 1 ) );
} ).ElseIf( uv.x.greaterThan( 1 - epsilon ), () => {
ret.assign( vec3( -1, 0, 0 ) );
} ).ElseIf( uv.y.greaterThan( 1 - epsilon ), () => {
ret.assign( vec3( 0, -1, 0 ) );
} ).ElseIf( uv.z.greaterThan( 1 - epsilon ), () => {
ret.assign( vec3( 0, 0, -1 ) );
} ).Else( () => {
const step = 0.01;
const x = texture.sample( uv.add( vec3( - step, 0.0, 0.0 ) ) ).r.sub( texture.sample( uv.add( vec3( step, 0.0, 0.0 ) ) ).r );
const y = texture.sample( uv.add( vec3( 0.0, - step, 0.0 ) ) ).r.sub( texture.sample( uv.add( vec3( 0.0, step, 0.0 ) ) ).r );
const z = texture.sample( uv.add( vec3( 0.0, 0.0, - step ) ) ).r.sub( texture.sample( uv.add( vec3( 0.0, 0.0, step ) ) ).r );
ret.assign( vec3( x, y, z ) );
} );
return ret.normalize();
} );
/**
* This type of uniform node represents a 3D texture.
*
* @augments TextureNode
*/
class Texture3DNode extends TextureNode {
static get type() {
return 'Texture3DNode';
}
/**
* Constructs a new 3D texture node.
*
* @param {Data3DTexture} value - The 3D texture.
* @param {?Node<vec2|vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
*/
constructor( value, uvNode = null, levelNode = null ) {
super( value, uvNode, levelNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isTexture3DNode = true;
}
/**
* Overwrites the default implementation to return a fixed value `'texture3D'`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return 'texture3D';
}
/**
* Returns a default uv node which is in context of 3D textures a three-dimensional
* uv node.
*
* @return {Node<vec3>} The default uv node.
*/
getDefaultUV() {
return vec3( 0.5, 0.5, 0.5 );
}
/**
* Overwritten with an empty implementation since the `updateMatrix` flag is ignored
* for 3D textures. The uv transformation matrix is not applied to 3D textures.
*
* @param {boolean} value - The update toggle.
*/
setUpdateMatrix( /*value*/ ) { } // Ignore .updateMatrix for 3d TextureNode
/**
* Generates the uv code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} uvNode - The uv node to generate code for.
* @return {string} The generated code snippet.
*/
generateUV( builder, uvNode ) {
return uvNode.build( builder, this.sampler === true ? 'vec3' : 'ivec3' );
}
/**
* Generates the offset code snippet.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node} offsetNode - The offset node to generate code for.
* @return {string} The generated code snippet.
*/
generateOffset( builder, offsetNode ) {
return offsetNode.build( builder, 'ivec3' );
}
/**
* Computes the normal for the given uv. These texture coordiantes represent a
* position inside the 3D texture. Unlike geometric normals, this normal
* represents a slope or gradient of scalar data inside the 3D texture.
*
* @param {Node<vec3>} uvNode - The uv node that defines a position in the 3D texture.
* @return {Node<vec3>} The normal representing the slope/gradient in the data.
*/
normal( uvNode ) {
return normal( { texture: this, uv: uvNode } );
}
}
/**
* TSL function for creating a 3D texture node.
*
* @tsl
* @function
* @param {Data3DTexture} value - The 3D texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @returns {Texture3DNode}
*/
const texture3D = /*@__PURE__*/ nodeProxy( Texture3DNode ).setParameterLength( 1, 3 );
/**
* TSL function for creating a texture node that fetches/loads texels without interpolation.
*
* @tsl
* @function
* @param {?(Texture|TextureNode)} [value=EmptyTexture] - The texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @param {?Node<float>} [biasNode=null] - The bias node.
* @returns {TextureNode}
*/
const texture3DLoad = ( ...params ) => texture3D( ...params ).setSampler( false );
/**
* TSL function for creating a texture node that fetches/loads texels without interpolation.
*
* @tsl
* @function
* @param {?(Texture|TextureNode)} [value=EmptyTexture] - The texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<int>} [levelNode=null] - The level node.
* @returns {TextureNode}
*/
const texture3DLevel = ( value, uvNode, levelNode ) => texture3D( value, uvNode ).level( levelNode );
/**
* A special type of reference node that allows to link values in
* `userData` fields to node objects.
* ```js
* sprite.userData.rotation = 1; // stores individual rotation per sprite
*
* const material = new THREE.SpriteNodeMaterial();
* material.rotationNode = userData( 'rotation', 'float' );
* ```
* Since `UserDataNode` is extended from {@link ReferenceNode}, the node value
* will automatically be updated when the `rotation` user data field changes.
*
* @augments ReferenceNode
*/
class UserDataNode extends ReferenceNode {
static get type() {
return 'UserDataNode';
}
/**
* Constructs a new user data node.
*
* @param {string} property - The property name that should be referenced by the node.
* @param {string} inputType - The node data type of the reference.
* @param {?Object} [userData=null] - A reference to the `userData` object. If not provided, the `userData` property of the 3D object that uses the node material is evaluated.
*/
constructor( property, inputType, userData = null ) {
super( property, inputType, userData );
/**
* A reference to the `userData` object. If not provided, the `userData`
* property of the 3D object that uses the node material is evaluated.
*
* @type {?Object}
* @default null
*/
this.userData = userData;
}
/**
* Overwritten to make sure {@link ReferenceNode#reference} points to the correct
* `userData` field.
*
* @param {(NodeFrame|NodeBuilder)} state - The current state to evaluate.
* @return {Object} A reference to the `userData` field.
*/
updateReference( state ) {
this.reference = this.userData !== null ? this.userData : state.object.userData;
return this.reference;
}
}
/**
* TSL function for creating a user data node.
*
* @tsl
* @function
* @param {string} name - The property name that should be referenced by the node.
* @param {string} inputType - The node data type of the reference.
* @param {?Object} userData - A reference to the `userData` object. If not provided, the `userData` property of the 3D object that uses the node material is evaluated.
* @returns {UserDataNode}
*/
const userData = ( name, inputType, userData ) => new UserDataNode( name, inputType, userData );
const _objectData = new WeakMap();
/**
* A node for representing motion or velocity vectors. Foundation
* for advanced post processing effects like motion blur or TRAA.
*
* The node keeps track of the model, view and projection matrices
* of the previous frame and uses them to compute offsets in NDC space.
* These offsets represent the final velocity.
*
* @augments Node
*/
class VelocityNode extends Node {
static get type() {
return 'VelocityNode';
}
/**
* Constructs a new vertex color node.
*/
constructor() {
super( 'vec2' );
/**
* An optional projection matrix that overrides the camera's projection matrix.
*
* @type {?Matrix4}
* @default null
*/
this.projectionMatrix = null;
/**
* Overwritten since velocity nodes are updated per object.
*
* @type {string}
* @default 'object'
*/
this.updateType = NodeUpdateType.OBJECT;
/**
* Overwritten since velocity nodes save data after the update.
*
* @type {string}
* @default 'object'
*/
this.updateAfterType = NodeUpdateType.OBJECT;
/**
* Uniform node representing the previous model matrix in world space.
*
* @type {UniformNode<mat4>}
* @default null
*/
this.previousModelWorldMatrix = uniform( new Matrix4() );
/**
* The projection matrix of the current frame.
*
* @type {UniformNode<mat4>}
*/
this.currentProjectionMatrix = uniform( new Matrix4() ).setGroup( renderGroup );
/**
* Uniform node representing the previous projection matrix.
*
* @type {UniformNode<mat4>}
* @default null
*/
this.previousProjectionMatrix = uniform( new Matrix4() ).setGroup( renderGroup );
/**
* Uniform node representing the previous view matrix.
*
* @type {UniformNode<mat4>}
* @default null
*/
this.previousCameraViewMatrix = uniform( new Matrix4() ).setGroup( renderGroup );
}
/**
* Sets the given projection matrix.
*
* @param {Matrix4} projectionMatrix - The projection matrix to set.
*/
setProjectionMatrix( projectionMatrix ) {
this.projectionMatrix = projectionMatrix;
}
/**
* Updates velocity specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( { frameId, camera, object } ) {
const previousModelMatrix = getPreviousMatrix( object );
this.previousModelWorldMatrix.value.copy( previousModelMatrix );
//
const cameraData = getData( camera );
if ( cameraData.frameId !== frameId ) {
cameraData.frameId = frameId;
if ( cameraData.previousProjectionMatrix === undefined ) {
cameraData.previousProjectionMatrix = new Matrix4();
cameraData.previousCameraViewMatrix = new Matrix4();
cameraData.currentProjectionMatrix = new Matrix4();
cameraData.currentCameraViewMatrix = new Matrix4();
cameraData.previousProjectionMatrix.copy( this.projectionMatrix || camera.projectionMatrix );
cameraData.previousCameraViewMatrix.copy( camera.matrixWorldInverse );
} else {
cameraData.previousProjectionMatrix.copy( cameraData.currentProjectionMatrix );
cameraData.previousCameraViewMatrix.copy( cameraData.currentCameraViewMatrix );
}
cameraData.currentProjectionMatrix.copy( this.projectionMatrix || camera.projectionMatrix );
cameraData.currentCameraViewMatrix.copy( camera.matrixWorldInverse );
this.previousProjectionMatrix.value.copy( cameraData.previousProjectionMatrix );
this.previousCameraViewMatrix.value.copy( cameraData.previousCameraViewMatrix );
this.currentProjectionMatrix.value.copy( cameraData.currentProjectionMatrix );
}
}
/**
* Overwritten to updated velocity specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
updateAfter( { object } ) {
getPreviousMatrix( object ).copy( object.matrixWorld );
}
/**
* Implements the velocity computation based on the previous and current vertex data.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {Node<vec2>} The motion vector.
*/
setup( /*builder*/ ) {
const previousModelViewMatrix = this.previousCameraViewMatrix.mul( this.previousModelWorldMatrix );
const clipPositionCurrent = this.currentProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal );
const clipPositionPrevious = this.previousProjectionMatrix.mul( previousModelViewMatrix ).mul( positionPrevious );
const ndcPositionCurrent = clipPositionCurrent.xy.div( clipPositionCurrent.w );
const ndcPositionPrevious = clipPositionPrevious.xy.div( clipPositionPrevious.w );
const velocity = sub( ndcPositionCurrent, ndcPositionPrevious );
return velocity;
}
}
function getData( object ) {
let objectData = _objectData.get( object );
if ( objectData === undefined ) {
objectData = {};
_objectData.set( object, objectData );
}
return objectData;
}
function getPreviousMatrix( object, index = 0 ) {
const objectData = getData( object );
let matrix = objectData[ index ];
if ( matrix === undefined ) {
objectData[ index ] = matrix = new Matrix4();
objectData[ index ].copy( object.matrixWorld );
}
return matrix;
}
/**
* TSL object that represents the velocity of a render pass.
*
* @tsl
* @type {VelocityNode}
*/
const velocity = /*@__PURE__*/ nodeImmutable( VelocityNode );
/**
* Represents a "Color Burn" blend mode.
*
* It's designed to darken the base layer's colors based on the color of the blend layer.
* It significantly increases the contrast of the base layer, making the colors more vibrant and saturated.
* The darker the color in the blend layer, the stronger the darkening and contrast effect on the base layer.
*
* @tsl
* @function
* @param {Node<vec3>} base - The base color.
* @param {Node<vec3>} blend - The blend color. A white (#ffffff) blend color does not alter the base color.
* @return {Node<vec3>} The result.
*/
const blendBurn = /*@__PURE__*/ Fn( ( [ base, blend ] ) => {
return min( 1.0, base.oneMinus().div( blend ) ).oneMinus();
}, { base: 'vec3', blend: 'vec3', return: 'vec3' } );
/**
* Represents a "Color Dodge" blend mode.
*
* It's designed to lighten the base layer's colors based on the color of the blend layer.
* It significantly increases the brightness of the base layer, making the colors lighter and more vibrant.
* The brighter the color in the blend layer, the stronger the lightening and contrast effect on the base layer.
*
* @tsl
* @function
* @param {Node<vec3>} base - The base color.
* @param {Node<vec3>} blend - The blend color. A black (#000000) blend color does not alter the base color.
* @return {Node<vec3>} The result.
*/
const blendDodge = /*@__PURE__*/ Fn( ( [ base, blend ] ) => {
return min( base.div( blend.oneMinus() ), 1.0 );
}, { base: 'vec3', blend: 'vec3', return: 'vec3' } );
/**
* Represents a "Screen" blend mode.
*
* Similar to `blendDodge()`, this mode also lightens the base layer's colors based on the color of the blend layer.
* The "Screen" blend mode is better for general brightening whereas the "Dodge" results in more subtle and nuanced
* effects.
*
* @tsl
* @function
* @param {Node<vec3>} base - The base color.
* @param {Node<vec3>} blend - The blend color. A black (#000000) blend color does not alter the base color.
* @return {Node<vec3>} The result.
*/
const blendScreen = /*@__PURE__*/ Fn( ( [ base, blend ] ) => {
return base.oneMinus().mul( blend.oneMinus() ).oneMinus();
}, { base: 'vec3', blend: 'vec3', return: 'vec3' } );
/**
* Represents a "Overlay" blend mode.
*
* It's designed to increase the contrast of the base layer based on the color of the blend layer.
* It amplifies the existing colors and contrast in the base layer, making lighter areas lighter and darker areas darker.
* The color of the blend layer significantly influences the resulting contrast and color shift in the base layer.
*
* @tsl
* @function
* @param {Node<vec3>} base - The base color.
* @param {Node<vec3>} blend - The blend color
* @return {Node<vec3>} The result.
*/
const blendOverlay = /*@__PURE__*/ Fn( ( [ base, blend ] ) => {
return mix( base.mul( 2.0 ).mul( blend ), base.oneMinus().mul( 2.0 ).mul( blend.oneMinus() ).oneMinus(), step( 0.5, base ) );
}, { base: 'vec3', blend: 'vec3', return: 'vec3' } );
/**
* This function blends two color based on their alpha values by replicating the behavior of `THREE.NormalBlending`.
* It assumes both input colors have non-premultiplied alpha.
*
* @tsl
* @function
* @param {Node<vec4>} base - The base color.
* @param {Node<vec4>} blend - The blend color
* @return {Node<vec4>} The result.
*/
const blendColor = /*@__PURE__*/ Fn( ( [ base, blend ] ) => {
const outAlpha = blend.a.add( base.a.mul( blend.a.oneMinus() ) );
return vec4( blend.rgb.mul( blend.a ).add( base.rgb.mul( base.a ).mul( blend.a.oneMinus() ) ).div( outAlpha ), outAlpha );
}, { base: 'vec4', blend: 'vec4', return: 'vec4' } );
/**
* TSL object that represents the luminance coefficients of the current working color space.
* It is evaluated once per build, so it can also be used as the default value of a parameter
* of a function with layout.
*
* @tsl
* @type {VarNode<vec3>}
*/
const workingLuminanceCoefficients = /*@__PURE__*/ Fn( () => vec3( ColorManagement.getLuminanceCoefficients( new Vector3() ) ) ).once()().toVar( 'workingLuminanceCoefficients' );
/**
* Computes a grayscale value for the given RGB color value.
*
* @tsl
* @function
* @param {Node<vec3>} color - The color value to compute the grayscale for.
* @param {Node<vec3>} [luminanceCoefficients] - The luminance coefficients. By default predefined values of the current working color space are used.
* @return {Node<float>} The grayscale value.
*/
const grayscale = /*@__PURE__*/ Fn( ( [ color, luminanceCoefficients = workingLuminanceCoefficients ] ) => {
return luminance( color.rgb, luminanceCoefficients );
}, { color: 'vec3', luminanceCoefficients: 'vec3', return: 'float' } );
/**
* Super-saturates or desaturates the given RGB color.
*
* @tsl
* @function
* @param {Node<vec3>} color - The input color.
* @param {Node<float>} [adjustment=1] - Specifies the amount of the conversion. A value under `1` desaturates the color, a value over `1` super-saturates it.
* @param {Node<vec3>} [luminanceCoefficients] - The luminance coefficients. By default predefined values of the current working color space are used.
* @return {Node<vec3>} The saturated color.
*/
const saturation = /*@__PURE__*/ Fn( ( [ color, adjustment = float( 1 ), luminanceCoefficients = workingLuminanceCoefficients ] ) => {
return adjustment.mix( luminance( color.rgb, luminanceCoefficients ), color.rgb ).max( 0.0 );
}, { color: 'vec3', adjustment: 'float', luminanceCoefficients: 'vec3', return: 'vec3' } );
/**
* Selectively enhance the intensity of less saturated RGB colors. Can result
* in a more natural and visually appealing image with enhanced color depth
* compared to {@link ColorAdjustment#saturation}.
*
* @tsl
* @function
* @param {Node<vec3>} color - The input color.
* @param {Node<float>} [adjustment=0] - Controls the intensity of the vibrance effect.
* @return {Node<vec3>} The updated color.
*/
const vibrance = /*@__PURE__*/ Fn( ( [ color, adjustment = float( 0 ) ] ) => {
const average = add( color.r, color.g, color.b ).div( 3.0 );
const mx = color.r.max( color.g.max( color.b ) );
const amt = mx.sub( average ).mul( adjustment ).mul( -3 );
return mix( color.rgb, mx, amt ).max( 0.0 );
}, { color: 'vec3', adjustment: 'float', return: 'vec3' } );
/**
* Updates the hue component of the given RGB color while preserving its luminance and saturation.
*
* @tsl
* @function
* @param {Node<vec3>} color - The input color.
* @param {Node<float>} [adjustment=1] - Defines the degree of hue rotation in radians. A positive value rotates the hue clockwise, while a negative value rotates it counterclockwise.
* @return {Node<vec3>} The updated color.
*/
const hue = /*@__PURE__*/ Fn( ( [ color, adjustment = float( 1 ) ] ) => {
const k = vec3( 0.57735, 0.57735, 0.57735 );
const cosAngle = adjustment.cos();
return vec3( color.rgb.mul( cosAngle ).add( k.cross( color.rgb ).mul( adjustment.sin() ).add( k.mul( dot( k, color.rgb ).mul( cosAngle.oneMinus() ) ) ) ) ).max( 0.0 );
}, { color: 'vec3', adjustment: 'float', return: 'vec3' } );
/**
* Computes the luminance for the given RGB color value.
*
* @tsl
* @function
* @param {Node<vec3>} color - The color value to compute the luminance for.
* @param {?Node<vec3>} luminanceCoefficients - The luminance coefficients. By default predefined values of the current working color space are used.
* @return {Node<float>} The luminance.
*/
const luminance = (
color,
luminanceCoefficients = vec3( ColorManagement.getLuminanceCoefficients( new Vector3() ) )
) => dot( color, luminanceCoefficients );
/**
* Color Decision List (CDL) v1.2
*
* Compact representation of color grading information, defined by slope, offset, power, and
* saturation. The CDL should be typically be given input in a log space (such as LogC, ACEScc,
* or AgX Log), and will return output in the same space. Output may require clamping >=0.
*
* @tsl
* @function
* @param {Node<vec4>} color Input (-Infinity < input < +Infinity)
* @param {Node<vec3>} slope Slope (0 ≤ slope < +Infinity)
* @param {Node<vec3>} offset Offset (-Infinity < offset < +Infinity; typically -1 < offset < 1)
* @param {Node<vec3>} power Power (0 < power < +Infinity)
* @param {Node<float>} saturation Saturation (0 ≤ saturation < +Infinity; typically 0 ≤ saturation < 4)
* @param {Node<vec3>} luminanceCoefficients Luminance coefficients for saturation term, typically Rec. 709
* @return {Node<vec4>} Output, -Infinity < output < +Infinity
*
* References:
* - ASC CDL v1.2
* - {@link https://blender.stackexchange.com/a/55239/43930}
* - {@link https://docs.acescentral.com/specifications/acescc/}
*/
const cdl = /*@__PURE__*/ Fn( ( [
color,
slope = vec3( 1 ),
offset = vec3( 0 ),
power = vec3( 1 ),
saturation = float( 1 ),
// ASC CDL v1.2 explicitly requires Rec. 709 luminance coefficients.
luminanceCoefficients = vec3( ColorManagement.getLuminanceCoefficients( new Vector3(), LinearSRGBColorSpace ) )
] ) => {
// NOTE: The ASC CDL v1.2 defines a [0, 1] clamp on the slope+offset term, and another on the
// saturation term. Per the ACEScc specification and Filament, limits may be omitted to support
// if negative inputs to the power expression are avoided. We use `max( in, 0.0 )`
// on final output, but the lower limit may not be required in all cases.
const luma = color.rgb.dot( vec3( luminanceCoefficients ) );
const v = max( color.rgb.mul( slope ).add( offset ), 0.0 );
const pv = v.pow( power );
If( v.r.greaterThan( 0.0 ), () => { v.r.assign( pv.r ); } ); // eslint-disable-line
If( v.g.greaterThan( 0.0 ), () => { v.g.assign( pv.g ); } ); // eslint-disable-line
If( v.b.greaterThan( 0.0 ), () => { v.b.assign( pv.b ); } ); // eslint-disable-line
v.assign( luma.add( v.sub( luma ).mul( saturation ) ).max( 0 ) );
return vec4( v.rgb, color.a );
}, { color: 'vec4', slope: 'vec3', offset: 'vec3', power: 'vec3', saturation: 'float', luminanceCoefficients: 'vec3', return: 'vec4' } );
/**
* TSL function for creating a posterize effect which reduces the number of colors
* in an image, resulting in a more blocky and stylized appearance.
*
* @tsl
* @function
* @param {Node} sourceNode - The input color.
* @param {Node} stepsNode - Controls the intensity of the posterization effect. A lower number results in a more blocky appearance.
* @returns {Node} The posterized color.
*/
const posterize = /*@__PURE__*/ Fn( ( [ source, steps ] ) => {
return source.mul( steps ).floor().div( steps );
} );
let _sharedFramebuffer = null;
/**
* `ViewportTextureNode` creates an internal texture for each node instance. This module
* shares a texture across all instances of `ViewportSharedTextureNode`. It should
* be the first choice when using data of the default/screen framebuffer for performance reasons.
*
* @augments ViewportTextureNode
*/
class ViewportSharedTextureNode extends ViewportTextureNode {
static get type() {
return 'ViewportSharedTextureNode';
}
/**
* Constructs a new viewport shared texture node.
*
* @param {Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
*/
constructor( uvNode = screenUV, levelNode = null ) {
if ( _sharedFramebuffer === null ) {
_sharedFramebuffer = new FramebufferTexture();
}
super( uvNode, levelNode, _sharedFramebuffer );
}
/**
* Overwritten so the method always returns the unique shared
* framebuffer texture.
*
* @return {FramebufferTexture} The shared framebuffer texture.
*/
getTextureForReference() {
return _sharedFramebuffer;
}
updateReference() {
return this;
}
}
/**
* TSL function for creating a shared viewport texture node.
*
* @tsl
* @function
* @param {?Node} [uvNode=screenUV] - The uv node.
* @param {?Node} [levelNode=null] - The level node.
* @returns {ViewportSharedTextureNode}
*/
const viewportSharedTexture = /*@__PURE__*/ nodeProxy( ViewportSharedTextureNode ).setParameterLength( 0, 2 );
const _size = /*@__PURE__*/ new Vector2();
/**
* Represents the texture of a pass node.
*
* @augments TextureNode
*/
class PassTextureNode extends TextureNode {
static get type() {
return 'PassTextureNode';
}
/**
* Constructs a new pass texture node.
*
* @param {PassNode} passNode - The pass node.
* @param {Texture} texture - The output texture.
*/
constructor( passNode, texture ) {
super( texture );
/**
* A reference to the pass node.
*
* @type {PassNode}
*/
this.passNode = passNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @default true
* @readonly
*/
this.isPassTextureNode = true;
this.setUpdateMatrix( false );
}
isCacheable( /*builder*/ ) {
return true;
}
setup( builder ) {
const properties = builder.getNodeProperties( this );
properties.passNode = this.passNode;
return super.setup( builder );
}
clone() {
return new this.constructor( this.passNode, this.value );
}
}
/**
* An extension of `PassTextureNode` which allows to manage more than one
* internal texture. Relevant for the `getPreviousTexture()` related API.
*
* @augments PassTextureNode
*/
class PassMultipleTextureNode extends PassTextureNode {
static get type() {
return 'PassMultipleTextureNode';
}
/**
* Constructs a new pass texture node.
*
* @param {PassNode} passNode - The pass node.
* @param {string} textureName - The output texture name.
* @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not.
*/
constructor( passNode, textureName, previousTexture = false ) {
// null is passed to the super call since this class does not
// use an external texture for rendering pass data into. Instead
// the texture is managed by the pass node itself
super( passNode, null );
/**
* The output texture name.
*
* @type {string}
*/
this.textureName = textureName;
/**
* Whether previous frame data should be used or not.
*
* @type {boolean}
*/
this.previousTexture = previousTexture;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @default true
* @readonly
*/
this.isPassMultipleTextureNode = true;
}
/**
* Updates the texture reference of this node.
*/
updateTexture() {
this.value = this.previousTexture ? this.passNode.getPreviousTexture( this.textureName ) : this.passNode.getTexture( this.textureName );
}
setup( builder ) {
this.updateTexture();
return super.setup( builder );
}
clone() {
const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture );
newNode.uvNode = this.uvNode;
newNode.levelNode = this.levelNode;
newNode.biasNode = this.biasNode;
newNode.sampler = this.sampler;
newNode.depthNode = this.depthNode;
newNode.compareNode = this.compareNode;
newNode.gradNode = this.gradNode;
newNode.gatherNode = this.gatherNode;
newNode.offsetNode = this.offsetNode;
return newNode;
}
}
/**
* Represents a render pass (sometimes called beauty pass) in context of post processing.
* This pass produces a render for the given scene and camera and can provide multiple outputs
* via MRT for further processing.
*
* ```js
* const postProcessing = new RenderPipeline( renderer );
*
* const scenePass = pass( scene, camera );
*
* postProcessing.outputNode = scenePass;
* ```
*
* @augments Node
*/
class PassNode extends Node {
static get type() {
return 'PassNode';
}
/**
* Constructs a new pass node.
*
* @param {('color'|'depth')} scope - The scope of the pass. The scope determines whether the node outputs color or depth.
* @param {Scene} scene - A reference to the scene.
* @param {Camera} camera - A reference to the camera.
* @param {Object} options - Options for the internal render target.
*/
constructor( scope, scene, camera, options = {} ) {
super( 'vec4' );
/**
* The scope of the pass. The scope determines whether the node outputs color or depth.
*
* @type {('color'|'depth')}
*/
this.scope = scope;
/**
* A reference to the scene.
*
* @type {Scene}
*/
this.scene = scene;
/**
* A reference to the camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* Options for the internal render target.
*
* @type {Object}
*/
this.options = options;
/**
* The pass's pixel width. Will be kept automatically kept in sync with the renderer's width.
* @private
* @type {number}
* @default 1
*/
this._width = 1;
/**
* The pass's pixel height. Will be kept automatically kept in sync with the renderer's height.
* @private
* @type {number}
* @default 1
*/
this._height = 1;
const renderTarget = new RenderTarget( this._width, this._height, { type: HalfFloatType, ...options, } );
renderTarget.texture.name = 'output';
let depthTexture = null;
if ( this.scope === PassNode.DEPTH || options.depthBuffer !== false ) {
if ( options.depthTexture !== undefined ) {
depthTexture = options.depthTexture;
} else {
depthTexture = new DepthTexture();
if ( renderTarget.stencilBuffer === true ) {
depthTexture.format = DepthStencilFormat;
depthTexture.type = UnsignedInt248Type;
}
}
depthTexture.isRenderTargetTexture = true;
//depthTexture.type = FloatType;
depthTexture.name = 'depth';
renderTarget.depthTexture = depthTexture;
}
/**
* The pass's render target.
*
* @type {RenderTarget}
*/
this.renderTarget = renderTarget;
/**
* An optional override material for the pass.
*
* @type {Material|null}
*/
this.overrideMaterial = null;
/**
* Whether the pass is transparent.
*
* @type {boolean}
* @default true
*/
this.transparent = true;
/**
* Whether the pass is opaque.
*
* @type {boolean}
* @default true
*/
this.opaque = true;
this.lighting = null;
/**
* Whether the renderer should automatically clear before rendering the pass.
*
* @type {boolean}
* @default true
*/
this.autoClear = options.autoClear !== undefined ? options.autoClear : true;
/**
* Whether the color buffer should be cleared.
*
* @type {boolean}
* @default true
*/
this.autoClearColor = options.autoClearColor !== undefined ? options.autoClearColor : true;
/**
* Whether the depth buffer should be cleared.
*
* @type {boolean}
* @default true
*/
this.autoClearDepth = options.autoClearDepth !== undefined ? options.autoClearDepth : true;
/**
* Whether the stencil buffer should be cleared.
*
* @type {boolean}
* @default true
*/
this.autoClearStencil = options.autoClearStencil !== undefined ? options.autoClearStencil : true;
/**
* An optional global context for the pass.
*
* @type {ContextNode|null}
*/
this.contextNode = null;
/**
* A cache for the context node.
*
* @private
* @type {?Object}
* @default null
*/
this._contextNodeCache = null;
/**
* A dictionary holding the internal result textures.
*
* @private
* @type {{ output: Texture, depth: ?DepthTexture }}
*/
this._textures = {
output: renderTarget.texture
};
if ( depthTexture !== null ) {
this._textures.depth = depthTexture;
}
/**
* A dictionary holding the internal texture nodes.
*
* @private
* @type {Object<string, TextureNode>}
*/
this._textureNodes = {};
/**
* A dictionary holding the internal depth nodes.
*
* @private
* @type {Object}
*/
this._linearDepthNodes = {};
/**
* A dictionary holding the internal viewZ nodes.
*
* @private
* @type {Object}
*/
this._viewZNodes = {};
/**
* A dictionary holding the texture data of the previous frame.
* Used for computing velocity/motion vectors.
*
* @private
* @type {Object<string, Texture>}
*/
this._previousTextures = {};
/**
* A dictionary holding the texture nodes of the previous frame.
* Used for computing velocity/motion vectors.
*
* @private
* @type {Object<string, TextureNode>}
*/
this._previousTextureNodes = {};
/**
* The `near` property of the camera as a uniform.
*
* @private
* @type {UniformNode}
*/
this._cameraNear = uniform( 0 );
/**
* The `far` property of the camera as a uniform.
*
* @private
* @type {UniformNode}
*/
this._cameraFar = uniform( 0 );
/**
* A MRT node configuring the MRT settings.
*
* @private
* @type {?MRTNode}
* @default null
*/
this._mrt = null;
/**
* Layer object for configuring the camera that is used
* to produce the pass.
*
* @private
* @type {?Layers}
* @default null
*/
this._layers = null;
/**
* Scales the resolution of the internal render target.
*
* @private
* @type {number}
* @default 1
*/
this._resolutionScale = 1;
/**
* Custom viewport definition.
*
* @private
* @type {?Vector4}
* @default null
*/
this._viewport = null;
/**
* Custom scissor definition.
*
* @private
* @type {?Vector4}
* @default null
*/
this._scissor = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPassNode = true;
/**
* The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders the
* scene once per frame in its {@link PassNode#updateBefore} method.
*
* @type {string}
* @default 'frame'
*/
this.updateBeforeType = NodeUpdateType.FRAME;
/**
* This flag is used for global cache.
*
* @type {boolean}
* @default true
*/
this.global = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the resolution scale for the pass.
* The resolution scale is a factor that is multiplied with the renderer's width and height.
*
* @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
* @return {PassNode} A reference to this pass.
*/
setResolutionScale( resolutionScale ) {
this._resolutionScale = resolutionScale;
return this;
}
/**
* Gets the current resolution scale of the pass.
*
* @return {number} The current resolution scale. A value of `1` means full resolution.
*/
getResolutionScale() {
return this._resolutionScale;
}
/**
* Sets the resolution for the pass.
* The resolution is a factor that is multiplied with the renderer's width and height.
*
* @param {number} resolution - The resolution to set. A value of `1` means full resolution.
* @return {PassNode} A reference to this pass.
* @deprecated since r181. Use {@link PassNode#setResolutionScale `setResolutionScale()`} instead.
*/
setResolution( resolution ) { // @deprecated, r181
warn( 'PassNode: .setResolution() is deprecated. Use .setResolutionScale() instead.' );
return this.setResolutionScale( resolution );
}
/**
* Gets the current resolution of the pass.
*
* @return {number} The current resolution. A value of `1` means full resolution.
* @deprecated since r181. Use {@link PassNode#getResolutionScale `getResolutionScale()`} instead.
*/
getResolution() { // @deprecated, r181
warn( 'PassNode: .getResolution() is deprecated. Use .getResolutionScale() instead.' );
return this.getResolutionScale();
}
/**
* Sets the layer configuration that should be used when rendering the pass.
*
* @param {Layers} layers - The layers object to set.
* @return {PassNode} A reference to this pass.
*/
setLayers( layers ) {
this._layers = layers;
return this;
}
/**
* Gets the current layer configuration of the pass.
*
* @return {?Layers} .
*/
getLayers() {
return this._layers;
}
/**
* Sets the given MRT node to setup MRT for this pass.
*
* @param {MRTNode} mrt - The MRT object.
* @return {PassNode} A reference to this pass.
*/
setMRT( mrt ) {
this._mrt = mrt;
return this;
}
/**
* Returns the current MRT node.
*
* @return {MRTNode} The current MRT node.
*/
getMRT() {
return this._mrt;
}
/**
* Returns the texture for the given output name.
*
* @param {string} name - The output name to get the texture for.
* @return {Texture} The texture.
*/
getTexture( name ) {
let texture = this._textures[ name ];
if ( texture === undefined ) {
if ( name === 'depth' ) {
throw new Error( 'THREE.PassNode: Depth texture is not available for this pass.' );
}
const refTexture = this.renderTarget.texture;
texture = refTexture.clone();
texture.name = name;
this._textures[ name ] = texture;
this.renderTarget.textures.push( texture );
}
return texture;
}
/**
* Returns the texture holding the data of the previous frame for the given output name.
*
* @param {string} name - The output name to get the texture for.
* @return {Texture} The texture holding the data of the previous frame.
*/
getPreviousTexture( name ) {
let texture = this._previousTextures[ name ];
if ( texture === undefined ) {
texture = this.getTexture( name ).clone();
this._previousTextures[ name ] = texture;
}
return texture;
}
/**
* Switches current and previous textures for the given output name.
*
* @param {string} name - The output name.
*/
toggleTexture( name ) {
const prevTexture = this._previousTextures[ name ];
if ( prevTexture !== undefined ) {
const texture = this._textures[ name ];
const index = this.renderTarget.textures.indexOf( texture );
this.renderTarget.textures[ index ] = prevTexture;
this._textures[ name ] = prevTexture;
this._previousTextures[ name ] = texture;
this._textureNodes[ name ].updateTexture();
this._previousTextureNodes[ name ].updateTexture();
}
}
/**
* Returns the texture node for the given output name.
*
* @param {string} [name='output'] - The output name to get the texture node for.
* @return {TextureNode} The texture node.
*/
getTextureNode( name = 'output' ) {
let textureNode = this._textureNodes[ name ];
if ( textureNode === undefined ) {
textureNode = new PassMultipleTextureNode( this, name );
textureNode.updateTexture();
this._textureNodes[ name ] = textureNode;
}
return textureNode;
}
/**
* Returns the previous texture node for the given output name.
*
* @param {string} [name='output'] - The output name to get the previous texture node for.
* @return {TextureNode} The previous texture node.
*/
getPreviousTextureNode( name = 'output' ) {
let textureNode = this._previousTextureNodes[ name ];
if ( textureNode === undefined ) {
if ( this._textureNodes[ name ] === undefined ) this.getTextureNode( name );
textureNode = new PassMultipleTextureNode( this, name, true );
textureNode.updateTexture();
this._previousTextureNodes[ name ] = textureNode;
}
return textureNode;
}
/**
* Returns a viewZ node of this pass.
*
* @param {string} [name='depth'] - The output name to get the viewZ node for. In most cases the default `'depth'` can be used however the parameter exists for custom depth outputs.
* @return {Node} The viewZ node.
*/
getViewZNode( name = 'depth' ) {
let viewZNode = this._viewZNodes[ name ];
if ( viewZNode === undefined ) {
const cameraNear = this._cameraNear;
const cameraFar = this._cameraFar;
this._viewZNodes[ name ] = viewZNode = perspectiveDepthToViewZ( this.getTextureNode( name ), cameraNear, cameraFar );
}
return viewZNode;
}
/**
* Returns a linear depth node of this pass.
*
* @param {string} [name='depth'] - The output name to get the linear depth node for. In most cases the default `'depth'` can be used however the parameter exists for custom depth outputs.
* @return {Node} The linear depth node.
*/
getLinearDepthNode( name = 'depth' ) {
let linearDepthNode = this._linearDepthNodes[ name ];
if ( linearDepthNode === undefined ) {
const cameraNear = this._cameraNear;
const cameraFar = this._cameraFar;
const viewZNode = this.getViewZNode( name );
// TODO: just if ( builder.camera.isPerspectiveCamera )
this._linearDepthNodes[ name ] = linearDepthNode = viewZToOrthographicDepth( viewZNode, cameraNear, cameraFar );
}
return linearDepthNode;
}
/**
* Precompiles the pass.
*
* Note that this method must be called after the pass configuration is complete.
* So calls like `setMRT()` and `getTextureNode()` must proceed the precompilation.
*
* @async
* @param {Renderer} renderer - The renderer.
* @return {Promise} A Promise that resolves when the compile has been finished.
* @see {@link Renderer#compileAsync}
*/
async compileAsync( renderer ) {
const currentRenderTarget = renderer.getRenderTarget();
const currentMRT = renderer.getMRT();
const currentContextNode = renderer.contextNode;
// the render target is configured like in setup() and updateBefore(), so the pipelines match the ones of the render
this.renderTarget.samples = this.options.samples === undefined ? renderer.samples : this.options.samples;
renderer.getDrawingBufferSize( _size );
this.setSize( _size.width, _size.height );
renderer.setRenderTarget( this.renderTarget );
renderer.setMRT( this._mrt );
if ( this.contextNode !== null ) renderer.contextNode = this._getContextNode( renderer );
await renderer.compileAsync( this.scene, this.camera );
renderer.setRenderTarget( currentRenderTarget );
renderer.setMRT( currentMRT );
renderer.contextNode = currentContextNode;
}
setup( { renderer } ) {
this.renderTarget.samples = this.options.samples === undefined ? renderer.samples : this.options.samples;
this.renderTarget.texture.type = renderer.getOutputBufferType();
if ( renderer.reversedDepthBuffer === true && this.renderTarget.depthTexture !== null ) {
this.renderTarget.depthTexture.type = FloatType;
}
return this.scope === PassNode.COLOR ? this.getTextureNode() : this.getLinearDepthNode();
}
updateBefore( frame ) {
const { renderer } = frame;
const { scene } = this;
let camera;
const outputRenderTarget = renderer.getOutputRenderTarget();
if ( outputRenderTarget && outputRenderTarget.isXRRenderTarget === true ) {
camera = renderer.xr.getCamera();
renderer.xr.updateCamera( camera );
_size.set( outputRenderTarget.width, outputRenderTarget.height );
} else {
camera = this.camera;
renderer.getDrawingBufferSize( _size );
}
this.setSize( _size.width, _size.height );
const currentRenderTarget = renderer.getRenderTarget();
const currentMRT = renderer.getMRT();
const currentAutoClear = renderer.autoClear;
const currentAutoClearColor = renderer.autoClearColor;
const currentAutoClearDepth = renderer.autoClearDepth;
const currentAutoClearStencil = renderer.autoClearStencil;
const currentTransparent = renderer.transparent;
const currentOpaque = renderer.opaque;
const currentLighting = renderer.lighting;
const currentMask = camera.layers.mask;
const currentContextNode = renderer.contextNode;
const currentOverrideMaterial = scene.overrideMaterial;
this._cameraNear.value = camera.near;
this._cameraFar.value = camera.far;
if ( this._layers !== null ) {
camera.layers.mask = this._layers.mask;
}
for ( const name in this._previousTextures ) {
this.toggleTexture( name );
}
if ( this.overrideMaterial !== null ) {
scene.overrideMaterial = this.overrideMaterial;
}
renderer.setRenderTarget( this.renderTarget );
renderer.setMRT( this._mrt );
renderer.autoClear = this.autoClear;
renderer.autoClearColor = this.autoClearColor;
renderer.autoClearDepth = this.autoClearDepth;
renderer.autoClearStencil = this.autoClearStencil;
renderer.transparent = this.transparent;
renderer.opaque = this.opaque;
if ( this.lighting !== null ) {
renderer.lighting = this.lighting;
}
if ( this.contextNode !== null ) {
renderer.contextNode = this._getContextNode( renderer );
}
const currentSceneName = scene.name;
scene.name = this.name ? this.name : scene.name;
renderer.render( scene, camera );
scene.name = currentSceneName;
scene.overrideMaterial = currentOverrideMaterial;
renderer.setRenderTarget( currentRenderTarget );
renderer.setMRT( currentMRT );
renderer.autoClear = currentAutoClear;
renderer.autoClearColor = currentAutoClearColor;
renderer.autoClearDepth = currentAutoClearDepth;
renderer.autoClearStencil = currentAutoClearStencil;
renderer.transparent = currentTransparent;
renderer.opaque = currentOpaque;
renderer.contextNode = currentContextNode;
renderer.lighting = currentLighting;
camera.layers.mask = currentMask;
}
/**
* Returns the context node of the pass merged with the context node of the renderer.
*
* @private
* @param {Renderer} renderer - The renderer.
* @return {ContextNode} The context node.
*/
_getContextNode( renderer ) {
const rendererContextNode = renderer.contextNode;
const cache = this._contextNodeCache;
if ( cache === null || cache.version !== this.version || cache.rendererContextNode !== rendererContextNode || cache.rendererVersion !== rendererContextNode.version ) {
this._contextNodeCache = {
version: this.version,
rendererContextNode,
rendererVersion: rendererContextNode.version,
context: context( mergeContext( rendererContextNode.getFlowContextData(), this.contextNode.getFlowContextData() ) )
};
}
return this._contextNodeCache.context;
}
/**
* Sets the size of the pass's render target. Honors the pixel ratio.
*
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( width, height ) {
this._width = width;
this._height = height;
const effectiveWidth = Math.floor( this._width * this._resolutionScale );
const effectiveHeight = Math.floor( this._height * this._resolutionScale );
this.renderTarget.setSize( effectiveWidth, effectiveHeight );
// scissor
if ( this._scissor !== null ) {
this.renderTarget.scissor.copy( this._scissor ).multiplyScalar( this._resolutionScale ).floor();
this.renderTarget.scissorTest = true;
} else {
this.renderTarget.scissorTest = false;
}
// viewport
if ( this._viewport !== null ) {
this.renderTarget.viewport.copy( this._viewport ).multiplyScalar( this._resolutionScale ).floor();
}
}
/**
* This method allows to define the pass's scissor rectangle. By default, the scissor rectangle is kept
* in sync with the pass's dimensions. To reverse the process and use auto-sizing again, call the method
* with `null` as the single argument.
*
* @param {?(number | Vector4)} x - The horizontal coordinate for the lower left corner of the box in logical pixel unit.
* Instead of passing four arguments, the method also works with a single four-dimensional vector.
* @param {number} y - The vertical coordinate for the lower left corner of the box in logical pixel unit.
* @param {number} width - The width of the scissor box in logical pixel unit.
* @param {number} height - The height of the scissor box in logical pixel unit.
*/
setScissor( x, y, width, height ) {
if ( x === null ) {
this._scissor = null;
} else {
if ( this._scissor === null ) this._scissor = new Vector4();
if ( x.isVector4 ) {
this._scissor.copy( x );
} else {
this._scissor.set( x, y, width, height );
}
}
}
/**
* This method allows to define the pass's viewport. By default, the viewport is kept in sync
* with the pass's dimensions. To reverse the process and use auto-sizing again, call the method
* with `null` as the single argument.
*
* @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the viewport origin in logical pixel unit.
* @param {number} y - The vertical coordinate for the lower left corner of the viewport origin in logical pixel unit.
* @param {number} width - The width of the viewport in logical pixel unit.
* @param {number} height - The height of the viewport in logical pixel unit.
*/
setViewport( x, y, width, height ) {
if ( x === null ) {
this._viewport = null;
} else {
if ( this._viewport === null ) this._viewport = new Vector4();
if ( x.isVector4 ) {
this._viewport.copy( x );
} else {
this._viewport.set( x, y, width, height );
}
}
}
/**
* Frees internal resources. Should be called when the node is no longer in use.
*/
dispose() {
super.dispose();
this.renderTarget.dispose();
}
/**
* @static
* @type {'color'}
* @default 'color'
*/
static get COLOR() {
return 'color';
}
/**
* @static
* @type {'depth'}
* @default 'depth'
*/
static get DEPTH() {
return 'depth';
}
}
/**
* TSL function for creating a pass node.
*
* @tsl
* @function
* @param {Scene} scene - A reference to the scene.
* @param {Camera} camera - A reference to the camera.
* @param {Object} options - Options for the internal render target.
* @returns {PassNode}
*/
const pass = ( scene, camera, options ) => new PassNode( PassNode.COLOR, scene, camera, options );
/**
* TSL function for creating a pass texture node.
*
* @tsl
* @function
* @param {PassNode} pass - The pass node.
* @param {Texture} texture - The output texture.
* @returns {PassTextureNode}
*/
const passTexture = ( pass, texture ) => new PassTextureNode( pass, texture );
/**
* TSL function for creating a depth pass node.
*
* @tsl
* @function
* @param {Scene} scene - A reference to the scene.
* @param {Camera} camera - A reference to the camera.
* @param {Object} options - Options for the internal render target.
* @returns {PassNode}
*/
const depthPass = ( scene, camera, options ) => new PassNode( PassNode.DEPTH, scene, camera, options );
/**
* Represents a render pass for producing a toon outline effect on compatible objects.
* Only 3D objects with materials of type `MeshToonMaterial` and `MeshToonNodeMaterial`
* will receive the outline.
*
* ```js
* const postProcessing = new RenderPipeline( renderer );
*
* const scenePass = toonOutlinePass( scene, camera );
*
* postProcessing.outputNode = scenePass;
* ```
* @augments PassNode
*/
class ToonOutlinePassNode extends PassNode {
static get type() {
return 'ToonOutlinePassNode';
}
/**
* Constructs a new outline pass node.
*
* @param {Scene} scene - A reference to the scene.
* @param {Camera} camera - A reference to the camera.
* @param {Node} colorNode - Defines the outline's color.
* @param {Node} thicknessNode - Defines the outline's thickness.
* @param {Node} alphaNode - Defines the outline's alpha.
*/
constructor( scene, camera, colorNode, thicknessNode, alphaNode ) {
super( PassNode.COLOR, scene, camera );
/**
* Defines the outline's color.
*
* @type {Node}
*/
this.colorNode = colorNode;
/**
* Defines the outline's thickness.
*
* @type {Node}
*/
this.thicknessNode = thicknessNode;
/**
* Defines the outline's alpha.
*
* @type {Node}
*/
this.alphaNode = alphaNode;
/**
* The outline material for toon materials without node modifications.
*
* @private
* @type {NodeMaterial}
*/
this._outlineMaterial = this._createMaterial();
/**
* An internal material cache.
*
* @private
* @type {Map<Material, NodeMaterial>}
*/
this._materialCache = new Map();
/**
* Disposes the outline material of a disposed toon material.
*
* @private
* @type {Function}
*/
this._onMaterialDispose = ( event ) => {
const originalMaterial = event.target;
originalMaterial.removeEventListener( 'dispose', this._onMaterialDispose );
this._materialCache.get( originalMaterial ).dispose();
this._materialCache.delete( originalMaterial );
};
/**
* The name of this pass.
*
* @type {string}
* @default 'Outline Pass'
*/
this.name = 'Outline Pass';
}
updateBefore( frame ) {
const { renderer } = frame;
const currentRenderObjectFunction = renderer.getRenderObjectFunction();
renderer.setRenderObjectFunction( ( object, scene, camera, geometry, material, group, lightsNode, clippingContext ) => {
// only render outline for supported materials
if ( material.isMeshToonMaterial || material.isMeshToonNodeMaterial ) {
if ( material.wireframe === false ) {
const outlineMaterial = this._getOutlineMaterial( material );
renderer.renderObject( object, scene, camera, geometry, outlineMaterial, group, lightsNode, clippingContext );
}
}
// default
renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext );
} );
super.updateBefore( frame );
renderer.setRenderObjectFunction( currentRenderObjectFunction );
}
/**
* Frees internal resources. Should be called when the node is no longer in use.
*/
dispose() {
super.dispose();
this._outlineMaterial.dispose();
for ( const [ originalMaterial, outlineMaterial ] of this._materialCache ) {
originalMaterial.removeEventListener( 'dispose', this._onMaterialDispose );
outlineMaterial.dispose();
}
this._materialCache.clear();
}
/**
* Creates the material used for outline rendering.
*
* @private
* @return {NodeMaterial} The outline material.
*/
_createMaterial() {
const material = new NodeMaterial();
material.isMeshToonOutlineMaterial = true;
material.name = 'Toon_Outline';
material.side = BackSide;
// vertex node
const outlineNormal = normalLocal.negate();
const mvp = cameraProjectionMatrix.mul( modelViewMatrix );
const ratio = float( 1.0 ); // TODO: support outline thickness ratio for each vertex
const pos = mvp.mul( vec4( positionLocal, 1.0 ) );
const pos2 = mvp.mul( vec4( positionLocal.add( outlineNormal ), 1.0 ) );
const norm = normalize( pos.sub( pos2 ) ); // NOTE: subtract pos2 from pos because BackSide objectNormal is negative
material.vertexNode = pos.add( norm.mul( this.thicknessNode ).mul( pos.w ).mul( ratio ) );
// color node
material.colorNode = vec4( this.colorNode, this.alphaNode );
return material;
}
/**
* For the given toon material, this method returns a corresponding
* outline material.
*
* @private
* @param {(MeshToonMaterial|MeshToonNodeMaterial)} originalMaterial - The toon material.
* @return {NodeMaterial} The outline material.
*/
_getOutlineMaterial( originalMaterial ) {
if ( ! originalMaterial.positionNode ) return this._outlineMaterial; // early out if there are no node modifications
let force = false;
let outlineMaterial = this._materialCache.get( originalMaterial );
// create
if ( outlineMaterial === undefined ) {
outlineMaterial = this._createMaterial();
this._materialCache.set( originalMaterial, outlineMaterial );
originalMaterial.addEventListener( 'dispose', this._onMaterialDispose );
force = true;
}
// update
if ( outlineMaterial.version !== originalMaterial.version || force === true ) {
outlineMaterial.positionNode = originalMaterial.positionNode || null;
outlineMaterial.version = originalMaterial.version;
}
return outlineMaterial;
}
}
/**
* TSL function for creating a toon outline pass node.
*
* @tsl
* @function
* @param {Scene} scene - A reference to the scene.
* @param {Camera} camera - A reference to the camera.
* @param {Color} color - Defines the outline's color.
* @param {number} [thickness=0.003] - Defines the outline's thickness.
* @param {number} [alpha=1] - Defines the outline's alpha.
* @returns {ToonOutlinePassNode}
*/
const toonOutlinePass = ( scene, camera, color = new Color( 0, 0, 0 ), thickness = 0.003, alpha = 1 ) => new ToonOutlinePassNode( scene, camera, nodeObject( color ), nodeObject( thickness ), nodeObject( alpha ) );
/**
* Linear tone mapping, exposure only.
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const linearToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
return color.mul( exposure ).clamp();
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
/**
* Reinhard tone mapping.
*
* Reference: {@link https://www.cs.utah.edu/docs/techreports/2002/pdf/UUCS-02-001.pdf}
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const reinhardToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
color = color.mul( exposure );
return color.div( color.add( 1.0 ) ).clamp();
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
/**
* Cineon tone mapping.
*
* Reference: {@link http://filmicworlds.com/blog/filmic-tonemapping-operators/}
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const cineonToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
// filmic operator by Jim Hejl and Richard Burgess-Dawson
color = color.mul( exposure );
color = color.sub( 0.004 ).max( 0.0 );
const a = color.mul( color.mul( 6.2 ).add( 0.5 ) );
const b = color.mul( color.mul( 6.2 ).add( 1.7 ) ).add( 0.06 );
return a.div( b ).pow( 2.2 );
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
// source: https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs
const RRTAndODTFit = /*@__PURE__*/ Fn( ( [ color ] ) => {
const a = color.mul( color.add( 0.0245786 ) ).sub( 0.000090537 );
const b = color.mul( color.mul( 0.983729 ).add( 0.4329510 ) ).add( 0.238081 );
return a.div( b );
}, { color: 'vec3', return: 'vec3' } );
/**
* ACESFilmic tone mapping.
*
* Reference: {@link https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs}
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const acesFilmicToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
const ACESInputMat = mat3(
0.59719, 0.35458, 0.04823,
0.07600, 0.90834, 0.01566,
0.02840, 0.13383, 0.83777
);
// ODT_SAT => XYZ => D60_2_D65 => sRGB
const ACESOutputMat = mat3(
1.60475, -0.53108, -0.07367,
-0.10208, 1.10813, -605e-5,
-327e-5, -0.07276, 1.07602
);
color = color.mul( exposure ).div( 0.6 );
color = ACESInputMat.mul( color );
// Apply RRT and ODT
color = RRTAndODTFit( color );
color = ACESOutputMat.mul( color );
// Clamp to [0, 1]
return color.clamp();
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
const LINEAR_REC2020_TO_LINEAR_SRGB = /*@__PURE__*/ mat3( vec3( 1.6605, -0.1246, -0.0182 ), vec3( -0.5876, 1.1329, -0.1006 ), vec3( -0.0728, -83e-4, 1.1187 ) );
const LINEAR_SRGB_TO_LINEAR_REC2020 = /*@__PURE__*/ mat3( vec3( 0.6274, 0.0691, 0.0164 ), vec3( 0.3293, 0.9195, 0.0880 ), vec3( 0.0433, 0.0113, 0.8956 ) );
const agxDefaultContrastApprox = /*@__PURE__*/ Fn( ( [ x_immutable ] ) => {
const x = vec3( x_immutable ).toVar();
const x2 = vec3( x.mul( x ) ).toVar();
const x4 = vec3( x2.mul( x2 ) ).toVar();
return float( 15.5 ).mul( x4.mul( x2 ) ).sub( mul( 40.14, x4.mul( x ) ) ).add( mul( 31.96, x4 ).sub( mul( 6.868, x2.mul( x ) ) ).add( mul( 0.4298, x2 ).add( mul( 0.1191, x ).sub( 0.00232 ) ) ) );
}, { x: 'vec3', return: 'vec3' } );
/**
* AgX tone mapping.
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const agxToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
const colortone = vec3( color ).toVar();
const AgXInsetMatrix = mat3( vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) );
const AgXOutsetMatrix = mat3( vec3( 1.1271005818144368, -0.1413297634984383, -0.14132976349843826 ), vec3( -0.11060664309660323, 1.157823702216272, -0.11060664309660294 ), vec3( -0.016493938717834573, -0.016493938717834257, 1.2519364065950405 ) );
const AgxMinEv = float( -12.47393 );
const AgxMaxEv = float( 4.026069 );
colortone.mulAssign( exposure );
colortone.assign( LINEAR_SRGB_TO_LINEAR_REC2020.mul( colortone ) );
colortone.assign( AgXInsetMatrix.mul( colortone ) );
colortone.assign( max( colortone, 1e-10 ) );
colortone.assign( log2( colortone ) );
colortone.assign( colortone.sub( AgxMinEv ).div( AgxMaxEv.sub( AgxMinEv ) ) );
colortone.assign( clamp( colortone, 0.0, 1.0 ) );
colortone.assign( agxDefaultContrastApprox( colortone ) );
colortone.assign( AgXOutsetMatrix.mul( colortone ) );
colortone.assign( pow( max( vec3( 0.0 ), colortone ), vec3( 2.2 ) ) );
colortone.assign( LINEAR_REC2020_TO_LINEAR_SRGB.mul( colortone ) );
colortone.assign( clamp( colortone, 0.0, 1.0 ) );
return colortone;
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
/**
* Neutral tone mapping.
*
* Reference: {@link https://modelviewer.dev/examples/tone-mapping}
*
* @tsl
* @function
* @param {Node<vec3>} color - The color that should be tone mapped.
* @param {Node<float>} exposure - The exposure.
* @return {Node<vec3>} The tone mapped color.
*/
const neutralToneMapping = /*@__PURE__*/ Fn( ( [ color, exposure ] ) => {
const StartCompression = float( 0.8 - 0.04 );
const Desaturation = float( 0.15 );
color = color.mul( exposure );
const x = min( color.r, min( color.g, color.b ) );
const offset = select( x.lessThan( 0.08 ), x.sub( mul( 6.25, x.mul( x ) ) ), 0.04 );
color.subAssign( offset );
const peak = max( color.r, max( color.g, color.b ) );
If( peak.lessThan( StartCompression ), () => {
return color;
} );
const d = sub( 1, StartCompression );
const newPeak = sub( 1, d.mul( d ).div( peak.add( d.sub( StartCompression ) ) ) );
color.mulAssign( newPeak.div( peak ) );
const g = sub( 1, div( 1, Desaturation.mul( peak.sub( newPeak ) ).add( 1 ) ) );
return mix( color, vec3( newPeak ), g );
}, { color: 'vec3', exposure: 'float', return: 'vec3' } );
/**
* This class represents native code sections. It is the base
* class for modules like {@link FunctionNode} which allows to implement
* functions with native shader languages.
*
* @augments Node
*/
class CodeNode extends Node {
static get type() {
return 'CodeNode';
}
/**
* Constructs a new code node.
*
* @param {string} [code=''] - The native code.
* @param {Array<Node>} [includes=[]] - An array of includes.
* @param {('js'|'wgsl'|'glsl')} [language=''] - The used language.
*/
constructor( code = '', includes = [], language = '' ) {
super( 'code' );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isCodeNode = true;
/**
* This flag is used for global cache.
*
* @type {boolean}
* @default true
*/
this.global = true;
/**
* The native code.
*
* @type {string}
* @default ''
*/
this.code = code;
/**
* An array of includes
*
* @type {Array<Node>}
* @default []
*/
this.includes = includes;
/**
* The used language.
*
* @type {('js'|'wgsl'|'glsl')}
* @default ''
*/
this.language = language;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the includes of this code node.
*
* @param {Array<Node>} includes - The includes to set.
* @return {CodeNode} A reference to this node.
*/
setIncludes( includes ) {
this.includes = includes;
return this;
}
/**
* Returns the includes of this code node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Array<Node>} The includes.
*/
getIncludes( /*builder*/ ) {
return this.includes;
}
generate( builder ) {
const includes = this.getIncludes( builder );
for ( const include of includes ) {
include.build( builder, 'void' );
}
const nodeCode = builder.getCodeFromNode( this, this.getNodeType( builder ) );
nodeCode.code = this.code;
return nodeCode.code;
}
serialize( data ) {
super.serialize( data );
data.code = this.code;
data.language = this.language;
}
deserialize( data ) {
super.deserialize( data );
this.code = data.code;
this.language = data.language;
}
}
/**
* TSL function for creating a code node.
*
* @tsl
* @function
* @param {string} [code] - The native code.
* @param {?Array<Node>} [includes=[]] - An array of includes.
* @param {?('js'|'wgsl'|'glsl')} [language=''] - The used language.
* @returns {CodeNode}
*/
const code = /*@__PURE__*/ nodeProxy( CodeNode ).setParameterLength( 1, 3 );
/**
* TSL function for creating a JS code node.
*
* @tsl
* @function
* @param {string} src - The native code.
* @param {Array<Node>} includes - An array of includes.
* @returns {CodeNode}
*/
const js = ( src, includes ) => code( src, includes, 'js' );
/**
* TSL function for creating a WGSL code node.
*
* @tsl
* @function
* @param {string} src - The native code.
* @param {Array<Node>} includes - An array of includes.
* @returns {CodeNode}
*/
const wgsl = ( src, includes ) => code( src, includes, 'wgsl' );
/**
* TSL function for creating a GLSL code node.
*
* @tsl
* @function
* @param {string} src - The native code.
* @param {Array<Node>} includes - An array of includes.
* @returns {CodeNode}
*/
const glsl = ( src, includes ) => code( src, includes, 'glsl' );
/**
* This class represents a native shader function. It can be used to implement
* certain aspects of a node material with native shader code. There are two predefined
* TSL functions for easier usage.
*
* - `wgslFn`: Creates a WGSL function node.
* - `glslFn`: Creates a GLSL function node.
*
* A basic example with one include looks like so:
*
* ```js
* const desaturateWGSLFn = wgslFn( `
* fn desaturate( color:vec3<f32> ) -> vec3<f32> {
* let lum = vec3<f32>( 0.299, 0.587, 0.114 );
* return vec3<f32>( dot( lum, color ) );
* }`
*);
* const someWGSLFn = wgslFn( `
* fn someFn( color:vec3<f32> ) -> vec3<f32> {
* return desaturate( color );
* }
* `, [ desaturateWGSLFn ] );
* material.colorNode = someWGSLFn( { color: texture( map ) } );
*```
* @augments CodeNode
*/
class FunctionNode extends CodeNode {
static get type() {
return 'FunctionNode';
}
/**
* Constructs a new function node.
*
* @param {string} [code=''] - The native code.
* @param {Array<Node>} [includes=[]] - An array of includes.
* @param {('js'|'wgsl'|'glsl')} [language=''] - The used language.
*/
constructor( code = '', includes = [], language = '' ) {
super( code, includes, language );
}
/**
* Returns the type of this function node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type.
*/
generateNodeType( builder ) {
return this.getNodeFunction( builder ).type;
}
/**
* Returns the type of a member of this function node.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The name of the member.
* @return {string} The type of the member.
*/
getMemberType( builder, name ) {
const type = this.getNodeType( builder );
const structType = builder.getStructTypeNode( type );
return structType.getMemberType( builder, name );
}
/**
* Returns the inputs of this function node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Array<NodeFunctionInput>} The inputs.
*/
getInputs( builder ) {
return this.getNodeFunction( builder ).inputs;
}
/**
* Returns the node function for this function node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {NodeFunction} The node function.
*/
getNodeFunction( builder ) {
const nodeData = builder.getDataFromNode( this );
let nodeFunction = nodeData.nodeFunction;
if ( nodeFunction === undefined ) {
nodeFunction = builder.parser.parseFunction( this.code );
nodeData.nodeFunction = nodeFunction;
}
return nodeFunction;
}
generate( builder, output ) {
const nodeFunction = this.getNodeFunction( builder );
const nodeData = builder.getDataFromNode( this );
const type = nodeFunction.type;
let propertyName = nodeData.functionPropertyName;
if ( propertyName === undefined ) {
// The includes and the function code are generated once per builder and shader stage,
// further calls only need the function name.
super.generate( builder );
const name = nodeFunction.name;
const nodeCode = builder.getCodeFromNode( this, type );
if ( name !== '' ) {
// use a custom property name
nodeCode.name = name;
builder.registerDeclaration( nodeCode );
}
propertyName = builder.getPropertyName( nodeCode );
nodeCode.code = nodeFunction.getCode( propertyName ) + '\n';
nodeData.functionPropertyName = propertyName;
}
if ( output === 'property' ) {
return propertyName;
} else {
return builder.format( `${ propertyName }()`, type, output );
}
}
}
const nativeFn = ( code, includes = [], language = '' ) => {
const functionNode = new FunctionNode( code, includes, language );
const fn = ( ...params ) => functionNode.call( ...params );
return nodeProxyConstructor( fn, functionNode );
};
const glslFn = ( code, includes ) => nativeFn( code, includes, 'glsl' );
const wgslFn = ( code, includes ) => nativeFn( code, includes, 'wgsl' );
/**
* Returns a node that represents the `z` coordinate in view space
* for the current fragment. It's a different representation of the
* default depth value.
*
* This value can be part of a computation that defines how the fog
* density increases when moving away from the camera.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The viewZ node.
*/
function getViewZNode( builder ) {
let viewZ;
const getViewZ = builder.context.getViewZ;
if ( getViewZ !== undefined ) {
viewZ = getViewZ( this );
}
return ( viewZ || positionView.z ).negate();
}
/**
* Constructs a new range factor node.
*
* @tsl
* @function
* @param {Node} near - Defines the near value.
* @param {Node} far - Defines the far value.
*/
const rangeFogFactor = /*@__PURE__*/ Fn( ( [ near, far ], builder ) => {
const viewZ = getViewZNode( builder );
return smoothstep( near, far, viewZ );
} );
/**
* Represents an exponential squared fog. This type of fog gives
* a clear view near the camera and a faster than exponentially
* densening fog farther from the camera.
*
* @tsl
* @function
* @param {Node} density - Defines the fog density.
*/
const densityFogFactor = /*@__PURE__*/ Fn( ( [ density ], builder ) => {
const viewZ = getViewZNode( builder );
return density.mul( density, viewZ, viewZ ).negate().exp().oneMinus();
} );
/**
* Constructs a new height fog factor node. This fog factor requires a Y-up coordinate system.
*
* @tsl
* @function
* @param {Node} density - Defines the fog density.
* @param {Node} height - The height threshold in world space. Everything below this y-coordinate is affected by fog.
*/
const exponentialHeightFogFactor = /*@__PURE__*/ Fn( ( [ density, height ], builder ) => {
const viewZ = getViewZNode( builder );
const distance = height.sub( positionWorld.y ).max( 0 ).toConst();
const m = distance.mul( viewZ ).toConst();
return density.mul( density, m, m ).negate().exp().oneMinus();
} );
/**
* This class can be used to configure a fog for the scene.
* Nodes of this type are assigned to `Scene.fogNode`.
*
* @tsl
* @function
* @param {Node} color - Defines the color of the fog.
* @param {Node} factor - Defines how the fog is factored in the scene.
*/
const fog = /*@__PURE__*/ Fn( ( [ color, factor ] ) => {
return vec4( factor.toFloat().mix( output.rgb, color.toVec3() ), output.a );
} );
let _rangeId = 0;
// Per-object seed shared by all range nodes, so the values differ between objects.
const objectSeed = /*@__PURE__*/ uniform( 0, 'uint' ).onObjectUpdate( ( { object } ) => {
const objectId = object !== null ? object.id : 0; // compute has no object
return Math.imul( objectId, 0x9E3779B9 ) >>> 0;
} );
/**
* `RangeNode` generates random per-instance values in a defined range.
* An exemplary use case for this utility node is to generate random per-instance
* colors:
* ```js
* const material = new MeshBasicNodeMaterial();
* material.colorNode = range( new Color( 0x000000 ), new Color( 0xFFFFFF ) );
* const mesh = new InstancedMesh( geometry, material, count );
* ```
* @augments Node
*/
class RangeNode extends Node {
static get type() {
return 'RangeNode';
}
/**
* Constructs a new range node.
*
* @param {Node<any>} [minNode=float()] - A node defining the lower bound of the range.
* @param {Node<any>} [maxNode=float()] - A node defining the upper bound of the range.
*/
constructor( minNode = float(), maxNode = float() ) {
super();
/**
* A node defining the lower bound of the range.
*
* @type {Node<any>}
* @default float()
*/
this.minNode = minNode;
/**
* A node defining the upper bound of the range.
*
* @type {Node<any>}
* @default float()
*/
this.maxNode = maxNode;
/**
* Salt of the random sequence. It uses a counter of range nodes instead of the
* global node id, so the values only change when range nodes are added or removed.
*
* @private
* @type {number}
*/
this._salt = Math.imul( _rangeId ++, 0x85EBCA6B ) >>> 0;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Returns the vector length which is computed based on the range definition.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {number} The vector length.
*/
getVectorLength( builder ) {
const minNode = this.getConstNode( this.minNode );
const maxNode = this.getConstNode( this.maxNode );
const minLength = builder.getTypeLength( getValueType( minNode.value ) );
const maxLength = builder.getTypeLength( getValueType( maxNode.value ) );
return minLength > maxLength ? minLength : maxLength;
}
/**
* This method is overwritten since the node type is inferred from range definition.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return builder.getTypeFromLength( this.getVectorLength( builder ) );
}
/**
* Returns a constant node from the given node by traversing it.
*
* @param {Node} node - The node to traverse.
* @returns {Node} The constant node, if found.
*/
getConstNode( node ) {
let output = null;
node.traverse( n => {
if ( n.isConstNode === true ) {
output = n;
}
} );
if ( output === null ) {
throw new NodeError( 'THREE.TSL: No "ConstNode" found in node graph.', this.stackTrace );
}
return output;
}
setup( builder ) {
const minNode = this.getConstNode( this.minNode );
const maxNode = this.getConstNode( this.maxNode );
const minValue = minNode.value;
const maxValue = maxNode.value;
const minLength = builder.getTypeLength( getValueType( minValue ) );
const maxLength = builder.getTypeLength( getValueType( maxValue ) );
const min = new Vector4();
const max = new Vector4();
if ( minLength === 1 ) min.setScalar( minValue );
else if ( minValue.isColor ) min.set( minValue.r, minValue.g, minValue.b, 1 );
else min.set( minValue.x, minValue.y, minValue.z || 0, minValue.w || 0 );
if ( maxLength === 1 ) max.setScalar( maxValue );
else if ( maxValue.isColor ) max.set( maxValue.r, maxValue.g, maxValue.b, 1 );
else max.set( maxValue.x, maxValue.y, maxValue.z || 0, maxValue.w || 0 );
// The values are hashed from the instance index instead of stored per object, so they work
// for any instance count. The salt decorrelates range nodes.
const seed = objectSeed.bitXor( uint( this._salt ) );
const index = instanceIndex.mul( 4 ).add( seed );
const random = vec4( hash( index ), hash( index.add( 1 ) ), hash( index.add( 2 ) ), hash( index.add( 3 ) ) );
return mix( vec4( min ), vec4( max ), random ).convert( this.getNodeType( builder ) );
}
}
/**
* TSL function for creating a range node.
*
* @tsl
* @function
* @param {Node<any>} [minNode=float()] - A node defining the lower bound of the range.
* @param {Node<any>} [maxNode=float()] - A node defining the upper bound of the range.
* @returns {RangeNode}
*/
const range = /*@__PURE__*/ nodeProxy( RangeNode ).setParameterLength( 2 );
/**
* `ComputeBuiltinNode` represents a compute-scope builtin value that expose information
* about the currently running dispatch and/or the device it is running on.
*
* This node can only be used with a WebGPU backend.
*
* @augments Node
*/
class ComputeBuiltinNode extends Node {
static get type() {
return 'ComputeBuiltinNode';
}
/**
* Constructs a new compute builtin node.
*
* @param {string} builtinName - The built-in name.
* @param {string} nodeType - The node type.
*/
constructor( builtinName, nodeType ) {
super( nodeType );
/**
* The built-in name.
*
* @private
* @type {string}
*/
this._builtinName = builtinName;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* This method is overwritten since hash is derived from the built-in name.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The hash.
*/
getHash( builder ) {
return this.getBuiltinName( builder );
}
/**
* This method is overwritten since the node type is simply derived from `nodeType`..
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( /*builder*/ ) {
return this.nodeType;
}
/**
* Sets the builtin name.
*
* @param {string} builtinName - The built-in name.
* @return {ComputeBuiltinNode} A reference to this node.
*/
setBuiltinName( builtinName ) {
this._builtinName = builtinName;
return this;
}
/**
* Returns the builtin name.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The builtin name.
*/
getBuiltinName( /*builder*/ ) {
return this._builtinName;
}
/**
* Whether the current node builder has the builtin or not.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether the builder has the builtin or not.
*/
hasBuiltin( builder ) {
return builder.hasBuiltin( this._builtinName );
}
generate( builder, output ) {
const builtinName = this.getBuiltinName( builder );
const nodeType = this.getNodeType( builder );
if ( builder.shaderStage === 'compute' ) {
return builder.format( builtinName, nodeType, output );
} else {
warn( `TSL: Compute built-in value "${builtinName}" can not be accessed in the ${builder.shaderStage} stage` );
return builder.generateConst( nodeType );
}
}
serialize( data ) {
super.serialize( data );
data.global = this.global;
data._builtinName = this._builtinName;
}
deserialize( data ) {
super.deserialize( data );
this.global = data.global;
this._builtinName = data._builtinName;
}
}
/**
* TSL function for creating a compute builtin node.
*
* @tsl
* @function
* @param {string} name - The built-in name.
* @param {string} nodeType - The node type.
* @returns {ComputeBuiltinNode}
*/
const computeBuiltin = ( name, nodeType ) => new ComputeBuiltinNode( name, nodeType );
/**
* Represents the number of workgroups dispatched by the compute shader.
* ```js
* // Run 512 invocations/threads with a workgroup size of 128.
* const computeFn = Fn(() => {
*
* // numWorkgroups.x = 4
* storageBuffer.element(0).assign(numWorkgroups.x)
*
* })().compute(512, [128]);
*
* // Run 512 invocations/threads with the default workgroup size of 64.
* const computeFn = Fn(() => {
*
* // numWorkgroups.x = 8
* storageBuffer.element(0).assign(numWorkgroups.x)
*
* })().compute(512);
* ```
*
* @tsl
* @type {ComputeBuiltinNode<uvec3>}
*/
const numWorkgroups = /*@__PURE__*/ computeBuiltin( 'numWorkgroups', 'uvec3' );
/**
* Represents the 3-dimensional index of the workgroup the current compute invocation belongs to.
* ```js
* // Execute 12 compute threads with a workgroup size of 3.
* const computeFn = Fn( () => {
*
* If( workgroupId.x.mod( 2 ).equal( 0 ), () => {
*
* storageBuffer.element( instanceIndex ).assign( instanceIndex );
*
* } ).Else( () => {
*
* storageBuffer.element( instanceIndex ).assign( 0 );
*
* } );
*
* } )().compute( 12, [ 3 ] );
*
* // workgroupId.x = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3];
* // Buffer Output = [0, 1, 2, 0, 0, 0, 6, 7, 8, 0, 0, 0];
* ```
*
* @tsl
* @type {ComputeBuiltinNode<uvec3>}
*/
const workgroupId = /*@__PURE__*/ computeBuiltin( 'workgroupId', 'uvec3' );
/**
* A non-linearized 3-dimensional representation of the current invocation's position within a 3D global grid.
*
* @tsl
* @type {ComputeBuiltinNode<uvec3>}
*/
const globalId = /*@__PURE__*/ computeBuiltin( 'globalId', 'uvec3' );
/**
* A non-linearized 3-dimensional representation of the current invocation's position within a 3D workgroup grid.
*
* @tsl
* @type {ComputeBuiltinNode<uvec3>}
*/
const localId = /*@__PURE__*/ computeBuiltin( 'localId', 'uvec3' );
/**
* A device dependent variable that exposes the size of the current invocation's subgroup.
*
* @tsl
* @type {ComputeBuiltinNode<uint>}
*/
const subgroupSize = /*@__PURE__*/ computeBuiltin( 'subgroupSize', 'uint' );
/**
* Represents a GPU control barrier that synchronizes compute operations within a given scope.
*
* This node can only be used with a WebGPU backend.
*
* @augments Node
*/
class BarrierNode extends Node {
/**
* Constructs a new barrier node.
*
* @param {string} scope - The scope defines the behavior of the node.
*/
constructor( scope ) {
super();
this.scope = scope;
this.isBarrierNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
setup( builder ) {
builder.allowEarlyReturns = false;
builder.allowGlobalVariables = false;
}
generate( builder ) {
const { scope } = this;
const { renderer, shaderStage } = builder;
const barrierMethod = `${scope}Barrier`;
if ( shaderStage !== 'compute' ) {
error( `TSL: "${barrierMethod}" is not supported in the ${shaderStage} stage and can only be executed in compute.` );
}
if ( renderer.backend.isWebGLBackend === true ) {
builder.addFlowCode( `\t// ${barrierMethod}() \n` );
} else {
builder.addLineFlowCode( `${barrierMethod}()`, this );
}
}
}
/**
* TSL function for creating a barrier node.
*
* @tsl
* @function
* @param {string} scope - The scope defines the behavior of the node..
* @returns {BarrierNode}
*/
const barrier = /*@__PURE__*/ nodeProxy( BarrierNode );
/**
* TSL function for creating a workgroup barrier. All compute shader
* invocations must wait for each invocation within a workgroup to
* complete before the barrier can be surpassed.
*
* @tsl
* @function
* @returns {BarrierNode}
*/
const workgroupBarrier = () => barrier( 'workgroup' ).toStack();
/**
* TSL function for creating a storage barrier. All invocations must
* wait for each access to variables within the 'storage' address space
* to complete before the barrier can be passed.
*
* @tsl
* @function
* @returns {BarrierNode}
*/
const storageBarrier = () => barrier( 'storage' ).toStack();
/**
* TSL function for creating a texture barrier. All invocations must
* wait for each access to variables within the 'texture' address space
* to complete before the barrier can be passed.
*
* @tsl
* @function
* @returns {BarrierNode}
*/
const textureBarrier = () => barrier( 'texture' ).toStack();
/**
* Represents an element of a 'workgroup' scoped buffer.
*
* @augments ArrayElementNode
*/
class WorkgroupInfoElementNode extends ArrayElementNode {
/**
* Constructs a new workgroup info element node.
*
* @param {Node} workgroupInfoNode - The workgroup info node.
* @param {Node} indexNode - The index node that defines the element access.
*/
constructor( workgroupInfoNode, indexNode ) {
super( workgroupInfoNode, indexNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWorkgroupInfoElementNode = true;
}
generate( builder, output ) {
let snippet;
const isAssignContext = builder.isContextAssign();
snippet = super.generate( builder );
if ( isAssignContext !== true ) {
const type = this.getNodeType( builder );
snippet = builder.format( snippet, type, output );
}
// TODO: Possibly activate clip distance index on index access rather than from clipping context
return snippet;
}
}
/**
* A node allowing the user to create a 'workgroup' scoped buffer within the
* context of a compute shader. Typically, workgroup scoped buffers are
* created to hold data that is transferred from a global storage scope into
* a local workgroup scope. For invocations within a workgroup, data
* access speeds on 'workgroup' scoped buffers can be significantly faster
* than similar access operations on globally accessible storage buffers.
*
* This node can only be used with a WebGPU backend.
*
* @augments Node
*/
class WorkgroupInfoNode extends Node {
/**
* Constructs a new workgroup info node.
*
* @param {string} scope - The address space the buffer is scoped to. Currently only `'Workgroup'` is used.
* @param {string} bufferType - The data type of a 'workgroup' scoped buffer element.
* @param {number} [bufferCount=0] - The number of elements in the buffer.
*/
constructor( scope, bufferType, bufferCount = 0 ) {
super( bufferType );
/**
* The buffer type.
*
* @type {string}
*/
this.bufferType = bufferType;
/**
* The buffer count.
*
* @type {number}
* @default 0
*/
this.bufferCount = bufferCount;
/**
* Whether the node is atomic or not.
*
* @type {boolean}
* @default false
*/
this.isAtomic = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWorkgroupInfoNode = true;
/**
* The data type of the array buffer.
*
* @type {string}
*/
this.elementType = bufferType;
/**
* The address space the buffer is scoped to. Currently only `'Workgroup'` is used.
*
* @type {string}
*/
this.scope = scope;
/**
* The name of the workgroup scoped buffer.
*
* @type {string}
* @default ''
*/
this.name = '';
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Sets the name of this node.
*
* @param {string} name - The name to set.
* @return {WorkgroupInfoNode} A reference to this node.
*/
setName( name ) {
this.name = name;
return this;
}
/**
* Sets the name/label of this node.
*
* @deprecated
* @param {string} name - The name to set.
* @return {WorkgroupInfoNode} A reference to this node.
*/
label( name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() ); // @deprecated r179
return this.setName( name );
}
/**
* Sets the scope of this node.
*
* @param {string} scope - The scope to set.
* @return {WorkgroupInfoNode} A reference to this node.
*/
setScope( scope ) {
this.scope = scope;
return this;
}
/**
* Defines whether the node is atomic or not.
*
* @param {boolean} value - The atomic flag.
* @return {WorkgroupInfoNode} A reference to this node.
*/
setAtomic( value ) {
this.isAtomic = value;
return this;
}
/**
* Convenience method for making this node atomic.
*
* @return {WorkgroupInfoNode} A reference to this node.
*/
toAtomic() {
return this.setAtomic( true );
}
/**
* The data type of the array buffer.
*
* @return {string} The element type.
*/
getElementType() {
return this.elementType;
}
/**
* Overwrites the default implementation since the input type
* is inferred from the scope.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( /*builder*/ ) {
return `${this.scope}Array`;
}
/**
* This method can be used to access elements via an index node.
*
* @param {IndexNode} indexNode - indexNode.
* @return {WorkgroupInfoElementNode} A reference to an element.
*/
element( indexNode ) {
return new WorkgroupInfoElementNode( this, indexNode );
}
generate( builder ) {
if ( builder.shaderStage !== 'compute' ) {
error( 'TSL: "workgroupArray()" can only be executed within the compute shader stage' );
}
const name = ( this.name !== '' ) ? this.name : `${this.scope}Array_${this.id}`;
return builder.getScopedArray( name, this.scope.toLowerCase(), this.bufferType, this.bufferCount, this.isAtomic );
}
}
/**
* TSL function for creating a workgroup info node.
* Creates a new 'workgroup' scoped array buffer.
*
* @tsl
* @function
* @param {string} type - The data type of a 'workgroup' scoped buffer element.
* @param {number} [count=0] - The number of elements in the buffer.
* @returns {WorkgroupInfoNode}
*/
const workgroupArray = ( type, count ) => new WorkgroupInfoNode( 'Workgroup', type, count );
/**
* `AtomicFunctionNode` represents any function that can operate on atomic variable types
* within a shader. In an atomic function, any modification to an atomic variable will
* occur as an indivisible step with a defined order relative to other modifications.
* Accordingly, even if multiple atomic functions are modifying an atomic variable at once
* atomic operations will not interfere with each other.
*
* This node can only be used with a WebGPU backend.
*
* @augments Node
*/
class AtomicFunctionNode extends Node {
static get type() {
return 'AtomicFunctionNode';
}
/**
* Constructs a new atomic function node.
*
* @param {string} method - The signature of the atomic function to construct.
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
*/
constructor( method, pointerNode, valueNode ) {
super( 'uint' );
/**
* The signature of the atomic function to construct.
*
* @type {string}
*/
this.method = method;
/**
* An atomic variable or element of an atomic buffer.
*
* @type {Node}
*/
this.pointerNode = pointerNode;
/**
* A value that modifies the atomic variable.
*
* @type {Node}
*/
this.valueNode = valueNode;
/**
* Creates a list of the parents for this node for detecting if the node needs to return a value.
*
* @type {boolean}
* @default true
*/
this.parents = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Overwrites the default implementation to return the type of
* the pointer node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The input type.
*/
getInputType( builder ) {
return this.pointerNode.getNodeType( builder );
}
/**
* Overwritten since the node type is inferred from the input type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The node type.
*/
generateNodeType( builder ) {
return this.getInputType( builder );
}
generate( builder ) {
const method = this.method;
if ( builder.shaderStage === 'vertex' ) {
error( `TSL: "${this.method}" is not supported in the vertex stage.` );
}
const properties = builder.getNodeProperties( this );
const parents = properties.parents;
const type = this.getNodeType( builder );
const inputType = this.getInputType( builder );
const a = this.pointerNode;
const b = this.valueNode;
const params = [];
params.push( `&${ a.build( builder, inputType ) }` );
if ( b !== null ) {
params.push( b.build( builder, inputType ) );
}
const methodSnippet = `${ builder.getMethod( method, type ) }( ${ params.join( ', ' ) } )`;
const isVoid = parents ? ( parents.length === 1 && parents[ 0 ].isStackNode === true ) : false;
if ( isVoid ) {
builder.addLineFlowCode( methodSnippet, this );
} else {
// The result is stored in a constant, declared in the block where the operation is generated.
const nodeVar = builder.getVarFromNode( this, null, type, undefined, true, true );
const propertyName = builder.getPropertyName( nodeVar );
builder.addLineFlowCode( `${ builder.generateLetStatement( type, propertyName ) } = ${ methodSnippet }`, this );
return propertyName;
}
}
static get ATOMIC_LOAD() {
return 'atomicLoad';
}
static get ATOMIC_STORE() {
return 'atomicStore';
}
static get ATOMIC_ADD() {
return 'atomicAdd';
}
static get ATOMIC_SUB() {
return 'atomicSub';
}
static get ATOMIC_MAX() {
return 'atomicMax';
}
static get ATOMIC_MIN() {
return 'atomicMin';
}
static get ATOMIC_AND() {
return 'atomicAnd';
}
static get ATOMIC_OR() {
return 'atomicOr';
}
static get ATOMIC_XOR() {
return 'atomicXor';
}
}
/**
* TSL function for creating an atomic function node.
*
* @tsl
* @function
* @param {string} method - The signature of the atomic function to construct.
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicNode = /*@__PURE__*/ nodeProxy( AtomicFunctionNode );
/**
* TSL function for appending an atomic function call into the programmatic flow of a compute shader.
*
* @tsl
* @function
* @param {string} method - The signature of the atomic function to construct.
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicFunc = ( method, pointerNode, valueNode ) => {
return atomicNode( method, pointerNode, valueNode ).toStack();
};
/**
* Loads the value stored in the atomic variable.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @returns {AtomicFunctionNode}
*/
const atomicLoad = ( pointerNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_LOAD, pointerNode, null );
/**
* Stores a value in the atomic variable.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicStore = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_STORE, pointerNode, valueNode );
/**
* Increments the value stored in the atomic variable.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicAdd = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_ADD, pointerNode, valueNode );
/**
* Decrements the value stored in the atomic variable.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicSub = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_SUB, pointerNode, valueNode );
/**
* Stores in an atomic variable the maximum between its current value and a parameter.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicMax = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_MAX, pointerNode, valueNode );
/**
* Stores in an atomic variable the minimum between its current value and a parameter.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicMin = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_MIN, pointerNode, valueNode );
/**
* Stores in an atomic variable the bitwise AND of its value with a parameter.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicAnd = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_AND, pointerNode, valueNode );
/**
* Stores in an atomic variable the bitwise OR of its value with a parameter.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicOr = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_OR, pointerNode, valueNode );
/**
* Stores in an atomic variable the bitwise XOR of its value with a parameter.
*
* @tsl
* @function
* @param {Node} pointerNode - An atomic variable or element of an atomic buffer.
* @param {Node} valueNode - The value that mutates the atomic variable.
* @returns {AtomicFunctionNode}
*/
const atomicXor = ( pointerNode, valueNode ) => atomicFunc( AtomicFunctionNode.ATOMIC_XOR, pointerNode, valueNode );
/**
* This class represents a set of built in WGSL shader functions that sync
* synchronously execute an operation across a subgroup, or 'warp', of compute
* or fragment shader invocations within a workgroup. Typically, these functions
* will synchronously execute an operation using data from all active invocations
* within the subgroup, then broadcast that result to all active invocations. In
* other graphics APIs, subgroup functions are also referred to as wave intrinsics
* (DirectX/HLSL) or warp intrinsics (CUDA).
*
* @augments Node
*/
class SubgroupFunctionNode extends Node {
static get type() {
return 'SubgroupFunctionNode';
}
/**
* Constructs a new function node.
*
* @param {string} method - The subgroup/wave intrinsic method to construct.
* @param {Node} [aNode=null] - The method's first argument.
* @param {Node} [bNode=null] - The method's second argument.
*/
constructor( method, aNode = null, bNode = null ) {
super();
/**
* The subgroup/wave intrinsic method to construct.
*
* @type {string}
*/
this.method = method;
/**
* The method's first argument.
*
* @type {Node}
*/
this.aNode = aNode;
/**
* The method's second argument.
*
* @type {Node}
*/
this.bNode = bNode;
}
getInputType( builder ) {
const aType = this.aNode ? this.aNode.getNodeType( builder ) : null;
const bType = this.bNode ? this.bNode.getNodeType( builder ) : null;
const aLen = builder.isMatrix( aType ) ? 0 : builder.getTypeLength( aType );
const bLen = builder.isMatrix( bType ) ? 0 : builder.getTypeLength( bType );
if ( aLen > bLen ) {
return aType;
} else {
return bType;
}
}
setup( builder ) {
builder.enableSubGroups();
return super.setup( builder );
}
generateNodeType( builder ) {
const method = this.method;
if ( method === SubgroupFunctionNode.SUBGROUP_ELECT ) {
return 'bool';
} else if ( method === SubgroupFunctionNode.SUBGROUP_BALLOT ) {
return 'uvec4';
} else {
return this.getInputType( builder );
}
}
generate( builder, output ) {
const method = this.method;
if ( builder.shaderStage === 'vertex' ) {
error( `TSL: "${this.method}" is not supported in the vertex shader stage.` );
}
const type = this.getNodeType( builder );
const inputType = this.getInputType( builder );
const a = this.aNode;
const b = this.bNode;
const params = [];
if (
method === SubgroupFunctionNode.SUBGROUP_BROADCAST ||
method === SubgroupFunctionNode.SUBGROUP_SHUFFLE ||
method === SubgroupFunctionNode.QUAD_BROADCAST
) {
const bType = b.getNodeType( builder );
params.push(
a.build( builder, type ),
b.build( builder, bType === 'float' ? 'int' : type )
);
} else if (
method === SubgroupFunctionNode.SUBGROUP_SHUFFLE_XOR ||
method === SubgroupFunctionNode.SUBGROUP_SHUFFLE_DOWN ||
method === SubgroupFunctionNode.SUBGROUP_SHUFFLE_UP
) {
params.push(
a.build( builder, type ),
b.build( builder, 'uint' )
);
} else {
if ( a !== null ) params.push( a.build( builder, inputType ) );
if ( b !== null ) params.push( b.build( builder, inputType ) );
}
const paramsString = params.length === 0 ? '()' : `( ${params.join( ', ' )} )`;
return builder.format( `${ builder.getMethod( method, type ) }${paramsString}`, type, output );
}
serialize( data ) {
super.serialize( data );
data.method = this.method;
}
deserialize( data ) {
super.deserialize( data );
this.method = data.method;
}
// 0 inputs
static get SUBGROUP_ELECT() {
return 'subgroupElect';
}
// 1 input
static get SUBGROUP_BALLOT() {
return 'subgroupBallot';
}
static get SUBGROUP_ADD() {
return 'subgroupAdd';
}
static get SUBGROUP_INCLUSIVE_ADD() {
return 'subgroupInclusiveAdd';
}
static get SUBGROUP_EXCLUSIVE_ADD() {
return 'subgroupExclusiveAdd';
}
static get SUBGROUP_MUL() {
return 'subgroupMul';
}
static get SUBGROUP_INCLUSIVE_MUL() {
return 'subgroupInclusiveMul';
}
static get SUBGROUP_EXCLUSIVE_MUL() {
return 'subgroupExclusiveMul';
}
static get SUBGROUP_AND() {
return 'subgroupAnd';
}
static get SUBGROUP_OR() {
return 'subgroupOr';
}
static get SUBGROUP_XOR() {
return 'subgroupXor';
}
static get SUBGROUP_MIN() {
return 'subgroupMin';
}
static get SUBGROUP_MAX() {
return 'subgroupMax';
}
static get SUBGROUP_ALL() {
return 'subgroupAll';
}
static get SUBGROUP_ANY() {
return 'subgroupAny';
}
static get SUBGROUP_BROADCAST_FIRST() {
return 'subgroupBroadcastFirst';
}
static get QUAD_SWAP_X() {
return 'quadSwapX';
}
static get QUAD_SWAP_Y() {
return 'quadSwapY';
}
static get QUAD_SWAP_DIAGONAL() {
return 'quadSwapDiagonal';
}
// 2 inputs
static get SUBGROUP_BROADCAST() {
return 'subgroupBroadcast';
}
static get SUBGROUP_SHUFFLE() {
return 'subgroupShuffle';
}
static get SUBGROUP_SHUFFLE_XOR() {
return 'subgroupShuffleXor';
}
static get SUBGROUP_SHUFFLE_UP() {
return 'subgroupShuffleUp';
}
static get SUBGROUP_SHUFFLE_DOWN() {
return 'subgroupShuffleDown';
}
static get QUAD_BROADCAST() {
return 'quadBroadcast';
}
}
/**
* Returns true if this invocation has the lowest subgroup_invocation_id
* among active invocations in the subgroup.
*
* @tsl
* @method
* @return {bool} The result of the computation.
*/
const subgroupElect = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_ELECT ).setParameterLength( 0 );
/**
* Returns a set of bitfields where the bit corresponding to subgroup_invocation_id
* is 1 if pred is true for that active invocation and 0 otherwise.
*
* @tsl
* @method
* @param {bool} pred - A boolean that sets the bit corresponding to the invocations subgroup invocation id.
* @return {vec4<u32>}- A bitfield corresponding to the pred value of each subgroup invocation.
*/
const subgroupBallot = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_BALLOT ).setParameterLength( 1 );
/**
* A reduction that adds e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The accumulated result of the reduction operation.
*/
const subgroupAdd = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_ADD ).setParameterLength( 1 );
/**
* An inclusive scan returning the sum of e for all active invocations with subgroup_invocation_id less than or equal to this invocation.
*
* @tsl
* @method
* @param {number} e - The value provided to the inclusive scan by the current invocation.
* @return {number} The accumulated result of the inclusive scan operation.
*/
const subgroupInclusiveAdd = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_INCLUSIVE_ADD ).setParameterLength( 1 );
/**
* An exclusive scan that returns the sum of e for all active invocations with subgroup_invocation_id less than this invocation.
*
* @tsl
* @method
* @param {number} e - The value provided to the exclusive scan by the current invocation.
* @return {number} The accumulated result of the exclusive scan operation.
*/
const subgroupExclusiveAdd = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_EXCLUSIVE_ADD ).setParameterLength( 1 );
/**
* A reduction that multiplies e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The accumulated result of the reduction operation.
*/
const subgroupMul = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_MUL ).setParameterLength( 1 );
/**
* An inclusive scan returning the product of e for all active invocations with subgroup_invocation_id less than or equal to this invocation.
*
* @tsl
* @method
* @param {number} e - The value provided to the inclusive scan by the current invocation.
* @return {number} The accumulated result of the inclusive scan operation.
*/
const subgroupInclusiveMul = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_INCLUSIVE_MUL ).setParameterLength( 1 );
/**
* An exclusive scan that returns the product of e for all active invocations with subgroup_invocation_id less than this invocation.
*
* @tsl
* @method
* @param {number} e - The value provided to the exclusive scan by the current invocation.
* @return {number} The accumulated result of the exclusive scan operation.
*/
const subgroupExclusiveMul = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_EXCLUSIVE_MUL ).setParameterLength( 1 );
/**
* A reduction that performs a bitwise and of e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The result of the reduction operation.
*/
const subgroupAnd = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_AND ).setParameterLength( 1 );
/**
* A reduction that performs a bitwise or of e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The result of the reduction operation.
*/
const subgroupOr = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_OR ).setParameterLength( 1 );
/**
* A reduction that performs a bitwise xor of e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The result of the reduction operation.
*/
const subgroupXor = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_XOR ).setParameterLength( 1 );
/**
* A reduction that performs a min of e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The result of the reduction operation.
*/
const subgroupMin = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_MIN ).setParameterLength( 1 );
/**
* A reduction that performs a max of e among all active invocations and returns that result.
*
* @tsl
* @method
* @param {number} e - The value provided to the reduction by the current invocation.
* @return {number} The result of the reduction operation.
*/
const subgroupMax = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_MAX ).setParameterLength( 1 );
/**
* Returns true if e is true for all active invocations in the subgroup.
*
* @tsl
* @method
* @param {boolean} e - The predicate provided by the current invocation.
* @return {bool} The result of the computation.
*/
const subgroupAll = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_ALL ).setParameterLength( 1 );
/**
* Returns true if e is true for any active invocation in the subgroup
*
* @tsl
* @method
* @param {boolean} e - The predicate provided by the current invocation.
* @return {bool} The result of the computation.
*/
const subgroupAny = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_ANY ).setParameterLength( 1 );
/**
* Broadcasts e from the active invocation with the lowest subgroup_invocation_id in the subgroup to all other active invocations.
*
* @tsl
* @method
* @param {number} e - The value to broadcast from the lowest subgroup invocation.
* @return {number} The broadcast value.
*/
const subgroupBroadcastFirst = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_BROADCAST_FIRST ).setParameterLength( 1 );
/**
* Swaps e between invocations in the quad in the X direction.
*
* @tsl
* @method
* @param {number} e - The value to swap from the current invocation.
* @return {number} The value received from the swap operation.
*/
const quadSwapX = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.QUAD_SWAP_X ).setParameterLength( 1 );
/**
* Swaps e between invocations in the quad in the Y direction.
*
* @tsl
* @method
* @param {number} e - The value to swap from the current invocation.
* @return {number} The value received from the swap operation.
*/
const quadSwapY = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.QUAD_SWAP_Y ).setParameterLength( 1 );
/**
* Swaps e between invocations in the quad diagonally.
*
* @tsl
* @method
* @param {number} e - The value to swap from the current invocation.
* @return {number} The value received from the swap operation.
*/
const quadSwapDiagonal = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.QUAD_SWAP_DIAGONAL ).setParameterLength( 1 );
/**
* Broadcasts e from the invocation whose subgroup_invocation_id matches id, to all active invocations.
*
* @tsl
* @method
* @param {number} e - The value to broadcast from subgroup invocation 'id'.
* @param {number} id - The subgroup invocation to broadcast from.
* @return {number} The broadcast value.
*/
const subgroupBroadcast = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_BROADCAST ).setParameterLength( 2 );
/**
* Returns v from the active invocation whose subgroup_invocation_id matches id
*
* @tsl
* @method
* @param {number} v - The value to return from subgroup invocation id^mask.
* @param {number} id - The subgroup invocation which returns the value v.
* @return {number} The broadcast value.
*/
const subgroupShuffle = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_SHUFFLE ).setParameterLength( 2 );
/**
* Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id ^ mask.
*
* @tsl
* @method
* @param {number} v - The value to return from subgroup invocation id^mask.
* @param {number} mask - A bitmask that determines the target invocation via a XOR operation.
* @return {number} The broadcast value.
*/
const subgroupShuffleXor = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_SHUFFLE_XOR ).setParameterLength( 2 );
/**
* Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id - delta
*
* @tsl
* @method
* @param {number} v - The value to return from subgroup invocation id^mask.
* @param {number} delta - A value that offsets the current in.
* @return {number} The broadcast value.
*/
const subgroupShuffleUp = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_SHUFFLE_UP ).setParameterLength( 2 );
/**
* Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id + delta
*
* @tsl
* @method
* @param {number} v - The value to return from subgroup invocation id^mask.
* @param {number} delta - A value that offsets the current subgroup invocation.
* @return {number} The broadcast value.
*/
const subgroupShuffleDown = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.SUBGROUP_SHUFFLE_DOWN ).setParameterLength( 2 );
/**
* Broadcasts e from the quad invocation with id equal to id.
*
* @tsl
* @method
* @param {number} e - The value to broadcast.
* @param {number} id - The quad invocation id to broadcast from.
* @return {number} The broadcast value.
*/
const quadBroadcast = /*@__PURE__*/ nodeProxyAssignable( SubgroupFunctionNode, SubgroupFunctionNode.QUAD_BROADCAST ).setParameterLength( 2 );
let uniformsLib;
function getLightData( light ) {
uniformsLib = uniformsLib || new WeakMap();
let uniforms = uniformsLib.get( light );
if ( uniforms === undefined ) uniformsLib.set( light, uniforms = {} );
return uniforms;
}
/**
* TSL function for getting a shadow matrix uniform node for the given light.
*
* @tsl
* @function
* @param {Light} light -The light source.
* @returns {UniformNode<mat4>} The shadow matrix uniform node.
*/
function lightShadowMatrix( light ) {
const data = getLightData( light );
return data.shadowMatrix || ( data.shadowMatrix = uniform( 'mat4' ).setGroup( renderGroup ).onRenderUpdate( ( frame ) => {
// normally, shadow matrices are updated in ShadowNode. However, if the shadow matrix is used outside
// of shadow rendering (like in ProjectorLightNode), the shadow matrix still requires an update
if ( light.castShadow !== true || frame.renderer.shadowMap.enabled === false ) {
if ( light.shadow.camera.coordinateSystem !== frame.camera.coordinateSystem ) {
light.shadow.camera.coordinateSystem = frame.camera.coordinateSystem;
light.shadow.camera.updateProjectionMatrix();
}
light.shadow.updateMatrices( light );
}
return light.shadow.matrix;
} ) );
}
/**
* TSL function for getting projected uv coordinates for the given light.
* Relevant when using maps with spot lights.
*
* @tsl
* @function
* @param {Light} light -The light source.
* @param {Node<vec3>} [position=positionWorld] -The position to project.
* @returns {Node<vec3>} The projected uvs.
*/
function lightProjectionUV( light, position = positionWorld ) {
const spotLightCoord = lightShadowMatrix( light ).mul( position );
const projectionUV = spotLightCoord.xyz.div( spotLightCoord.w );
return projectionUV;
}
/**
* TSL function for getting the position in world space for the given light.
*
* @tsl
* @function
* @param {Light} light -The light source.
* @returns {UniformNode<vec3>} The light's position in world space.
*/
function lightPosition( light ) {
const data = getLightData( light );
return data.position || ( data.position = uniform( new Vector3() ).setGroup( renderGroup ).onRenderUpdate( ( _, self ) => self.value.setFromMatrixPosition( light.matrixWorld ) ) );
}
/**
* TSL function for getting the light target position in world space for the given light.
*
* @tsl
* @function
* @param {Light} light -The light source.
* @returns {UniformNode<vec3>} The light target position in world space.
*/
function lightTargetPosition( light ) {
const data = getLightData( light );
return data.targetPosition || ( data.targetPosition = uniform( new Vector3() ).setGroup( renderGroup ).onRenderUpdate( ( _, self ) => self.value.setFromMatrixPosition( light.target.matrixWorld ) ) );
}
/**
* TSL function for getting the position in view space for the given light.
*
* @tsl
* @function
* @param {Light} light - The light source.
* @returns {Node<vec3>} The light's position in view space.
*/
function lightViewPosition( light ) {
const data = getLightData( light );
return data.viewPosition || ( data.viewPosition = ( Fn( ( { camera } ) => {
// Each sub-camera of an array camera has its own view space.
if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
return cameraViewMatrix.mul( vec4( lightPosition( light ), 1 ) ).xyz;
}
return data.viewPositionUniform || ( data.viewPositionUniform = uniform( new Vector3() ).setGroup( renderGroup ).onRenderUpdate( ( { camera }, self ) => {
self.value = self.value || new Vector3();
self.value.setFromMatrixPosition( light.matrixWorld );
self.value.applyMatrix4( camera.matrixWorldInverse );
} ) );
} ).once() )() );
}
/**
* TSL function for getting the light target direction for the given light.
*
* @tsl
* @function
* @param {Light} light -The light source.
* @returns {Node<vec3>} The light's target direction.
*/
const lightTargetDirection = ( light ) => cameraViewMatrix.transformDirection( lightPosition( light ).sub( lightTargetPosition( light ) ) );
/**
* A node representing the total diffuse light.
*
* @type {Node<vec3>}
*/
const totalDiffuse = property( 'vec3', 'totalDiffuse' );
/**
* A node representing the total specular light.
*
* @type {Node<vec3>}
*/
const totalSpecular = property( 'vec3', 'totalSpecular' );
/**
* A node representing the outgoing light.
*
* @type {Node<vec3>}
*/
const outgoingLight = property( 'vec3', 'outgoingLight' );
/**
* Sorts an array of lights in ascending order by their IDs.
*
* @private
* @param {Array<Light>} lights - The array of lights to sort.
* @return {Array<Light>} The sorted array of lights.
*/
const sortLights = ( lights ) => {
return lights.sort( ( a, b ) => a.id - b.id );
};
/**
* Finds and returns a lighting node associated with a specific light ID.
*
* @private
* @param {number} id - The ID of the light to search for.
* @param {Array<LightingNode>} lightNodes - The array of lighting nodes to search within.
* @return {?LightingNode} The matching lighting node, or null if not found.
*/
const getLightNodeById = ( id, lightNodes ) => {
for ( const lightNode of lightNodes ) {
if ( lightNode.isAnalyticLightNode && lightNode.light.id === id ) {
return lightNode;
}
}
return null;
};
/**
* WeakMap cache mapping light objects to their corresponding lighting node instances.
*
* @private
* @type {WeakMap<Light, LightingNode>}
*/
const _lightsNodeRef = /*@__PURE__*/ new WeakMap();
/**
* Array used to temporarily store light IDs and shadow casting states for hashing.
*
* @private
* @type {Array<number>}
*/
const _hashData = [];
/**
* 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 = totalDiffuse;
/**
* A node representing the total specular light.
*
* @type {Node<vec3>}
*/
this.totalSpecularNode = totalSpecular;
/**
* A node representing the outgoing light.
*
* @type {Node<vec3>}
*/
this.outgoingLightNode = outgoingLight;
/**
* An array representing the lights in the scene.
*
* @private
* @type {Array<Light>}
*/
this._lights = [];
/**
* `LightsNode` sets this property to `true` by default.
*
* @type {boolean}
* @default true
*/
this.global = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Overwrites the default {@link Node#customCacheKey} implementation by including
* light data into the cache key.
*
* @return {number} The custom cache key.
*/
customCacheKey() {
const builtinLights = this.getBuiltinLights();
for ( let i = 0; i < builtinLights.length; i ++ ) {
const light = builtinLights[ i ];
_hashData.push( light.id );
_hashData.push( light.castShadow ? 1 : 0 );
if ( light.castShadow === true ) {
// force rebuild on version change to avoid the usage of removed shadow nodes
const lightNode = _lightsNodeRef.get( light );
const lightVersion = ( lightNode ) ? lightNode.version : 0; // light nodes start at version 0, see #35019
_hashData.push( lightVersion );
}
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 );
}
}
const cacheKey = hashArray( _hashData );
_hashData.length = 0;
return cacheKey;
}
/**
* 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 ) {
const nodeData = builder.getDataFromNode( this );
if ( nodeData.lightNodesHash === undefined ) {
const lightNodes = this.setupLightsNode( builder );
nodeData.lightNodes = lightNodes;
const hash = [];
for ( const lightNode of lightNodes ) {
hash.push( lightNode.getHash() );
}
nodeData.lightNodesHash = 'lights-' + hash.join( ',' );
}
return nodeData.lightNodesHash;
}
/**
* 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.
* @return {Array<LightingNode>} The array of lighting nodes.
*/
setupLightsNode( builder ) {
const nodeData = builder.getDataFromNode( this );
const lightNodes = [];
const previousLightNodes = nodeData.lightNodes || null;
const materialLightings = builder.context.materialLightings;
const builtinLights = this.getBuiltinLights();
const lights = sortLights( [ ...materialLightings, ...builtinLights ] );
for ( const light of lights ) {
if ( light.isNode ) {
lightNodes.push( light );
} else {
let lightNode = null;
if ( previousLightNodes !== null ) {
lightNode = getLightNodeById( light.id, previousLightNodes );
}
if ( lightNode === null ) {
const lightNodeClass = light._lightNode;
if ( lightNodeClass === undefined ) {
warn( `LightsNode.setupNodeLights: Light node not found for ${ light.constructor.name }` );
continue;
}
if ( _lightsNodeRef.has( light ) === false ) {
_lightsNodeRef.set( light, new lightNodeClass( light ) );
}
lightNode = _lightsNodeRef.get( light );
}
lightNodes.push( lightNode );
}
}
return lightNodes;
}
/**
* Sets up a direct light in the lighting model.
*
* @param {Object} builder - The builder object containing the context and stack.
* @param {Object} lightNode - The light node.
* @param {Object} lightData - The light object containing color and direction properties.
*/
setupDirectLight( builder, lightNode, lightData ) {
const { lightingModel, reflectedLight } = builder.context;
lightingModel.direct( {
...lightData,
lightNode,
reflectedLight
}, builder );
}
/**
* Sets up a direct rect area light in the lighting model.
*
* @param {Object} builder - The builder object containing the context and stack.
* @param {Object} lightNode - The light node.
* @param {Object} lightData - The light object containing color and area light properties.
*/
setupDirectRectAreaLight( builder, lightNode, lightData ) {
const { lightingModel, reflectedLight } = builder.context;
lightingModel.directRectArea( {
...lightData,
lightNode,
reflectedLight
}, builder );
}
/**
* 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 );
}
if ( builder.context.lightProbeIrradiance !== undefined ) {
builder.context.irradiance.addAssign( builder.context.lightProbeIrradiance );
}
}
getLightNodes( builder ) {
const nodeData = builder.getDataFromNode( this );
if ( nodeData.lightNodes === undefined ) {
nodeData.lightNodes = this.setupLightsNode( builder );
}
return nodeData.lightNodes;
}
/**
* 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 ) {
const currentLightsNode = builder.lightsNode;
builder.lightsNode = this;
let outgoingLightNode = this.outgoingLightNode;
const context = builder.context;
const lightingModel = context.lightingModel;
if ( lightingModel ) {
const { totalDiffuseNode, totalSpecularNode } = this;
context.outgoingLight = outgoingLightNode;
builder.addStack();
lightingModel.start( 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 );
}
}
totalDiffuseNode.assign( totalDiffuse );
totalSpecularNode.assign( directSpecular.add( indirectSpecular ) );
outgoingLightNode.assign( totalDiffuseNode.add( totalSpecularNode ) );
lightingModel.finish( builder );
outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );
}
builder.lightsNode = currentLightsNode;
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;
return this;
}
/**
* Returns an array of the scene's lights.
*
* @return {Array<Light>} The scene's lights.
*/
getLights() {
return this._lights;
}
/**
* Returns an array of the scene's lights.
*
* The light variations are shader-dependent;
* if this array changes, the shader needs to be recreated.
*
* @return {Array<Light>} The scene's lights.
*/
getBuiltinLights() {
return this._lights;
}
/**
* Whether the scene has lights or not.
*
* @type {boolean}
*/
get hasLights() {
return this._lights.length > 0;
}
}
/**
* TSL function for creating an instance of `LightsNode` and configuring
* it with the given array of lights.
*
* @tsl
* @function
* @param {Array<Light>} lights - An array of lights.
* @return {LightsNode} The created lights node.
*/
const lights = ( lights = [] ) => new LightsNode().setLights( lights );
/**
* Data structure for the renderer. It allows defining values
* with chained, hierarchical keys. Keys are meant to be
* objects since the module internally works with Weak Maps
* for performance reasons.
*
* @private
*/
class ChainMap {
/**
* Constructs a new Chain Map.
*/
constructor() {
/**
* A map of Weak Maps by their key length.
*
* @type {Object<number, WeakMap>}
*/
this.weakMaps = {};
}
/**
* Returns the Weak Map for the given keys.
*
* @param {Array<Object>} keys - List of keys.
* @return {WeakMap} The weak map.
*/
_getWeakMap( keys ) {
const length = keys.length;
let weakMap = this.weakMaps[ length ];
if ( weakMap === undefined ) {
weakMap = new WeakMap();
this.weakMaps[ length ] = weakMap;
}
return weakMap;
}
/**
* Returns the value for the given array of keys.
*
* @param {Array<Object>} keys - List of keys.
* @return {any} The value. Returns `undefined` if no value was found.
*/
get( keys ) {
let map = this._getWeakMap( keys );
for ( let i = 0; i < keys.length - 1; i ++ ) {
map = map.get( keys[ i ] );
if ( map === undefined ) return undefined;
}
return map.get( keys[ keys.length - 1 ] );
}
/**
* Sets the value for the given keys.
*
* @param {Array<Object>} keys - List of keys.
* @param {any} value - The value to set.
* @return {ChainMap} A reference to this Chain Map.
*/
set( keys, value ) {
let map = this._getWeakMap( keys );
for ( let i = 0; i < keys.length - 1; i ++ ) {
const key = keys[ i ];
if ( map.has( key ) === false ) map.set( key, new WeakMap() );
map = map.get( key );
}
map.set( keys[ keys.length - 1 ], value );
return this;
}
/**
* Deletes a value for the given keys.
*
* @param {Array<Object>} keys - The keys.
* @return {boolean} Returns `true` if the value has been removed successfully and `false` if the value has not be found.
*/
delete( keys ) {
let map = this._getWeakMap( keys );
for ( let i = 0; i < keys.length - 1; i ++ ) {
map = map.get( keys[ i ] );
if ( map === undefined ) return false;
}
return map.delete( keys[ keys.length - 1 ] );
}
}
const _shadowMaterialLib = /*@__PURE__*/ new WeakMap();
const _shadowRenderObjectLibrary = /*@__PURE__*/ new ChainMap();
const _shadowRenderObjectKeys = [];
/**
* Retrieves or creates a shadow material for the given light source.
*
* This function checks if a shadow material already exists for the provided light.
* If not, it creates a new `NodeMaterial` configured for shadow rendering and stores it
* in the `shadowMaterialLib` for future use.
*
* @private
* @function
* @param {Light} light - The light source for which the shadow material is needed.
* If the light is a point light, a depth node is calculated
* using the linear shadow distance.
* @returns {NodeMaterial} The shadow material associated with the given light.
*/
const _getShadowMaterial = ( light ) => {
let material = _shadowMaterialLib.get( light );
if ( material === undefined ) {
material = new NodeMaterial();
material.colorNode = vec4( 0, 0, 0, 1 );
material.isShadowPassMaterial = true; // Use to avoid other overrideMaterial override material.colorNode unintentionally when using material.shadowNode
material.name = 'ShadowMaterial';
material.blending = NoBlending;
material.fog = false;
_shadowMaterialLib.set( light, material );
}
return material;
};
/**
* Disposes the shadow material for the given light source.
*
* @private
* @param {Light} light - The light source.
*/
const _disposeShadowMaterial = ( light ) => {
const material = _shadowMaterialLib.get( light );
if ( material !== undefined ) {
material.dispose();
_shadowMaterialLib.delete( light );
}
};
/**
* Creates a function to render shadow objects in a scene.
*
* @private
* @function
* @param {Renderer} renderer - The renderer.
* @param {LightShadow} shadow - The light shadow object containing shadow properties.
* @param {number} shadowType - The type of shadow map (e.g., BasicShadowMap).
* @return {shadowRenderObjectFunction} A function that renders shadow objects.
*/
const _getShadowRenderObjectFunction = ( renderer, shadow, shadowType ) => {
_shadowRenderObjectKeys[ 0 ] = renderer;
_shadowRenderObjectKeys[ 1 ] = shadow;
let renderObjectFunction = _shadowRenderObjectLibrary.get( _shadowRenderObjectKeys );
if ( renderObjectFunction === undefined || renderObjectFunction.shadowType !== shadowType ) {
renderObjectFunction = ( object, scene, _camera, geometry, material, group, lightsNode, clippingContext, passId ) => {
if ( object.castShadow === true || ( object.receiveShadow && shadowType === VSMShadowMap ) ) {
object.onBeforeShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group );
renderer.renderObject( object, scene, _camera, geometry, material, group, lightsNode, clippingContext, passId );
object.onAfterShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group );
}
};
renderObjectFunction.shadowType = shadowType;
_shadowRenderObjectLibrary.set( _shadowRenderObjectKeys, renderObjectFunction );
}
_shadowRenderObjectKeys[ 0 ] = null;
_shadowRenderObjectKeys[ 1 ] = null;
return renderObjectFunction;
};
/**
* Base class for all shadow nodes.
*
* Shadow nodes encapsulate shadow related logic and are always coupled to lighting nodes.
* Lighting nodes might share the same shadow node type or use specific ones depending on
* their requirements.
*
* @augments Node
*/
class ShadowBaseNode extends Node {
static get type() {
return 'ShadowBaseNode';
}
/**
* Constructs a new shadow base node.
*
* @param {Light} light - The shadow casting light.
*/
constructor( light ) {
super();
/**
* The shadow casting light.
*
* @type {Light}
*/
this.light = light;
/**
* Overwritten since shadows are updated by default per render.
*
* @type {string}
* @default 'render'
*/
this.updateBeforeType = NodeUpdateType.RENDER;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isShadowBaseNode = true;
}
isCacheable( /*builder*/ ) {
return false;
}
/**
* Retrieves or creates a shadow material for the shadow casting light source.
*
* This method checks if a shadow material already exists for the provided light in the internal library.
* If not, it creates a new `NodeMaterial` configured for shadow rendering and stores it for future use.
*
* @return {NodeMaterial} The shadow material associated with the given light.
*/
getShadowMaterial() {
return _getShadowMaterial( this.light );
}
/**
* Disposes the shadow material for the shadow casting light source.
*/
disposeShadowMaterial() {
_disposeShadowMaterial( this.light );
}
/**
* Returns a function to render shadow objects in a scene for the given light shadow and renderer.
*
* @param {Renderer} renderer - The renderer.
* @param {LightShadow} [shadow=this.light.shadow] - The light shadow object containing shadow properties.
* @return {Function} A function that renders shadow objects.
*/
getShadowRenderObjectFunction( renderer, shadow = this.light.shadow ) {
return _getShadowRenderObjectFunction( renderer, shadow, renderer.shadowMap.type );
}
/**
* Setups the shadow position node which is by default the predefined TSL node object `shadowPositionWorld`.
*
* @param {NodeBuilder} object - A configuration object that must at least hold a material reference.
*/
setupShadowPosition( { context, material } ) {
// Use assign inside an Fn()
shadowPositionWorld.assign( material.receivedShadowPositionNode || context.shadowPositionWorld || positionWorld );
}
}
/**
* TSL object that represents the vertex position in world space during the shadow pass.
*
* @tsl
* @type {Node<vec3>}
*/
const shadowPositionWorld = /*@__PURE__*/ property( 'vec3', 'shadowPositionWorld' );
// the uniforms of the shadows used by the filters. the filters are called for each material, the uniforms are
// created once per shadow so the materials share the uniforms of the render group
const _shadowUniforms = /*@__PURE__*/ new WeakMap();
function getShadowUniforms( shadow ) {
let uniforms = _shadowUniforms.get( shadow );
if ( uniforms === undefined ) {
uniforms = {
mapSize: reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ),
radius: reference( 'radius', 'float', shadow ).setGroup( renderGroup )
};
_shadowUniforms.set( shadow, uniforms );
}
return uniforms;
}
/**
* A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map
* with a binary `[0,1]` result.
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
* @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
* @return {Node<float>} The filtering result.
*/
const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, depthLayer } ) => {
let basic = texture( depthTexture, shadowCoord.xy ).setName( 't_basic' );
if ( depthTexture.isArrayTexture ) {
basic = basic.depth( depthLayer );
}
return basic.compare( shadowCoord.z );
} );
/**
* A shadow filtering function performing PCF filtering with Vogel disk sampling and IGN.
*
* Uses 5 samples distributed via Vogel disk pattern, rotated per-pixel using Interleaved
* Gradient Noise (IGN) to break up banding artifacts. Combined with hardware PCF (4-tap
* filtering per sample), this effectively provides 20 filtered taps with better distribution.
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
* @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The filtering result.
*/
const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => {
const depthCompare = ( uv, compare ) => {
let depth = texture( depthTexture, uv );
if ( depthTexture.isArrayTexture ) {
depth = depth.depth( depthLayer );
}
return depth.compare( compare );
};
const { mapSize, radius } = getShadowUniforms( shadow );
const texelSize = vec2( 1 ).div( mapSize );
const radiusScaled = radius.mul( texelSize );
// Use IGN to rotate sampling pattern per pixel (phi = IGN * 2π)
const phi = interleavedGradientNoise( screenCoordinate.xy ).mul( 6.28318530718 );
// 5 samples using Vogel disk distribution
return add(
depthCompare( shadowCoord.xy.add( vogelDiskSample( 0, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vogelDiskSample( 1, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vogelDiskSample( 2, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vogelDiskSample( 3, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vogelDiskSample( 4, 5, phi ).mul( radiusScaled ) ), shadowCoord.z )
).mul( 1 / 5 );
} );
/**
* A shadow filtering function performing VSM filtering.
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
* @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
* @return {Node<float>} The filtering result.
*/
const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, depthLayer }, builder ) => {
let distribution = texture( depthTexture ).sample( shadowCoord.xy );
if ( depthTexture.isArrayTexture ) {
distribution = distribution.depth( depthLayer );
}
distribution = distribution.rg;
const mean = distribution.x;
const variance = max( 0.0000001, distribution.y.mul( distribution.y ) );
const hardShadow = ( builder.renderer.reversedDepthBuffer ) ? step( mean, shadowCoord.z ) : step( shadowCoord.z, mean );
const output = float( 1 ).toVar(); // default, fully lit
If( hardShadow.notEqual( 1.0 ), () => {
// Distance from mean
const d = shadowCoord.z.sub( mean );
// Chebyshev's inequality for upper bound on probability
let p_max = variance.div( variance.add( d.mul( d ) ) );
// Reduce light bleeding by remapping [amount, 1] to [0, 1]
p_max = clamp( sub( p_max, 0.3 ).div( 0.65 ) );
output.assign( max( hardShadow, p_max ) );
} );
return output;
} );
/**
* Represents the shader code for the first VSM render pass.
*
* @private
* @method
* @param {Object} inputs - The input parameter object.
* @param {Node<float>} inputs.samples - The number of samples
* @param {Node<float>} inputs.radius - The radius.
* @param {Node<float>} inputs.size - The size.
* @param {TextureNode} inputs.shadowPass - A reference to the render target's depth data.
* @return {Node<vec2>} The VSM output.
*/
const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass, depthLayer } ) => {
const mean = float( 0 ).toVar( 'meanVertical' );
const squaredMean = float( 0 ).toVar( 'squareMeanVertical' );
const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( -1 ) );
Loop( { start: int( 0 ), end: int( samples ), type: 'int', condition: '<' }, ( { i } ) => {
const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
let depth = shadowPass.sample( add( screenCoordinate.xy, vec2( 0, uvOffset ).mul( radius ) ).div( size ) );
if ( shadowPass.value.isArrayTexture ) {
depth = depth.depth( depthLayer );
}
depth = depth.x;
mean.addAssign( depth );
squaredMean.addAssign( depth.mul( depth ) );
} );
mean.divAssign( samples );
squaredMean.divAssign( samples );
const std_dev = sqrt( squaredMean.sub( mean.mul( mean ) ).max( 0 ) );
return vec2( mean, std_dev );
} );
/**
* Represents the shader code for the second VSM render pass.
*
* @private
* @method
* @param {Object} inputs - The input parameter object.
* @param {Node<float>} inputs.samples - The number of samples
* @param {Node<float>} inputs.radius - The radius.
* @param {Node<float>} inputs.size - The size.
* @param {TextureNode} inputs.shadowPass - The result of the first VSM render pass.
* @return {Node<vec2>} The VSM output.
*/
const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass, depthLayer } ) => {
const mean = float( 0 ).toVar( 'meanHorizontal' );
const squaredMean = float( 0 ).toVar( 'squareMeanHorizontal' );
const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( -1 ) );
Loop( { start: int( 0 ), end: int( samples ), type: 'int', condition: '<' }, ( { i } ) => {
const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
let distribution = shadowPass.sample( add( screenCoordinate.xy, vec2( uvOffset, 0 ).mul( radius ) ).div( size ) );
if ( shadowPass.value.isArrayTexture ) {
distribution = distribution.depth( depthLayer );
}
mean.addAssign( distribution.x );
squaredMean.addAssign( add( distribution.y.mul( distribution.y ), distribution.x.mul( distribution.x ) ) );
} );
mean.divAssign( samples );
squaredMean.divAssign( samples );
const std_dev = sqrt( squaredMean.sub( mean.mul( mean ) ).max( 0 ) );
return vec2( mean, std_dev );
} );
const _shadowFilterLib = [ BasicShadowFilter, PCFShadowFilter, null /* PCFSoftShadowMap, removed */, VSMShadowFilter ];
//
let _rendererState;
const _quadMesh = /*@__PURE__*/ new QuadMesh();
/**
* Represents the default shadow implementation for lighting nodes.
*
* @augments ShadowBaseNode
*/
class ShadowNode extends ShadowBaseNode {
static get type() {
return 'ShadowNode';
}
/**
* Constructs a new shadow node.
*
* @param {Light} light - The shadow casting light.
* @param {?LightShadow} [shadow=null] - An optional light shadow.
*/
constructor( light, shadow = null ) {
super( light );
/**
* The light shadow which defines the properties light's
* shadow.
*
* @type {?LightShadow}
* @default null
*/
this.shadow = shadow || light.shadow;
/**
* A reference to the shadow map which is a render target.
*
* @type {?RenderTarget}
* @default null
*/
this.shadowMap = null;
/**
* Only relevant for VSM shadows. Render target for the
* first VSM render pass.
*
* @type {?RenderTarget}
* @default null
*/
this.vsmShadowMapVertical = null;
/**
* Only relevant for VSM shadows. Render target for the
* second VSM render pass.
*
* @type {?RenderTarget}
* @default null
*/
this.vsmShadowMapHorizontal = null;
/**
* Only relevant for VSM shadows. Node material which
* is used to render the first VSM pass.
*
* @type {?NodeMaterial}
* @default null
*/
this.vsmMaterialVertical = null;
/**
* Only relevant for VSM shadows. Node material which
* is used to render the second VSM pass.
*
* @type {?NodeMaterial}
* @default null
*/
this.vsmMaterialHorizontal = null;
/**
* A reference to the output node which defines the
* final result of this shadow node.
*
* @type {?Node}
* @private
* @default null
*/
this._node = null;
/**
* The current shadow map type of this shadow node.
*
* @type {?number}
* @private
* @default null
*/
this._currentShadowType = null;
/**
* A Weak Map holding the current frame ID per camera. Used
* to control the update of shadow maps.
*
* @type {WeakMap<Camera,number>}
* @private
*/
this._cameraFrameId = new WeakMap();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isShadowNode = true;
/**
* This index can be used when overriding setupRenderTarget with a RenderTarget Array to specify the depth layer.
*
* @type {number}
* @readonly
* @default 0
*/
this.depthLayer = 0;
}
/**
* Setups the shadow filtering.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Object} inputs - A configuration object that defines the shadow filtering.
* @param {Function} inputs.filterFn - This function defines the filtering type of the shadow map e.g. PCF.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
* @param {Node<vec3>} inputs.shadowCoord - Shadow coordinates which are used to sample from the shadow map.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The result node of the shadow filtering.
*/
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow, depthLayer } ) {
const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
.and( shadowCoord.x.lessThanEqual( 1 ) )
.and( shadowCoord.y.greaterThanEqual( 0 ) )
.and( shadowCoord.y.lessThanEqual( 1 ) )
.and( shadowCoord.z.lessThanEqual( 1 ) );
const shadowNode = filterFn( { depthTexture, shadowCoord, shadow, depthLayer } );
return frustumTest.select( shadowNode, float( 1 ) );
}
/**
* Setups the shadow coordinates.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Node<vec3>} shadowPosition - A node representing the shadow position.
* @return {Node<vec3>} The shadow coordinates.
*/
setupShadowCoord( builder, shadowPosition ) {
const { shadow } = this;
const { renderer } = builder;
const bias = shadow.biasNode || reference( 'bias', 'float', shadow ).setGroup( renderGroup );
let shadowCoord = shadowPosition;
let coordZ;
if ( shadow.camera.isOrthographicCamera || renderer.logarithmicDepthBuffer !== true ) {
shadowCoord = shadowCoord.xyz.div( shadowCoord.w );
coordZ = shadowCoord.z;
} else {
const w = shadowCoord.w;
shadowCoord = shadowCoord.xy.div( w ); // <-- Only divide X/Y coords since we don't need Z
// The normally available "cameraNear" and "cameraFar" nodes cannot be used here because they do not get
// updated to use the shadow camera. So, we have to declare our own "local" ones here.
// TODO: How do we get the cameraNear/cameraFar nodes to use the shadow camera so we don't have to declare local ones here?
const cameraNearLocal = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
const cameraFarLocal = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
coordZ = viewZToLogarithmicDepth( w.negate(), cameraNearLocal, cameraFarLocal );
}
shadowCoord = vec3(
shadowCoord.x,
shadowCoord.y.oneMinus(), // follow webgpu standards
renderer.reversedDepthBuffer ? coordZ.sub( bias ) : coordZ.add( bias )
);
return shadowCoord;
}
/**
* Returns the shadow filtering function for the given shadow type.
*
* @param {number} type - The shadow type.
* @return {Function} The filtering function.
*/
getShadowFilterFn( type ) {
return _shadowFilterLib[ type ];
}
setupRenderTarget( shadow, builder ) {
const depthTexture = new DepthTexture( shadow.mapSize.width, shadow.mapSize.height );
depthTexture.name = 'ShadowDepthTexture';
depthTexture.compareFunction = builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare;
const shadowMap = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
shadowMap.texture.name = 'ShadowMap';
shadowMap.texture.type = shadow.mapType;
shadowMap.depthTexture = depthTexture;
return { shadowMap, depthTexture };
}
/**
* Setups the shadow output node.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {Node<vec3>} The shadow output node.
*/
setupShadow( builder ) {
const { renderer, camera } = builder;
const { light, shadow } = this;
const { depthTexture, shadowMap } = this.setupRenderTarget( shadow, builder );
const shadowMapType = renderer.shadowMap.type;
const hasTextureCompare = renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE );
if ( shadowMapType === PCFShadowMap && hasTextureCompare ) {
depthTexture.minFilter = LinearFilter;
depthTexture.magFilter = LinearFilter;
} else {
depthTexture.minFilter = NearestFilter;
depthTexture.magFilter = NearestFilter;
}
shadow.camera.coordinateSystem = camera.coordinateSystem;
shadow.camera._reversedDepth = renderer.reversedDepthBuffer;
shadow.camera.updateProjectionMatrix();
// VSM
if ( shadowMapType === VSMShadowMap && shadow.isPointLightShadow !== true ) {
depthTexture.compareFunction = null; // VSM does not use textureSampleCompare()/texture2DCompare()
if ( shadowMap.depth > 1 ) {
if ( ! shadowMap._vsmShadowMapVertical ) {
shadowMap._vsmShadowMapVertical = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depth: shadowMap.depth, depthBuffer: false } );
shadowMap._vsmShadowMapVertical.texture.name = 'VSMVertical';
}
this.vsmShadowMapVertical = shadowMap._vsmShadowMapVertical;
if ( ! shadowMap._vsmShadowMapHorizontal ) {
shadowMap._vsmShadowMapHorizontal = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depth: shadowMap.depth, depthBuffer: false } );
shadowMap._vsmShadowMapHorizontal.texture.name = 'VSMHorizontal';
}
this.vsmShadowMapHorizontal = shadowMap._vsmShadowMapHorizontal;
} else {
this.vsmShadowMapVertical = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
this.vsmShadowMapHorizontal = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
}
let shadowPassVertical = texture( depthTexture );
if ( depthTexture.isArrayTexture ) {
shadowPassVertical = shadowPassVertical.depth( this.depthLayer );
}
let shadowPassHorizontal = texture( this.vsmShadowMapVertical.texture );
if ( depthTexture.isArrayTexture ) {
shadowPassHorizontal = shadowPassHorizontal.depth( this.depthLayer );
}
const samples = reference( 'blurSamples', 'float', shadow ).setGroup( renderGroup );
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const size = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
const sharedContext = context( builder.getSharedContext() );
let material = this.vsmMaterialVertical || ( this.vsmMaterialVertical = new NodeMaterial() );
material.contextNode = sharedContext;
material.fragmentNode = VSMPassVertical( { samples, radius, size, shadowPass: shadowPassVertical, depthLayer: this.depthLayer } );
material.name = 'VSMVertical';
material = this.vsmMaterialHorizontal || ( this.vsmMaterialHorizontal = new NodeMaterial() );
material.contextNode = sharedContext;
material.fragmentNode = VSMPassHorizontal( { samples, radius, size, shadowPass: shadowPassHorizontal, depthLayer: this.depthLayer } );
material.name = 'VSMHorizontal';
}
//
const shadowIntensity = reference( 'intensity', 'float', shadow ).setGroup( renderGroup );
const normalBias = reference( 'normalBias', 'float', shadow ).setGroup( renderGroup );
const shadowMatrix = lightShadowMatrix( light );
const shadowNormalBias = normalWorld.mul( normalBias );
let shadowPosition;
if ( ! renderer.highPrecision || builder.material.receivedShadowPositionNode || builder.context.shadowPositionWorld ) {
shadowPosition = shadowMatrix.mul( shadowPositionWorld.add( shadowNormalBias ) );
} else {
const highpShadowModelMatrix = uniform( 'mat4' ).onObjectUpdate( ( { object }, self ) => {
return self.value.multiplyMatrices( shadowMatrix.value, object.matrixWorld );
} );
shadowPosition = highpShadowModelMatrix.mul( positionLocal ).add( shadowMatrix.mul( vec4( shadowNormalBias, 0 ) ) );
}
const shadowCoord = this.setupShadowCoord( builder, shadowPosition );
//
const filterFn = shadow.filterNode || this.getShadowFilterFn( renderer.shadowMap.type ) || null;
if ( filterFn === null ) {
throw new Error( 'THREE.WebGPURenderer: Shadow map type not supported yet.' );
}
const shadowDepthTexture = ( shadowMapType === VSMShadowMap && shadow.isPointLightShadow !== true ) ? this.vsmShadowMapHorizontal.texture : depthTexture;
const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow, depthLayer: this.depthLayer } );
let shadowColor;
if ( renderer.shadowMap.transmitted === true ) {
if ( shadowMap.texture.isCubeTexture ) {
// For cube shadow maps (point lights), use cubeTexture with vec3 coordinates
shadowColor = cubeTexture( shadowMap.texture, shadowCoord.xyz );
} else {
shadowColor = texture( shadowMap.texture, shadowCoord );
if ( depthTexture.isArrayTexture ) {
shadowColor = shadowColor.depth( this.depthLayer );
}
}
}
//
let shadowOutput;
if ( shadowColor ) {
shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar();
} else {
shadowOutput = mix( 1, shadowNode, shadowIntensity ).toVar();
}
this.shadowMap = shadowMap;
this.shadow.map = shadowMap;
this.shadow.needsUpdate = true;
// Shadow Output + Inspector
const inspectName = `${ this.light.type } Shadow [ ${ this.light.name || 'ID: ' + this.light.id } ]`;
if ( shadowColor ) {
shadowColor.toInspector( `${ inspectName } / Color`, () => {
if ( this.shadowMap.texture.isCubeTexture ) {
return cubeTexture( this.shadowMap.texture, equirectDirection() );
}
return texture( this.shadowMap.texture );
} );
}
return shadowOutput.toInspector( `${ inspectName } / Depth`, () => {
const shadowCameraNear = reference( 'near', 'float', this.shadow.camera );
const shadowCameraFar = reference( 'far', 'float', this.shadow.camera );
let depthNode;
if ( this.shadowMap.texture.isCubeTexture ) {
depthNode = cubeTexture( this.shadowMap.depthTexture, equirectDirection() ).r;
} else {
depthNode = texture( this.shadowMap.depthTexture ).r;
}
let linearDepth;
if ( this.shadow.camera.isPerspectiveCamera ) {
linearDepth = perspectiveDepthToViewZ( depthNode, shadowCameraNear, shadowCameraFar );
} else {
linearDepth = orthographicDepthToViewZ( depthNode, shadowCameraNear, shadowCameraFar );
}
linearDepth = viewZToOrthographicDepth( linearDepth, shadowCameraNear, shadowCameraFar );
return linearDepth.oneMinus();
} );
}
/**
* The implementation performs the setup of the output node. An output is only
* produces if shadow mapping is globally enabled in the renderer.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {ShaderCallNodeInternal} The output node.
*/
setup( builder ) {
if ( builder.renderer.shadowMap.enabled === false ) return;
return Fn( () => {
const currentShadowType = builder.renderer.shadowMap.type;
if ( this._currentShadowType !== currentShadowType ) {
this._reset();
this._node = null;
}
let node = this._node;
this.setupShadowPosition( builder );
if ( node === null ) {
this._node = node = this.setupShadow( builder );
this._currentShadowType = currentShadowType;
}
if ( builder.material.receivedShadowNode ) {
node = builder.material.receivedShadowNode( node );
}
return node;
} )();
}
/**
* Renders the shadow. The logic of this function could be included
* into {@link ShadowNode#updateShadow} however more specialized shadow
* nodes might require a custom shadow map rendering. By having a
* dedicated method, it's easier to overwrite the default behavior.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
renderShadow( frame ) {
const { shadow, shadowMap, light } = this;
const { renderer, scene } = frame;
shadow.updateMatrices( light );
shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth );
const currentSceneName = scene.name;
scene.name = `Shadow Map [ ${ light.name || 'ID: ' + light.id } ]`;
renderer.render( scene, shadow.camera );
scene.name = currentSceneName;
}
/**
* Updates the shadow.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
updateShadow( frame ) {
const { shadowMap, shadow } = this;
const { renderer, scene, camera } = frame;
const shadowType = renderer.shadowMap.type;
const _shadowCameraLayer = shadow.camera.layers.mask;
if ( ( shadow.camera.layers.mask & 0xFFFFFFFE ) === 0 ) {
shadow.camera.layers.mask = camera.layers.mask;
}
const currentRenderObjectFunction = renderer.getRenderObjectFunction();
_rendererState = resetRendererAndSceneState( renderer, scene, _rendererState );
scene.overrideMaterial = this.getShadowMaterial();
renderer.setRenderObjectFunction( this.getShadowRenderObjectFunction( renderer, this.shadow || this.light.shadow ) );
renderer.setClearColor( 0x000000, 0 );
renderer.setRenderTarget( shadowMap );
const currentMatrixWorldAutoUpdate = scene.matrixWorldAutoUpdate;
// The scene was already updated by the enclosing render.
scene.matrixWorldAutoUpdate = false;
this.renderShadow( frame );
scene.matrixWorldAutoUpdate = currentMatrixWorldAutoUpdate;
renderer.setRenderObjectFunction( currentRenderObjectFunction );
// vsm blur pass
if ( shadowType === VSMShadowMap && shadow.isPointLightShadow !== true ) {
this.vsmPass( renderer );
}
shadow.camera.layers.mask = _shadowCameraLayer;
restoreRendererAndSceneState( renderer, scene, _rendererState );
}
/**
* For VSM additional render passes are required.
*
* @param {Renderer} renderer - A reference to the current renderer.
*/
vsmPass( renderer ) {
const { shadow } = this;
const depth = this.shadowMap.depth;
this.vsmShadowMapVertical.setSize( shadow.mapSize.width, shadow.mapSize.height, depth );
this.vsmShadowMapHorizontal.setSize( shadow.mapSize.width, shadow.mapSize.height, depth );
renderer.setRenderTarget( this.vsmShadowMapVertical );
_quadMesh.material = this.vsmMaterialVertical;
_quadMesh.render( renderer );
renderer.setRenderTarget( this.vsmShadowMapHorizontal );
_quadMesh.material = this.vsmMaterialHorizontal;
_quadMesh.render( renderer );
}
/**
* Frees the internal resources of this shadow node.
*/
dispose() {
this._reset();
super.dispose();
}
/**
* Resets the resouce state of this shadow node.
*
* @private
*/
_reset() {
this._currentShadowType = null;
this.disposeShadowMaterial();
if ( this.shadowMap ) {
this.shadowMap.dispose();
this.shadowMap = null;
}
if ( this.vsmShadowMapVertical !== null ) {
this.vsmShadowMapVertical.dispose();
this.vsmShadowMapVertical = null;
this.vsmMaterialVertical.dispose();
this.vsmMaterialVertical = null;
}
if ( this.vsmShadowMapHorizontal !== null ) {
this.vsmShadowMapHorizontal.dispose();
this.vsmShadowMapHorizontal = null;
this.vsmMaterialHorizontal.dispose();
this.vsmMaterialHorizontal = null;
}
}
/**
* The implementation performs the update of the shadow map if necessary.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
updateBefore( frame ) {
// do not render shadow maps during precompilation
if ( frame.renderer._precompilationState !== null ) return;
const { shadow } = this;
let needsUpdate = shadow.needsUpdate || shadow.autoUpdate;
if ( needsUpdate ) {
if ( this._cameraFrameId.get( frame.camera ) === frame.frameId ) {
needsUpdate = false;
}
this._cameraFrameId.set( frame.camera, frame.frameId );
}
if ( needsUpdate ) {
this.updateShadow( frame );
shadow.needsUpdate = false;
}
}
}
/**
* TSL function for creating an instance of `ShadowNode`.
*
* @tsl
* @function
* @param {Light} light - The shadow casting light.
* @param {?LightShadow} [shadow] - The light shadow.
* @return {ShadowNode} The created shadow node.
*/
const shadow = ( light, shadow ) => new ShadowNode( light, shadow );
const _clearColor$1 = /*@__PURE__*/ new Color();
const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
const _lightPositionWorld = /*@__PURE__*/ new Vector3();
const _lookTarget = /*@__PURE__*/ new Vector3();
// Cube map face directions and up vectors for point light shadows
// Face order: +X, -X, +Y, -Y, +Z, -Z
// WebGPU coordinate system - same face orientations as CubeCamera
const _cubeDirectionsWebGPU = [
/*@__PURE__*/ new Vector3( -1, 0, 0 ), /*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
/*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, -1 )
];
const _cubeUpsWebGPU = [
/*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, -1 ),
/*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 )
];
// WebGL coordinate system - standard OpenGL convention
const _cubeDirectionsWebGL = [
/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( -1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
/*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, -1 )
];
const _cubeUpsWebGL = [
/*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ),
/*@__PURE__*/ new Vector3( 0, 0, -1 ), /*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, -1, 0 )
];
/**
* Returns a function that gives the depth the cube shadow map stores for the point at
* `direction * distance`, from the largest absolute component of the direction. Each face
* stores the depth along its own axis, and perspective depth is `depthA + depthB / max( |x|, |y|, |z| )`,
* so everything but the division is computed once.
*
* @private
* @param {Node<float>} distance - The distance from the light to the fragment.
* @param {Node<float>} near - The near plane of the shadow camera.
* @param {Node<float>} far - The far plane of the shadow camera.
* @param {Node<float>} bias - The shadow bias.
* @param {Renderer} renderer - The renderer.
* @return {function(Node<float>):Node<float>} The function, which takes the largest absolute component of the direction.
*/
const getPointShadowDepthFn = ( distance, near, far, bias, renderer ) => {
const { reversedDepthBuffer, logarithmicDepthBuffer } = renderer;
// logarithmic depth gets a separate depth function
if ( logarithmicDepthBuffer === true ) {
return ( maxAbs ) => viewZToLogarithmicDepth( maxAbs.mul( distance ).negate(), near, far ).add( bias );
}
// default and reverse depth share one
const depthRange = far.sub( near );
const nearFar = near.mul( far ).div( depthRange.mul( distance ) );
let depthA, depthB;
if ( reversedDepthBuffer === true ) {
depthA = near.div( depthRange ).add( bias ).negate().toConst();
depthB = nearFar.toConst();
} else {
depthA = far.div( depthRange ).add( bias ).toConst();
depthB = nearFar.negate().toConst();
}
return ( maxAbs ) => depthA.add( depthB.div( maxAbs ) );
};
const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) => {
return cubeTexture( depthTexture, bd3D ).compare( dp );
} );
/**
* A shadow filtering function for point lights using Vogel disk sampling and IGN.
*
* Uses 5 samples distributed via Vogel disk pattern in tangent space around the
* sample direction, rotated per-pixel using Interleaved Gradient Noise (IGN).
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {CubeDepthTexture} inputs.depthTexture - A reference to the shadow cube map.
* @param {Node<vec3>} inputs.bd3D - The normalized direction from light to fragment.
* @param {Node<float>} inputs.distance - The distance from the light to the fragment.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The filtering result.
*/
const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, distance, shadow }, builder ) => {
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
const near = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
const far = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
const texelSize = radius.div( mapSize.x ).toConst();
// Build a tangent-space coordinate system for applying offsets
const absDir = abs( bd3D );
const tangent = normalize( cross( bd3D, absDir.x.greaterThan( absDir.z ).select( vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ) ) ).toConst();
const bitangent = cross( bd3D, tangent ).toConst();
// Use IGN to rotate sampling pattern per pixel (phi = IGN * 2π)
const phi = interleavedGradientNoise( screenCoordinate.xy ).mul( 6.28318530718 ).toConst();
// Each sample is compared against the depth of the point next to the fragment in its direction,
// on whichever face of the cube it falls, so the comparison is the same on every face.
const getDepth = getPointShadowDepthFn( distance, near, far, bias, builder.renderer );
// Accumulate five samples using Vogel disk distribution in tangent space.
const result = float( 0.0 ).toVar();
Loop( 5, ( { i } ) => {
const sample = vogelDiskSample( i, 5, phi );
const direction = bd3D.add( tangent.mul( sample.x ).add( bitangent.mul( sample.y ) ).mul( texelSize ) ).toConst();
const absDirection = abs( direction ).toConst();
const maxAbs = max( absDirection.x, absDirection.y, absDirection.z ).toConst();
result.addAssign( cubeTexture( depthTexture, direction ).compare( getDepth( maxAbs ) ) );
} );
return result.mul( 1.0 / 5.0 );
} );
const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCoord, shadow }, builder ) => {
// for point lights, the uniform @vShadowCoord is re-purposed to hold
// the vector from the light to the world-space position of the fragment.
const shadowPosition = shadowCoord.xyz.toConst();
const shadowPositionAbs = shadowPosition.abs().toConst();
const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z );
const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near );
const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far );
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
const result = float( 1.0 ).toVar();
If( viewZ.sub( shadowCameraFar ).lessThanEqual( 0.0 ).and( viewZ.sub( shadowCameraNear ).greaterThanEqual( 0.0 ) ), () => {
// bd3D = base direction 3D (direction from light to fragment)
const bd3D = shadowPosition.normalize();
const distance = shadowPosition.length();
const getDepth = getPointShadowDepthFn( distance, shadowCameraNear, shadowCameraFar, bias, builder.renderer );
const dp = getDepth( viewZ.div( distance ) );
// percentage-closer filtering using cube texture sampling
result.assign( filterFn( { depthTexture, bd3D, dp, distance, shadow } ) );
} );
return result;
} );
/**
* Represents the shadow implementation for point light nodes.
*
* @augments ShadowNode
*/
class PointShadowNode extends ShadowNode {
static get type() {
return 'PointShadowNode';
}
/**
* Constructs a new point shadow node.
*
* @param {PointLight} light - The shadow casting point light.
* @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
*/
constructor( light, shadow = null ) {
super( light, shadow );
}
/**
* Overwrites the default implementation to return point light shadow specific
* filtering functions.
*
* @param {number} type - The shadow type.
* @return {Function} The filtering function.
*/
getShadowFilterFn( type ) {
return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;
}
/**
* Overwrites the default implementation so the unaltered shadow position is used.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Node<vec3>} shadowPosition - A node representing the shadow position.
* @return {Node<vec3>} The shadow coordinates.
*/
setupShadowCoord( builder, shadowPosition ) {
return shadowPosition;
}
/**
* Overwrites the default implementation to only use point light specific
* shadow filter functions.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Object} inputs - A configuration object that defines the shadow filtering.
* @param {Function} inputs.filterFn - This function defines the filtering type of the shadow map e.g. PCF.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's depth texture.
* @param {Node<vec3>} inputs.shadowCoord - Shadow coordinates which are used to sample from the shadow map.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The result node of the shadow filtering.
*/
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
return pointShadowFilter( { filterFn, depthTexture, shadowCoord, shadow } );
}
/**
* Overwrites the default implementation to create a CubeRenderTarget with CubeDepthTexture.
*
* @param {LightShadow} shadow - The light shadow object.
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {Object} An object containing the shadow map and depth texture.
*/
setupRenderTarget( shadow, builder ) {
const depthTexture = new CubeDepthTexture( shadow.mapSize.width );
depthTexture.name = 'PointShadowDepthTexture';
depthTexture.compareFunction = builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare;
const shadowMap = builder.createCubeRenderTarget( shadow.mapSize.width );
shadowMap.texture.name = 'PointShadowMap';
shadowMap.depthTexture = depthTexture;
return { shadowMap, depthTexture };
}
/**
* Overwrites the default implementation with point light specific
* rendering code.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
renderShadow( frame ) {
const { shadow, shadowMap, light } = this;
const { renderer, scene } = frame;
const camera = shadow.camera;
const shadowMatrix = shadow.matrix;
// Select cube directions/ups based on coordinate system
const isWebGPU = renderer.coordinateSystem === WebGPUCoordinateSystem;
const cubeDirections = isWebGPU ? _cubeDirectionsWebGPU : _cubeDirectionsWebGL;
const cubeUps = isWebGPU ? _cubeUpsWebGPU : _cubeUpsWebGL;
shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.width );
//
const previousAutoClear = renderer.autoClear;
const previousClearColor = renderer.getClearColor( _clearColor$1 );
const previousClearAlpha = renderer.getClearAlpha();
renderer.autoClear = false;
renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
// Render each cube face
for ( let face = 0; face < 6; face ++ ) {
// Set render target to the specific cube face
renderer.setRenderTarget( shadowMap, face );
renderer.clear();
// Update shadow camera matrices for this face
const far = light.distance || camera.far;
if ( far !== camera.far ) {
camera.far = far;
camera.updateProjectionMatrix();
}
_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
camera.position.copy( _lightPositionWorld );
_lookTarget.copy( camera.position );
_lookTarget.add( cubeDirections[ face ] );
camera.up.copy( cubeUps[ face ] );
camera.lookAt( _lookTarget );
camera.updateMatrixWorld();
shadowMatrix.makeTranslation( - _lightPositionWorld.x, - _lightPositionWorld.y, - _lightPositionWorld.z );
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
shadow._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
//
const currentSceneName = scene.name;
scene.name = `Point Light Shadow [ ${ light.name || 'ID: ' + light.id } ] - Face ${ face + 1 }`;
renderer.render( scene, camera );
scene.name = currentSceneName;
}
//
renderer.autoClear = previousAutoClear;
renderer.setClearColor( previousClearColor, previousClearAlpha );
}
}
/**
* TSL function for creating an instance of `PointShadowNode`.
*
* @tsl
* @function
* @param {PointLight} light - The shadow casting point light.
* @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
* @return {PointShadowNode} The created point shadow node.
*/
const pointShadow = ( light, shadow ) => new PointShadowNode( light, shadow );
/**
* Base class for analytic light nodes.
*
* @augments LightingNode
*/
class AnalyticLightNode extends LightingNode {
static get type() {
return 'AnalyticLightNode';
}
/**
* Constructs a new analytic light node.
*
* @param {?Light} [light=null] - The light source.
*/
constructor( light = null ) {
super();
/**
* The light source.
*
* @type {?Light}
* @default null
*/
this.light = light;
/**
* The light's color value.
*
* @type {Color}
*/
this.color = new Color();
/**
* The light's color node. Points to `colorNode` of the light source, if set. Otherwise
* it creates a uniform node based on {@link AnalyticLightNode#color}.
*
* @type {Node}
*/
this.colorNode = ( light && light.colorNode ) || uniform( this.color ).setGroup( renderGroup );
/**
* This property is used to retain a reference to the original value of {@link AnalyticLightNode#colorNode}.
* The final color node is represented by a different node when using shadows.
*
* @type {?Node}
* @default null
*/
this.baseColorNode = null;
/**
* Represents the light's shadow.
*
* @type {?ShadowNode}
* @default null
*/
this.shadowNode = null;
/**
* Represents the light's shadow color.
*
* @type {?Node}
* @default null
*/
this.shadowColorNode = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isAnalyticLightNode = true;
/**
* Overwritten since analytic light nodes are updated
* once per frame.
*
* @type {string}
* @default 'frame'
*/
this.updateType = NodeUpdateType.FRAME;
if ( light && light.shadow ) {
this._shadowDisposeListener = () => {
this.disposeShadow();
};
light.addEventListener( 'dispose', this._shadowDisposeListener );
}
}
dispose() {
if ( this._shadowDisposeListener ) {
this.light.removeEventListener( 'dispose', this._shadowDisposeListener );
}
super.dispose();
}
/**
* Frees internal resources related to shadows.
*/
disposeShadow() {
if ( this.shadowNode === null ) return;
this.shadowNode.dispose();
this.shadowNode = null;
this.shadowColorNode = null;
this.colorNode = this.baseColorNode;
this.baseColorNode = null;
this.needsUpdate = true;
}
getHash() {
return this.light.uuid;
}
/**
* Returns a node representing a direction vector which points from the current
* position in view space to the light's position in view space.
*
* @param {NodeBuilder} builder - The builder object used for setting up the light.
* @return {Node<vec3>} The light vector node.
*/
getLightVector( builder ) {
return lightViewPosition( this.light ).sub( builder.context.positionView || positionView );
}
/**
* Sets up the direct lighting for the analytic light node.
*
* @abstract
* @param {NodeBuilder} builder - The builder object used for setting up the light.
* @return {Object|undefined} The direct light data (color and direction).
*/
setupDirect( /*builder*/ ) { }
/**
* Sets up the direct rect area lighting for the analytic light node.
*
* @abstract
* @param {NodeBuilder} builder - The builder object used for setting up the light.
* @return {Object|undefined} The direct rect area light data.
*/
setupDirectRectArea( /*builder*/ ) { }
/**
* Setups the shadow node for this light. The method exists so concrete light classes
* can setup different types of shadow nodes.
*
* @return {ShadowNode} The created shadow node.
*/
setupShadowNode() {
return shadow( this.light );
}
/**
* Setups the shadow for this light. This method is only executed if the light
* cast shadows and the current build object receives shadows. It incorporates
* shadows into the lighting computation.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupShadow( builder ) {
const { renderer } = builder;
if ( renderer.shadowMap.enabled === false ) return;
let shadowColorNode = this.shadowColorNode;
if ( shadowColorNode === null ) {
const customShadowNode = this.light.shadow.shadowNode;
let shadowNode;
if ( customShadowNode !== undefined ) {
shadowNode = nodeObject( customShadowNode );
} else {
shadowNode = this.setupShadowNode();
}
this.shadowNode = shadowNode;
this.shadowColorNode = shadowColorNode = this.colorNode.mul( shadowNode );
this.baseColorNode = this.colorNode;
}
//
if ( builder.context.getShadow ) {
shadowColorNode = builder.context.getShadow( this, builder );
}
this.colorNode = shadowColorNode;
}
/**
* Unlike most other nodes, lighting nodes do not return a output node in {@link Node#setup}.
* The main purpose of lighting nodes is to configure the current {@link LightingModel} and/or
* invocate the respective interface methods.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup( builder ) {
this.colorNode = this.baseColorNode || this.colorNode;
if ( this.light.castShadow ) {
if ( builder.object.receiveShadow ) {
this.setupShadow( builder );
}
} else if ( this.shadowNode !== null ) {
this.disposeShadow();
}
const directLightData = this.setupDirect( builder );
const directRectAreaLightData = this.setupDirectRectArea( builder );
if ( directLightData ) {
builder.lightsNode.setupDirectLight( builder, this, directLightData );
}
if ( directRectAreaLightData ) {
builder.lightsNode.setupDirectRectAreaLight( builder, this, directRectAreaLightData );
}
}
/**
* The update method is used to update light uniforms per frame.
* Potentially overwritten in concrete light nodes to update light
* specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( /*frame*/ ) {
const { light } = this;
this.color.copy( light.color ).multiplyScalar( light.intensity );
}
}
/**
* Calculates, at a given distance from a light, the falloff of the light's brightness.
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {Node<float>} inputs.lightDistance - The distance of the light's position to the current fragment position.
* @param {Node<float>} inputs.cutoffDistance - The light's cutoff distance.
* @param {Node<float>} inputs.decayExponent - The light's decay exponent.
* @return {Node<float>} The distance falloff.
*/
const getDistanceAttenuation = /*@__PURE__*/ Fn( ( { lightDistance, cutoffDistance, decayExponent } ) => {
// based upon Frostbite 3 Moving to Physically-based Rendering
// page 32, equation 26: E[window1]
// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
const distanceFalloff = lightDistance.pow( decayExponent ).max( 0.01 ).reciprocal();
return cutoffDistance.greaterThan( 0 ).select(
distanceFalloff.mul( lightDistance.div( cutoffDistance ).pow4().oneMinus().clamp().pow2() ),
distanceFalloff
);
}, { lightDistance: 'float', cutoffDistance: 'float', decayExponent: 'float', return: 'float' } ); // validated
const directPointLight = ( { color, lightVector, cutoffDistance, decayExponent } ) => {
const lightDirection = lightVector.normalize();
const lightDistance = lightVector.length();
const attenuation = getDistanceAttenuation( {
lightDistance,
cutoffDistance,
decayExponent
} );
const lightColor = color.mul( attenuation );
return { lightDirection, lightColor };
};
/**
* Module for representing point lights as nodes.
*
* @augments AnalyticLightNode
*/
class PointLightNode extends AnalyticLightNode {
static get type() {
return 'PointLightNode';
}
/**
* Constructs a new point light node.
*
* @param {?PointLight} [light=null] - The point light source.
*/
constructor( light = null ) {
super( light );
/**
* Uniform node representing the cutoff distance.
*
* @type {UniformNode<float>}
*/
this.cutoffDistanceNode = uniform( 0 ).setGroup( renderGroup );
/**
* Uniform node representing the decay exponent.
*
* @type {UniformNode<float>}
*/
this.decayExponentNode = uniform( 2 ).setGroup( renderGroup );
}
/**
* Overwritten to updated point light specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
const { light } = this;
super.update( frame );
this.cutoffDistanceNode.value = light.distance;
this.decayExponentNode.value = light.decay;
}
/**
* Overwritten to setup point light specific shadow.
*
* @return {PointShadowNode}
*/
setupShadowNode() {
return pointShadow( this.light );
}
setupDirect( builder ) {
return directPointLight( {
color: this.colorNode,
lightVector: this.getLightVector( builder ),
cutoffDistance: this.cutoffDistanceNode,
decayExponent: this.decayExponentNode
} );
}
}
const GOLDEN_ANGLE = 2.399963229728653;
/**
* Returns the mip level of a PMREM that has been prefiltered for the given roughness.
* Uses the inverse of `PMREMGenerator.lodToRoughness()`.
*
* @tsl
* @function
* @param {Node<float>} roughness - The roughness.
* @param {Node<float>} maxLod - The last mip level of the PMREM.
* @return {Node<float>} The mip level.
* @see {@link https://github.com/google/filament/blob/main/shaders/src/surface_light_indirect.fs | Filament: perceptualRoughnessToLod()}
*/
const roughnessToMip = ( roughness, maxLod ) => {
roughness = float( roughness ).clamp();
return float( maxLod ).mul( roughness ).mul( float( 2.0 ).sub( roughness ) );
};
// Gaussian blur using stratified inverse-CDF samples on a golden-angle spiral.
const sphericalGaussianBlur = /*@__PURE__*/ Fn( ( { SAMPLES, sigma, direction, envMap } ) => {
const outputDirection = vec3( direction ).toVar();
const up = select( abs( outputDirection.z ).lessThan( 0.999 ), vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
const tangent = normalize( cross( up, outputDirection ) ).toVar();
const bitangent = cross( outputDirection, tangent ).toVar();
// Truncate the kernel at three standard deviations or at the antipode.
const thetaMax = min( sigma.mul( 3.0 ), Math.PI );
const truncation = exp( thetaMax.mul( thetaMax ).mul( -0.5 ).div( sigma.mul( sigma ) ) ).oneMinus().toVar();
const color = vec3( 0.0 ).toVar();
const accumWeight = float( 0.0 ).toVar();
Loop( { start: int( 0 ), end: SAMPLES }, ( { i } ) => {
// Stratified inverse-CDF sampling of the Gaussian, placed on a golden-angle spiral.
const stratum = float( i ).add( 0.5 ).div( float( SAMPLES ) );
const theta = sigma.mul( sqrt( log( stratum.mul( truncation ).oneMinus() ).mul( -2 ) ) ).toVar();
const phi = float( i ).mul( GOLDEN_ANGLE ).toVar();
const offset = tangent.mul( cos( phi ) ).add( bitangent.mul( sin( phi ) ) );
const sampleDirection = outputDirection.mul( cos( theta ) ).add( offset.mul( sin( theta ) ) );
// Correct the planar sample density to solid angle.
const weight = sin( theta ).div( theta ).toVar();
color.addAssign( envMap.sample( sampleDirection ).level( 0 ).rgb.mul( weight ) );
accumWeight.addAssign( weight );
} );
return vec4( color.div( accumWeight ), 1.0 );
} );
// Van der Corput radical inverse.
const radicalInverse_VdC = /*@__PURE__*/ Fn( ( [ bits_immutable ] ) => {
const bits = uint( bits_immutable ).toVar();
bits.assign( bits.shiftLeft( uint( 16 ) ).bitOr( bits.shiftRight( uint( 16 ) ) ) );
bits.assign( bits.bitAnd( uint( 0x55555555 ) ).shiftLeft( uint( 1 ) ).bitOr( bits.bitAnd( uint( 0xAAAAAAAA ) ).shiftRight( uint( 1 ) ) ) );
bits.assign( bits.bitAnd( uint( 0x33333333 ) ).shiftLeft( uint( 2 ) ).bitOr( bits.bitAnd( uint( 0xCCCCCCCC ) ).shiftRight( uint( 2 ) ) ) );
bits.assign( bits.bitAnd( uint( 0x0F0F0F0F ) ).shiftLeft( uint( 4 ) ).bitOr( bits.bitAnd( uint( 0xF0F0F0F0 ) ).shiftRight( uint( 4 ) ) ) );
bits.assign( bits.bitAnd( uint( 0x00FF00FF ) ).shiftLeft( uint( 8 ) ).bitOr( bits.bitAnd( uint( 0xFF00FF00 ) ).shiftRight( uint( 8 ) ) ) );
return float( bits ).mul( 2.3283064365386963e-10 ); // / 0x100000000
} );
// Hammersley sequence.
const hammersley = /*@__PURE__*/ Fn( ( [ i, N ] ) => {
return vec2( float( i ).div( float( N ) ), radicalInverse_VdC( i ) );
} );
// GGX convolution using VNDF importance sampling. Each sample reads the mip level of the
// source cube map that matches its solid angle (filtered importance sampling), which keeps
// the estimate smooth even for tiny, very bright light sources.
const ggxConvolution = /*@__PURE__*/ Fn( ( { roughness, lodBias, envMap, direction, GGX_SAMPLES } ) => {
const N = vec3( direction ).toVar();
const prefilteredColor = vec3( 0.0 ).toVar();
// For very low roughness, just sample the environment directly
If( roughness.lessThan( 0.001 ), () => {
prefilteredColor.assign( envMap.sample( N ).level( 0 ).rgb );
} ).Else( () => {
const alpha = roughness.mul( roughness ).toConst();
const alpha2 = alpha.mul( alpha ).toConst();
// Tangent space basis for VNDF sampling
const up = select( abs( N.z ).lessThan( 0.999 ), vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
const tangent = normalize( cross( up, N ) ).toVar();
const bitangent = cross( N, tangent ).toVar();
const totalWeight = float( 0.0 ).toVar();
Loop( { start: uint( 0 ), end: GGX_SAMPLES }, ( { i } ) => {
const Xi = hammersley( i, GGX_SAMPLES );
// With V = N, sample the reflected direction directly.
const invQ = float( 1.0 ).div( Xi.x.oneMinus().add( alpha2.mul( Xi.x ) ) ).toConst();
const NdotL = Xi.x.oneMinus().sub( alpha2.mul( Xi.x ) ).mul( invQ ).toConst();
If( NdotL.greaterThan( 0.0 ), () => {
const phi = Xi.y.mul( 2.0 * Math.PI ).toConst();
const sinTheta = alpha.mul( 2.0 ).mul( sqrt( Xi.x.mul( Xi.x.oneMinus() ) ) ).mul( invQ ).toConst();
const L = N.mul( NdotL ).add( tangent.mul( cos( phi ) ).add( bitangent.mul( sin( phi ) ) ).mul( sinTheta ) ).toConst();
// Match the source mip to the sample's solid angle; see lodBias.
const d = alpha2.mul( invQ );
const lod = max( log2( d ).add( lodBias ), 0.0 );
// Weight by NdotL for the split-sum approximation
prefilteredColor.addAssign( envMap.sample( L ).level( lod ).rgb.mul( NdotL ) );
totalWeight.addAssign( NdotL );
} );
} );
prefilteredColor.divAssign( totalWeight );
} );
return vec4( prefilteredColor, 1.0 );
} );
// GGX convolution that weights every texel of a small source mip. Noise free and,
// for the wide lobes of the rough mip levels, cheaper than importance sampling.
const ggxIntegration = /*@__PURE__*/ Fn( ( { roughness, sourceLod, sourceSize, envMap, direction } ) => {
const N = vec3( direction ).toVar();
const alpha = roughness.mul( roughness ).toConst();
const alpha2 = alpha.mul( alpha ).toConst();
const texelSize = float( 2.0 ).div( float( sourceSize ) ).toConst();
const prefilteredColor = vec3( 0.0 ).toVar();
const totalWeight = float( 0.0 ).toVar();
// Pair opposite texels: only the one in N's hemisphere contributes.
Loop( { start: int( 0 ), end: int( 3 ), name: 'face' }, ( { face } ) => {
Loop( { start: int( 0 ), end: sourceSize, name: 'y' }, ( { y } ) => {
Loop( { start: int( 0 ), end: sourceSize, name: 'x' }, ( { x } ) => {
const uv = vec2( x, y ).add( 0.5 ).mul( texelSize ).sub( 1.0 ).toConst();
const texelDirection = select( face.equal( 0 ), vec3( 1.0, uv ), select( face.equal( 1 ), vec3( uv.x, 1.0, uv.y ), vec3( uv, 1.0 ) ) ).toVar();
const invDistance = inverseSqrt( dot( uv, uv ).add( 1.0 ) ).toConst();
const NdotL = dot( N, texelDirection ).toVar();
texelDirection.mulAssign( select( NdotL.lessThan( 0.0 ), -1, 1.0 ) );
NdotL.assign( abs( NdotL ).mul( invDistance ) );
// With V = N, NdotH squared is ( 1 + NdotL ) / 2. Common factors
// in the GGX distribution and texel solid angle cancel when normalized.
const d = alpha2.add( 1.0 ).add( alpha2.sub( 1.0 ).mul( NdotL ) );
const weight = NdotL.mul( invDistance ).mul( invDistance ).mul( invDistance ).div( d.mul( d ) ).toConst();
prefilteredColor.addAssign( envMap.sample( texelDirection ).level( sourceLod ).rgb.mul( weight ) );
totalWeight.addAssign( weight );
} );
} );
} );
return vec4( prefilteredColor.div( totalWeight ), 1.0 );
} );
/**
* This class represents a cube render target. It is a special version
* of `WebGLCubeRenderTarget` which is compatible with `WebGPURenderer`.
*
* @augments RenderTarget
*/
class CubeRenderTarget extends RenderTarget {
/**
* Constructs a new cube render target.
*
* @param {number} [size=1] - The size of the render target.
* @param {RenderTarget~Options} [options] - The configuration object.
*/
constructor( size = 1, options = {} ) {
super( size, size, options );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isCubeRenderTarget = true;
const image = { width: size, height: size, depth: 1 };
const images = [ image, image, image, image, image, image ];
/**
* Overwritten with a different texture type.
*
* @type {DataArrayTexture}
*/
this.texture = new CubeTexture( images );
this._setTextureOptions( options );
// By convention -- likely based on the RenderMan spec from the 1990's -- cube maps are specified by WebGL (and three.js)
// in a coordinate system in which positive-x is to the right when looking up the positive-z axis -- in other words,
// in a left-handed coordinate system. By continuing this convention, preexisting cube maps continued to render correctly.
// three.js uses a right-handed coordinate system. So environment maps used in three.js appear to have px and nx swapped
// and the flag isRenderTargetTexture controls this conversion. The flip is not required when using WebGLCubeRenderTarget.texture
// as a cube texture (this is detected when isRenderTargetTexture is set to true for cube textures).
this.texture.isRenderTargetTexture = true;
}
/**
* Converts the given equirectangular texture to a cube map.
*
* @param {Renderer} renderer - The renderer.
* @param {Texture} texture - The equirectangular texture.
* @return {CubeRenderTarget} A reference to this cube render target.
*/
fromEquirectangularTexture( renderer, texture$1 ) {
const currentMinFilter = texture$1.minFilter;
const currentGenerateMipmaps = texture$1.generateMipmaps;
texture$1.generateMipmaps = true;
this.texture.type = texture$1.type;
this.texture.colorSpace = texture$1.colorSpace;
this.texture.generateMipmaps = texture$1.generateMipmaps;
this.texture.minFilter = texture$1.minFilter;
this.texture.magFilter = texture$1.magFilter;
const geometry = new BoxGeometry( 5, 5, 5 );
const uvNode = equirectUV( positionWorldDirection );
const material = new NodeMaterial();
material.colorNode = texture( texture$1, uvNode, 0 );
material.side = BackSide;
material.blending = NoBlending;
const mesh = new Mesh( geometry, material );
const scene = new Scene();
scene.add( mesh );
// Avoid blurred poles
if ( texture$1.minFilter === LinearMipmapLinearFilter ) texture$1.minFilter = LinearFilter;
const camera = new CubeCamera( 1, 10, this );
const currentMRT = renderer.getMRT();
renderer.setMRT( null );
camera.update( renderer, scene );
renderer.setMRT( currentMRT );
texture$1.minFilter = currentMinFilter;
texture$1.generateMipmaps = currentGenerateMipmaps;
mesh.geometry.dispose();
mesh.material.dispose();
return this;
}
/**
* Clears this cube render target.
*
* @param {Renderer} renderer - The renderer.
* @param {boolean} [color=true] - Whether the color buffer should be cleared or not.
* @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.
* @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.
*/
clear( renderer, color = true, depth = true, stencil = true ) {
const currentRenderTarget = renderer.getRenderTarget();
for ( let i = 0; i < 6; i ++ ) {
renderer.setRenderTarget( this, i );
renderer.clear( color, depth, stencil );
}
renderer.setRenderTarget( currentRenderTarget );
}
}
// Smaller inputs are upsampled so that every PMREM has enough mip levels.
const MIN_SIZE = 256;
// Log2 of the face size of the roughest mip level. Smaller faces can't
// represent the diffuse irradiance that is stored in this level.
const LOD_MIN = 3;
// Spiral samples per pass of the initial fromScene() blur.
const BLUR_SAMPLES = 20;
// GGX VNDF samples for the sharp mip levels.
const GGX_SAMPLES = 256;
// Integrate a small source mip for the rough levels to avoid sampling noise.
const INTEGRATION_SIZE = 16;
const INTEGRATION_LEVELS = 3;
const _origin = /*@__PURE__*/ new Vector3();
const _clearColor = /*@__PURE__*/ new Color();
// maps materials to their uniforms dictionary
const _uniformsMap = new WeakMap();
/**
* Generates a Prefiltered, Mipmapped Radiance Environment Map (PMREM) for
* image-based lighting. The result is a cube render target whose mip levels
* store GGX-filtered radiance at increasing roughness, from a mirror at level 0
* to roughness 1. See {@link PMREMGenerator.lodToRoughness} for the mapping.
*
* Filtering assumes the view direction equals the surface normal. Sharp levels
* use GGX visible normal sampling (Heitz, 2018), while rough levels integrate a
* lower-resolution source cubemap.
*
* @see {@link https://jcgt.org/published/0007/04/01/ | Sampling the GGX Distribution of Visible Normals}
*/
class PMREMGenerator {
/**
* Constructs a new PMREM generator.
*
* @param {Renderer} renderer - The renderer.
*/
constructor( renderer ) {
this._renderer = renderer;
this._cubeSize = 0;
this._sourceTarget = null;
this._cubeCamera = new CubeCamera( 1, 10, null );
this._boxMesh = new Mesh( new BoxGeometry( 5, 5, 5 ), null );
this._cubemapMaterial = null;
this._equirectMaterial = null;
this._blurMaterial = null;
this._ggxMaterial = null;
this._integrationMaterial = null;
}
/**
* Generates a PMREM from a scene, optionally applying a Gaussian blur before
* GGX filtering.
*
* @param {Scene} scene - The scene to be captured.
* @param {number} [sigma=0] - The blur radius in radians.
* @param {number} [near=0.1] - The near plane distance.
* @param {number} [far=100] - The far plane distance.
* @param {Object} [options={}] - The configuration options.
* @param {number} [options.size=256] - The texture size of the PMREM, rounded down to a power of two and at least 256.
* @param {Vector3} [options.position=origin] - The position of the internal cube camera that renders the scene.
* @param {?CubeRenderTarget} [options.renderTarget=null] - The render target to use.
* @return {CubeRenderTarget} The resulting PMREM.
*/
fromScene( scene, sigma = 0, near = 0.1, far = 100, options = {} ) {
const {
size = 256,
position = _origin,
renderTarget = null,
} = options;
const renderer = this._renderer;
this._setSize( size );
if ( renderer.hasInitialized() === false ) {
throw new Error( 'THREE.PMREMGenerator: .fromScene() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
const pmremTarget = renderTarget || this._allocateTarget();
const sourceTarget = this._getSourceTarget( true );
if ( sigma > 0 ) {
sourceTarget.texture.mipmapsAutoUpdate = false;
}
// Clear every captured face, independent of the application's clear settings.
const autoClear = renderer.autoClear;
const autoClearColor = renderer.autoClearColor;
const autoClearDepth = renderer.autoClearDepth;
const autoClearStencil = renderer.autoClearStencil;
const background = scene.background;
renderer.autoClear = true;
renderer.autoClearColor = true;
renderer.autoClearDepth = true;
renderer.autoClearStencil = true;
if ( background === null ) scene.background = renderer.getClearColor( _clearColor );
const cubeCamera = new CubeCamera( near, far, sourceTarget );
cubeCamera.position.copy( position );
cubeCamera.update( renderer, scene );
renderer.autoClear = autoClear;
renderer.autoClearColor = autoClearColor;
renderer.autoClearDepth = autoClearDepth;
renderer.autoClearStencil = autoClearStencil;
scene.background = background;
if ( sigma > 0 ) {
sourceTarget.texture.mipmapsAutoUpdate = true;
this._blur( pmremTarget, sigma );
}
this._applyPMREM( pmremTarget );
return pmremTarget;
}
/**
* Asynchronous version of {@link PMREMGenerator#fromScene}.
*
* @deprecated
* @param {Scene} scene - The scene to be captured.
* @param {number} [sigma=0] - The blur radius in radians.
* @param {number} [near=0.1] - The near plane distance.
* @param {number} [far=100] - The far plane distance.
* @param {Object} [options={}] - The configuration options.
* @param {number} [options.size=256] - The texture size of the PMREM, rounded down to a power of two and at least 256.
* @param {Vector3} [options.position=origin] - The position of the internal cube camera that renders the scene.
* @param {?CubeRenderTarget} [options.renderTarget=null] - The render target to use.
* @return {Promise<CubeRenderTarget>} A Promise that resolves with the PMREM.
* @see {@link PMREMGenerator#fromScene}
*/
async fromSceneAsync( scene, sigma = 0, near = 0.1, far = 100, options = {} ) {
warnOnce( 'PMREMGenerator: ".fromSceneAsync()" is deprecated. Use "await renderer.init()" instead.' ); // @deprecated r181
await this._renderer.init();
return this.fromScene( scene, sigma, near, far, options );
}
/**
* Generates a PMREM from an LDR or HDR equirectangular texture. The cube face
* size is one quarter of the image width, rounded down to a power of two and
* at least 256.
*
* @param {Texture} equirectangular - The equirectangular texture to be converted.
* @param {?CubeRenderTarget} [renderTarget=null] - The render target to use.
* @return {CubeRenderTarget} The resulting PMREM.
* @see {@link PMREMGenerator#fromEquirectangularAsync}
*/
fromEquirectangular( equirectangular, renderTarget = null ) {
if ( this._renderer.hasInitialized() === false ) {
throw new Error( 'THREE.PMREMGenerator: .fromEquirectangular() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
return this._fromTexture( equirectangular, renderTarget );
}
/**
* Asynchronous version of {@link PMREMGenerator#fromEquirectangular}.
*
* @deprecated
* @param {Texture} equirectangular - The equirectangular texture to be converted.
* @param {?CubeRenderTarget} [renderTarget=null] - The render target to use.
* @return {Promise<CubeRenderTarget>} The resulting PMREM.
* @see {@link PMREMGenerator#fromEquirectangular}
*/
async fromEquirectangularAsync( equirectangular, renderTarget = null ) {
warnOnce( 'PMREMGenerator: ".fromEquirectangularAsync()" is deprecated. Use "await renderer.init()" instead.' ); // @deprecated r181
await this._renderer.init();
return this._fromTexture( equirectangular, renderTarget );
}
/**
* Generates a PMREM from an LDR or HDR cubemap. The cube face size matches
* the input, rounded down to a power of two and at least 256.
*
* @param {Texture} cubemap - The cubemap texture to be converted.
* @param {?CubeRenderTarget} [renderTarget=null] - The render target to use.
* @return {CubeRenderTarget} The resulting PMREM.
* @see {@link PMREMGenerator#fromCubemapAsync}
*/
fromCubemap( cubemap, renderTarget = null ) {
if ( this._renderer.hasInitialized() === false ) {
throw new Error( 'THREE.PMREMGenerator: .fromCubemap() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
return this._fromTexture( cubemap, renderTarget );
}
/**
* Asynchronous version of {@link PMREMGenerator#fromCubemap}.
*
* @deprecated
* @param {Texture} cubemap - The cubemap texture to be converted.
* @param {?CubeRenderTarget} [renderTarget=null] - The render target to use.
* @return {Promise<CubeRenderTarget>} The resulting PMREM.
* @see {@link PMREMGenerator#fromCubemap}
*/
async fromCubemapAsync( cubemap, renderTarget = null ) {
warnOnce( 'PMREMGenerator: ".fromCubemapAsync()" is deprecated. Use "await renderer.init()" instead.' ); // @deprecated r181
await this._renderer.init();
return this._fromTexture( cubemap, renderTarget );
}
/**
* Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during
* your texture's network fetch for increased concurrency.
*
* @returns {Promise}
*/
async compileCubemapShader() {
if ( this._cubemapMaterial === null ) {
this._cubemapMaterial = _getCubemapMaterial();
await this._compileMaterial( this._cubemapMaterial );
}
}
/**
* Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during
* your texture's network fetch for increased concurrency.
*
* @returns {Promise}
*/
async compileEquirectangularShader() {
if ( this._equirectMaterial === null ) {
this._equirectMaterial = _getEquirectMaterial();
await this._compileMaterial( this._equirectMaterial );
}
}
/**
* Disposes of the PMREMGenerator's internal memory. The PMREMs it returned
* belong to the caller and are not disposed.
*/
dispose() {
if ( this._sourceTarget !== null ) this._sourceTarget.dispose();
if ( this._cubemapMaterial !== null ) this._cubemapMaterial.dispose();
if ( this._equirectMaterial !== null ) this._equirectMaterial.dispose();
if ( this._blurMaterial !== null ) this._blurMaterial.dispose();
if ( this._ggxMaterial !== null ) this._ggxMaterial.dispose();
if ( this._integrationMaterial !== null ) this._integrationMaterial.dispose();
this._boxMesh.geometry.dispose();
}
/**
* Returns the roughness a mip level of a PMREM has been prefiltered for.
* The inverse mapping is used by the renderers when sampling a PMREM.
*
* @param {number} lod - The mip level.
* @param {number} maxLod - The last mip level of the PMREM.
* @return {number} The roughness.
*/
static lodToRoughness( lod, maxLod ) {
return maxLod > 0 ? 1 - Math.sqrt( 1 - lod / maxLod ) : 0;
}
// private interface
_setSize( cubeSize ) {
this._cubeSize = Math.max( MIN_SIZE, floorPowerOfTwo( cubeSize ) );
}
_fromTexture( texture, renderTarget ) {
if ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping ) {
this._setSize( texture.image.length === 0 ? MIN_SIZE : ( texture.image[ 0 ].width || texture.image[ 0 ].image.width ) );
} else { // Equirectangular
this._setSize( texture.image.width / 4 );
}
const pmremTarget = renderTarget || this._allocateTarget();
this._textureToCubemap( texture );
this._applyPMREM( pmremTarget );
return pmremTarget;
}
_allocateTarget() {
const size = this._cubeSize;
const pmremTarget = _createRenderTarget( size, false, false );
// Allocate only the prefiltered mip levels without automatic mipmap generation.
const maxLod = Math.log2( size ) - LOD_MIN;
for ( let lod = 0; lod <= maxLod; lod ++ ) {
pmremTarget.texture.mipmaps.push( { width: size >> lod, height: size >> lod } );
}
pmremTarget.texture.name = 'PMREM';
pmremTarget.texture.isPMREMTexture = true;
return pmremTarget;
}
_getSourceTarget( depthBuffer = false ) {
const size = this._cubeSize;
const sourceTarget = this._sourceTarget;
if ( sourceTarget === null || sourceTarget.width !== size || ( depthBuffer && sourceTarget.depthBuffer === false ) ) {
if ( sourceTarget !== null ) sourceTarget.dispose();
this._sourceTarget = _createRenderTarget( size, true, depthBuffer );
}
return this._sourceTarget;
}
async _compileMaterial( material ) {
this._boxMesh.material = material;
await this._renderer.compile( this._boxMesh, this._cubeCamera.children[ 0 ] );
}
/**
* Renders the box mesh with the given material into all six faces of a cube render target.
*
* @private
* @param {CubeRenderTarget} target - The render target.
* @param {number} lod - The mip level to render into.
* @param {NodeMaterial} material - The material.
*/
_renderCube( target, lod, material ) {
const boxMesh = this._boxMesh;
const cubeCamera = this._cubeCamera;
boxMesh.material = material;
cubeCamera.renderTarget = target;
cubeCamera.activeMipmapLevel = lod;
cubeCamera.update( this._renderer, boxMesh );
}
_textureToCubemap( texture ) {
let material;
if ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping ) {
if ( this._cubemapMaterial === null ) {
this._cubemapMaterial = _getCubemapMaterial();
}
material = this._cubemapMaterial;
} else {
if ( this._equirectMaterial === null ) {
this._equirectMaterial = _getEquirectMaterial();
}
material = this._equirectMaterial;
}
_uniformsMap.get( material ).envMap.value = texture;
this._renderCube( this._getSourceTarget(), 0, material );
}
/**
* Prefilters the source cubemap into the mip levels of the PMREM. The sharp levels
* integrate the GGX lobe of their roughness with VNDF importance sampling, reading the
* mip level of the source that matches the solid angle of each sample. The rough
* levels weight every texel of a small source mip with the GGX lobe instead.
*
* @private
* @param {CubeRenderTarget} pmremTarget - The PMREM.
*/
_applyPMREM( pmremTarget ) {
if ( this._ggxMaterial === null ) {
this._ggxMaterial = _getGGXMaterial();
this._integrationMaterial = _getIntegrationMaterial();
}
const size = this._cubeSize;
const maxLod = pmremTarget.texture.mipmaps.length - 1;
const ggxUniforms = _uniformsMap.get( this._ggxMaterial );
ggxUniforms.envMap.value = this._sourceTarget.texture;
const integrationUniforms = _uniformsMap.get( this._integrationMaterial );
integrationUniforms.envMap.value = this._sourceTarget.texture;
integrationUniforms.sourceLod.value = Math.log2( size / INTEGRATION_SIZE );
for ( let lod = 0; lod <= maxLod; lod ++ ) {
const roughness = PMREMGenerator.lodToRoughness( lod, maxLod );
if ( lod > maxLod - INTEGRATION_LEVELS ) {
integrationUniforms.roughness.value = roughness;
this._renderCube( pmremTarget, lod, this._integrationMaterial );
} else {
// For V = N, pdf( L ) = D( H ) / 4. Match the source texel solid angle to
// 1 / ( GGX_SAMPLES * pdf ), with a half-mip bias to reduce sampling noise.
// The shader supplies log2 of the GGX denominator.
const lodBias = roughness > 0 ? Math.log2( size ) + 0.5 * Math.log2( 6 / ( GGX_SAMPLES * Math.pow( roughness, 4 ) ) ) + 0.5 : 0;
ggxUniforms.roughness.value = roughness;
ggxUniforms.lodBias.value = lodBias;
this._renderCube( pmremTarget, lod, this._ggxMaterial );
}
}
}
/**
* Applies the initial fromScene() blur in two passes using a golden-angle
* spiral kernel. Level 0 of the PMREM serves as the intermediate target.
*
* @private
* @param {CubeRenderTarget} pmremTarget - The PMREM.
* @param {number} sigma - The blur radius in radians.
*/
_blur( pmremTarget, sigma ) {
if ( this._blurMaterial === null ) {
this._blurMaterial = _getBlurMaterial();
}
const material = this._blurMaterial;
const uniforms = _uniformsMap.get( material );
const sourceTarget = this._sourceTarget;
// Split the blur variance between two passes. Clamp sigma to the sphere's
// maximum angular distance.
uniforms.sigma.value = Math.min( sigma, Math.PI ) / Math.SQRT2;
uniforms.envMap.value = sourceTarget.texture;
this._renderCube( pmremTarget, 0, material );
uniforms.envMap.value = pmremTarget.texture;
this._renderCube( sourceTarget, 0, material );
}
}
function _createRenderTarget( size, generateMipmaps, depthBuffer ) {
return new CubeRenderTarget( size, {
minFilter: LinearMipmapLinearFilter,
generateMipmaps: generateMipmaps,
type: HalfFloatType,
colorSpace: LinearSRGBColorSpace,
depthBuffer: depthBuffer
} );
}
function _getMaterial( type, uniforms, fragmentNode ) {
const material = new NodeMaterial();
material.name = `PMREM_${ type }`;
material.fragmentNode = fragmentNode;
material.side = BackSide;
material.blending = NoBlending;
material.depthTest = false;
material.depthWrite = false;
_uniformsMap.set( material, uniforms );
return material;
}
function _getBlurMaterial() {
const uniforms = {
envMap: cubeTexture(),
sigma: uniform( 0 )
};
return _getMaterial( 'blur', uniforms, sphericalGaussianBlur( {
...uniforms,
direction: positionWorldDirection,
SAMPLES: int( BLUR_SAMPLES )
} ) );
}
function _getGGXMaterial() {
const uniforms = {
envMap: cubeTexture(),
roughness: uniform( 0 ),
lodBias: uniform( 0 )
};
return _getMaterial( 'ggx', uniforms, ggxConvolution( {
...uniforms,
direction: positionWorldDirection,
GGX_SAMPLES: uint( GGX_SAMPLES )
} ) );
}
function _getIntegrationMaterial() {
const uniforms = {
envMap: cubeTexture(),
roughness: uniform( 0 ),
sourceLod: uniform( 0 ),
sourceSize: uniform( INTEGRATION_SIZE, 'int' ) // a uniform so the loops aren't unrolled
};
return _getMaterial( 'integration', uniforms, ggxIntegration( {
...uniforms,
direction: positionWorldDirection
} ) );
}
function _getCubemapMaterial() {
const uniforms = {
envMap: cubeTexture()
};
return _getMaterial( 'cubemap', uniforms, uniforms.envMap.sample( positionWorldDirection ) );
}
function _getEquirectMaterial() {
const uniforms = {
envMap: texture()
};
const fragmentNode = Fn( () => {
// Average four subpixel samples to preserve small, bright features.
const direction = positionWorldDirection;
const dx = direction.dFdx().mul( 0.25 ).toConst();
const dy = direction.dFdy().mul( 0.25 ).toConst();
const color = uniforms.envMap.sample( equirectUV( direction.sub( dx ).sub( dy ).normalize() ) ).level( 0 ).rgb
.add( uniforms.envMap.sample( equirectUV( direction.add( dx ).sub( dy ).normalize() ) ).level( 0 ).rgb )
.add( uniforms.envMap.sample( equirectUV( direction.sub( dx ).add( dy ).normalize() ) ).level( 0 ).rgb )
.add( uniforms.envMap.sample( equirectUV( direction.add( dx ).add( dy ).normalize() ) ).level( 0 ).rgb );
return vec4( color.mul( 0.25 ), 1.0 );
} )();
return _getMaterial( 'equirect', uniforms, fragmentNode );
}
const _cache = new WeakMap();
/**
* Generates a PMREM from the given texture.
*
* @private
* @param {Texture} texture - The texture to create the PMREM for.
* @param {Renderer} renderer - The renderer.
* @param {PMREMGenerator} generator - The PMREM generator.
* @return {?Texture} The PMREM.
*/
function _getPMREMFromTexture( texture, renderer, generator ) {
const cache = _getCache( renderer );
let renderTarget = cache.get( texture );
const pmremVersion = renderTarget !== undefined ? renderTarget.texture.pmremVersion : -1;
if ( pmremVersion !== texture.pmremVersion ) {
const image = texture.image;
if ( texture.isCubeTexture ) {
if ( isCubeMapReady( image ) ) {
renderTarget = generator.fromCubemap( texture, renderTarget );
} else {
return null;
}
} else {
if ( isEquirectangularMapReady( image ) ) {
renderTarget = generator.fromEquirectangular( texture, renderTarget );
} else {
return null;
}
}
renderTarget.texture.pmremVersion = texture.pmremVersion;
// add dispose event listener for new PMREMs
if ( cache.has( texture ) === false ) {
const onDispose = () => {
texture.removeEventListener( 'dispose', onDispose );
const pmrem = cache.get( texture );
if ( pmrem !== undefined ) {
pmrem.texture.pmremVersion = -1;
pmrem.dispose();
cache.delete( texture );
}
};
texture.addEventListener( 'dispose', onDispose );
}
//
cache.set( texture, renderTarget );
}
return renderTarget.texture;
}
/**
* Returns a cache that stores generated PMREMs for the respective textures.
* A cache must be maintained per renderer since PMREMs are render target textures
* which can't be shared across render contexts.
*
* @private
* @param {Renderer} renderer - The renderer.
* @return {WeakMap<Texture, CubeRenderTarget>} The PMREM cache.
*/
function _getCache( renderer ) {
let rendererCache = _cache.get( renderer );
if ( rendererCache === undefined ) {
rendererCache = new WeakMap();
_cache.set( renderer, rendererCache );
}
return rendererCache;
}
/**
* This node represents a PMREM which is a special type of preprocessed
* environment map intended for PBR materials.
*
* ```js
* const material = new MeshStandardNodeMaterial();
* material.envNode = pmremTexture( envMap );
* ```
*
* @augments Node
*/
class PMREMNode extends Node {
static get type() {
return 'PMREMNode';
}
/**
* Constructs a new PMREM node.
*
* @param {Texture} value - The input texture.
* @param {Node<vec3>} [uvNode=null] - The uv node.
* @param {Node<float>} [levelNode=null] - The level node.
*/
constructor( value, uvNode = null, levelNode = null ) {
super( 'vec3' );
/**
* Reference to the input texture.
*
* @private
* @type {Texture}
*/
this._value = value;
/**
* Reference to the generated PMREM.
*
* @private
* @type {Texture | null}
* @default null
*/
this._pmrem = null;
/**
* The uv node.
*
* @type {Node<vec3>}
*/
this.uvNode = uvNode;
/**
* The level node.
*
* @type {Node<float>}
*/
this.levelNode = levelNode;
/**
* Reference to a PMREM generator.
*
* @private
* @type {?PMREMGenerator}
* @default null
*/
this._generator = null;
const defaultTexture = new CubeTexture();
defaultTexture.isRenderTargetTexture = true;
/**
* The texture node holding the generated PMREM.
*
* @private
* @type {CubeTextureNode}
*/
this._texture = cubeTexture( defaultTexture );
/**
* A uniform representing the last mip level of the PMREM.
*
* @private
* @type {UniformNode<float>}
*/
this._maxLod = uniform( 0 );
/**
* A uniform representing the width of the sharpest mip level of the PMREM.
*
* @private
* @type {UniformNode<float>}
*/
this._size = uniform( 0 );
/**
* The `updateBeforeType` is set to `NodeUpdateType.RENDER`.
*
* @type {string}
* @default 'render'
*/
this.updateBeforeType = NodeUpdateType.RENDER;
}
set value( value ) {
this._value = value;
this._pmrem = null;
}
/**
* The node's texture value.
*
* @type {Texture}
*/
get value() {
return this._value;
}
/**
* Uses the given PMREM texture to update internal values.
*
* @param {Texture} texture - The PMREM texture.
*/
updateFromTexture( texture ) {
const mipmaps = texture.isCompressedCubeTexture ? texture.image[ 0 ].mipmaps : texture.mipmaps;
this._texture.value = texture;
this._maxLod.value = mipmaps.length - 1;
this._size.value = mipmaps[ 0 ].width;
}
updateBefore( frame ) {
let pmrem = this._pmrem;
const pmremVersion = pmrem ? pmrem.pmremVersion : -1;
const texture = this._value;
if ( pmremVersion !== texture.pmremVersion ) {
if ( texture.isPMREMTexture === true ) {
pmrem = texture;
} else {
if ( this._generator === null ) {
this._generator = new PMREMGenerator( frame.renderer );
}
pmrem = _getPMREMFromTexture( texture, frame.renderer, this._generator );
}
if ( pmrem !== null ) {
this._pmrem = pmrem;
this.updateFromTexture( pmrem );
}
}
}
setup( builder ) {
this.updateBefore( builder );
//
let uvNode = this.uvNode;
if ( uvNode === null && builder.context.getUV ) {
uvNode = builder.context.getUV( this, builder );
}
//
let levelNode = this.levelNode;
if ( levelNode === null && builder.context.getTextureLevel ) {
levelNode = builder.context.getTextureLevel( this );
}
//
return this._texture.sample( materialEnvRotation.mul( uvNode ) ).level( roughnessToMip( levelNode, this._maxLod ) ).rgb;
}
dispose() {
super.dispose();
if ( this._generator !== null ) this._generator.dispose();
}
}
/**
* Returns `true` if the given cube map image has been fully loaded.
*
* @private
* @param {?Array<(Image|Object)>} [image] - The cube map image.
* @return {boolean} Whether the given cube map is ready or not.
*/
function isCubeMapReady( image ) {
if ( image === null || image === undefined ) return false;
let count = 0;
const length = 6;
for ( let i = 0; i < length; i ++ ) {
if ( image[ i ] !== undefined ) count ++;
}
return count === length;
}
/**
* Returns `true` if the given equirectangular image has been fully loaded.
*
* @private
* @param {(Image|Object)} image - The equirectangular image.
* @return {boolean} Whether the given cube map is ready or not.
*/
function isEquirectangularMapReady( image ) {
if ( image === null || image === undefined ) return false;
return image.height > 0;
}
/**
* TSL function for creating a PMREM node.
*
* @tsl
* @function
* @param {Texture} value - The input texture.
* @param {?Node<vec3>} [uvNode=null] - The uv node.
* @param {?Node<float>} [levelNode=null] - The level node.
* @returns {PMREMNode}
*/
const pmremTexture = /*@__PURE__*/ nodeProxy( PMREMNode ).setParameterLength( 1, 3 );
/**
* Creates a 2x2 checkerboard pattern that can be used as procedural texture data.
*
* @tsl
* @function
* @param {Node<vec2>} coord - The uv coordinates.
* @return {Node<float>} The result data.
*/
const checker = /*@__PURE__*/ Fn( ( [ coord = uv$1() ] ) => {
const uv = coord.mul( 2.0 );
const cx = uv.x.floor();
const cy = uv.y.floor();
const result = cx.add( cy ).mod( 2.0 );
return result.sign();
}, { coord: 'vec2', return: 'float' } );
/**
* Generates a circle based on the uv coordinates.
*
* @tsl
* @function
* @param {Node<vec2>} coord - The uv to generate the circle.
* @return {Node<float>} The circle shape.
*/
const shapeCircle = /*@__PURE__*/ Fn( ( [ coord = uv$1() ], { renderer, material } ) => {
const len2 = lengthSq( coord.mul( 2 ).sub( 1 ) );
let alpha;
if ( material.alphaToCoverage && renderer.currentSamples > 0 ) {
const dlen = float( len2.fwidth() ).toVar();
alpha = smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus();
} else {
alpha = select( len2.greaterThan( 1.0 ), 0, 1 );
}
return alpha;
} );
const mx_rotate2d = ( input, amount = 0 ) => {
input = vec2( input );
amount = float( amount );
const rotationRadians = mul( amount, Math.PI / 180.0 );
const sa = sin( rotationRadians );
const ca = cos( rotationRadians );
const x = input.x;
const y = input.y;
return vec2( add( mul( ca, x ), mul( sa, y ) ), sub( mul( ca, y ), mul( sa, x ) ) );
};
const mx_rotate3d = ( input, amount = 0, axis = vec3( 0, 1, 0 ) ) => {
input = vec3( input );
amount = float( amount );
axis = vec3( axis );
const normalizedAxis = normalize( axis );
const rotationRadians = mul( amount, Math.PI / 180.0 );
const s = sin( rotationRadians );
const c = cos( rotationRadians );
const oc = sub( 1, c );
// based on https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
// but the code in the Wikipedia article is for v' = M * v, where as
// MaterialX is v' = v * M, thus the order of parameters into the first cross are reversed
return input.mul( c )
.add( input.cross( normalizedAxis ).mul( s ) )
.add( normalizedAxis.mul( normalizedAxis.dot( input ).mul( oc ) ) );
};
const mx_select = /*@__PURE__*/ Fn( ( [ b_immutable, t_immutable, f_immutable ] ) => {
const f = float( f_immutable ).toVar();
const t = float( t_immutable ).toVar();
const b = bool( b_immutable ).toVar();
return select( b, t, f ).uniformFlow();
}, { b: 'bool', t: 'float', f: 'float', return: 'float' } );
const mx_negate_if = /*@__PURE__*/ Fn( ( [ val_immutable, b_immutable ] ) => {
const b = bool( b_immutable ).toVar();
const val = float( val_immutable ).toVar();
return select( b, val.negate(), val ).uniformFlow();
}, { val: 'float', b: 'bool', return: 'float' } );
const mx_floor = /*@__PURE__*/ Fn( ( [ x_immutable ] ) => {
const x = float( x_immutable ).toVar();
return int( floor( x ) );
}, { x: 'float', return: 'int' } );
const mx_floorfrac = /*@__PURE__*/ Fn( ( [ x_immutable, i ] ) => {
const x = float( x_immutable ).toVar();
i.assign( mx_floor( x ) );
return x.sub( float( i ) );
} );
const mxBilerpValue = ( v0, v1, v2, v3, s, t ) => {
const s1 = float( sub( 1.0, s ) ).toVar();
return sub( 1.0, t ).mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) );
};
const mxTrilerpValue = ( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r ) => {
const s1 = float( sub( 1.0, s ) ).toVar();
const t1 = float( sub( 1.0, t ) ).toVar();
const r1 = float( sub( 1.0, r ) ).toVar();
return r1.mul( t1.mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) ) ).add( r.mul( t1.mul( v4.mul( s1 ).add( v5.mul( s ) ) ).add( t.mul( v6.mul( s1 ).add( v7.mul( s ) ) ) ) ) );
};
const mx_bilerp_0 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_immutable, v3_immutable, s_immutable, t_immutable ] ) => {
const t = float( t_immutable ).toVar();
const s = float( s_immutable ).toVar();
const v3 = float( v3_immutable ).toVar();
const v2 = float( v2_immutable ).toVar();
const v1 = float( v1_immutable ).toVar();
const v0 = float( v0_immutable ).toVar();
return mxBilerpValue( v0, v1, v2, v3, s, t );
}, {
v0: 'float',
v1: 'float',
v2: 'float',
v3: 'float',
s: 'float',
t: 'float',
return: 'float'
} );
const mx_bilerp_1 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_immutable, v3_immutable, s_immutable, t_immutable ] ) => {
const t = float( t_immutable ).toVar();
const s = float( s_immutable ).toVar();
const v3 = vec3( v3_immutable ).toVar();
const v2 = vec3( v2_immutable ).toVar();
const v1 = vec3( v1_immutable ).toVar();
const v0 = vec3( v0_immutable ).toVar();
return mxBilerpValue( v0, v1, v2, v3, s, t );
}, {
v0: 'vec3',
v1: 'vec3',
v2: 'vec3',
v3: 'vec3',
s: 'float',
t: 'float',
return: 'vec3'
} );
const mx_bilerp = /*@__PURE__*/ overloadingFn( [ mx_bilerp_0, mx_bilerp_1 ] );
const mx_trilerp_0 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_immutable, v3_immutable, v4_immutable, v5_immutable, v6_immutable, v7_immutable, s_immutable, t_immutable, r_immutable ] ) => {
const r = float( r_immutable ).toVar();
const t = float( t_immutable ).toVar();
const s = float( s_immutable ).toVar();
const v7 = float( v7_immutable ).toVar();
const v6 = float( v6_immutable ).toVar();
const v5 = float( v5_immutable ).toVar();
const v4 = float( v4_immutable ).toVar();
const v3 = float( v3_immutable ).toVar();
const v2 = float( v2_immutable ).toVar();
const v1 = float( v1_immutable ).toVar();
const v0 = float( v0_immutable ).toVar();
return mxTrilerpValue( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r );
}, {
v0: 'float',
v1: 'float',
v2: 'float',
v3: 'float',
v4: 'float',
v5: 'float',
v6: 'float',
v7: 'float',
s: 'float',
t: 'float',
r: 'float',
return: 'float'
} );
const mx_trilerp_1 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_immutable, v3_immutable, v4_immutable, v5_immutable, v6_immutable, v7_immutable, s_immutable, t_immutable, r_immutable ] ) => {
const r = float( r_immutable ).toVar();
const t = float( t_immutable ).toVar();
const s = float( s_immutable ).toVar();
const v7 = vec3( v7_immutable ).toVar();
const v6 = vec3( v6_immutable ).toVar();
const v5 = vec3( v5_immutable ).toVar();
const v4 = vec3( v4_immutable ).toVar();
const v3 = vec3( v3_immutable ).toVar();
const v2 = vec3( v2_immutable ).toVar();
const v1 = vec3( v1_immutable ).toVar();
const v0 = vec3( v0_immutable ).toVar();
return mxTrilerpValue( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r );
}, {
v0: 'vec3',
v1: 'vec3',
v2: 'vec3',
v3: 'vec3',
v4: 'vec3',
v5: 'vec3',
v6: 'vec3',
v7: 'vec3',
s: 'float',
t: 'float',
r: 'float',
return: 'vec3'
} );
const mx_trilerp = /*@__PURE__*/ overloadingFn( [ mx_trilerp_0, mx_trilerp_1 ] );
const mx_gradient_float_0 = /*@__PURE__*/ Fn( ( [ hash_immutable, x_immutable, y_immutable ] ) => {
const y = float( y_immutable ).toVar();
const x = float( x_immutable ).toVar();
const hash = uint( hash_immutable ).toVar();
const h = uint( hash.bitAnd( uint( 7 ) ) ).toVar();
const u = float( mx_select( h.lessThan( uint( 4 ) ), x, y ) ).toVar();
const v = float( mul( 2.0, mx_select( h.lessThan( uint( 4 ) ), y, x ) ) ).toVar();
return mx_negate_if( u, bool( h.bitAnd( uint( 1 ) ) ) ).add( mx_negate_if( v, bool( h.bitAnd( uint( 2 ) ) ) ) );
}, { hash: 'uint', x: 'float', y: 'float', return: 'float' } );
const mx_gradient_float_1 = /*@__PURE__*/ Fn( ( [ hash_immutable, x_immutable, y_immutable, z_immutable ] ) => {
const z = float( z_immutable ).toVar();
const y = float( y_immutable ).toVar();
const x = float( x_immutable ).toVar();
const hash = uint( hash_immutable ).toVar();
const h = uint( hash.bitAnd( uint( 15 ) ) ).toVar();
const u = float( mx_select( h.lessThan( uint( 8 ) ), x, y ) ).toVar();
const v = float( mx_select( h.lessThan( uint( 4 ) ), y, mx_select( h.equal( uint( 12 ) ).or( h.equal( uint( 14 ) ) ), x, z ) ) ).toVar();
return mx_negate_if( u, bool( h.bitAnd( uint( 1 ) ) ) ).add( mx_negate_if( v, bool( h.bitAnd( uint( 2 ) ) ) ) );
}, {
hash: 'uint',
x: 'float',
y: 'float',
z: 'float',
return: 'float'
} );
const mx_gradient_float = /*@__PURE__*/ overloadingFn( [ mx_gradient_float_0, mx_gradient_float_1 ] );
const mx_gradient_vec3_0 = /*@__PURE__*/ Fn( ( [ hash_immutable, x_immutable, y_immutable ] ) => {
const y = float( y_immutable ).toVar();
const x = float( x_immutable ).toVar();
const hash = uvec3( hash_immutable ).toVar();
return vec3( mx_gradient_float( hash.x, x, y ), mx_gradient_float( hash.y, x, y ), mx_gradient_float( hash.z, x, y ) );
}, { hash: 'uvec3', x: 'float', y: 'float', return: 'vec3' } );
const mx_gradient_vec3_1 = /*@__PURE__*/ Fn( ( [ hash_immutable, x_immutable, y_immutable, z_immutable ] ) => {
const z = float( z_immutable ).toVar();
const y = float( y_immutable ).toVar();
const x = float( x_immutable ).toVar();
const hash = uvec3( hash_immutable ).toVar();
return vec3( mx_gradient_float( hash.x, x, y, z ), mx_gradient_float( hash.y, x, y, z ), mx_gradient_float( hash.z, x, y, z ) );
}, {
hash: 'uvec3',
x: 'float',
y: 'float',
z: 'float',
return: 'vec3'
} );
const mx_gradient_vec3 = /*@__PURE__*/ overloadingFn( [ mx_gradient_vec3_0, mx_gradient_vec3_1 ] );
const mx_gradient_scale2d_0 = /*@__PURE__*/ Fn( ( [ v_immutable ] ) => {
const v = float( v_immutable ).toVar();
return mul( 0.6616, v );
}, { v: 'float', return: 'float' } );
const mx_gradient_scale3d_0 = /*@__PURE__*/ Fn( ( [ v_immutable ] ) => {
const v = float( v_immutable ).toVar();
return mul( 0.9820, v );
}, { v: 'float', return: 'float' } );
const mx_gradient_scale2d_1 = /*@__PURE__*/ Fn( ( [ v_immutable ] ) => {
const v = vec3( v_immutable ).toVar();
return mul( 0.6616, v );
}, { v: 'vec3', return: 'vec3' } );
const mx_gradient_scale2d = /*@__PURE__*/ overloadingFn( [ mx_gradient_scale2d_0, mx_gradient_scale2d_1 ] );
const mx_gradient_scale3d_1 = /*@__PURE__*/ Fn( ( [ v_immutable ] ) => {
const v = vec3( v_immutable ).toVar();
return mul( 0.9820, v );
}, { v: 'vec3', return: 'vec3' } );
const mx_gradient_scale3d = /*@__PURE__*/ overloadingFn( [ mx_gradient_scale3d_0, mx_gradient_scale3d_1 ] );
const mx_rotl32 = /*@__PURE__*/ Fn( ( [ x_immutable, k_immutable ] ) => {
const k = int( k_immutable ).toVar();
const x = uint( x_immutable ).toVar();
return x.shiftLeft( k ).bitOr( x.shiftRight( int( 32 ).sub( k ) ) );
}, { x: 'uint', k: 'int', return: 'uint' } );
const mx_bjmix = /*@__PURE__*/ Fn( ( [ a, b, c ] ) => {
a = uint( a ).toVar();
b = uint( b ).toVar();
c = uint( c ).toVar();
a.subAssign( c );
a.bitXorAssign( mx_rotl32( c, int( 4 ) ) );
c.addAssign( b );
b.subAssign( a );
b.bitXorAssign( mx_rotl32( a, int( 6 ) ) );
a.addAssign( c );
c.subAssign( b );
c.bitXorAssign( mx_rotl32( b, int( 8 ) ) );
b.addAssign( a );
a.subAssign( c );
a.bitXorAssign( mx_rotl32( c, int( 16 ) ) );
c.addAssign( b );
b.subAssign( a );
b.bitXorAssign( mx_rotl32( a, int( 19 ) ) );
a.addAssign( c );
c.subAssign( b );
c.bitXorAssign( mx_rotl32( b, int( 4 ) ) );
b.addAssign( a );
return uvec3( a, b, c );
}, { a: 'uint', b: 'uint', c: 'uint', return: 'uvec3' } );
const mx_bjfinal = /*@__PURE__*/ Fn( ( [ a_immutable, b_immutable, c_immutable ] ) => {
const c = uint( c_immutable ).toVar();
const b = uint( b_immutable ).toVar();
const a = uint( a_immutable ).toVar();
c.bitXorAssign( b );
c.subAssign( mx_rotl32( b, int( 14 ) ) );
a.bitXorAssign( c );
a.subAssign( mx_rotl32( c, int( 11 ) ) );
b.bitXorAssign( a );
b.subAssign( mx_rotl32( a, int( 25 ) ) );
c.bitXorAssign( b );
c.subAssign( mx_rotl32( b, int( 16 ) ) );
a.bitXorAssign( c );
a.subAssign( mx_rotl32( c, int( 4 ) ) );
b.bitXorAssign( a );
b.subAssign( mx_rotl32( a, int( 14 ) ) );
c.bitXorAssign( b );
c.subAssign( mx_rotl32( b, int( 24 ) ) );
return c;
}, { a: 'uint', b: 'uint', c: 'uint', return: 'uint' } );
const mx_bits_to_01 = /*@__PURE__*/ Fn( ( [ bits_immutable ] ) => {
const bits = uint( bits_immutable ).toVar();
return float( bits ).div( float( uint( 0xffffffff ) ) );
}, { bits: 'uint', return: 'float' } );
const mx_fade = /*@__PURE__*/ Fn( ( [ t_immutable ] ) => {
const t = float( t_immutable ).toVar();
return t.mul( t ).mul( t ).mul( t.mul( t.mul( 6.0 ).sub( 15.0 ) ).add( 10.0 ) );
}, { t: 'float', return: 'float' } );
const mx_hash_int_0 = /*@__PURE__*/ Fn( ( [ x_immutable ] ) => {
const x = int( x_immutable ).toVar();
const len = uint( uint( 1 ) ).toVar();
const seed = uint( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ).toVar();
return mx_bjfinal( seed.add( uint( x ) ), seed, seed );
}, { x: 'int', return: 'uint' } );
const mx_hash_int_1 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable ] ) => {
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const len = uint( uint( 2 ) ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
a.addAssign( uint( x ) );
b.addAssign( uint( y ) );
return mx_bjfinal( a, b, c );
}, { x: 'int', y: 'int', return: 'uint' } );
const mx_hash_int_2 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_immutable ] ) => {
const z = int( z_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const len = uint( uint( 3 ) ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
a.addAssign( uint( x ) );
b.addAssign( uint( y ) );
c.addAssign( uint( z ) );
return mx_bjfinal( a, b, c );
}, { x: 'int', y: 'int', z: 'int', return: 'uint' } );
const mx_hash_int_3 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_immutable, xx_immutable ] ) => {
const xx = int( xx_immutable ).toVar();
const z = int( z_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const len = uint( uint( 4 ) ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
a.addAssign( uint( x ) );
b.addAssign( uint( y ) );
c.addAssign( uint( z ) );
const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
a.assign( mixed.x );
b.assign( mixed.y );
c.assign( mixed.z );
a.addAssign( uint( xx ) );
return mx_bjfinal( a, b, c );
}, {
x: 'int',
y: 'int',
z: 'int',
xx: 'int',
return: 'uint'
} );
const mx_hash_int_4 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_immutable, xx_immutable, yy_immutable ] ) => {
const yy = int( yy_immutable ).toVar();
const xx = int( xx_immutable ).toVar();
const z = int( z_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const len = uint( uint( 5 ) ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
a.addAssign( uint( x ) );
b.addAssign( uint( y ) );
c.addAssign( uint( z ) );
const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
a.assign( mixed.x );
b.assign( mixed.y );
c.assign( mixed.z );
a.addAssign( uint( xx ) );
b.addAssign( uint( yy ) );
return mx_bjfinal( a, b, c );
}, {
x: 'int',
y: 'int',
z: 'int',
xx: 'int',
yy: 'int',
return: 'uint'
} );
const mx_hash_int = /*@__PURE__*/ overloadingFn( [ mx_hash_int_0, mx_hash_int_1, mx_hash_int_2, mx_hash_int_3, mx_hash_int_4 ] );
const mx_hash_vec3_0 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable ] ) => {
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const h = uint( mx_hash_int( x, y ) ).toVar();
const result = uvec3().toVar();
result.x.assign( h.bitAnd( int( 0xFF ) ) );
result.y.assign( h.shiftRight( int( 8 ) ).bitAnd( int( 0xFF ) ) );
result.z.assign( h.shiftRight( int( 16 ) ).bitAnd( int( 0xFF ) ) );
return result;
}, { x: 'int', y: 'int', return: 'uvec3' } );
const mx_hash_vec3_1 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_immutable ] ) => {
const z = int( z_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const h = uint( mx_hash_int( x, y, z ) ).toVar();
const result = uvec3().toVar();
result.x.assign( h.bitAnd( int( 0xFF ) ) );
result.y.assign( h.shiftRight( int( 8 ) ).bitAnd( int( 0xFF ) ) );
result.z.assign( h.shiftRight( int( 16 ) ).bitAnd( int( 0xFF ) ) );
return result;
}, { x: 'int', y: 'int', z: 'int', return: 'uvec3' } );
const mx_hash_vec3 = /*@__PURE__*/ overloadingFn( [ mx_hash_vec3_0, mx_hash_vec3_1 ] );
const mx_perlin_noise_float_0 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec2( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar();
const fx = float( mx_floorfrac( p.x, X ) ).toVar();
const fy = float( mx_floorfrac( p.y, Y ) ).toVar();
const u = float( mx_fade( fx ) ).toVar();
const v = float( mx_fade( fy ) ).toVar();
const result = float( mx_bilerp( mx_gradient_float( mx_hash_int( X, Y ), fx, fy ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y ), fx.sub( 1.0 ), fy ), mx_gradient_float( mx_hash_int( X, Y.add( int( 1 ) ) ), fx, fy.sub( 1.0 ) ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y.add( int( 1 ) ) ), fx.sub( 1.0 ), fy.sub( 1.0 ) ), u, v ) ).toVar();
return mx_gradient_scale2d( result );
}, { p: 'vec2', return: 'float' } );
const mx_perlin_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec3( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const fx = float( mx_floorfrac( p.x, X ) ).toVar();
const fy = float( mx_floorfrac( p.y, Y ) ).toVar();
const fz = float( mx_floorfrac( p.z, Z ) ).toVar();
const u = float( mx_fade( fx ) ).toVar();
const v = float( mx_fade( fy ) ).toVar();
const w = float( mx_fade( fz ) ).toVar();
const result = float( mx_trilerp( mx_gradient_float( mx_hash_int( X, Y, Z ), fx, fy, fz ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y, Z ), fx.sub( 1.0 ), fy, fz ), mx_gradient_float( mx_hash_int( X, Y.add( int( 1 ) ), Z ), fx, fy.sub( 1.0 ), fz ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y.add( int( 1 ) ), Z ), fx.sub( 1.0 ), fy.sub( 1.0 ), fz ), mx_gradient_float( mx_hash_int( X, Y, Z.add( int( 1 ) ) ), fx, fy, fz.sub( 1.0 ) ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y, Z.add( int( 1 ) ) ), fx.sub( 1.0 ), fy, fz.sub( 1.0 ) ), mx_gradient_float( mx_hash_int( X, Y.add( int( 1 ) ), Z.add( int( 1 ) ) ), fx, fy.sub( 1.0 ), fz.sub( 1.0 ) ), mx_gradient_float( mx_hash_int( X.add( int( 1 ) ), Y.add( int( 1 ) ), Z.add( int( 1 ) ) ), fx.sub( 1.0 ), fy.sub( 1.0 ), fz.sub( 1.0 ) ), u, v, w ) ).toVar();
return mx_gradient_scale3d( result );
}, { p: 'vec3', return: 'float' } );
const mx_perlin_noise_float = /*@__PURE__*/ overloadingFn( [ mx_perlin_noise_float_0, mx_perlin_noise_float_1 ] );
const mx_perlin_noise_vec3_0 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec2( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar();
const fx = float( mx_floorfrac( p.x, X ) ).toVar();
const fy = float( mx_floorfrac( p.y, Y ) ).toVar();
const u = float( mx_fade( fx ) ).toVar();
const v = float( mx_fade( fy ) ).toVar();
const result = vec3( mx_bilerp( mx_gradient_vec3( mx_hash_vec3( X, Y ), fx, fy ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y ), fx.sub( 1.0 ), fy ), mx_gradient_vec3( mx_hash_vec3( X, Y.add( int( 1 ) ) ), fx, fy.sub( 1.0 ) ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y.add( int( 1 ) ) ), fx.sub( 1.0 ), fy.sub( 1.0 ) ), u, v ) ).toVar();
return mx_gradient_scale2d( result );
}, { p: 'vec2', return: 'vec3' } );
const mx_perlin_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec3( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const fx = float( mx_floorfrac( p.x, X ) ).toVar();
const fy = float( mx_floorfrac( p.y, Y ) ).toVar();
const fz = float( mx_floorfrac( p.z, Z ) ).toVar();
const u = float( mx_fade( fx ) ).toVar();
const v = float( mx_fade( fy ) ).toVar();
const w = float( mx_fade( fz ) ).toVar();
const result = vec3( mx_trilerp( mx_gradient_vec3( mx_hash_vec3( X, Y, Z ), fx, fy, fz ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y, Z ), fx.sub( 1.0 ), fy, fz ), mx_gradient_vec3( mx_hash_vec3( X, Y.add( int( 1 ) ), Z ), fx, fy.sub( 1.0 ), fz ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y.add( int( 1 ) ), Z ), fx.sub( 1.0 ), fy.sub( 1.0 ), fz ), mx_gradient_vec3( mx_hash_vec3( X, Y, Z.add( int( 1 ) ) ), fx, fy, fz.sub( 1.0 ) ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y, Z.add( int( 1 ) ) ), fx.sub( 1.0 ), fy, fz.sub( 1.0 ) ), mx_gradient_vec3( mx_hash_vec3( X, Y.add( int( 1 ) ), Z.add( int( 1 ) ) ), fx, fy.sub( 1.0 ), fz.sub( 1.0 ) ), mx_gradient_vec3( mx_hash_vec3( X.add( int( 1 ) ), Y.add( int( 1 ) ), Z.add( int( 1 ) ) ), fx.sub( 1.0 ), fy.sub( 1.0 ), fz.sub( 1.0 ) ), u, v, w ) ).toVar();
return mx_gradient_scale3d( result );
}, { p: 'vec3', return: 'vec3' } );
const mx_perlin_noise_vec3 = /*@__PURE__*/ overloadingFn( [ mx_perlin_noise_vec3_0, mx_perlin_noise_vec3_1 ] );
const mx_cell_noise_float_0 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = float( p_immutable ).toVar();
const ix = int( mx_floor( p ) ).toVar();
return mx_bits_to_01( mx_hash_int( ix ) );
}, { p: 'float', return: 'float' } );
const mx_cell_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec2( p_immutable ).toVar();
const ix = int( mx_floor( p.x ) ).toVar();
const iy = int( mx_floor( p.y ) ).toVar();
return mx_bits_to_01( mx_hash_int( ix, iy ) );
}, { p: 'vec2', return: 'float' } );
const mx_cell_noise_float_2 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec3( p_immutable ).toVar();
const ix = int( mx_floor( p.x ) ).toVar();
const iy = int( mx_floor( p.y ) ).toVar();
const iz = int( mx_floor( p.z ) ).toVar();
return mx_bits_to_01( mx_hash_int( ix, iy, iz ) );
}, { p: 'vec3', return: 'float' } );
const mx_cell_noise_float_3 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec4( p_immutable ).toVar();
const ix = int( mx_floor( p.x ) ).toVar();
const iy = int( mx_floor( p.y ) ).toVar();
const iz = int( mx_floor( p.z ) ).toVar();
const iw = int( mx_floor( p.w ) ).toVar();
return mx_bits_to_01( mx_hash_int( ix, iy, iz, iw ) );
}, { p: 'vec4', return: 'float' } );
const mx_cell_noise_float$1 = /*@__PURE__*/ overloadingFn( [ mx_cell_noise_float_0, mx_cell_noise_float_1, mx_cell_noise_float_2, mx_cell_noise_float_3 ] );
const mx_cell_noise_vec3_0 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = float( p_immutable ).toVar();
const ix = int( mx_floor( p ) ).toVar();
return vec3(
mx_bits_to_01( mx_hash_int( ix, int( 0 ) ) ),
mx_bits_to_01( mx_hash_int( ix, int( 1 ) ) ),
mx_bits_to_01( mx_hash_int( ix, int( 2 ) ) )
);
}, { p: 'float', return: 'vec3' } );
const mx_cell_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec2( p_immutable ).toVar();
const ix = int( mx_floor( p.x ) ).toVar();
const iy = int( mx_floor( p.y ) ).toVar();
return vec3(
mx_bits_to_01( mx_hash_int( ix, iy, int( 0 ) ) ),
mx_bits_to_01( mx_hash_int( ix, iy, int( 1 ) ) ),
mx_bits_to_01( mx_hash_int( ix, iy, int( 2 ) ) )
);
}, { p: 'vec2', return: 'vec3' } );
const mx_cell_noise_vec3_2 = /*@__PURE__*/ Fn( ( [ positionInput ] ) => {
const position = vec3( positionInput ).toVar();
const ix = int( floor( position.x ) ).toVar();
const iy = int( floor( position.y ) ).toVar();
const iz = int( floor( position.z ) ).toVar();
const seed = uint( 0xdeadbeef + ( 4 << 2 ) + 13 ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( seed ) ) );
a.addAssign( uint( ix ) );
b.addAssign( uint( iy ) );
c.addAssign( uint( iz ) );
const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
const hash0 = mx_bjfinal( mixed.x, mixed.y, mixed.z );
const hash1 = mx_bjfinal( add( mixed.x, uint( 1 ) ), mixed.y, mixed.z );
const hash2 = mx_bjfinal( add( mixed.x, uint( 2 ) ), mixed.y, mixed.z );
return vec3(
mx_bits_to_01( hash0 ),
mx_bits_to_01( hash1 ),
mx_bits_to_01( hash2 )
);
}, { p: 'vec3', return: 'vec3' } );
const mx_cell_noise_vec3_3 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
const p = vec4( p_immutable ).toVar();
const ix = int( mx_floor( p.x ) ).toVar();
const iy = int( mx_floor( p.y ) ).toVar();
const iz = int( mx_floor( p.z ) ).toVar();
const iw = int( mx_floor( p.w ) ).toVar();
const seed = uint( 0xdeadbeef + ( 5 << 2 ) + 13 ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( seed ) ) );
a.addAssign( uint( ix ) );
b.addAssign( uint( iy ) );
c.addAssign( uint( iz ) );
const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
a.assign( mixed.x );
b.assign( mixed.y );
c.assign( mixed.z );
a.addAssign( uint( iw ) );
return vec3(
mx_bits_to_01( mx_bjfinal( a, b, c ) ),
mx_bits_to_01( mx_bjfinal( a, add( b, uint( 1 ) ), c ) ),
mx_bits_to_01( mx_bjfinal( a, add( b, uint( 2 ) ), c ) )
);
}, { p: 'vec4', return: 'vec3' } );
const mx_cell_noise_vec3$1 = /*@__PURE__*/ overloadingFn( [ mx_cell_noise_vec3_0, mx_cell_noise_vec3_1, mx_cell_noise_vec3_2, mx_cell_noise_vec3_3 ] );
const mx_cell_noise_vec3_3d = /*@__PURE__*/ Fn( ( [ positionInput ] ) => {
const position = vec3( positionInput ).toVar();
const ix = int( floor( position.x ) ).toVar();
const iy = int( floor( position.y ) ).toVar();
const iz = int( floor( position.z ) ).toVar();
const seed = uint( 0xdeadbeef + ( 4 << 2 ) + 13 ).toVar();
const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
a.assign( b.assign( c.assign( seed ) ) );
a.addAssign( uint( ix ) );
b.addAssign( uint( iy ) );
c.addAssign( uint( iz ) );
const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
const hash0 = mx_bjfinal( mixed.x, mixed.y, mixed.z );
const hash1 = mx_bjfinal( add( mixed.x, uint( 1 ) ), mixed.y, mixed.z );
const hash2 = mx_bjfinal( add( mixed.x, uint( 2 ) ), mixed.y, mixed.z );
return vec3(
mx_bits_to_01( hash0 ),
mx_bits_to_01( hash1 ),
mx_bits_to_01( hash2 )
);
}, { position: 'vec3', return: 'vec3' } );
const mx_fractal_noise_float_2d$1 = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
const diminish = float( diminish_immutable ).toVar();
const lacunarity = float( lacunarity_immutable ).toVar();
const octaves = int( octaves_immutable ).toVar();
const p = vec2( p_immutable ).toVar();
const result = float( 0.0 ).toVar();
const amplitude = float( 1.0 ).toVar();
Loop( octaves, () => {
result.addAssign( amplitude.mul( mx_perlin_noise_float( p ) ) );
amplitude.mulAssign( diminish );
p.mulAssign( lacunarity );
} );
return result;
}, {
p: 'vec2',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
return: 'float'
} );
const mx_fractal_noise_float$1 = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
const diminish = float( diminish_immutable ).toVar();
const lacunarity = float( lacunarity_immutable ).toVar();
const octaves = int( octaves_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const result = float( 0.0 ).toVar();
const amplitude = float( 1.0 ).toVar();
Loop( octaves, () => {
result.addAssign( amplitude.mul( mx_perlin_noise_float( p ) ) );
amplitude.mulAssign( diminish );
p.mulAssign( lacunarity );
} );
return result;
}, {
p: 'vec3',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
return: 'float'
} );
const mx_fractal_noise_vec3$1 = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
const diminish = float( diminish_immutable ).toVar();
const lacunarity = float( lacunarity_immutable ).toVar();
const octaves = int( octaves_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const result = vec3( 0.0 ).toVar();
const amplitude = float( 1.0 ).toVar();
Loop( octaves, () => {
result.addAssign( amplitude.mul( mx_perlin_noise_vec3( p ) ) );
amplitude.mulAssign( diminish );
p.mulAssign( lacunarity );
} );
return result;
}, {
p: 'vec3',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
return: 'vec3'
} );
const mx_fractal_noise_vec2$1 = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
const diminish = float( diminish_immutable ).toVar();
const lacunarity = float( lacunarity_immutable ).toVar();
const octaves = int( octaves_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
return vec2( mx_fractal_noise_float$1( p, octaves, lacunarity, diminish ), mx_fractal_noise_float$1( p.add( vec3( int( 19 ), int( 193 ), int( 17 ) ) ), octaves, lacunarity, diminish ) );
}, {
p: 'vec3',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
return: 'vec2'
} );
const mx_fractal_noise_vec4$1 = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
const diminish = float( diminish_immutable ).toVar();
const lacunarity = float( lacunarity_immutable ).toVar();
const octaves = int( octaves_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const c = vec3( mx_fractal_noise_vec3$1( p, octaves, lacunarity, diminish ) ).toVar();
const f = float( mx_fractal_noise_float$1( p.add( vec3( int( 19 ), int( 193 ), int( 17 ) ) ), octaves, lacunarity, diminish ) ).toVar();
return vec4( c, f );
}, {
p: 'vec3',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
return: 'vec4'
} );
const mx_worley_distance_0 = /*@__PURE__*/ Fn( ( [ p_immutable, x_immutable, y_immutable, xoff_immutable, yoff_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const yoff = int( yoff_immutable ).toVar();
const xoff = int( xoff_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const p = vec2( p_immutable ).toVar();
const tmp = vec3( mx_cell_noise_vec3$1( vec2( x.add( xoff ), y.add( yoff ) ) ) ).toVar();
const off = vec2( tmp.x, tmp.y ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec2( vec2( float( x ), float( y ) ).add( off ) ).toVar();
const diff = vec2( cellpos.sub( p ) ).toVar();
If( metric.equal( int( 2 ) ), () => {
return abs( diff.x ).add( abs( diff.y ) );
} );
If( metric.equal( int( 3 ) ), () => {
return max( abs( diff.x ), abs( diff.y ) );
} );
return dot( diff, diff );
}, {
p: 'vec2',
x: 'int',
y: 'int',
xoff: 'int',
yoff: 'int',
jitter: 'float',
metric: 'int',
return: 'float'
} );
const mx_worley_distance_1 = /*@__PURE__*/ Fn( ( [ p_immutable, x_immutable, y_immutable, z_immutable, xoff_immutable, yoff_immutable, zoff_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const zoff = int( zoff_immutable ).toVar();
const yoff = int( yoff_immutable ).toVar();
const xoff = int( xoff_immutable ).toVar();
const z = int( z_immutable ).toVar();
const y = int( y_immutable ).toVar();
const x = int( x_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const off = vec3( mx_cell_noise_vec3_3d( vec3( x.add( xoff ), y.add( yoff ), z.add( zoff ) ) ) ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec3( vec3( float( x ), float( y ), float( z ) ).add( off ) ).toVar();
const diff = vec3( cellpos.sub( p ) ).toVar();
If( metric.equal( int( 2 ) ), () => {
return abs( diff.x ).add( abs( diff.y ) ).add( abs( diff.z ) );
} );
If( metric.equal( int( 3 ) ), () => {
return max( abs( diff.x ), abs( diff.y ), abs( diff.z ) );
} );
return dot( diff, diff );
}, {
p: 'vec3',
x: 'int',
y: 'int',
z: 'int',
xoff: 'int',
yoff: 'int',
zoff: 'int',
jitter: 'float',
metric: 'int',
return: 'float'
} );
const mx_worley_distance = /*@__PURE__*/ overloadingFn( [ mx_worley_distance_0, mx_worley_distance_1 ] );
const mx_perlin_noise_float_scaled = ( texcoord, amplitude = 1, pivot = 0 ) => mx_perlin_noise_float( texcoord ).mul( amplitude ).add( pivot );
const mx_worley_noise_float_3d$1 = /*@__PURE__*/ Fn( ( [ positionInput, jitterInput, styleInput ] ) => {
const position = vec3( positionInput ).toVar();
const jitter = float( jitterInput ).toVar();
const style = int( styleInput ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const localpos = vec3( mx_floorfrac( position.x, X ), mx_floorfrac( position.y, Y ), mx_floorfrac( position.z, Z ) ).toVar();
const sqdist = float( 1e6 ).toVar();
const minpos = vec3( 0, 0, 0 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
Loop( { start: -1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, int( 0 ) ) ).toVar();
const off = vec3( mx_cell_noise_vec3_3d( vec3( X.add( x ), Y.add( y ), Z.add( z ) ) ) ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec3( vec3( float( x ), float( y ), float( z ) ).add( off ).sub( localpos ) ).toVar();
If( dist.lessThan( sqdist ), () => {
sqdist.assign( dist );
minpos.assign( cellpos );
} );
} );
} );
} );
If( style.equal( int( 1 ) ), () => {
sqdist.assign( mx_cell_noise_float$1( minpos.add( position ) ) );
} ).Else( () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { position: 'vec3', jitter: 'float', style: 'int', return: 'float' } );
const mx_worley_noise_float_2d$1 = /*@__PURE__*/ Fn( ( [ texcoordInput, jitterInput, styleInput ] ) => {
const texcoord = vec2( texcoordInput ).toVar();
const jitter = float( jitterInput ).toVar();
const style = int( styleInput ).toVar();
const floorPos = floor( texcoord ).toVar();
const localpos = vec2( fract( texcoord.x ), fract( texcoord.y ) ).toVar();
const sqdist = float( 1e6 ).toVar();
const minpos = vec2( 0, 0 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
const cell = vec2( float( x ), float( y ) ).toVar();
const seed = vec2( cell.x.add( floorPos.x ), cell.y.add( floorPos.y ) ).toVar();
const off = vec2( mx_cell_noise_float$1( vec3( seed.x, seed.y, 0 ) ), mx_cell_noise_float$1( vec3( seed.x, seed.y, 1 ) ) ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec2( cell.add( off ).sub( localpos ) ).toVar();
const dist = dot( cellpos, cellpos ).toVar();
If( dist.lessThan( sqdist ), () => {
sqdist.assign( dist );
minpos.assign( cellpos );
} );
} );
} );
If( style.equal( int( 1 ) ), () => {
sqdist.assign( mx_cell_noise_float$1( minpos.add( texcoord ) ) );
} ).Else( () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { texcoord: 'vec2', jitter: 'float', style: 'int', return: 'float' } );
const mx_worley_noise_vec2_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec2( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar();
const localpos = vec2( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ) ).toVar();
const sqdist = vec2( 1e6, 1e6 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
const dist = float( mx_worley_distance( localpos, x, y, X, Y, jitter, metric ) ).toVar();
If( dist.lessThan( sqdist.x ), () => {
sqdist.y.assign( sqdist.x );
sqdist.x.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.y ), () => {
sqdist.y.assign( dist );
} );
} );
} );
If( metric.equal( int( 0 ) ), () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { p: 'vec2', jitter: 'float', metric: 'int', return: 'vec2' } );
const mx_worley_noise_vec3_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec2( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar();
const localpos = vec2( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ) ).toVar();
const sqdist = vec3( 1e6, 1e6, 1e6 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
const dist = float( mx_worley_distance( localpos, x, y, X, Y, jitter, metric ) ).toVar();
If( dist.lessThan( sqdist.x ), () => {
sqdist.z.assign( sqdist.y );
sqdist.y.assign( sqdist.x );
sqdist.x.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.y ), () => {
sqdist.z.assign( sqdist.y );
sqdist.y.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.z ), () => {
sqdist.z.assign( dist );
} );
} );
} );
If( metric.equal( int( 0 ) ), () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { p: 'vec2', jitter: 'float', metric: 'int', return: 'vec3' } );
const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
const sqdist = vec2( 1e6, 1e6 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
Loop( { start: -1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, metric ) ).toVar();
If( dist.lessThan( sqdist.x ), () => {
sqdist.y.assign( sqdist.x );
sqdist.x.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.y ), () => {
sqdist.y.assign( dist );
} );
} );
} );
} );
If( metric.equal( int( 0 ) ), () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { p: 'vec3', jitter: 'float', metric: 'int', return: 'vec2' } );
const mx_worley_noise_vec2$1 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec2_0, mx_worley_noise_vec2_1 ] );
const mx_worley_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
const sqdist = vec3( 1e6, 1e6, 1e6 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
Loop( { start: -1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, metric ) ).toVar();
If( dist.lessThan( sqdist.x ), () => {
sqdist.z.assign( sqdist.y );
sqdist.y.assign( sqdist.x );
sqdist.x.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.y ), () => {
sqdist.z.assign( sqdist.y );
sqdist.y.assign( dist );
} ).ElseIf( dist.lessThan( sqdist.z ), () => {
sqdist.z.assign( dist );
} );
} );
} );
} );
If( metric.equal( int( 0 ) ), () => {
sqdist.assign( sqrt( sqdist ) );
} );
return sqdist;
}, { p: 'vec3', jitter: 'float', metric: 'int', return: 'vec3' } );
const mx_worley_noise_vec3$1 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec3_0, mx_worley_noise_vec3_1 ] );
const mx_worley_noise_vec3_style_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, style_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const style = int( style_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec2( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar();
const localpos = vec2( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ) ).toVar();
const sqdist = float( 1e6 ).toVar();
const minpos = vec2( 0, 0 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
const dist = float( mx_worley_distance( localpos, x, y, X, Y, jitter, metric ) ).toVar();
const tmp = vec3( mx_cell_noise_vec3$1( vec2( X.add( x ), Y.add( y ) ) ) ).toVar();
const off = vec2( tmp.x, tmp.y ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec2( vec2( float( x ), float( y ) ).add( off ).sub( localpos ) ).toVar();
If( dist.lessThan( sqdist ), () => {
sqdist.assign( dist );
minpos.assign( cellpos );
} );
} );
} );
const result = vec3( mx_worley_noise_vec3$1( p, jitter, metric ) ).toVar();
If( style.equal( int( 1 ) ), () => {
result.assign( mx_cell_noise_vec3$1( minpos.add( p ) ) );
} );
return result;
}, {
p: 'vec2',
jitter: 'float',
style: 'int',
metric: 'int',
return: 'vec3'
} );
const mx_worley_noise_vec3_style_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, style_immutable, metric_immutable ] ) => {
const metric = int( metric_immutable ).toVar();
const style = int( style_immutable ).toVar();
const jitter = float( jitter_immutable ).toVar();
const p = vec3( p_immutable ).toVar();
const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
const sqdist = float( 1e6 ).toVar();
const minpos = vec3( 0, 0, 0 ).toVar();
Loop( { start: -1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
Loop( { start: -1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
Loop( { start: -1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, metric ) ).toVar();
const off = vec3( mx_cell_noise_vec3_3d( vec3( X.add( x ), Y.add( y ), Z.add( z ) ) ) ).toVar();
off.subAssign( 0.5 );
off.mulAssign( jitter );
off.addAssign( 0.5 );
const cellpos = vec3( vec3( float( x ), float( y ), float( z ) ).add( off ).sub( localpos ) ).toVar();
If( dist.lessThan( sqdist ), () => {
sqdist.assign( dist );
minpos.assign( cellpos );
} );
} );
} );
} );
const result = vec3( mx_worley_noise_vec3$1( p, jitter, metric ) ).toVar();
If( style.equal( int( 1 ) ), () => {
result.assign( mx_cell_noise_vec3_3d( minpos.add( p ) ) );
} );
return result;
}, {
p: 'vec3',
jitter: 'float',
style: 'int',
metric: 'int',
return: 'vec3'
} );
const mx_worley_noise_vec3_style$1 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec3_style_0, mx_worley_noise_vec3_style_1 ] );
// Unified Noise 2D
const mx_unifiednoise2d$1 = /*@__PURE__*/ Fn( ( [
noiseTypeInput,
texcoordInput,
freqInput,
offsetInput,
jitterInput,
outminInput,
outmaxInput,
clampoutputInput,
octavesInput,
lacunarityInput,
diminishInput,
styleInput
] ) => {
const noiseType = int( noiseTypeInput ).toVar();
const texcoord = vec2( texcoordInput ).toVar();
const freq = vec2( freqInput ).toVar();
const offset = vec2( offsetInput ).toVar();
const jitter = float( jitterInput ).toVar();
const outmin = float( outminInput ).toVar();
const outmax = float( outmaxInput ).toVar();
const clampoutput = float( clampoutputInput ).toVar();
const octaves = int( octavesInput ).toVar();
const lacunarity = float( lacunarityInput ).toVar();
const diminish = float( diminishInput ).toVar();
const style = int( styleInput ).toVar();
const applyFreq = mul( texcoord, freq ).toVar();
const applyOffset = add( applyFreq, offset ).toVar();
const cellJitterMult = mul( sub( jitter, 1 ), 90000 ).toVar();
const applyCellJitter = mx_rotate2d( applyOffset, cellJitterMult ).toVar();
const fractalInput = vec3( applyOffset.x, applyOffset.y, cellJitterMult ).toVar();
const result = float( 0 ).toVar();
If( noiseType.equal( int( 0 ) ), () => {
result.assign( mx_perlin_noise_float_scaled( applyCellJitter, 0.5, 0.5 ) );
} );
If( noiseType.equal( int( 1 ) ), () => {
result.assign( mx_cell_noise_float$1( applyCellJitter ) );
} );
If( noiseType.equal( int( 2 ) ), () => {
result.assign( mx_worley_noise_float_2d$1( applyOffset, jitter, style ) );
} );
If( noiseType.equal( int( 3 ) ), () => {
result.assign( mx_fractal_noise_float$1( fractalInput, octaves, lacunarity, diminish ) );
} );
const ranged = add( outmin, mul( result, sub( outmax, outmin ) ) ).toVar();
const clamped = clamp( ranged, outmin, outmax ).toVar();
const output = ranged.toVar();
If( clampoutput.equal( float( 1 ) ), () => {
output.assign( clamped );
} );
return output;
}, {
noiseType: 'int',
texcoord: 'vec2',
freq: 'vec2',
offset: 'vec2',
jitter: 'float',
outmin: 'float',
outmax: 'float',
clampoutput: 'float',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
style: 'int',
return: 'float'
} );
// Unified Noise 3D
const mx_unifiednoise3d$1 = /*@__PURE__*/ Fn( ( [
noiseTypeInput,
positionInput,
freqInput,
offsetInput,
jitterInput,
outminInput,
outmaxInput,
clampoutputInput,
octavesInput,
lacunarityInput,
diminishInput,
styleInput
] ) => {
const noiseType = int( noiseTypeInput ).toVar();
const position = vec3( positionInput ).toVar();
const freq = vec3( freqInput ).toVar();
const offset = vec3( offsetInput ).toVar();
const jitter = float( jitterInput ).toVar();
const outmin = float( outminInput ).toVar();
const outmax = float( outmaxInput ).toVar();
const clampoutput = float( clampoutputInput ).toVar();
const octaves = int( octavesInput ).toVar();
const lacunarity = float( lacunarityInput ).toVar();
const diminish = float( diminishInput ).toVar();
const style = int( styleInput ).toVar();
const applyFreq = mul( position, freq ).toVar();
const applyOffset = add( applyFreq, offset ).toVar();
const cellJitterMult = mul( sub( jitter, 1 ), 90000 ).toVar();
const applyCellJitter = mx_rotate3d( applyOffset, cellJitterMult, vec3( 0.1, 1, 0 ) ).toVar();
const perlin = mx_perlin_noise_float_scaled( applyCellJitter, 0.5, 0.5 );
const cell = mx_cell_noise_float$1( applyCellJitter );
const worley = mx_worley_noise_float_3d$1( applyOffset, jitter, style );
const fractal = mx_fractal_noise_float$1( applyCellJitter, octaves, lacunarity, diminish );
const result = perlin.toVar();
If( noiseType.equal( int( 1 ) ), () => {
result.assign( cell );
} );
If( noiseType.equal( int( 2 ) ), () => {
result.assign( worley );
} );
If( noiseType.equal( int( 3 ) ), () => {
result.assign( fractal );
} );
const ranged = add( outmin, mul( result, sub( outmax, outmin ) ) ).toVar();
const clamped = clamp( ranged, outmin, outmax ).toVar();
const output = ranged.toVar();
If( clampoutput.equal( float( 1 ) ), () => {
output.assign( clamped );
} );
return output;
}, {
noiseType: 'int',
position: 'vec3',
freq: 'vec3',
offset: 'vec3',
jitter: 'float',
outmin: 'float',
outmax: 'float',
clampoutput: 'float',
octaves: 'int',
lacunarity: 'float',
diminish: 'float',
style: 'int',
return: 'float'
} );
const mx_hsvtorgb = /*@__PURE__*/ Fn( ( [ hsv ] ) => {
const s = hsv.y;
const v = hsv.z;
const result = vec3().toVar();
If( s.lessThan( 0.0001 ), () => {
result.assign( vec3( v, v, v ) );
} ).Else( () => {
let h = hsv.x;
h = h.sub( floor( h ) ).mul( 6.0 ).toVar(); // TODO: check what .toVar() is needed in node system cache
const hi = int( trunc( h ) );
const f = h.sub( float( hi ) );
const p = v.mul( s.oneMinus() );
const q = v.mul( s.mul( f ).oneMinus() );
const t = v.mul( s.mul( f.oneMinus() ).oneMinus() );
If( hi.equal( int( 0 ) ), () => {
result.assign( vec3( v, t, p ) );
} ).ElseIf( hi.equal( int( 1 ) ), () => {
result.assign( vec3( q, v, p ) );
} ).ElseIf( hi.equal( int( 2 ) ), () => {
result.assign( vec3( p, v, t ) );
} ).ElseIf( hi.equal( int( 3 ) ), () => {
result.assign( vec3( p, q, v ) );
} ).ElseIf( hi.equal( int( 4 ) ), () => {
result.assign( vec3( t, p, v ) );
} ).Else( () => {
result.assign( vec3( v, p, q ) );
} );
} );
return result;
}, { hsv: 'vec3', return: 'vec3' } );
const mx_rgbtohsv = /*@__PURE__*/ Fn( ( [ c_immutable ] ) => {
const c = vec3( c_immutable ).toVar();
const r = float( c.x ).toVar();
const g = float( c.y ).toVar();
const b = float( c.z ).toVar();
const mincomp = float( min( r, min( g, b ) ) ).toVar();
const maxcomp = float( max( r, max( g, b ) ) ).toVar();
const delta = float( maxcomp.sub( mincomp ) ).toVar();
const h = float().toVar(), s = float().toVar(), v = float().toVar();
v.assign( maxcomp );
If( maxcomp.greaterThan( 0.0 ), () => {
s.assign( delta.div( maxcomp ) );
} ).Else( () => {
s.assign( 0.0 );
} );
If( s.lessThanEqual( 0.0 ), () => {
h.assign( 0.0 );
} ).Else( () => {
If( r.greaterThanEqual( maxcomp ), () => {
h.assign( g.sub( b ).div( delta ) );
} ).ElseIf( g.greaterThanEqual( maxcomp ), () => {
h.assign( add( 2.0, b.sub( r ).div( delta ) ) );
} ).Else( () => {
h.assign( add( 4.0, r.sub( g ).div( delta ) ) );
} );
h.mulAssign( 1.0 / 6.0 );
If( h.lessThan( 0.0 ), () => {
h.addAssign( 1.0 );
} );
} );
return vec3( h, s, v );
}, { c: 'vec3', return: 'vec3' } );
const mx_srgb_texture_to_lin_rec709 = /*@__PURE__*/ Fn( ( [ color_immutable ] ) => {
const color = vec3( color_immutable ).toVar();
const isAbove = bvec3( greaterThan( color, vec3( 0.04045 ) ) ).toVar();
const linSeg = vec3( color.div( 12.92 ) ).toVar();
const powSeg = vec3( pow( max( color.add( vec3( 0.055 ) ), vec3( 0.0 ) ).div( 1.055 ), vec3( 2.4 ) ) ).toVar();
return mix( linSeg, powSeg, isAbove );
}, { color: 'vec3', return: 'vec3' } );
const mx_aastep = ( threshold, value ) => {
threshold = float( threshold );
value = float( value );
const afwidth = vec2( value.dFdx(), value.dFdy() ).length().mul( 0.70710678118654757 );
return smoothstep( threshold.sub( afwidth ), threshold.add( afwidth ), value );
};
const _ramp = ( a, b, uv, p ) => mix( a, b, uv[ p ].clamp() );
const mx_ramplr = ( valuel, valuer, texcoord = uv$1() ) => _ramp( valuel, valuer, texcoord, 'x' );
const mx_ramptb = ( valueb, valuet, texcoord = uv$1() ) => _ramp( valueb, valuet, texcoord, 'y' );
// Bilinear ramp: interpolate between four corners (tl, tr, bl, br) using texcoord.x and texcoord.y
const mx_ramp4 = (
valuetl, valuetr, valuebl, valuebr, texcoord = uv$1()
) => {
const u = texcoord.x.clamp();
const v = texcoord.y.clamp();
const top = mix( valuetl, valuetr, u );
const bottom = mix( valuebl, valuebr, u );
return mix( bottom, top, v );
};
const _split = ( a, b, center, uv, p ) => mix( a, b, mx_aastep( center, uv[ p ] ) );
const mx_splitlr = ( valuel, valuer, center, texcoord = uv$1() ) => _split( valuel, valuer, center, texcoord, 'x' );
const mx_splittb = ( valueb, valuet, center, texcoord = uv$1() ) => _split( valueb, valuet, center, texcoord, 'y' );
const mx_transform_uv = ( uv_scale = 1, uv_offset = 0, uv_geo = uv$1() ) => uv_geo.mul( uv_scale ).add( uv_offset );
const mx_safepower = ( in1, in2 = 1 ) => {
in1 = float( in1 );
return in1.abs().pow( in2 ).mul( in1.sign() );
};
const mx_contrast = ( input, amount = 1, pivot = .5 ) => float( input ).sub( pivot ).mul( amount ).add( pivot );
const mx_noise_float = ( texcoord = uv$1(), amplitude = 1, pivot = 0 ) => mx_perlin_noise_float( texcoord.convert( 'vec2|vec3' ) ).mul( amplitude ).add( pivot );
//export const mx_noise_vec2 = ( texcoord = uv(), amplitude = 1, pivot = 0 ) => mx_perlin_noise_vec3( texcoord.convert( 'vec2|vec3' ) ).mul( amplitude ).add( pivot );
const mx_noise_vec3 = ( texcoord = uv$1(), amplitude = 1, pivot = 0 ) => mx_perlin_noise_vec3( texcoord.convert( 'vec2|vec3' ) ).mul( amplitude ).add( pivot );
const mx_perlin_noise_vec4_0 = /*@__PURE__*/ Fn( ( [ p ] ) => vec4( mx_perlin_noise_vec3( p ), mx_perlin_noise_float( p.add( vec2( 19, 73 ) ) ) ), { p: 'vec2', return: 'vec4' } );
const mx_perlin_noise_vec4_1 = /*@__PURE__*/ Fn( ( [ p ] ) => vec4( mx_perlin_noise_vec3( p ), mx_perlin_noise_float( p.add( vec3( 19, 73, 29 ) ) ) ), { p: 'vec3', return: 'vec4' } );
const mx_perlin_noise_vec4 = /*@__PURE__*/ overloadingFn( [ mx_perlin_noise_vec4_0, mx_perlin_noise_vec4_1 ] );
const mx_noise_vec4 = ( texcoord = uv$1(), amplitude = 1, pivot = 0 ) => mx_perlin_noise_vec4( texcoord.convert( 'vec2|vec3' ) ).mul( amplitude ).add( pivot );
const mx_smoothstep = ( inNode, low = 0, high = 1 ) => {
const range = sub( high, low );
const safeRange = max( abs( range ), float( EPSILON ) );
const t = clamp( div( sub( inNode, low ), safeRange ), float( 0 ), float( 1 ) );
const hermite = mul( mul( t, t ), sub( float( 3 ), mul( float( 2 ), t ) ) );
const fallback = step( high, inNode );
const useFallback = step( high, low );
return mix( hermite, fallback, useFallback );
};
const mx_cell_noise_vec3 = ( texcoord = uv$1() ) => mx_cell_noise_vec3$1( texcoord.convert( 'vec2|vec3' ) );
const mx_worley_noise_float_2d = ( texcoord = uv$1(), jitter = 1, style = 0 ) => mx_worley_noise_float_2d$1( texcoord, jitter, style );
const mx_worley_noise_float_3d = ( texcoord = uv$1(), jitter = 1, style = 0 ) => mx_worley_noise_float_3d$1( texcoord, jitter, style );
const mx_unifiednoise2d = ( noiseType, texcoord = uv$1(), freq = vec2( 1, 1 ), offset = vec2( 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5, style = 0 ) => mx_unifiednoise2d$1( noiseType, texcoord, freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish, style );
const mx_unifiednoise3d = ( noiseType, texcoord = uv$1(), freq = vec3( 1, 1, 1 ), offset = vec3( 0, 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5, style = 0 ) => mx_unifiednoise3d$1( noiseType, texcoord, freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish, style );
const mx_worley_noise_float = ( texcoord = uv$1(), jitter = 1, style = 0 ) => mx_worley_noise_float_3d( texcoord.convert( 'vec2|vec3' ), jitter, style );
const mx_worley_noise_vec2 = ( texcoord = uv$1(), jitter = 1 ) => mx_worley_noise_vec2$1( texcoord.convert( 'vec2|vec3' ), jitter, int( 1 ) );
const mx_worley_noise_vec3 = ( texcoord = uv$1(), jitter = 1, metric = 1 ) => mx_worley_noise_vec3$1( texcoord.convert( 'vec2|vec3' ), jitter, int( metric ) );
const mx_worley_noise_vec3_style = ( texcoord = uv$1(), jitter = 1, style = 0, metric = 0 ) => mx_worley_noise_vec3_style$1( texcoord.convert( 'vec2|vec3' ), jitter, int( style ), int( metric ) );
const mx_cell_noise_float = ( texcoord = uv$1() ) => mx_cell_noise_float$1( texcoord.convert( 'vec2|vec3' ) );
const mx_fractal_noise_float_2d = ( texcoord = uv$1(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => mx_fractal_noise_float_2d$1( texcoord, int( octaves ), lacunarity, diminish ).mul( amplitude );
const mx_fractal_noise_float = ( position = uv$1(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => mx_fractal_noise_float$1( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
const mx_fractal_noise_vec2 = ( position = uv$1(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => mx_fractal_noise_vec2$1( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
const mx_fractal_noise_vec3 = ( position = uv$1(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => mx_fractal_noise_vec3$1( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
const mx_fractal_noise_vec4 = ( position = uv$1(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => mx_fractal_noise_vec4$1( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
// === Moved from MaterialXLoader.js ===
// Math ops
const mx_add = ( in1, in2 = float( 0 ) ) => add( in1, in2 );
const mx_subtract = ( in1, in2 = float( 0 ) ) => sub( in1, in2 );
const mx_multiply = ( in1, in2 = float( 1 ) ) => mul( in1, in2 );
const mx_divide = ( in1, in2 = float( 1 ) ) => div( in1, in2 );
const mx_modulo = ( in1, in2 = float( 1 ) ) => sub( in1, mul( in2, floor( div( in1, in2 ) ) ) );
const mx_power = ( in1, in2 = float( 1 ) ) => pow( in1, in2 );
const mx_atan2 = ( in1 = float( 0 ), in2 = float( 1 ) ) => atan( in1, in2 );
const mx_timer = () => time;
const mx_frame = () => frameId;
const mx_invert = ( in1, amount = float( 1 ) ) => sub( amount, in1 );
const mx_ifgreater = ( value1, value2, in1, in2 ) => value1.greaterThan( value2 ).mix( in2, in1 );
const mx_ifgreatereq = ( value1, value2, in1, in2 ) => value1.greaterThanEqual( value2 ).mix( in2, in1 );
const mx_ifequal = ( value1, value2, in1, in2 ) => value1.equal( value2 ).mix( in2, in1 );
// Enhanced separate node to support multi-output referencing (outx, outy, outz, outw)
const mx_separate = ( in1, channelOrOut = null ) => {
if ( typeof channelOrOut === 'string' ) {
const map = { x: 0, r: 0, y: 1, g: 1, z: 2, b: 2, w: 3, a: 3 };
const c = channelOrOut.replace( /^out/, '' ).toLowerCase();
if ( map[ c ] !== undefined ) return in1.element( map[ c ] );
}
if ( typeof channelOrOut === 'number' ) {
return in1.element( channelOrOut );
}
if ( typeof channelOrOut === 'string' && channelOrOut.length === 1 ) {
const map = { x: 0, r: 0, y: 1, g: 1, z: 2, b: 2, w: 3, a: 3 };
if ( map[ channelOrOut ] !== undefined ) return in1.element( map[ channelOrOut ] );
}
return in1;
};
const mx_place2d = (
texcoord, pivot = vec2( 0, 0 ), scale = vec2( 1, 1 ), rotate = float( 0 ), offset = vec2( 0, 0 ), operationorder = int( 0 )
) => {
const centered = sub( texcoord, pivot );
const srt = add( sub( mx_rotate2d( div( centered, scale ), rotate ), offset ), pivot );
const trs = add( div( mx_rotate2d( sub( centered, offset ), rotate ), scale ), pivot );
if ( typeof operationorder === 'number' ) return Math.abs( operationorder ) > Number.EPSILON ? trs : srt;
return mix( srt, trs, step( 0.5, float( operationorder ) ) );
};
const mx_heighttonormal = ( input, scale = 1, texcoord = uv$1() ) => {
const sobelScale = float( 1.0 / 16.0 );
const height = float( input );
const uvNode = vec2( texcoord );
const dHdS = vec2( height.dFdx(), height.dFdy() ).mul( float( scale ) ).mul( sobelScale );
const dUdS = vec2( uvNode.x.dFdx(), uvNode.x.dFdy() );
const dVdS = vec2( uvNode.y.dFdx(), uvNode.y.dFdy() );
const tangent = vec3( dUdS.x, dVdS.x, dHdS.x );
const bitangent = vec3( dUdS.y, dVdS.y, dHdS.y );
let n = cross( tangent, bitangent );
const invalid = dot( n, n ).lessThan( float( 1e-12 ) );
n = invalid.mix( n, vec3( 0, 0, 1 ) );
const mirrored = n.z.lessThan( float( 0 ) );
n = mirrored.mix( n, n.mul( -1 ) );
return normalize( n ).mul( 0.5 ).add( 0.5 );
};
const EON_EPSILON = 1e-7;
const FON_A_COEFFICIENT = 0.5 - 2 / ( 3 * Math.PI );
const FON_AVERAGE_ALBEDO_COEFFICIENT = 2 / 3 - 28 / ( 15 * Math.PI );
const FON_DirectionalAlbedo = /*@__PURE__*/ Fn( ( { mu, roughness, A } ) => {
const muComp = mu.oneMinus();
const gOverPi = muComp.mul(
muComp.mul(
muComp.mul(
muComp.mul( 0.0714429953 ).sub( 0.332181442 )
).add( 0.491881867 )
).add( 0.0571085289 )
);
return A.mul( roughness.mul( gOverPi ).add( 1.0 ) );
}, { mu: 'float', roughness: 'float', A: 'float', return: 'float' } );
// Portsmouth et al. 2025, "EON: A Practical Energy-Preserving Rough Diffuse BRDF"
// https://jcgt.org/published/0014/01/06/
const BRDF_EON = /*@__PURE__*/ Fn( ( { lightDirection, diffuseColor, roughness, normalView: normalView$1 = normalView, viewDirection = positionViewDirection } ) => {
const rho = diffuseColor.clamp();
const dotNL = normalView$1.dot( lightDirection ).clamp();
const dotNV = normalView$1.dot( viewDirection ).clamp();
const s = lightDirection.dot( viewDirection ).sub( dotNL.mul( dotNV ) );
const sOverT = s.greaterThan( 0.0 ).select( s.div( dotNL.max( dotNV ).max( EON_EPSILON ) ), s );
const A = roughness.mul( FON_A_COEFFICIENT ).add( 1.0 ).reciprocal();
const singleScatter = rho.mul( 1 / Math.PI, A, roughness.mul( sOverT ).add( 1.0 ) );
const averageAlbedo = A.mul( roughness.mul( FON_AVERAGE_ALBEDO_COEFFICIENT ).add( 1.0 ) );
const albedoV = FON_DirectionalAlbedo( { mu: dotNV, roughness, A } );
const albedoL = FON_DirectionalAlbedo( { mu: dotNL, roughness, A } );
const rhoMultiScatter = rho.mul( rho, averageAlbedo ).div( rho.mul( averageAlbedo.oneMinus() ).oneMinus().max( EON_EPSILON ) );
const multiScatter = rhoMultiScatter.mul(
1 / Math.PI,
albedoV.oneMinus().max( EON_EPSILON ),
albedoL.oneMinus().max( EON_EPSILON )
).div( averageAlbedo.oneMinus().max( EON_EPSILON ) );
const eon = singleScatter.add( multiScatter );
return roughness.lessThanEqual( EON_EPSILON ).select( rho.mul( 1 / Math.PI ), eon );
} );
const EON_DirectionalAlbedo = /*@__PURE__*/ Fn( ( { diffuseColor, roughness, dotNV } ) => {
const rho = diffuseColor.clamp();
const A = roughness.mul( FON_A_COEFFICIENT ).add( 1.0 ).reciprocal();
const directionalAlbedo = FON_DirectionalAlbedo( { mu: dotNV.clamp(), roughness, A } );
const averageAlbedo = A.mul( roughness.mul( FON_AVERAGE_ALBEDO_COEFFICIENT ).add( 1.0 ) );
const rhoMultiScatter = rho.mul( rho, averageAlbedo ).div( rho.mul( averageAlbedo.oneMinus() ).oneMinus().max( EON_EPSILON ) );
const eonAlbedo = rho.mul( directionalAlbedo ).add( rhoMultiScatter.mul( directionalAlbedo.oneMinus() ) );
return roughness.lessThanEqual( EON_EPSILON ).select( rho, eonAlbedo );
}, { diffuseColor: 'vec3', roughness: 'float', dotNV: 'float', return: 'vec3' } );
const F_Schlick = /*@__PURE__*/ Fn( ( { f0, f90, dotVH } ) => {
// Original approximation by Christophe Schlick '94
// float fresnel = pow( 1.0 - dotVH, 5.0 );
// Optimized variant (presented by Epic at SIGGRAPH '13)
// https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
const fresnel = dotVH.mul( -5.55473 ).sub( 6.98316 ).mul( dotVH ).exp2();
return f0.mul( fresnel.oneMinus() ).add( f90.mul( fresnel ) );
} ); // validated
// Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2
// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
const V_GGX_SmithCorrelated = /*@__PURE__*/ Fn( ( { alpha, dotNL, dotNV } ) => {
const a2 = alpha.pow2();
const gv = dotNL.mul( a2.add( a2.oneMinus().mul( dotNV.pow2() ) ).sqrt() );
const gl = dotNV.mul( a2.add( a2.oneMinus().mul( dotNL.pow2() ) ).sqrt() );
return div( 0.5, gv.add( gl ).max( EPSILON ) );
}, { alpha: 'float', dotNL: 'float', dotNV: 'float', return: 'float' } ); // validated
// https://google.github.io/filament/Filament.md.html#materialsystem/anisotropicmodel/anisotropicspecularbrdf
const V_GGX_SmithCorrelated_Anisotropic = /*@__PURE__*/ Fn( ( { alphaT, alphaB, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL } ) => {
const gv = dotNL.mul( vec3( alphaT.mul( dotTV ), alphaB.mul( dotBV ), dotNV ).length() );
const gl = dotNV.mul( vec3( alphaT.mul( dotTL ), alphaB.mul( dotBL ), dotNL ).length() );
return div( 0.5, gv.add( gl ).max( EPSILON ) );
}, {
alphaT: 'float',
alphaB: 'float',
dotTV: 'float',
dotBV: 'float',
dotTL: 'float',
dotBL: 'float',
dotNV: 'float',
dotNL: 'float',
return: 'float'
} );
// Microfacet Models for Refraction through Rough Surfaces - equation (33)
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
// alpha is "roughness squared" in Disney’s reparameterization
const D_GGX = /*@__PURE__*/ Fn( ( { alpha, dotNH } ) => {
const a2 = alpha.pow2();
const denom = dotNH.pow2().mul( a2.oneMinus() ).oneMinus(); // avoid alpha = 0 with dotNH = 1
return a2.div( denom.pow2() ).mul( 1 / Math.PI );
}, { alpha: 'float', dotNH: 'float', return: 'float' } ); // validated
const RECIPROCAL_PI = /*@__PURE__*/ float( 1 / Math.PI );
// https://google.github.io/filament/Filament.md.html#materialsystem/anisotropicmodel/anisotropicspecularbrdf
const D_GGX_Anisotropic = /*@__PURE__*/ Fn( ( { alphaT, alphaB, dotNH, dotTH, dotBH } ) => {
const a2 = alphaT.mul( alphaB );
const v = vec3( alphaB.mul( dotTH ), alphaT.mul( dotBH ), a2.mul( dotNH ) );
const v2 = v.dot( v );
const w2 = a2.div( v2 );
return RECIPROCAL_PI.mul( a2.mul( w2.pow2() ) );
}, {
alphaT: 'float',
alphaB: 'float',
dotNH: 'float',
dotTH: 'float',
dotBH: 'float',
return: 'float'
} );
// GGX Distribution, Schlick Fresnel, GGX_SmithCorrelated Visibility
const BRDF_GGX = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness, f, normalView: normalView$1 = normalView, viewDirection = positionViewDirection, USE_IRIDESCENCE, USE_ANISOTROPY } ) => {
const alpha = roughness.max( 0.045 ).pow2(); // punctual lights need a minimum roughness to show a highlight
const halfDir = lightDirection.add( viewDirection ).normalize();
const dotNL = normalView$1.dot( lightDirection ).clamp();
const dotNV = normalView$1.dot( viewDirection ).clamp(); // @ TODO: Move to core dotNV
const dotNH = normalView$1.dot( halfDir ).clamp();
const dotVH = viewDirection.dot( halfDir ).clamp();
let F = F_Schlick( { f0, f90, dotVH } );
let V, D;
if ( defined( USE_IRIDESCENCE ) ) {
F = iridescence.mix( F, f );
}
if ( defined( USE_ANISOTROPY ) ) {
const dotTL = anisotropyT.dot( lightDirection );
const dotTV = anisotropyT.dot( viewDirection );
const dotTH = anisotropyT.dot( halfDir );
const dotBL = anisotropyB.dot( lightDirection );
const dotBV = anisotropyB.dot( viewDirection );
const dotBH = anisotropyB.dot( halfDir );
const clampedAlphaT = alphaT.max( alpha );
V = V_GGX_SmithCorrelated_Anisotropic( { alphaT: clampedAlphaT, alphaB: alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL } );
D = D_GGX_Anisotropic( { alphaT: clampedAlphaT, alphaB: alpha, dotNH, dotTH, dotBH } );
} else {
V = V_GGX_SmithCorrelated( { alpha, dotNL, dotNV } );
D = D_GGX( { alpha, dotNH } );
}
return F.mul( V ).mul( D );
} ); // validated
const BRDF_Lambert = /*@__PURE__*/ Fn( ( inputs ) => {
return inputs.diffuseColor.mul( 1 / Math.PI ); // punctual light
} ); // validated
// https://github.com/google/filament/blob/master/shaders/src/brdf.fs
const D_Charlie = /*@__PURE__*/ Fn( ( { roughness, dotNH } ) => {
const alpha = roughness.pow2();
// Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF"
const invAlpha = float( 1.0 ).div( alpha );
const cos2h = dotNH.pow2();
const sin2h = cos2h.oneMinus().max( 0.0078125 ); // 2^(-14/2), so sin2h^2 > 0 in fp16
return float( 2.0 ).add( invAlpha ).mul( sin2h.pow( invAlpha.mul( 0.5 ) ) ).div( 2.0 * Math.PI );
}, { roughness: 'float', dotNH: 'float', return: 'float' } );
// https://github.com/google/filament/blob/master/shaders/src/brdf.fs
const V_Neubelt = /*@__PURE__*/ Fn( ( { dotNV, dotNL } ) => {
// Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"
return float( 1.0 ).div( float( 4.0 ).mul( dotNL.add( dotNV ).sub( dotNL.mul( dotNV ) ) ) ).clamp();
}, { dotNV: 'float', dotNL: 'float', return: 'float' } );
const BRDF_Sheen = /*@__PURE__*/ Fn( ( { lightDirection } ) => {
const halfDir = lightDirection.add( positionViewDirection ).normalize();
const dotNL = normalView.dot( lightDirection ).clamp();
const dotNV = normalView.dot( positionViewDirection ).clamp();
const dotNH = normalView.dot( halfDir ).clamp();
const D = D_Charlie( { roughness: sheenRoughness, dotNH } );
const V = V_Neubelt( { dotNV, dotNL } );
return sheen.mul( D ).mul( V );
} );
/**
* Precomputed DFG LUT for physically based specular lighting, used by both
* image-based lighting and direct-light multi-scattering energy compensation
* Resolution: 16x16
* Samples: 4096 per texel
* Format: RG16F (2 half floats per texel: scale, bias)
*/
const DATA = new Uint16Array( [
0x30b5, 0x3ad1, 0x314c, 0x3a4d, 0x33d2, 0x391c, 0x35ef, 0x3828, 0x37f3, 0x36a6, 0x38d1, 0x3539, 0x3979, 0x3410, 0x39f8, 0x3252, 0x3a53, 0x30f0, 0x3a94, 0x2fc9, 0x3abf, 0x2e35, 0x3ada, 0x2d05, 0x3ae8, 0x2c1f, 0x3aed, 0x2ae0, 0x3aea, 0x29d1, 0x3ae1, 0x28ff,
0x3638, 0x38e4, 0x364a, 0x38ce, 0x3699, 0x385e, 0x374e, 0x372c, 0x3839, 0x35a4, 0x38dc, 0x3462, 0x396e, 0x32c4, 0x39de, 0x3134, 0x3a2b, 0x3003, 0x3a59, 0x2e3a, 0x3a6d, 0x2ce1, 0x3a6e, 0x2bba, 0x3a5f, 0x2a33, 0x3a49, 0x290a, 0x3a2d, 0x2826, 0x3a0a, 0x26e8,
0x3894, 0x36d7, 0x3897, 0x36c9, 0x38a3, 0x3675, 0x38bc, 0x35ac, 0x38ee, 0x349c, 0x393e, 0x3332, 0x3997, 0x3186, 0x39e2, 0x3038, 0x3a13, 0x2e75, 0x3a29, 0x2cf5, 0x3a2d, 0x2bac, 0x3a21, 0x29ff, 0x3a04, 0x28bc, 0x39dc, 0x2790, 0x39ad, 0x261a, 0x3978, 0x24fa,
0x39ac, 0x34a8, 0x39ac, 0x34a3, 0x39ae, 0x3480, 0x39ae, 0x3423, 0x39b1, 0x330e, 0x39c2, 0x31a9, 0x39e0, 0x3063, 0x39fc, 0x2eb5, 0x3a0c, 0x2d1d, 0x3a14, 0x2bcf, 0x3a07, 0x29ff, 0x39e9, 0x28a3, 0x39be, 0x273c, 0x3989, 0x25b3, 0x394a, 0x2488, 0x3907, 0x2345,
0x3a77, 0x3223, 0x3a76, 0x321f, 0x3a73, 0x3204, 0x3a6a, 0x31b3, 0x3a58, 0x3114, 0x3a45, 0x303b, 0x3a34, 0x2eb6, 0x3a26, 0x2d31, 0x3a1e, 0x2bef, 0x3a0b, 0x2a0d, 0x39ec, 0x28a1, 0x39c0, 0x271b, 0x3987, 0x2580, 0x3944, 0x2449, 0x38fa, 0x22bd, 0x38ac, 0x2155,
0x3b07, 0x2fca, 0x3b06, 0x2fca, 0x3b00, 0x2fb8, 0x3af4, 0x2f7c, 0x3adb, 0x2eea, 0x3ab4, 0x2e00, 0x3a85, 0x2cec, 0x3a5e, 0x2bc5, 0x3a36, 0x2a00, 0x3a0d, 0x2899, 0x39dc, 0x2707, 0x39a0, 0x2562, 0x395a, 0x2424, 0x390b, 0x2268, 0x38b7, 0x20fd, 0x385f, 0x1fd1,
0x3b69, 0x2cb9, 0x3b68, 0x2cbb, 0x3b62, 0x2cbb, 0x3b56, 0x2cae, 0x3b3b, 0x2c78, 0x3b0d, 0x2c0a, 0x3acf, 0x2ae3, 0x3a92, 0x2998, 0x3a54, 0x2867, 0x3a17, 0x26d0, 0x39d3, 0x253c, 0x3989, 0x2402, 0x3935, 0x2226, 0x38dc, 0x20bd, 0x387d, 0x1f54, 0x381d, 0x1db3,
0x3ba9, 0x296b, 0x3ba8, 0x296f, 0x3ba3, 0x297b, 0x3b98, 0x2987, 0x3b7f, 0x2976, 0x3b4e, 0x2927, 0x3b0e, 0x2895, 0x3ac2, 0x27b7, 0x3a73, 0x263b, 0x3a23, 0x24e7, 0x39d0, 0x239b, 0x3976, 0x21d9, 0x3917, 0x207e, 0x38b2, 0x1ee7, 0x384b, 0x1d53, 0x37c7, 0x1c1e,
0x3bd2, 0x25cb, 0x3bd1, 0x25d3, 0x3bcd, 0x25f0, 0x3bc2, 0x261f, 0x3bad, 0x2645, 0x3b7d, 0x262d, 0x3b3e, 0x25c4, 0x3aec, 0x250f, 0x3a93, 0x243a, 0x3a32, 0x22ce, 0x39d0, 0x215b, 0x3969, 0x202a, 0x38fe, 0x1e6e, 0x388f, 0x1cf1, 0x381f, 0x1b9b, 0x3762, 0x19dd,
0x3be9, 0x21ab, 0x3be9, 0x21b7, 0x3be5, 0x21e5, 0x3bdd, 0x2241, 0x3bc9, 0x22a7, 0x3ba0, 0x22ec, 0x3b62, 0x22cd, 0x3b0f, 0x2247, 0x3aae, 0x2175, 0x3a44, 0x2088, 0x39d4, 0x1f49, 0x3960, 0x1dbe, 0x38e9, 0x1c77, 0x3870, 0x1ae8, 0x37f1, 0x1953, 0x3708, 0x181b,
0x3bf6, 0x1cea, 0x3bf6, 0x1cfb, 0x3bf3, 0x1d38, 0x3bec, 0x1dbd, 0x3bda, 0x1e7c, 0x3bb7, 0x1f25, 0x3b7d, 0x1f79, 0x3b2c, 0x1f4c, 0x3ac6, 0x1ea6, 0x3a55, 0x1dbb, 0x39da, 0x1cbd, 0x395a, 0x1b9d, 0x38d8, 0x1a00, 0x3855, 0x18ac, 0x37ab, 0x173c, 0x36b7, 0x1598,
0x3bfc, 0x1736, 0x3bfc, 0x1759, 0x3bf9, 0x17e7, 0x3bf4, 0x1896, 0x3be4, 0x1997, 0x3bc6, 0x1aa8, 0x3b91, 0x1b84, 0x3b43, 0x1bd2, 0x3ade, 0x1b8a, 0x3a65, 0x1acd, 0x39e2, 0x19d3, 0x3957, 0x18cd, 0x38ca, 0x17b3, 0x383e, 0x1613, 0x376d, 0x14bf, 0x366f, 0x135e,
0x3bff, 0x101b, 0x3bff, 0x1039, 0x3bfc, 0x10c8, 0x3bf9, 0x1226, 0x3bea, 0x1428, 0x3bcf, 0x1584, 0x3b9f, 0x16c5, 0x3b54, 0x179a, 0x3af0, 0x17ce, 0x3a76, 0x1771, 0x39ea, 0x16a4, 0x3956, 0x15a7, 0x38bf, 0x14a7, 0x3829, 0x1379, 0x3735, 0x11ea, 0x362d, 0x10a1,
0x3c00, 0x061b, 0x3c00, 0x066a, 0x3bfe, 0x081c, 0x3bfa, 0x0a4c, 0x3bed, 0x0d16, 0x3bd5, 0x0fb3, 0x3ba9, 0x114d, 0x3b63, 0x127c, 0x3b01, 0x132f, 0x3a85, 0x1344, 0x39f4, 0x12d2, 0x3957, 0x120d, 0x38b5, 0x1122, 0x3817, 0x103c, 0x3703, 0x0ed3, 0x35f0, 0x0d6d,
0x3c00, 0x007a, 0x3c00, 0x0089, 0x3bfe, 0x011d, 0x3bfb, 0x027c, 0x3bf0, 0x04fa, 0x3bda, 0x0881, 0x3bb1, 0x0acd, 0x3b6f, 0x0c97, 0x3b10, 0x0d7b, 0x3a93, 0x0df1, 0x39fe, 0x0def, 0x3959, 0x0d8a, 0x38af, 0x0ce9, 0x3808, 0x0c31, 0x36d5, 0x0af0, 0x35b9, 0x09a3,
0x3c00, 0x0000, 0x3c00, 0x0001, 0x3bff, 0x0015, 0x3bfb, 0x0059, 0x3bf2, 0x00fd, 0x3bdd, 0x01df, 0x3bb7, 0x031c, 0x3b79, 0x047c, 0x3b1d, 0x05d4, 0x3aa0, 0x06d5, 0x3a08, 0x075a, 0x395d, 0x075e, 0x38aa, 0x06f7, 0x37f4, 0x0648, 0x36ac, 0x0576, 0x3586, 0x049f
] );
let lut = null;
const DFGLUT = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
if ( lut === null ) {
lut = new DataTexture( DATA, 16, 16, RGFormat, HalfFloatType );
lut.name = 'DFG_LUT';
lut.minFilter = LinearFilter;
lut.magFilter = LinearFilter;
lut.wrapS = ClampToEdgeWrapping;
lut.wrapT = ClampToEdgeWrapping;
lut.generateMipmaps = false;
lut.needsUpdate = true;
}
const uv = vec2( roughness, dotNV );
return texture( lut, uv ).rg;
} );
const EnvironmentBRDF = /*@__PURE__*/ Fn( ( inputs ) => {
const { dotNV, specularColor, specularF90, roughness } = inputs;
const fab = DFGLUT( { dotNV, roughness } );
return specularColor.mul( fab.x ).add( specularF90.mul( fab.y ) );
} );
const Schlick_to_F0 = /*@__PURE__*/ Fn( ( { f, f90, dotVH } ) => {
const x = dotVH.oneMinus().saturate();
const x2 = x.mul( x );
const x5 = x.mul( x2, x2 ).clamp( 0, .9999 );
return f.sub( vec3( f90 ).mul( x5 ) ).div( x5.oneMinus() );
}, { f: 'vec3', f90: 'float', dotVH: 'float', return: 'vec3' } );
// Rect Area Light
// Real-Time Polygonal-Light Shading with Linearly Transformed Cosines
// by Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt
// code: https://github.com/selfshadow/ltc_code/
const LTC_Uv = /*@__PURE__*/ Fn( ( { N, V, roughness } ) => {
const LUT_SIZE = 64.0;
const LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;
const LUT_BIAS = 0.5 / LUT_SIZE;
const dotNV = N.dot( V ).saturate();
// texture parameterized by sqrt( GGX alpha ) and sqrt( 1 - cos( theta ) )
const uv = vec2( roughness, dotNV.oneMinus().sqrt() );
uv.assign( uv.mul( LUT_SCALE ).add( LUT_BIAS ) );
return uv;
}, { N: 'vec3', V: 'vec3', roughness: 'float', return: 'vec2' } );
const LTC_ClippedSphereFormFactor = /*@__PURE__*/ Fn( ( { f } ) => {
// Real-Time Area Lighting: a Journey from Research to Production (p.102)
// An approximation of the form factor of a horizon-clipped rectangle.
const l = f.length();
return max( l.mul( l ).add( f.z ).div( l.add( 1.0 ) ), 0 );
}, { f: 'vec3', return: 'float' } );
const LTC_EdgeVectorFormFactor = /*@__PURE__*/ Fn( ( { v1, v2 } ) => {
const x = v1.dot( v2 );
const y = x.abs().toVar();
// rational polynomial approximation to theta / sin( theta ) / 2PI
const a = y.mul( 0.0145206 ).add( 0.4965155 ).mul( y ).add( 0.8543985 ).toVar();
const b = y.add( 4.1616724 ).mul( y ).add( 3.4175940 ).toVar();
const v = a.div( b );
const theta_sintheta = x.greaterThan( 0.0 ).select( v, max( x.mul( x ).oneMinus(), 1e-7 ).inverseSqrt().mul( 0.5 ).sub( v ) );
return v1.cross( v2 ).mul( theta_sintheta );
}, { v1: 'vec3', v2: 'vec3', return: 'vec3' } );
const LTC_Evaluate = /*@__PURE__*/ Fn( ( { N, V, P, mInv, p0, p1, p2, p3 } ) => {
// bail if point is on back side of plane of light
// assumes ccw winding order of light vertices
const v1 = p1.sub( p0 ).toVar();
const v2 = p3.sub( p0 ).toVar();
const lightNormal = v1.cross( v2 );
const result = vec3().toVar();
If( lightNormal.dot( P.sub( p0 ) ).greaterThanEqual( 0.0 ), () => {
// construct orthonormal basis around N
const T1 = V.sub( N.mul( V.dot( N ) ) ).normalize();
const T2 = N.cross( T1 ).negate(); // negated from paper; possibly due to a different handedness of world coordinate system
// compute transform
const mat = mInv.mul( mat3( T1, T2, N ).transpose() ).toVar();
// transform rect
// & project rect onto sphere
const coords0 = mat.mul( p0.sub( P ) ).normalize().toVar();
const coords1 = mat.mul( p1.sub( P ) ).normalize().toVar();
const coords2 = mat.mul( p2.sub( P ) ).normalize().toVar();
const coords3 = mat.mul( p3.sub( P ) ).normalize().toVar();
// calculate vector form factor
const vectorFormFactor = vec3( 0 ).toVar();
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords0, v2: coords1 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords1, v2: coords2 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords2, v2: coords3 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords3, v2: coords0 } ) );
// adjust for horizon clipping
result.assign( vec3( LTC_ClippedSphereFormFactor( { f: vectorFormFactor } ) ) );
} );
return result;
}, {
N: 'vec3',
V: 'vec3',
P: 'vec3',
mInv: 'mat3',
p0: 'vec3',
p1: 'vec3',
p2: 'vec3',
p3: 'vec3',
return: 'vec3'
} );
const LTC_Evaluate_Volume = /*@__PURE__*/ Fn( ( { P, p0, p1, p2, p3 } ) => {
// bail if point is on back side of plane of light
// assumes ccw winding order of light vertices
const v1 = p1.sub( p0 ).toVar();
const v2 = p3.sub( p0 ).toVar();
const lightNormal = v1.cross( v2 );
const result = vec3().toVar();
If( lightNormal.dot( P.sub( p0 ) ).greaterThanEqual( 0.0 ), () => {
// transform rect
// & project rect onto sphere
const coords0 = p0.sub( P ).normalize().toVar();
const coords1 = p1.sub( P ).normalize().toVar();
const coords2 = p2.sub( P ).normalize().toVar();
const coords3 = p3.sub( P ).normalize().toVar();
// calculate vector form factor
const vectorFormFactor = vec3( 0 ).toVar();
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords0, v2: coords1 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords1, v2: coords2 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords2, v2: coords3 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords3, v2: coords0 } ) );
// adjust for horizon clipping
result.assign( vec3( LTC_ClippedSphereFormFactor( { f: vectorFormFactor.abs() } ) ) );
} );
return result;
}, {
P: 'vec3',
p0: 'vec3',
p1: 'vec3',
p2: 'vec3',
p3: 'vec3',
return: 'vec3'
} );
const getGeometryRoughness = /*@__PURE__*/ Fn( ( builder ) => {
if ( builder.geometry.hasAttribute( 'normal' ) === false ) {
return float( 0 );
}
const dxy = normalViewGeometry.dFdx().abs().max( normalViewGeometry.dFdy().abs() );
const geometryRoughness = dxy.x.max( dxy.y ).max( dxy.z );
return geometryRoughness;
} );
/**
* This computes a parallax corrected normal which is used for box-projected cube mapping (BPCEM).
*
* Reference: {@link https://devlog-martinsh.blogspot.com/2011/09/box-projected-cube-environment-mapping.html}
*
* ```js
* const uvNode = getParallaxCorrectNormal( reflectVector, vec3( 200, 100, 100 ), vec3( 0, - 50, 0 ) );
* material.envNode = pmremTexture( renderTarget.texture, uvNode );
* ```
*
* @tsl
* @function
* @param {Node<vec3>} normal - The normal to correct.
* @param {Node<vec3>} cubeSize - The cube size should reflect the size of the environment (BPCEM is usually applied in closed environments like rooms).
* @param {Node<vec3>} cubePos - The cube position.
* @return {Node<vec3>} The parallax corrected normal.
*/
const getParallaxCorrectNormal = /*@__PURE__*/ Fn( ( [ normal, cubeSize, cubePos ] ) => {
const nDir = normalize( normal ).toConst();
const positive = nDir.greaterThanEqual( 0 );
// keep axis-aligned directions away from zero so the distances stay finite
const safeDir = positive.select( nDir.max( EPSILON ), nDir.min( EPSILON.negate() ) );
const extent = cubeSize.mul( 0.5 ).toConst();
const distances = positive.select( extent, extent.negate() ).add( cubePos ).sub( positionWorld ).div( safeDir ).toConst();
const correction = min( distances.x, distances.y, distances.z ).toConst();
return positionWorld.add( nDir.mul( correction ) ).sub( cubePos );
} );
const getRoughness = /*@__PURE__*/ Fn( ( inputs ) => {
const { roughness } = inputs;
const geometryRoughness = getGeometryRoughness();
// Minimum roughness, so even a perfect mirror samples a prefiltered level of the environment map.
// Matches Filament's desktop MIN_PERCEPTUAL_ROUGHNESS: https://github.com/google/filament/blob/main/shaders/src/surface_material.fs
let roughnessFactor = roughness.max( 0.045 );
roughnessFactor = roughnessFactor.add( geometryRoughness );
roughnessFactor = roughnessFactor.min( 1.0 );
return roughnessFactor;
} );
const getShIrradianceAt = /*@__PURE__*/ Fn( ( [ normal, shCoefficients ] ) => {
// normal is assumed to have unit length
const x = normal.x, y = normal.y, z = normal.z;
// band 0
let result = shCoefficients.element( 0 ).mul( 0.886227 );
// band 1
result = result.add( shCoefficients.element( 1 ).mul( 2.0 * 0.511664 ).mul( y ) );
result = result.add( shCoefficients.element( 2 ).mul( 2.0 * 0.511664 ).mul( z ) );
result = result.add( shCoefficients.element( 3 ).mul( 2.0 * 0.511664 ).mul( x ) );
// band 2
result = result.add( shCoefficients.element( 4 ).mul( 2.0 * 0.429043 ).mul( x ).mul( y ) );
result = result.add( shCoefficients.element( 5 ).mul( 2.0 * 0.429043 ).mul( y ).mul( z ) );
result = result.add( shCoefficients.element( 6 ).mul( z.mul( z ).mul( 0.743125 ).sub( 0.247708 ) ) );
result = result.add( shCoefficients.element( 7 ).mul( 2.0 * 0.429043 ).mul( x ).mul( z ) );
result = result.add( shCoefficients.element( 8 ).mul( 0.429043 ).mul( mul( x, x ).sub( mul( y, y ) ) ) );
return result;
} );
var Three_TSL = /*#__PURE__*/Object.freeze({
__proto__: null,
BRDF_EON: BRDF_EON,
BRDF_GGX: BRDF_GGX,
BRDF_Lambert: BRDF_Lambert,
BRDF_Sheen: BRDF_Sheen,
BasicPointShadowFilter: BasicPointShadowFilter,
BasicShadowFilter: BasicShadowFilter,
Break: Break,
Const: Const,
Continue: Continue,
DFGLUT: DFGLUT,
D_GGX: D_GGX,
D_GGX_Anisotropic: D_GGX_Anisotropic,
Discard: Discard,
EON_DirectionalAlbedo: EON_DirectionalAlbedo,
EPSILON: EPSILON,
EnvironmentBRDF: EnvironmentBRDF,
F_Schlick: F_Schlick,
Fn: Fn,
HALF_PI: HALF_PI,
INFINITY: INFINITY,
If: If,
LTC_Evaluate: LTC_Evaluate,
LTC_Evaluate_Volume: LTC_Evaluate_Volume,
LTC_Uv: LTC_Uv,
Loop: Loop,
NodeAccess: NodeAccess,
NodeShaderStage: NodeShaderStage,
NodeType: NodeType,
NodeUpdateType: NodeUpdateType,
OnAfterObjectUpdate: OnAfterObjectUpdate,
OnAfterRenderPipeline: OnAfterRenderPipeline,
OnBeforeFrameUpdate: OnBeforeFrameUpdate,
OnBeforeMaterialUpdate: OnBeforeMaterialUpdate,
OnBeforeObjectUpdate: OnBeforeObjectUpdate,
OnBeforeRenderPipeline: OnBeforeRenderPipeline,
OnFrameUpdate: OnFrameUpdate,
OnMaterialUpdate: OnMaterialUpdate,
OnObjectUpdate: OnObjectUpdate,
PCFShadowFilter: PCFShadowFilter,
PI: PI,
PI2: PI2,
PointShadowFilter: PointShadowFilter,
Return: Return,
Schlick_to_F0: Schlick_to_F0,
ShaderNode: ShaderNode,
Stack: Stack,
Switch: Switch,
TBNViewMatrix: TBNViewMatrix,
TWO_PI: TWO_PI,
VSMShadowFilter: VSMShadowFilter,
V_GGX_SmithCorrelated: V_GGX_SmithCorrelated,
V_GGX_SmithCorrelated_Anisotropic: V_GGX_SmithCorrelated_Anisotropic,
Var: Var,
abs: abs,
acesFilmicToneMapping: acesFilmicToneMapping,
acos: acos,
acosh: acosh,
add: add,
addMethodChaining: addMethodChaining,
addNodeElement: addNodeElement,
agxToneMapping: agxToneMapping,
all: all,
alphaT: alphaT,
ambientOcclusion: ambientOcclusion,
and: and,
anisotropy: anisotropy,
anisotropyB: anisotropyB,
anisotropyT: anisotropyT,
any: any,
array: array,
asin: asin,
asinh: asinh,
assign: assign,
atan: atan,
atanh: atanh,
atomicAdd: atomicAdd,
atomicAnd: atomicAnd,
atomicFunc: atomicFunc,
atomicLoad: atomicLoad,
atomicMax: atomicMax,
atomicMin: atomicMin,
atomicOr: atomicOr,
atomicStore: atomicStore,
atomicSub: atomicSub,
atomicXor: atomicXor,
attenuationColor: attenuationColor,
attenuationDistance: attenuationDistance,
attribute: attribute,
attributeArray: attributeArray,
backgroundBlurriness: backgroundBlurriness,
backgroundIntensity: backgroundIntensity,
backgroundRotation: backgroundRotation,
batch: batch,
batchColor: batchColor,
batchIndirectIndex: batchIndirectIndex,
bentNormalView: bentNormalView,
billboarding: billboarding,
bitAnd: bitAnd,
bitNot: bitNot,
bitOr: bitOr,
bitXor: bitXor,
bitangentGeometry: bitangentGeometry,
bitangentLocal: bitangentLocal,
bitangentView: bitangentView,
bitangentViewFrame: bitangentViewFrame,
bitangentWorld: bitangentWorld,
bitcast: bitcast,
blendBurn: blendBurn,
blendColor: blendColor,
blendDodge: blendDodge,
blendOverlay: blendOverlay,
blendScreen: blendScreen,
bool: bool,
buffer: buffer,
bufferAttribute: bufferAttribute,
builtin: builtin,
builtinAOContext: builtinAOContext,
builtinGIContext: builtinGIContext,
builtinShadowContext: builtinShadowContext,
bumpMap: bumpMap,
bvec2: bvec2,
bvec3: bvec3,
bvec4: bvec4,
bypass: bypass,
cache: cache,
call: call,
cameraFar: cameraFar,
cameraIndex: cameraIndex,
cameraNear: cameraNear,
cameraNormalMatrix: cameraNormalMatrix,
cameraPosition: cameraPosition,
cameraProjectionMatrix: cameraProjectionMatrix,
cameraProjectionMatrixInverse: cameraProjectionMatrixInverse,
cameraViewMatrix: cameraViewMatrix,
cameraViewport: cameraViewport,
cameraWorldMatrix: cameraWorldMatrix,
cbrt: cbrt,
cdl: cdl,
ceil: ceil,
checker: checker,
cineonToneMapping: cineonToneMapping,
clamp: clamp,
clearcoat: clearcoat,
clearcoatNormalView: clearcoatNormalView,
clearcoatRoughness: clearcoatRoughness,
clipSpace: clipSpace,
clipping: clipping,
clippingAlpha: clippingAlpha,
code: code,
color: color,
colorSpaceToWorking: colorSpaceToWorking,
colorToDirection: colorToDirection,
compute: compute,
computeKernel: computeKernel,
computeSkinning: computeSkinning,
context: context,
convert: convert,
convertColorSpace: convertColorSpace,
convertToTexture: convertToTexture,
cos: cos,
cosh: cosh,
countLeadingZeros: countLeadingZeros,
countOneBits: countOneBits,
countTrailingZeros: countTrailingZeros,
cross: cross,
cubeTexture: cubeTexture,
cubeTextureBase: cubeTextureBase,
dFdx: dFdx,
dFdy: dFdy,
dashSize: dashSize,
debug: debug,
decrement: decrement,
decrementBefore: decrementBefore,
defaultBuildStages: defaultBuildStages,
defaultShaderStages: defaultShaderStages,
defined: defined,
degrees: degrees,
deltaTime: deltaTime,
densityFogFactor: densityFogFactor,
depth: depth,
depthPass: depthPass,
determinant: determinant,
difference: difference,
diffuseColor: diffuseColor,
diffuseContribution: diffuseContribution,
diffuseRoughness: diffuseRoughness,
directPointLight: directPointLight,
directionToColor: directionToColor,
directionToFaceDirection: directionToFaceDirection,
dispersion: dispersion,
distance: distance,
div: div,
dot: dot,
dot4I8Packed: dot4I8Packed,
dot4U8Packed: dot4U8Packed,
drawIndex: drawIndex,
dynamicBufferAttribute: dynamicBufferAttribute,
element: element,
emissive: emissive,
equal: equal,
equirectDirection: equirectDirection,
equirectUV: equirectUV,
exp: exp,
exp2: exp2,
exponentialHeightFogFactor: exponentialHeightFogFactor,
expression: expression,
faceDirection: faceDirection,
faceForward: faceForward,
faceforward: faceforward,
float: float,
floatBitsToInt: floatBitsToInt,
floatBitsToUint: floatBitsToUint,
floor: floor,
fog: fog,
fract: fract,
frameGroup: frameGroup,
frameId: frameId,
frontFacing: frontFacing,
fwidth: fwidth,
gain: gain,
gapSize: gapSize,
getConstNodeType: getConstNodeType,
getCurrentStack: getCurrentStack,
getDistanceAttenuation: getDistanceAttenuation,
getGeometryRoughness: getGeometryRoughness,
getNormalFromDepth: getNormalFromDepth,
getParallaxCorrectNormal: getParallaxCorrectNormal,
getRoughness: getRoughness,
getScreenPosition: getScreenPosition,
getScreenPositionFromClip: getScreenPositionFromClip,
getShIrradianceAt: getShIrradianceAt,
getTextureIndex: getTextureIndex,
getViewPosition: getViewPosition,
globalId: globalId,
glsl: glsl,
glslFn: glslFn,
grayscale: grayscale,
greaterThan: greaterThan,
greaterThanEqual: greaterThanEqual,
hardwareClipping: hardwareClipping,
hash: hash,
highpModelNormalViewMatrix: highpModelNormalViewMatrix,
highpModelViewMatrix: highpModelViewMatrix,
hue: hue,
increment: increment,
incrementBefore: incrementBefore,
inspect: inspect,
instance: instance,
instanceColor: instanceColor,
instanceIndex: instanceIndex,
instancedArray: instancedArray,
instancedBufferAttribute: instancedBufferAttribute,
instancedDynamicBufferAttribute: instancedDynamicBufferAttribute,
instancedMesh: instancedMesh,
int: int,
intBitsToFloat: intBitsToFloat,
interleavedGradientNoise: interleavedGradientNoise,
inverse: inverse,
inverseSqrt: inverseSqrt,
inversesqrt: inversesqrt,
invocationLocalIndex: invocationLocalIndex,
invocationSubgroupIndex: invocationSubgroupIndex,
ior: ior,
iridescence: iridescence,
iridescenceIOR: iridescenceIOR,
iridescenceThickness: iridescenceThickness,
isBackgroundDepth: isBackgroundDepth,
isolate: isolate,
ivec2: ivec2,
ivec3: ivec3,
ivec4: ivec4,
js: js,
label: label,
length: length,
lengthSq: lengthSq,
lessThan: lessThan,
lessThanEqual: lessThanEqual,
lightPosition: lightPosition,
lightProjectionUV: lightProjectionUV,
lightShadowMatrix: lightShadowMatrix,
lightTargetDirection: lightTargetDirection,
lightTargetPosition: lightTargetPosition,
lightViewPosition: lightViewPosition,
lightingContext: lightingContext,
lights: lights,
linearDepth: linearDepth,
linearToneMapping: linearToneMapping,
localId: localId,
log: log,
log2: log2,
logarithmicDepthToViewZ: logarithmicDepthToViewZ,
luminance: luminance,
mat2: mat2,
mat3: mat3,
mat4: mat4,
matcapUV: matcapUV,
materialAO: materialAO,
materialAlphaTest: materialAlphaTest,
materialAnisotropy: materialAnisotropy,
materialAnisotropyVector: materialAnisotropyVector,
materialAttenuationColor: materialAttenuationColor,
materialAttenuationDistance: materialAttenuationDistance,
materialClearcoat: materialClearcoat,
materialClearcoatNormal: materialClearcoatNormal,
materialClearcoatRoughness: materialClearcoatRoughness,
materialColor: materialColor,
materialDiffuseRoughness: materialDiffuseRoughness,
materialDispersion: materialDispersion,
materialEmissive: materialEmissive,
materialEnvIntensity: materialEnvIntensity,
materialEnvRotation: materialEnvRotation,
materialIOR: materialIOR,
materialIridescence: materialIridescence,
materialIridescenceIOR: materialIridescenceIOR,
materialIridescenceThickness: materialIridescenceThickness,
materialLightMap: materialLightMap,
materialLineDashOffset: materialLineDashOffset,
materialLineDashSize: materialLineDashSize,
materialLineGapSize: materialLineGapSize,
materialLineScale: materialLineScale,
materialLineWidth: materialLineWidth,
materialMetalness: materialMetalness,
materialNormal: materialNormal,
materialOpacity: materialOpacity,
materialPointSize: materialPointSize,
materialReference: materialReference,
materialReflectivity: materialReflectivity,
materialRefractionRatio: materialRefractionRatio,
materialRetroreflectivity: materialRetroreflectivity,
materialRotation: materialRotation,
materialRoughness: materialRoughness,
materialSheen: materialSheen,
materialSheenRoughness: materialSheenRoughness,
materialShininess: materialShininess,
materialSpecular: materialSpecular,
materialSpecularColor: materialSpecularColor,
materialSpecularIntensity: materialSpecularIntensity,
materialSpecularStrength: materialSpecularStrength,
materialThickness: materialThickness,
materialTransmission: materialTransmission,
max: max,
maxMipLevel: maxMipLevel,
mediumpModelViewMatrix: mediumpModelViewMatrix,
metalness: metalness,
min: min,
mix: mix,
mixElement: mixElement,
mod: mod,
modelDirection: modelDirection,
modelNormalMatrix: modelNormalMatrix,
modelPosition: modelPosition,
modelRadius: modelRadius,
modelScale: modelScale,
modelViewMatrix: modelViewMatrix,
modelViewPosition: modelViewPosition,
modelViewProjection: modelViewProjection,
modelWorldMatrix: modelWorldMatrix,
modelWorldMatrixInverse: modelWorldMatrixInverse,
morphColor: morphColor,
morphReference: morphReference,
mrt: mrt,
mul: mul,
mx_aastep: mx_aastep,
mx_add: mx_add,
mx_atan2: mx_atan2,
mx_cell_noise_float: mx_cell_noise_float,
mx_cell_noise_vec3: mx_cell_noise_vec3,
mx_contrast: mx_contrast,
mx_divide: mx_divide,
mx_fractal_noise_float: mx_fractal_noise_float,
mx_fractal_noise_float_2d: mx_fractal_noise_float_2d,
mx_fractal_noise_vec2: mx_fractal_noise_vec2,
mx_fractal_noise_vec3: mx_fractal_noise_vec3,
mx_fractal_noise_vec4: mx_fractal_noise_vec4,
mx_frame: mx_frame,
mx_heighttonormal: mx_heighttonormal,
mx_hsvtorgb: mx_hsvtorgb,
mx_ifequal: mx_ifequal,
mx_ifgreater: mx_ifgreater,
mx_ifgreatereq: mx_ifgreatereq,
mx_invert: mx_invert,
mx_modulo: mx_modulo,
mx_multiply: mx_multiply,
mx_noise_float: mx_noise_float,
mx_noise_vec3: mx_noise_vec3,
mx_noise_vec4: mx_noise_vec4,
mx_place2d: mx_place2d,
mx_power: mx_power,
mx_ramp4: mx_ramp4,
mx_ramplr: mx_ramplr,
mx_ramptb: mx_ramptb,
mx_rgbtohsv: mx_rgbtohsv,
mx_rotate2d: mx_rotate2d,
mx_rotate3d: mx_rotate3d,
mx_safepower: mx_safepower,
mx_separate: mx_separate,
mx_smoothstep: mx_smoothstep,
mx_splitlr: mx_splitlr,
mx_splittb: mx_splittb,
mx_srgb_texture_to_lin_rec709: mx_srgb_texture_to_lin_rec709,
mx_subtract: mx_subtract,
mx_timer: mx_timer,
mx_transform_uv: mx_transform_uv,
mx_unifiednoise2d: mx_unifiednoise2d,
mx_unifiednoise3d: mx_unifiednoise3d,
mx_worley_noise_float: mx_worley_noise_float,
mx_worley_noise_float_2d: mx_worley_noise_float_2d,
mx_worley_noise_float_3d: mx_worley_noise_float_3d,
mx_worley_noise_vec2: mx_worley_noise_vec2,
mx_worley_noise_vec3: mx_worley_noise_vec3,
mx_worley_noise_vec3_style: mx_worley_noise_vec3_style,
negate: negate,
negateOnBackSide: negateOnBackSide,
neutralToneMapping: neutralToneMapping,
nodeArray: nodeArray,
nodeImmutable: nodeImmutable,
nodeObject: nodeObject,
nodeObjectAssignable: nodeObjectAssignable,
nodeObjects: nodeObjects,
nodeProxy: nodeProxy,
nodeProxyAssignable: nodeProxyAssignable,
nodeProxyConstructor: nodeProxyConstructor,
normalFlat: normalFlat,
normalGeometry: normalGeometry,
normalLocal: normalLocal,
normalMap: normalMap,
normalView: normalView,
normalViewGeometry: normalViewGeometry,
normalWorld: normalWorld,
normalWorldGeometry: normalWorldGeometry,
normalize: normalize,
not: not,
notEqual: notEqual,
numWorkgroups: numWorkgroups,
objectDirection: objectDirection,
objectGroup: objectGroup,
objectPosition: objectPosition,
objectRadius: objectRadius,
objectScale: objectScale,
objectViewPosition: objectViewPosition,
objectWorldMatrix: objectWorldMatrix,
oneMinus: oneMinus,
or: or,
orthographicDepthToViewZ: orthographicDepthToViewZ,
oscSawtooth: oscSawtooth,
oscSine: oscSine,
oscSquare: oscSquare,
oscTriangle: oscTriangle,
output: output,
outputStruct: outputStruct,
overloadingFn: overloadingFn,
overrideNode: overrideNode,
overrideNodes: overrideNodes,
pack4xI8: pack4xI8,
pack4xI8Clamp: pack4xI8Clamp,
pack4xU8: pack4xU8,
pack4xU8Clamp: pack4xU8Clamp,
packHalf2x16: packHalf2x16,
packNormalToRGB: packNormalToRGB,
packSnorm2x16: packSnorm2x16,
packSnorm4x8: packSnorm4x8,
packUnorm2x16: packUnorm2x16,
packUnorm4x8: packUnorm4x8,
parabola: parabola,
parallaxDirection: parallaxDirection,
parallaxUV: parallaxUV,
parameter: parameter,
pass: pass,
passTexture: passTexture,
pcurve: pcurve,
perspectiveDepthToViewZ: perspectiveDepthToViewZ,
pmremTexture: pmremTexture,
pointShadow: pointShadow,
pointUV: pointUV,
pointWidth: pointWidth,
positionGeometry: positionGeometry,
positionLocal: positionLocal,
positionPrevious: positionPrevious,
positionView: positionView,
positionViewDirection: positionViewDirection,
positionWorld: positionWorld,
positionWorldDirection: positionWorldDirection,
posterize: posterize,
pow: pow,
pow2: pow2,
pow3: pow3,
pow4: pow4,
premultiplyAlpha: premultiplyAlpha,
projectionDepth: projectionDepth,
property: property,
quadBroadcast: quadBroadcast,
quadSwapDiagonal: quadSwapDiagonal,
quadSwapX: quadSwapX,
quadSwapY: quadSwapY,
radians: radians,
rand: rand,
range: range,
rangeFogFactor: rangeFogFactor,
reciprocal: reciprocal,
reference: reference,
referenceAttribute: referenceAttribute,
referenceBuffer: referenceBuffer,
reflect: reflect,
reflectVector: reflectVector,
reflectView: reflectView,
reflector: reflector,
refract: refract,
refractVector: refractVector,
refractView: refractView,
reinhardToneMapping: reinhardToneMapping,
remap: remap,
remapClamp: remapClamp,
renderGroup: renderGroup,
renderOutput: renderOutput,
rendererReference: rendererReference,
replaceDefaultUV: replaceDefaultUV,
retroreflectivity: retroreflectivity,
rotate: rotate,
rotateUV: rotateUV,
roughness: roughness,
round: round,
rtt: rtt,
sRGBTransferEOTF: sRGBTransferEOTF,
sRGBTransferOETF: sRGBTransferOETF,
sample: sample,
sampler: sampler,
samplerComparison: samplerComparison,
saturate: saturate,
saturation: saturation,
screenCoordinate: screenCoordinate,
screenDPR: screenDPR,
screenSize: screenSize,
screenUV: screenUV,
select: select,
setCurrentStack: setCurrentStack,
setName: setName,
shaderStages: shaderStages,
shadow: shadow,
shadowPositionWorld: shadowPositionWorld,
shapeCircle: shapeCircle,
sharedUniformGroup: sharedUniformGroup,
sheen: sheen,
sheenRoughness: sheenRoughness,
shiftLeft: shiftLeft,
shiftRight: shiftRight,
shininess: shininess,
sign: sign,
sin: sin,
sinc: sinc,
sinh: sinh,
skinning: skinning,
smoothstep: smoothstep,
smoothstepElement: smoothstepElement,
specularColor: specularColor,
specularColorBlended: specularColorBlended,
specularF90: specularF90,
spherizeUV: spherizeUV,
split: split,
spritesheetUV: spritesheetUV,
sqrt: sqrt,
stack: stack,
step: step,
stepElement: stepElement,
storage: storage,
storageBarrier: storageBarrier,
storageElement: storageElement,
storageTexture: storageTexture,
storageTexture3D: storageTexture3D,
struct: struct,
sub: sub,
subBuild: subBuild,
subgroupAdd: subgroupAdd,
subgroupAll: subgroupAll,
subgroupAnd: subgroupAnd,
subgroupAny: subgroupAny,
subgroupBallot: subgroupBallot,
subgroupBroadcast: subgroupBroadcast,
subgroupBroadcastFirst: subgroupBroadcastFirst,
subgroupElect: subgroupElect,
subgroupExclusiveAdd: subgroupExclusiveAdd,
subgroupExclusiveMul: subgroupExclusiveMul,
subgroupInclusiveAdd: subgroupInclusiveAdd,
subgroupInclusiveMul: subgroupInclusiveMul,
subgroupIndex: subgroupIndex,
subgroupMax: subgroupMax,
subgroupMin: subgroupMin,
subgroupMul: subgroupMul,
subgroupOr: subgroupOr,
subgroupShuffle: subgroupShuffle,
subgroupShuffleDown: subgroupShuffleDown,
subgroupShuffleUp: subgroupShuffleUp,
subgroupShuffleXor: subgroupShuffleXor,
subgroupSize: subgroupSize,
subgroupXor: subgroupXor,
tan: tan,
tangentGeometry: tangentGeometry,
tangentLocal: tangentLocal,
tangentView: tangentView,
tangentViewFrame: tangentViewFrame,
tangentWorld: tangentWorld,
tanh: tanh,
texture: texture,
texture3D: texture3D,
texture3DLevel: texture3DLevel,
texture3DLoad: texture3DLoad,
textureBarrier: textureBarrier,
textureBicubic: textureBicubic,
textureBicubicLevel: textureBicubicLevel,
textureLevel: textureLevel,
textureLoad: textureLoad,
textureSize: textureSize,
textureStore: textureStore,
thickness: thickness,
time: time,
toneMapping: toneMapping,
toneMappingExposure: toneMappingExposure,
toonOutlinePass: toonOutlinePass,
transformDirection: transformDirection,
transformNormal: transformNormal,
transformNormalByInverseViewMatrix: transformNormalByInverseViewMatrix,
transformNormalByViewMatrix: transformNormalByViewMatrix,
transformNormalToView: transformNormalToView,
transmission: transmission,
transpose: transpose,
triNoise3D: triNoise3D,
triplanarTexture: triplanarTexture,
triplanarTextures: triplanarTextures,
trunc: trunc,
uint: uint,
uintBitsToFloat: uintBitsToFloat,
uniform: uniform,
uniformArray: uniformArray,
uniformCubeTexture: uniformCubeTexture,
uniformFlow: uniformFlow,
uniformGroup: uniformGroup,
uniformTexture: uniformTexture,
unpack4xI8: unpack4xI8,
unpack4xU8: unpack4xU8,
unpackHalf2x16: unpackHalf2x16,
unpackNormal: unpackNormal,
unpackRGBToNormal: unpackRGBToNormal,
unpackSnorm2x16: unpackSnorm2x16,
unpackSnorm4x8: unpackSnorm4x8,
unpackUnorm2x16: unpackUnorm2x16,
unpackUnorm4x8: unpackUnorm4x8,
unpremultiplyAlpha: unpremultiplyAlpha,
userData: userData,
uv: uv$1,
uvec2: uvec2,
uvec3: uvec3,
uvec4: uvec4,
varying: varying,
varyingProperty: varyingProperty,
vec2: vec2,
vec3: vec3,
vec4: vec4,
vectorComponents: vectorComponents,
velocity: velocity,
vertexColor: vertexColor,
vertexIndex: vertexIndex,
vertexStage: vertexStage,
vibrance: vibrance,
viewZToLogarithmicDepth: viewZToLogarithmicDepth,
viewZToOrthographicDepth: viewZToOrthographicDepth,
viewZToPerspectiveDepth: viewZToPerspectiveDepth,
viewZToReversedOrthographicDepth: viewZToReversedOrthographicDepth,
viewZToReversedPerspectiveDepth: viewZToReversedPerspectiveDepth,
viewport: viewport,
viewportCoordinate: viewportCoordinate,
viewportDepthTexture: viewportDepthTexture,
viewportLinearDepth: viewportLinearDepth,
viewportMipTexture: viewportMipTexture,
viewportOpaqueMipTexture: viewportOpaqueMipTexture,
viewportSafeUV: viewportSafeUV,
viewportSharedTexture: viewportSharedTexture,
viewportSize: viewportSize,
viewportTexture: viewportTexture,
viewportUV: viewportUV,
vogelDiskSample: vogelDiskSample,
wgsl: wgsl,
wgslFn: wgslFn,
workgroupArray: workgroupArray,
workgroupBarrier: workgroupBarrier,
workgroupId: workgroupId,
workingLuminanceCoefficients: workingLuminanceCoefficients,
workingToColorSpace: workingToColorSpace,
xor: xor
});
export { AONode, AnalyticLightNode, ArrayElementNode, ArrayNode, AssignNode, AtomicFunctionNode, AttributeNode, BRDF_EON, BRDF_GGX, BRDF_Lambert, BRDF_Sheen, BarrierNode, BasicPointShadowFilter, BasicShadowFilter, BitcastNode, BitcountNode, BlendMode, Break, BufferAttributeNode, BufferNode, BuiltinNode, BumpMapNode, BypassNode, ChainMap, ClippingNode, CodeNode, Color4, ColorSpaceNode, ComputeBuiltinNode, ComputeNode, ConditionalNode, Const, ConstNode, ContextNode, Continue, ConvertNode, CubeRenderTarget, CubeTextureNode, DFGLUT, D_GGX, D_GGX_Anisotropic, DebugNode, Discard, EON_DirectionalAlbedo, EPSILON, EnvironmentBRDF, EventNode, ExpressionNode, F_Schlick, FlipNode, Fn, FrontFacingNode, FunctionCallNode, FunctionNode, FunctionOverloadingNode, HALF_PI, INFINITY, If, IndexNode, InputNode, InspectorBase, InspectorNode, IrradianceNode, IsolateNode, JoinNode, LTC_Evaluate, LTC_Evaluate_Volume, LTC_Uv, LightingContextNode, LightingNode, LightsNode, Loop, LoopNode, MRTNode, MaterialNode, MaterialReferenceNode, MathNode, MaxMipLevelNode, MemberNode, ModelNode, Node, NodeAccess, NodeError, NodeMaterial, NodeMaterialObserver, NodeShaderStage, NodeType, NodeUpdateType, NodeUtils, NormalMapNode, Object3DNode, OnAfterObjectUpdate, OnAfterRenderPipeline, OnBeforeFrameUpdate, OnBeforeMaterialUpdate, OnBeforeObjectUpdate, OnBeforeRenderPipeline, OnFrameUpdate, OnMaterialUpdate, OnObjectUpdate, OperatorNode, OutputStructNode, OverrideContextNode, PCFShadowFilter, PI, PI2, PMREMGenerator, PMREMNode, PackFloatNode, Packed4x8IntegerNode, ParameterNode, PassNode, PointLightNode, PointShadowFilter, PointShadowNode, PointUVNode, PropertyNode, QuadMesh, RTTNode, RangeNode, ReferenceAttributeNode, ReferenceBaseNode, ReferenceElementNode, ReferenceNode, ReflectorNode, RenderOutputNode, RendererReferenceNode, RendererUtils, Return, RotateNode, SampleNode, Schlick_to_F0, ScreenNode, SetNode, ShaderNode, ShadowBaseNode, ShadowNode, SplitNode, Stack, StackNode, StackTrace, StorageArrayElementNode, StorageBufferAttribute, StorageBufferNode, StorageInstancedBufferAttribute, StorageTexture3DNode, StorageTextureNode, StructNode, StructTypeNode, SubBuildNode, SubgroupFunctionNode, Switch, TBNViewMatrix, TWO_PI, Texture3DNode, TextureNode, TextureSizeNode, Three_TSL, ToneMappingNode, ToonOutlinePassNode, UniformArrayNode, UniformGroupNode, UniformNode, UnpackFloatNode, UserDataNode, VSMShadowFilter, V_GGX_SmithCorrelated, V_GGX_SmithCorrelated_Anisotropic, Var, VarNode, VaryingNode, VelocityNode, VertexColorNode, ViewportDepthNode, ViewportDepthTextureNode, ViewportSharedTextureNode, ViewportTextureNode, WorkgroupInfoNode, abs, acesFilmicToneMapping, acos, acosh, add, addMethodChaining, addNodeElement, agxToneMapping, all, alphaT, ambientOcclusion, and, anisotropy, anisotropyB, anisotropyT, any, array, asin, asinh, assign, atan, atanh, atomicAdd, atomicAnd, atomicFunc, atomicLoad, atomicMax, atomicMin, atomicOr, atomicStore, atomicSub, atomicXor, attenuationColor, attenuationDistance, attribute, attributeArray, backgroundBlurriness, backgroundIntensity, backgroundRotation, batch, batchColor, batchIndirectIndex, bentNormalView, billboarding, bitAnd, bitNot, bitOr, bitXor, bitangentGeometry, bitangentLocal, bitangentView, bitangentViewFrame, bitangentWorld, bitcast, blendBurn, blendColor, blendDodge, blendOverlay, blendScreen, bool, buffer, bufferAttribute, builtin, builtinAOContext, builtinGIContext, builtinShadowContext, bumpMap, bvec2, bvec3, bvec4, bypass, cache, call, cameraFar, cameraIndex, cameraNear, cameraNormalMatrix, cameraPosition, cameraProjectionMatrix, cameraProjectionMatrixInverse, cameraViewMatrix, cameraViewport, cameraWorldMatrix, cbrt, cdl, ceil, checker, cineonToneMapping, clamp, clearcoat, clearcoatNormalView, clearcoatRoughness, clipSpace, clipping, clippingAlpha, code, color, colorSpaceToWorking, colorToDirection, compute, computeKernel, computeSkinning, context, convert, convertColorSpace, convertToTexture, cos, cosh, countLeadingZeros, countOneBits, countTrailingZeros, cross, cubeTexture, cubeTextureBase, dFdx, dFdy, dashSize, debug, decrement, decrementBefore, defaultBuildStages, defaultShaderStages, defined, degrees, deltaTime, densityFogFactor, depth, depthPass, determinant, difference, diffuseColor, diffuseContribution, diffuseRoughness, directPointLight, directionToColor, directionToFaceDirection, dispersion, distance, div, dot, dot4I8Packed, dot4U8Packed, drawIndex, dynamicBufferAttribute, element, emissive, equal, equirectDirection, equirectUV, exp, exp2, exponentialHeightFogFactor, expression, faceDirection, faceForward, faceforward, float, floatBitsToInt, floatBitsToUint, floor, fog, fract, frameGroup, frameId, frontFacing, fwidth, gain, gapSize, getConstNodeType, getCurrentStack, getDistanceAttenuation, getGeometryRoughness, getNormalFromDepth, getParallaxCorrectNormal, getRoughness, getScreenPosition, getScreenPositionFromClip, getShIrradianceAt, getTextureIndex, getTextureType, getTypeFromLength, getViewPosition, globalId, glsl, glslFn, grayscale, greaterThan, greaterThanEqual, hardwareClipping, hash, hashArray, hashString, highpModelNormalViewMatrix, highpModelViewMatrix, hue, increment, incrementBefore, inspect, instance, instanceColor, instanceIndex, instancedArray, instancedBufferAttribute, instancedDynamicBufferAttribute, instancedMesh, int, intBitsToFloat, interleavedGradientNoise, inverse, inverseSqrt, inversesqrt, invocationLocalIndex, invocationSubgroupIndex, ior, iridescence, iridescenceIOR, iridescenceThickness, isArrayAsParameter, isBackgroundDepth, isolate, ivec2, ivec3, ivec4, js, label, length, lengthSq, lessThan, lessThanEqual, lightPosition, lightProjectionUV, lightShadowMatrix, lightTargetDirection, lightTargetPosition, lightViewPosition, lightingContext, lights, linearDepth, linearToneMapping, localId, log, log2, logarithmicDepthToViewZ, luminance, mat2, mat3, mat4, matcapUV, materialAO, materialAlphaTest, materialAnisotropy, materialAnisotropyVector, materialAttenuationColor, materialAttenuationDistance, materialClearcoat, materialClearcoatNormal, materialClearcoatRoughness, materialColor, materialDiffuseRoughness, materialDispersion, materialEmissive, materialEnvIntensity, materialEnvRotation, materialIOR, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialLightMap, materialLineDashOffset, materialLineDashSize, materialLineGapSize, materialLineScale, materialLineWidth, materialMetalness, materialNormal, materialOpacity, materialPointSize, materialReference, materialReflectivity, materialRefractionRatio, materialRetroreflectivity, materialRotation, materialRoughness, materialSheen, materialSheenRoughness, materialShininess, materialSpecular, materialSpecularColor, materialSpecularIntensity, materialSpecularStrength, materialThickness, materialTransmission, max, maxMipLevel, mediumpModelViewMatrix, mergeContext, metalness, min, mix, mixElement, mod, modelDirection, modelNormalMatrix, modelPosition, modelRadius, modelScale, modelViewMatrix, modelViewPosition, modelViewProjection, modelWorldMatrix, modelWorldMatrixInverse, morphColor, morphReference, mrt, mul, mx_aastep, mx_add, mx_atan2, mx_cell_noise_float, mx_cell_noise_vec3, mx_contrast, mx_divide, mx_fractal_noise_float, mx_fractal_noise_float_2d, mx_fractal_noise_vec2, mx_fractal_noise_vec3, mx_fractal_noise_vec4, mx_frame, mx_heighttonormal, mx_hsvtorgb, mx_ifequal, mx_ifgreater, mx_ifgreatereq, mx_invert, mx_modulo, mx_multiply, mx_noise_float, mx_noise_vec3, mx_noise_vec4, mx_place2d, mx_power, mx_ramp4, mx_ramplr, mx_ramptb, mx_rgbtohsv, mx_rotate2d, mx_rotate3d, mx_safepower, mx_separate, mx_smoothstep, mx_splitlr, mx_splittb, mx_srgb_texture_to_lin_rec709, mx_subtract, mx_timer, mx_transform_uv, mx_unifiednoise2d, mx_unifiednoise3d, mx_worley_noise_float, mx_worley_noise_float_2d, mx_worley_noise_float_3d, mx_worley_noise_vec2, mx_worley_noise_vec3, mx_worley_noise_vec3_style, negate, negateOnBackSide, neutralToneMapping, nodeArray, nodeImmutable, nodeObject, nodeObjectAssignable, nodeObjects, nodeProxy, nodeProxyAssignable, nodeProxyConstructor, normalFlat, normalGeometry, normalLocal, normalMap, normalView, normalViewGeometry, normalWorld, normalWorldGeometry, normalize, not, notEqual, numWorkgroups, objectDirection, objectGroup, objectPosition, objectRadius, objectScale, objectViewPosition, objectWorldMatrix, oneMinus, or, orthographicDepthToViewZ, oscSawtooth, oscSine, oscSquare, oscTriangle, output, outputStruct, overloadingFn, overrideNode, overrideNodes, pack4xI8, pack4xI8Clamp, pack4xU8, pack4xU8Clamp, packHalf2x16, packNormalToRGB, packSnorm2x16, packSnorm4x8, packUnorm2x16, packUnorm4x8, parabola, parallaxDirection, parallaxUV, parameter, pass, passTexture, pcurve, perspectiveDepthToViewZ, pmremTexture, pointShadow, pointUV, pointWidth, positionGeometry, positionLocal, positionPrevious, positionView, positionViewDirection, positionWorld, positionWorldDirection, posterize, pow, pow2, pow3, pow4, premultiplyAlpha, projectionDepth, property, quadBroadcast, quadSwapDiagonal, quadSwapX, quadSwapY, radians, rand, range, rangeFogFactor, reciprocal, reference, reference$1, referenceAttribute, referenceBuffer, reflect, reflectVector, reflectView, reflector, refract, refractVector, refractView, reinhardToneMapping, remap, remapClamp, renderGroup, renderOutput, rendererReference, replaceDefaultUV, retroreflectivity, rotate, rotateUV, roughness, round, rtt, sRGBTransferEOTF, sRGBTransferOETF, sample, sampler, samplerComparison, saturate, saturation, screenCoordinate, screenDPR, screenSize, screenUV, select, setCurrentStack, setName, shaderStages, shadow, shadowPositionWorld, shapeCircle, sharedUniformGroup, sheen, sheenRoughness, shiftLeft, shiftRight, shininess, sign, sin, sinc, sinh, skinning, smoothstep, smoothstepElement, specularColor, specularColorBlended, specularF90, spherizeUV, split, spritesheetUV, sqrt, stack, step, stepElement, storage, storageBarrier, storageElement, storageTexture, storageTexture3D, struct, sub, subBuild, subgroupAdd, subgroupAll, subgroupAnd, subgroupAny, subgroupBallot, subgroupBroadcast, subgroupBroadcastFirst, subgroupElect, subgroupExclusiveAdd, subgroupExclusiveMul, subgroupInclusiveAdd, subgroupInclusiveMul, subgroupIndex, subgroupMax, subgroupMin, subgroupMul, subgroupOr, subgroupShuffle, subgroupShuffleDown, subgroupShuffleUp, subgroupShuffleXor, subgroupSize, subgroupXor, tan, tangentGeometry, tangentLocal, tangentView, tangentViewFrame, tangentWorld, tanh, texture, texture3D, texture3DLevel, texture3DLoad, textureBarrier, textureBicubic, textureBicubicLevel, textureLevel, textureLoad, textureSize, textureStore, thickness, time, toneMapping, toneMappingExposure, toonOutlinePass, transformDirection, transformNormal, transformNormalByInverseViewMatrix, transformNormalByViewMatrix, transformNormalToView, transmission, transpose, triNoise3D, triplanarTexture, triplanarTextures, trunc, uint, uintBitsToFloat, uniform, uniformArray, uniformCubeTexture, uniformFlow, uniformGroup, uniformTexture, unpack4xI8, unpack4xU8, unpackHalf2x16, unpackNormal, unpackRGBToNormal, unpackSnorm2x16, unpackSnorm4x8, unpackUnorm2x16, unpackUnorm4x8, unpremultiplyAlpha, userData, uv$1 as uv, uvec2, uvec3, uvec4, varying, varyingProperty, vec2, vec3, vec4, vectorComponents, velocity, vertexColor, vertexIndex, vertexStage, vibrance, viewZToLogarithmicDepth, viewZToOrthographicDepth, viewZToPerspectiveDepth, viewZToReversedOrthographicDepth, viewZToReversedPerspectiveDepth, viewport, viewportCoordinate, viewportDepthTexture, viewportLinearDepth, viewportMipTexture, viewportOpaqueMipTexture, viewportSafeUV, viewportSharedTexture, viewportSize, viewportTexture, viewportUV, vogelDiskSample, wgsl, wgslFn, workgroupArray, workgroupBarrier, workgroupId, workingLuminanceCoefficients, workingToColorSpace, xor };