Skip to content

⬅️ Back to Table of Contents

📄 MorphNode.js

📊 Analysis Summary

Metric Count
🔧 Functions 4
🧱 Classes 1
📦 Imports 20
📊 Variables & Constants 28

📚 Table of Contents

🛠️ File Location:

📂 src/nodes/accessors/MorphNode.js

📦 Imports

Name Source
Node ../core/Node.js
NodeUpdateType ../core/constants.js
float ../tsl/TSLBase.js
nodeProxy ../tsl/TSLBase.js
Fn ../tsl/TSLBase.js
ivec2 ../tsl/TSLBase.js
int ../tsl/TSLBase.js
If ../tsl/TSLBase.js
uniform ../core/UniformNode.js
reference ./ReferenceNode.js
positionLocal ./Position.js
normalLocal ./Normal.js
textureLoad ./TextureNode.js
instanceIndex ../core/IndexNode.js
vertexIndex ../core/IndexNode.js
Loop ../utils/LoopNode.js
DataArrayTexture ../../textures/DataArrayTexture.js
Vector2 ../../math/Vector2.js
Vector4 ../../math/Vector4.js
FloatType ../../constants.js

Variables & Constants

Name Type Kind Value Exported
_morphTextures WeakMap<WeakKey, any> let/var new WeakMap()
_morphVec4 Vector4 let/var new Vector4()
bufferAttrib any let/var textureLoad( bufferMap, ivec2( x, y ) ).depth( depth ).xyz
hasMorphPosition boolean let/var geometry.morphAttributes.position !== undefined
hasMorphNormals boolean let/var geometry.morphAttributes.normal !== undefined
hasMorphColors boolean let/var geometry.morphAttributes.color !== undefined
morphAttribute any let/var geometry.morphAttributes.position \|\| geometry.morphAttributes.normal \|\| g...
morphTargetsCount any let/var ( morphAttribute !== undefined ) ? morphAttribute.length : 0
morphTargets any let/var geometry.morphAttributes.position \|\| []
morphNormals any let/var geometry.morphAttributes.normal \|\| []
morphColors any let/var geometry.morphAttributes.color \|\| []
vertexDataCount number let/var 0
width number let/var geometry.attributes.position.count * vertexDataCount
height number let/var 1
maxTextureSize 4096 let/var 4096
buffer Float32Array<ArrayBuffer> let/var new Float32Array( width * height * 4 * morphTargetsCount )
bufferTexture DataArrayTexture let/var new DataArrayTexture( buffer, width, height, morphTargetsCount )
vertexDataStride number let/var vertexDataCount * 4
morphTarget any let/var morphTargets[ i ]
morphNormal any let/var morphNormals[ i ]
morphColor any let/var morphColors[ i ]
offset number let/var width * height * 4 * i
stride number let/var j * vertexDataStride
hasMorphPosition boolean let/var geometry.morphAttributes.position !== undefined
hasMorphNormals boolean let/var geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== unde...
morphAttribute any let/var geometry.morphAttributes.position \|\| geometry.morphAttributes.normal \|\| g...
morphTargetsCount any let/var ( morphAttribute !== undefined ) ? morphAttribute.length : 0
morphBaseInfluence UniformNode<any> let/var this.morphBaseInfluence

Functions

getEntry(geometry: any): any

Parameters:

  • geometry any

Returns: any

Calls:

  • _morphTextures.get
  • entry.texture.dispose
  • Math.ceil
  • _morphVec4.fromBufferAttribute
  • _morphTextures.set
  • bufferTexture.dispose
  • _morphTextures.delete
  • geometry.removeEventListener
  • geometry.addEventListener

Internal Comments:

// instead of using attributes, the WebGL 2 code path encodes morph targets (x2)
// into an array of data textures. Each layer represents a single morph target. (x2)
// fill buffer (x2)

Code
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;

}

disposeTexture(): void

Returns: void

Calls:

  • bufferTexture.dispose
  • _morphTextures.delete
  • geometry.removeEventListener
Code
function disposeTexture() {

            bufferTexture.dispose();

            _morphTextures.delete( geometry );

            geometry.removeEventListener( 'dispose', disposeTexture );

        }

MorphNode.setup(builder: NodeBuilder): void

JSDoc:

/**
     * Setups the morph node by assigning the transformed vertex data to predefined node variables.
     *
     * @param {NodeBuilder} builder - The current node builder.
     */

Parameters:

  • builder NodeBuilder

Returns: void

Calls:

  • geometry.hasAttribute
  • getEntry
  • positionLocal.mulAssign
  • normalLocal.mulAssign
  • int (from ../tsl/TSLBase.js)
  • Loop (from ../utils/LoopNode.js)
  • float( 0 ).toVar
  • influence.assign
  • textureLoad (from ./TextureNode.js)
  • ivec2 (from ../tsl/TSLBase.js)
  • int( i ).add
  • reference( 'morphTargetInfluences', 'float' ).element( i ).toVar
  • If (from ../tsl/TSLBase.js)
  • influence.notEqual
  • positionLocal.addAssign
  • getMorph
  • normalLocal.addAssign

Internal Comments:

// nodes (x2)

Code
setup( builder ) {

        const { geometry } = builder;

        const hasMorphPosition = geometry.morphAttributes.position !== undefined;
        const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined;

        const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
        const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

        // nodes

        const { texture: bufferMap, stride, size } = getEntry( geometry );

        if ( hasMorphPosition === true ) positionLocal.mulAssign( this.morphBaseInfluence );
        if ( hasMorphNormals === true ) normalLocal.mulAssign( this.morphBaseInfluence );

        const width = int( size.width );

        Loop( morphTargetsCount, ( { i } ) => {

            const influence = float( 0 ).toVar();

            if ( this.mesh.count > 1 && ( this.mesh.morphTexture !== null && this.mesh.morphTexture !== undefined ) ) {

                influence.assign( textureLoad( this.mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r );

            } else {

                influence.assign( reference( 'morphTargetInfluences', 'float' ).element( i ).toVar() );

            }

            If( influence.notEqual( 0 ), () => {

                if ( hasMorphPosition === true ) {

                    positionLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 0 )
                    } ) );

                }

                if ( hasMorphNormals === true ) {

                    normalLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 1 )
                    } ) );

                }

            } );

        } );

    }

MorphNode.update(): void

JSDoc:

/**
     * Updates the state of the morphed mesh by updating the base influence.
     *
     * @param {NodeFrame} frame - The current node frame.
     */

Returns: void

Calls:

  • this.mesh.morphTargetInfluences.reduce
Code
update( /*frame*/ ) {

        const morphBaseInfluence = this.morphBaseInfluence;

        if ( this.mesh.geometry.morphTargetsRelative ) {

            morphBaseInfluence.value = 1;

        } else {

            morphBaseInfluence.value = 1 - this.mesh.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 );

        }

    }

Classes

MorphNode

Class Code
class MorphNode extends Node {

    static get type() {

        return 'MorphNode';

    }

    /**
     * Constructs a new morph node.
     *
     * @param {Mesh} mesh - The mesh holding the morph targets.
     */
    constructor( mesh ) {

        super( 'void' );

        /**
         * The mesh holding the morph targets.
         *
         * @type {Mesh}
         */
        this.mesh = mesh;

        /**
         * A uniform node which represents the morph base influence value.
         *
         * @type {UniformNode<float>}
         */
        this.morphBaseInfluence = uniform( 1 );

        /**
         * The update type overwritten since morph nodes are updated per object.
         *
         * @type {string}
         */
        this.updateType = NodeUpdateType.OBJECT;

    }

    /**
     * Setups the morph node by assigning the transformed vertex data to predefined node variables.
     *
     * @param {NodeBuilder} builder - The current node builder.
     */
    setup( builder ) {

        const { geometry } = builder;

        const hasMorphPosition = geometry.morphAttributes.position !== undefined;
        const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined;

        const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
        const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

        // nodes

        const { texture: bufferMap, stride, size } = getEntry( geometry );

        if ( hasMorphPosition === true ) positionLocal.mulAssign( this.morphBaseInfluence );
        if ( hasMorphNormals === true ) normalLocal.mulAssign( this.morphBaseInfluence );

        const width = int( size.width );

        Loop( morphTargetsCount, ( { i } ) => {

            const influence = float( 0 ).toVar();

            if ( this.mesh.count > 1 && ( this.mesh.morphTexture !== null && this.mesh.morphTexture !== undefined ) ) {

                influence.assign( textureLoad( this.mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r );

            } else {

                influence.assign( reference( 'morphTargetInfluences', 'float' ).element( i ).toVar() );

            }

            If( influence.notEqual( 0 ), () => {

                if ( hasMorphPosition === true ) {

                    positionLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 0 )
                    } ) );

                }

                if ( hasMorphNormals === true ) {

                    normalLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 1 )
                    } ) );

                }

            } );

        } );

    }

    /**
     * Updates the state of the morphed mesh by updating the base influence.
     *
     * @param {NodeFrame} frame - The current node frame.
     */
    update( /*frame*/ ) {

        const morphBaseInfluence = this.morphBaseInfluence;

        if ( this.mesh.geometry.morphTargetsRelative ) {

            morphBaseInfluence.value = 1;

        } else {

            morphBaseInfluence.value = 1 - this.mesh.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 );

        }

    }

}

Methods

setup(builder: NodeBuilder): void
Code
setup( builder ) {

        const { geometry } = builder;

        const hasMorphPosition = geometry.morphAttributes.position !== undefined;
        const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined;

        const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
        const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;

        // nodes

        const { texture: bufferMap, stride, size } = getEntry( geometry );

        if ( hasMorphPosition === true ) positionLocal.mulAssign( this.morphBaseInfluence );
        if ( hasMorphNormals === true ) normalLocal.mulAssign( this.morphBaseInfluence );

        const width = int( size.width );

        Loop( morphTargetsCount, ( { i } ) => {

            const influence = float( 0 ).toVar();

            if ( this.mesh.count > 1 && ( this.mesh.morphTexture !== null && this.mesh.morphTexture !== undefined ) ) {

                influence.assign( textureLoad( this.mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r );

            } else {

                influence.assign( reference( 'morphTargetInfluences', 'float' ).element( i ).toVar() );

            }

            If( influence.notEqual( 0 ), () => {

                if ( hasMorphPosition === true ) {

                    positionLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 0 )
                    } ) );

                }

                if ( hasMorphNormals === true ) {

                    normalLocal.addAssign( getMorph( {
                        bufferMap,
                        influence,
                        stride,
                        width,
                        depth: i,
                        offset: int( 1 )
                    } ) );

                }

            } );

        } );

    }
update(): void
Code
update( /*frame*/ ) {

        const morphBaseInfluence = this.morphBaseInfluence;

        if ( this.mesh.geometry.morphTargetsRelative ) {

            morphBaseInfluence.value = 1;

        } else {

            morphBaseInfluence.value = 1 - this.mesh.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 );

        }

    }