three
Version:
JavaScript 3D library
2,002 lines (1,383 loc) • 1.72 MB
JavaScript
/**
* @license
* Copyright 2010-2025 Three.js Authors
* SPDX-License-Identifier: MIT
*/
import { Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, EventDispatcher, MathUtils, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBuffer, InterleavedBufferAttribute, DynamicDrawUsage, NoColorSpace, UnsignedIntType, IntType, NearestFilter, Sphere, BackSide, Euler, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, ObjectSpaceNormalMap, InstancedInterleavedBuffer, InstancedBufferAttribute, DataArrayTexture, FloatType, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, Material, NormalBlending, LineBasicMaterial, LineDashedMaterial, NoBlending, MeshNormalMaterial, SRGBColorSpace, WebGLCubeRenderTarget, BoxGeometry, Mesh, Scene, LinearFilter, CubeCamera, CubeTexture, EquirectangularReflectionMapping, EquirectangularRefractionMapping, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, OrthographicCamera, PerspectiveCamera, RenderTarget, CubeUVReflectionMapping, BufferGeometry, BufferAttribute, LinearSRGBColorSpace, RGBAFormat, HalfFloatType, Texture, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, ShadowMaterial, Uint32BufferAttribute, Uint16BufferAttribute, arrayNeedsUint32, DoubleSide, Camera, DepthStencilFormat, DepthFormat, UnsignedInt248Type, UnsignedByteType, Plane, Object3D, LinearMipMapLinearFilter, Float32BufferAttribute, UVMapping, LessCompare, VSMShadowMap, RGFormat, BasicShadowMap, SphereGeometry, LinearMipmapNearestFilter, NearestMipmapLinearFilter, Float16BufferAttribute, REVISION, ArrayCamera, PlaneGeometry, FrontSide, CustomBlending, AddEquation, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, PCFShadowMap, Frustum, DataTexture, RedIntegerFormat, RedFormat, ShortType, ByteType, UnsignedShortType, RGIntegerFormat, RGBIntegerFormat, RGBFormat, RGBAIntegerFormat, warnOnce, createCanvasElement, ReverseSubtractEquation, SubtractEquation, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcAlphaFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt5999Type, AlphaFormat, LuminanceFormat, LuminanceAlphaFormat, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, RED_GREEN_RGTC2_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, ClampToEdgeWrapping, RepeatWrapping, NearestMipmapNearestFilter, NotEqualCompare, GreaterCompare, GreaterEqualCompare, EqualCompare, LessEqualCompare, AlwaysCompare, NeverCompare, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, MaxEquation, MinEquation, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js';
export { AdditiveAnimationBlendMode, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BatchedMesh, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxHelper, BufferGeometryLoader, Cache, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, ConstantAlphaFactor, ConstantColorFactor, Controls, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CustomToneMapping, Cylindrical, Data3DTexture, DataTextureLoader, DataUtils, DefaultLoadingManager, DetachedBindMode, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, ExtrudeGeometry, Fog, FogExp2, GLBufferAttribute, GLSL1, GLSL3, GridHelper, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, InstancedBufferGeometry, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, Interpolant, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, KeyframeTrack, LOD, LatheGeometry, Layers, Light, Line, Line3, LineCurve, LineCurve3, LineLoop, LineSegments, LinearInterpolant, LinearMipMapNearestFilter, LinearTransfer, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, MeshDepthMaterial, MeshDistanceMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NormalAnimationBlendMode, NumberKeyframeTrack, OctahedronGeometry, OneMinusConstantAlphaFactor, OneMinusConstantColorFactor, PCFSoftShadowMap, Path, PlaneHelper, PointLightHelper, Points, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RGDepthPacking, RawShaderMaterial, Ray, Raycaster, RenderTarget3D, RenderTargetArray, RingGeometry, ShaderMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Spherical, SphericalHarmonics3, SplineCurve, SpotLightHelper, Sprite, StaticCopyUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TetrahedronGeometry, TextureLoader, TextureUtils, TimestampQuery, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLRenderTarget, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding } from './three.core.js';
const refreshUniforms = [
'alphaMap',
'alphaTest',
'anisotropy',
'anisotropyMap',
'anisotropyRotation',
'aoMap',
'attenuationColor',
'attenuationDistance',
'bumpMap',
'clearcoat',
'clearcoatMap',
'clearcoatNormalMap',
'clearcoatNormalScale',
'clearcoatRoughness',
'color',
'dispersion',
'displacementMap',
'emissive',
'emissiveMap',
'envMap',
'gradientMap',
'ior',
'iridescence',
'iridescenceIOR',
'iridescenceMap',
'iridescenceThicknessMap',
'lightMap',
'map',
'matcap',
'metalness',
'metalnessMap',
'normalMap',
'normalScale',
'opacity',
'roughness',
'roughnessMap',
'sheen',
'sheenColor',
'sheenColorMap',
'sheenRoughnessMap',
'shininess',
'specular',
'specularColor',
'specularColorMap',
'specularIntensity',
'specularIntensityMap',
'specularMap',
'thickness',
'transmission',
'transmissionMap'
];
/**
* 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 material 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 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, material, object } = renderObject;
data = {
material: this.getMaterialData( material ),
geometry: {
id: geometry.id,
attributes: this.getAttributesData( geometry.attributes ),
indexVersion: geometry.index ? geometry.index.version : null,
drawRange: { start: geometry.drawRange.start, count: geometry.drawRange.count }
},
worldMatrix: object.matrixWorld.clone()
};
if ( object.center ) {
data.center = object.center.clone();
}
if ( object.morphTargetInfluences ) {
data.morphTargetInfluences = object.morphTargetInfluences.slice();
}
if ( renderObject.bundle !== null ) {
data.version = renderObject.bundle.version;
}
if ( data.material.transmission > 0 ) {
const { width, height } = renderObject.context;
data.bufferWidth = width;
data.bufferHeight = height;
}
this.renderObjects.set( renderObject, data );
}
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 ] = {
version: attribute.version
};
}
return attributesData;
}
/**
* Returns `true` if the node builder's material uses
* node properties.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether the node builder's material uses node properties or not.
*/
containsNode( builder ) {
const material = builder.material;
for ( const property in material ) {
if ( material[ property ] && material[ property ].isNode )
return true;
}
if ( builder.renderer.nodes.modelViewMatrix !== null || builder.renderer.nodes.modelNormalViewMatrix !== null )
return true;
return false;
}
/**
* 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 ) {
const data = {};
for ( const property of this.refreshUniforms ) {
const value = material[ property ];
if ( value === null || value === undefined ) continue;
if ( typeof value === 'object' && value.clone !== undefined ) {
if ( value.isTexture === true ) {
data[ property ] = { id: value.id, version: value.version };
} else {
data[ property ] = value.clone();
}
} else {
data[ property ] = value;
}
}
return data;
}
/**
* Returns `true` if the given render object has not changed its state.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the given render object has changed its state or not.
*/
equals( renderObject ) {
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 = renderObjectData.material;
for ( const property in materialData ) {
const value = materialData[ property ];
const mtlValue = material[ property ];
if ( value.equals !== undefined ) {
if ( value.equals( mtlValue ) === false ) {
value.copy( mtlValue );
return false;
}
} else if ( mtlValue.isTexture === true ) {
if ( value.id !== mtlValue.id || value.version !== mtlValue.version ) {
value.id = mtlValue.id;
value.version = mtlValue.version;
return false;
}
} else if ( value !== mtlValue ) {
materialData[ property ] = mtlValue;
return false;
}
}
if ( materialData.transmission > 0 ) {
const { width, height } = renderObject.context;
if ( renderObjectData.bufferWidth !== width || renderObjectData.bufferHeight !== height ) {
renderObjectData.bufferWidth = width;
renderObjectData.bufferHeight = height;
return false;
}
}
// geometry
const storedGeometryData = renderObjectData.geometry;
const attributes = geometry.attributes;
const storedAttributes = storedGeometryData.attributes;
const storedAttributeNames = Object.keys( storedAttributes );
const currentAttributeNames = Object.keys( attributes );
if ( storedGeometryData.id !== geometry.id ) {
storedGeometryData.id = geometry.id;
return false;
}
if ( storedAttributeNames.length !== currentAttributeNames.length ) {
renderObjectData.geometry.attributes = this.getAttributesData( attributes );
return false;
}
// compare each attribute
for ( const name of storedAttributeNames ) {
const storedAttributeData = storedAttributes[ name ];
const attribute = attributes[ name ];
if ( attribute === undefined ) {
// attribute was removed
delete storedAttributes[ name ];
return false;
}
if ( storedAttributeData.version !== attribute.version ) {
storedAttributeData.version = attribute.version;
return false;
}
}
// check index
const index = geometry.index;
const storedIndexVersion = storedGeometryData.indexVersion;
const currentIndexVersion = index ? index.version : null;
if ( storedIndexVersion !== currentIndexVersion ) {
storedGeometryData.indexVersion = currentIndexVersion;
return false;
}
// check drawRange
if ( storedGeometryData.drawRange.start !== geometry.drawRange.start || storedGeometryData.drawRange.count !== geometry.drawRange.count ) {
storedGeometryData.drawRange.start = geometry.drawRange.start;
storedGeometryData.drawRange.count = geometry.drawRange.count;
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 ] ) {
morphChanged = true;
}
}
if ( morphChanged ) return true;
}
// center
if ( renderObjectData.center ) {
if ( renderObjectData.center.equals( object.center ) === false ) {
renderObjectData.center.copy( object.center );
return true;
}
}
// bundle
if ( renderObject.bundle !== null ) {
renderObjectData.version = renderObject.bundle.version;
}
return true;
}
/**
* Checks if the given render object requires a refresh.
*
* @param {RenderObject} renderObject - The render object.
* @param {NodeFrame} nodeFrame - The current node frame.
* @return {boolean} Whether the given render object requires a refresh or not.
*/
needsRefresh( renderObject, nodeFrame ) {
if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) )
return true;
const { renderId } = nodeFrame;
if ( this.renderId !== renderId ) {
this.renderId = renderId;
return true;
}
const isStatic = renderObject.object.static === true;
const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
if ( isStatic || isBundle )
return false;
const notEqual = this.equals( renderObject ) !== true;
return notEqual;
}
}
// 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 ( value instanceof Array ) {
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.
*
* @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.
*
* @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.
*
* @method
* @param {...number} params - A list of parameters.
* @return {number} The hash.
*/
const hash$1 = ( ...params ) => cyrb53( params );
/**
* Computes a cache key for the given node.
*
* @method
* @param {Object|Node} object - The object to be hashed.
* @param {boolean} [force=false] - Whether to force a cache key computation or not.
* @return {number} The hash.
*/
function getCacheKey$1( object, force = false ) {
const values = [];
if ( object.isNode === true ) {
values.push( object.id );
object = object.getSelf();
}
for ( const { property, childNode } of getNodeChildren( object ) ) {
values.push( cyrb53( property.slice( 0, -4 ) ), childNode.getCacheKey( force ) );
}
return cyrb53( values );
}
/**
* This generator function can be used to iterate over the node children
* of the given object.
*
* @generator
* @param {Object} node - The object to be hashed.
* @param {boolean} [toJSON=false] - Whether to return JSON or not.
* @yields {Object} A result node holding the property, index (if available) and the child node.
*/
function* getNodeChildren( node, toJSON = false ) {
for ( const property in node ) {
// Ignore private properties.
if ( property.startsWith( '_' ) === true ) continue;
const object = node[ property ];
if ( Array.isArray( object ) === true ) {
for ( let i = 0; i < object.length; i ++ ) {
const child = object[ i ];
if ( child && ( child.isNode === true || toJSON && typeof child.toJSON === 'function' ) ) {
yield { property, index: i, childNode: child };
}
}
} else if ( object && object.isNode === true ) {
yield { property, childNode: object };
} else if ( typeof object === 'object' ) {
for ( const subProperty in object ) {
const child = object[ subProperty ];
if ( child && ( child.isNode === true || toJSON && typeof child.toJSON === 'function' ) ) {
yield { property, index: subProperty, childNode: child };
}
}
}
}
}
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.
*
* @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.
*
* @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.
*
* @method
* @param {string} type - The data type.
* @return {number} The length.
*/
function getLengthFromType( type ) {
if ( /float|int|uint/.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;
console.error( 'THREE.TSL: Unsupported type:', type );
}
/**
* Returns the data type for the given value.
*
* @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 value/object for the given data type and parameters.
*
* @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.
*
* @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.
*
* @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 );
}
/**
* Converts the given Base64 string to an array buffer.
*
* @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;
}
var NodeUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
arrayBufferToBase64: arrayBufferToBase64,
base64ToArrayBuffer: base64ToArrayBuffer,
getCacheKey: getCacheKey$1,
getDataFromObject: getDataFromObject,
getLengthFromType: getLengthFromType,
getNodeChildren: getNodeChildren,
getTypeFromLength: getTypeFromLength,
getTypedArrayFromType: getTypedArrayFromType,
getValueFromType: getValueFromType,
getValueType: getValueType,
hash: hash$1,
hashArray: hashArray,
hashString: hashString
});
/**
* 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' ];
let _nodeId = 0;
/**
* Base class for all nodes.
*
* @augments EventDispatcher
*/
class Node extends EventDispatcher {
static get type() {
return 'Node';
}
/**
* Constructs a new node.
*
* @param {?string} nodeType - The node type.
*/
constructor( nodeType = null ) {
super();
/**
* The node type. This represents the result type of the node (e.g. `float` or `vec3`).
*
* @type {?string}
* @default null
*/
this.nodeType = nodeType;
/**
* The update type of the node's {@link Node#update} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateType = NodeUpdateType.NONE;
/**
* The update type of the node's {@link Node#updateBefore} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateBeforeType = NodeUpdateType.NONE;
/**
* The update type of the node's {@link Node#updateAfter} method. Possible values are listed in {@link NodeUpdateType}.
*
* @type {string}
* @default 'none'
*/
this.updateAfterType = NodeUpdateType.NONE;
/**
* The UUID of the node.
*
* @type {string}
* @readonly
*/
this.uuid = MathUtils.generateUUID();
/**
* The version of the node. The version automatically is increased when {@link Node#needsUpdate} is set to `true`.
*
* @type {number}
* @readonly
* @default 0
*/
this.version = 0;
/**
* Whether this node is global or not. This property is relevant for the internal
* node caching system. All nodes which should be declared just once should
* set this flag to `true` (a typical example is {@link AttributeNode}).
*
* @type {boolean}
* @default false
*/
this.global = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNode = true;
// private
/**
* The cache key of this node.
*
* @private
* @type {?number}
* @default null
*/
this._cacheKey = null;
/**
* The cache key 's version.
*
* @private
* @type {number}
* @default 0
*/
this._cacheKeyVersion = 0;
Object.defineProperty( this, 'id', { value: _nodeId ++ } );
}
/**
* Set this property to `true` when the node should be regenerated.
*
* @type {boolean}
* @default false
* @param {boolean} value
*/
set needsUpdate( value ) {
if ( value === true ) {
this.version ++;
}
}
/**
* The type of the class. The value is usually the constructor name.
*
* @type {string}
* @readonly
*/
get type() {
return this.constructor.type;
}
/**
* Convenient method for defining {@link Node#update}.
*
* @param {Function} callback - The update method.
* @param {string} updateType - The update type.
* @return {Node} A reference to this node.
*/
onUpdate( callback, updateType ) {
this.updateType = updateType;
this.update = callback.bind( this.getSelf() );
return this;
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `FRAME`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onFrameUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.FRAME );
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `RENDER`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onRenderUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.RENDER );
}
/**
* Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but
* this method automatically sets the update type to `OBJECT`.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onObjectUpdate( callback ) {
return this.onUpdate( callback, NodeUpdateType.OBJECT );
}
/**
* Convenient method for defining {@link Node#updateReference}.
*
* @param {Function} callback - The update method.
* @return {Node} A reference to this node.
*/
onReference( callback ) {
this.updateReference = callback.bind( this.getSelf() );
return this;
}
/**
* The `this` reference might point to a Proxy so this method can be used
* to get the reference to the actual node instance.
*
* @return {Node} A reference to the node.
*/
getSelf() {
// Returns non-node object.
return this.self || this;
}
/**
* Nodes might refer to other objects like materials. This method allows to dynamically update the reference
* to such objects based on a given state (e.g. the current node frame or builder).
*
* @param {any} state - This method can be invocated in different contexts so `state` can refer to any object type.
* @return {any} The updated reference.
*/
updateReference( /*state*/ ) {
return this;
}
/**
* By default this method returns the value of the {@link Node#global} flag. This method
* can be overwritten in derived classes if an analytical way is required to determine the
* global status.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {boolean} Whether this node is global or not.
*/
isGlobal( /*builder*/ ) {
return this.global;
}
/**
* Generator function that can be used to iterate over the child nodes.
*
* @generator
* @yields {Node} A child node.
*/
* getChildren() {
for ( const { childNode } of getNodeChildren( this ) ) {
yield childNode;
}
}
/**
* Calling this method dispatches the `dispose` event. This event can be used
* to register event listeners for clean up tasks.
*/
dispose() {
this.dispatchEvent( { type: 'dispose' } );
}
/**
* Callback for {@link Node#traverse}.
*
* @callback traverseCallback
* @param {Node} node - The current node.
*/
/**
* Can be used to traverse through the node's hierarchy.
*
* @param {traverseCallback} callback - A callback that is executed per node.
*/
traverse( callback ) {
callback( this );
for ( const childNode of this.getChildren() ) {
childNode.traverse( callback );
}
}
/**
* Returns the cache key for this node.
*
* @param {boolean} [force=false] - When set to `true`, a recomputation of the cache key is forced.
* @return {number} The cache key of the node.
*/
getCacheKey( force = false ) {
force = force || this.version !== this._cacheKeyVersion;
if ( force === true || this._cacheKey === null ) {
this._cacheKey = hash$1( getCacheKey$1( this, force ), this.customCacheKey() );
this._cacheKeyVersion = this.version;
}
return this._cacheKey;
}
/**
* Generate a custom cache key for this node.
*
* @return {number} The cache key of the node.
*/
customCacheKey() {
return 0;
}
/**
* Returns the references to this node which is by default `this`.
*
* @return {Node} A reference to this node.
*/
getScope() {
return this;
}
/**
* Returns the hash of the node which is used to identify the node. By default it's
* the {@link Node#uuid} however derived node classes might have to overwrite this method
* depending on their implementation.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The hash.
*/
getHash( /*builder*/ ) {
return this.uuid;
}
/**
* Returns the update type of {@link Node#update}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateType() {
return this.updateType;
}
/**
* Returns the update type of {@link Node#updateBefore}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateBeforeType() {
return this.updateBeforeType;
}
/**
* Returns the update type of {@link Node#updateAfter}.
*
* @return {NodeUpdateType} The update type.
*/
getUpdateAfterType() {
return this.updateAfterType;
}
/**
* Certain types are composed of multiple elements. For example a `vec3`
* is composed of three `float` values. This method returns the type of
* these elements.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
getElementType( builder ) {
const type = this.getNodeType( builder );
const elementType = builder.getElementType( type );
return elementType;
}
/**
* Returns the node member type for the given name.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {string} name - The name of the member.
* @return {string} The type of the node.
*/
getMemberType( /*builder, name*/ ) {
return 'void';
}
/**
* Returns the node's type.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {string} The type of the node.
*/
getNodeType( builder ) {
const nodeProperties = builder.getNodeProperties( this );
if ( nodeProperties.outputNode ) {
return nodeProperties.outputNode.getNodeType( builder );
}
return this.nodeType;
}
/**
* This method is used during the build process of a node and ensures
* equal nodes are not built multiple times but just once. For example if
* `attribute( 'uv' )` is used multiple times by the user, the build
* process makes sure to process just the first node.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The shared node if possible. Otherwise `this` is returned.
*/
getShared( builder ) {
const hash = this.getHash( builder );
const nodeFromHash = builder.getNodeFromHash( hash );
return nodeFromHash || this;
}
/**
* Represents the setup stage which is the first step of the build process, see {@link Node#build} method.
* This method is often overwritten in derived modules to prepare the node which is used as the output/result.
* The output node must be returned in the `return` statement.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?Node} The output node.
*/
setup( builder ) {
const nodeProperties = builder.getNodeProperties( this );
let index = 0;
for ( const childNode of this.getChildren() ) {
nodeProperties[ 'node' + index ++ ] = childNode;
}
// return a outputNode if exists or null
return nodeProperties.outputNode || null;
}
/**
* Represents the analyze stage which is the second step of the build process, see {@link Node#build} method.
* This stage analyzes the node hierarchy and ensures descendent nodes are built.
*
* @param {NodeBuilder} builder - The current node builder.
*/
analyze( builder ) {
const usageCount = builder.increaseUsage( this );
if ( usageCount === 1 ) {
// node flow children
const nodeProperties = builder.getNodeProperties( this );
for ( const childNode of Object.values( nodeProperties ) ) {
if ( childNode && childNode.isNode === true ) {
childNode.build( builder );
}
}
}
}
/**
* Represents the generate stage which is the third step of the build process, see {@link Node#build} method.
* This state builds the output node and returns the resulting shader string.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?string} output - Can be used to define the output type.
* @return {?string} The generated shader string.
*/
generate( builder, output ) {
const { outputNode } = builder.getNodeProperties( this );
if ( outputNode && outputNode.isNode === true ) {
return outputNode.build( builder, output );
}
}
/**
* The method can be implemented to update the node's internal state before it is used to render an object.
* The {@link Node#updateBeforeType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
updateBefore( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* The method can be implemented to update the node's internal state after it was used to render an object.
* The {@link Node#updateAfterType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
updateAfter( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* The method can be implemented to update the node's internal state when it is used to render an object.
* The {@link Node#updateType} property defines how often the update is executed.
*
* @abstract
* @param {NodeFrame} frame - A reference to the current node frame.
* @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching).
*/
update( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
/**
* This method performs the build of a node. The behavior of this method as well as its return value depend
* on the current build stage (setup, analyze or generate).
*
* @param {NodeBuilder} builder - The current node builder.
* @param {?string} output - Can be used to define the output type.
* @return {?string} When this method is executed in the setup or analyze stage, `null` is returned. In the generate stage, the generated shader string.
*/
build( builder, output = null ) {
const refNode = this.getShared( builder );
if ( this !== refNode ) {
return refNode.build( builder, output );
}
builder.addNode( this );
builder.addChain( this );
/* Build stages expected results:
- "setup" -> Node
- "analyze" -> null
- "generate" -> String
*/
let result = null;
const buildStage = builder.getBuildStage();
if ( buildStage === 'setup' ) {
this.updateReference( builder );
const properties = builder.getNodeProperties( this );
if ( properties.initialized !== true ) {
//const stackNodesBeforeSetup = builder.stack.nodes.length;
properties.initialized = true;
const outputNode = this.setup( builder ); // return a node or null
const isNodeOutput = outputNode && outputNode.isNode === true;
/*if ( isNodeOutput && builder.stack.nodes.length !== stackNodesBeforeSetup ) {
// !! no outputNode !!
//outputNode = builder.stack;
}*/
for ( const childNode of Object.values( properties ) ) {
if ( childNode && childNode.isNode === true ) {
childNode.build( builder );
}
}
if ( isNodeOutput ) {
outputNode.build( builder );
}
properties.outputNode = outputNode;
}
} else if ( buildStage === 'analyze' ) {
this.analyze( builder );
} else if ( buildStage === 'generate' ) {
const isGenerateOnce = this.generate.length === 1;
if ( isGenerateOnce ) {
const type = this.getNodeType( builder );
const nodeData = builder.getDataFromNode( this );
result = nodeData.snippet;
if ( result === undefined ) {
if ( nodeData.generated === undefined ) {
nodeData.generated = true;
result = this.generate( builder ) || '';
nodeData.snippet = result;
} else {
console.warn( 'THREE.Node: Recursion detected.', this );
result = '';
}
} else if ( nodeData.flowCodes !== undefined && builder.context.nodeBlock !== undefined ) {
builder.addFlowCodeHierarchy( this, builder.context.nodeBlock );
}
result = builder.format( result, type, output );
} else {
result = this.generate( builder, output ) || '';
}
}
builder.removeChain( this );
builder.addSequentialNode( this );
return result;
}
/**
* Returns the child nodes as a JSON object.
*
* @return {Array<Object>} An iterable list of serialized child objects as JSON.
*/
getSerializeChildren() {
return getNodeChildren( this );
}
/**
* Serializes the node to JSON.
*
* @param {Object} json - The output JSON object.
*/
serialize( json ) {
const nodeChildren = this.getSerializeChildren();
const inputNodes = {};
for ( const { property, index, childNode } of nodeChildren ) {
if ( index !== undefined ) {
if ( inputNodes[ property ] === undefined ) {
inputNodes[ property ] = Number.isInteger( index ) ? [] : {};
}
inputNodes[ property ][ index ] = childNode.toJSON( json.meta ).uuid;
} else {
inputNodes[ property ] = childNode.toJSON( json.meta ).uuid;
}
}
if ( Object.keys( inputNodes ).length > 0 ) {
json.inputNodes = inputNodes;
}
}
/**
* Deserializes the node from the given JSON.
*
* @param {Object} json - The JSON object.
*/
deserialize( json ) {
if ( json.inputNodes !== undefined ) {
const nodes = json.meta.nodes;
for ( const property in json.inputNodes ) {
if ( Array.isArray( json.inputNodes[ property ] ) ) {
const inputArray = [];
for ( const uuid of json.inputNodes[ property ] ) {
inputArray.push( nodes[ uuid ] );
}
this[ property ] = inputArray;
} else if ( typeof json.inputNodes[ property ] === 'object' ) {
const inputObject = {};
for ( const subProperty in json.inputNodes[ property ] ) {
const uuid = json.inputNodes[ property ][ subProperty ];
inputObject[ subProperty ] = nodes[ uuid ];
}
this[ property ] = inputObject;
} else {
const uuid = json.inputNodes[ property ];
this[ property ] = nodes[ uuid ];
}
}
}
}
/**
* Serializes the node into the three.js JSON Object/Scene format.
*
* @param {?Object} meta - An optional JSON object that already holds serialized data from other scene objects.
* @return {Object} The serialized node.
*/
toJSON( meta ) {
const { uuid, type } = this;
const isRoot = ( meta === undefined || typeof meta === 'string' );
if ( isRoot ) {
meta = {
textures: {},
images: {},
nodes: {}
};
}
// serialize
let data = meta.nodes[ uuid ];
if ( data === undefined ) {
data = {
uuid,
type,
meta,
metadata: {
version: 4.6,
type: 'Node',
generator: 'Node.toJSON'
}
};
if ( isRoot !== true ) meta.nodes[ data.uuid ] = data;
this.serialize( data );
delete data.meta;
}
// TODO: Copied from Object3D.toJSON
function extractFromCache( cache ) {
const values = [];
for ( const key in cache ) {
const data = cache[ key ];
delete data.metadata;
values.push( data );
}
return values;
}
if ( isRoot ) {
const textures = extractFromCache( meta.textures );
const images = extractFromCache( meta.images );
const nodes = extractFromCache( meta.nodes );
if ( textures.length > 0 ) data.textures = textures;
if ( images.length > 0 ) data.images = images;
if ( nodes.length > 0 ) data.nodes = nodes;
}
return data;
}
}
/**
* Base class for representing element access on an array-like
* node data structures.
*
* @augments Node
*/
class ArrayElementNode extends Node { // @TODO: If extending from TempNode it breaks webgpu_compute
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;
}
/**
* 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.
*/
getNodeType( builder ) {
return this.node.getElementType( builder );
}
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