Skip to content

⬅️ Back to Table of Contents

📄 SceneOptimizer.js

📊 Analysis Summary

Metric Count
🔧 Functions 12
🧱 Classes 1
📊 Variables & Constants 25

📚 Table of Contents

🛠️ File Location:

📂 examples/jsm/utils/SceneOptimizer.js

Variables & Constants

Name Type Kind Value Exported
hash number let/var 0
uintArray any let/var *not shown*
byte number let/var uintArray[ i ]
mapProps string[] let/var [ 'map', 'alphaMap', 'aoMap', 'bumpMap', 'displacementMap', 'emissiveMap', 'e...
map any let/var material[ prop ]
emissiveHash any let/var material.emissive ? material.emissive.getHexString() : 0
attenuationHash any let/var material.attenuationColor ? material.attenuationColor.getHexString() : 0
sheenColorHash any let/var material.sheenColor ? material.sheenColor.getHexString() : 0
attribute any let/var geometry.attributes[ name ]
indexHash number \| "noIndex" let/var geometry.index ? this._bufferToHash( geometry.index.array ) : 'noIndex'
batchGroups Map<any, any> let/var new Map()
singleGroups Map<any, any> let/var new Map()
uniqueGeometries Set<any> let/var new Set()
meshesToRemove Set<any> let/var new Set()
maxGeometries any let/var group.totalInstances
batchedMaterial any let/var new group.materialProps.constructor( group.materialProps )
batchedMesh any let/var new THREE.BatchedMesh( maxGeometries, maxVertices, maxIndices, batchedMaterial )
referenceMesh any let/var group.meshes[ 0 ]
geometryIds Map<any, any> let/var new Map()
inverseParentMatrix any let/var new THREE.Matrix4()
localMatrix any let/var new THREE.Matrix4()
children any[] let/var [ ...object.children ]
totalOriginalMeshes number let/var meshesToRemove.size + singleGroups.size
totalFinalMeshes number let/var batchGroups.size + singleGroups.size
stats { originalMeshes: number; batchedMesh... let/var { originalMeshes: totalOriginalMeshes, batchedMeshes: batchGroups.size, singl...

Functions

SceneOptimizer._bufferToHash(buffer: any): number

Parameters:

  • buffer any

Returns: number

Code
_bufferToHash( buffer ) {

        let hash = 0;
        if ( buffer.byteLength !== 0 ) {

            let uintArray;
            if ( buffer.buffer ) {

                uintArray = new Uint8Array(
                    buffer.buffer,
                    buffer.byteOffset,
                    buffer.byteLength
                );

            } else {

                uintArray = new Uint8Array( buffer );

            }

            for ( let i = 0; i < buffer.byteLength; i ++ ) {

                const byte = uintArray[ i ];
                hash = ( hash << 5 ) - hash + byte;
                hash |= 0;

            }

        }

        return hash;

    }

SceneOptimizer._getMaterialPropertiesHash(material: any): string

Parameters:

  • material any

Returns: string

Calls:

  • `mapProps .map( ( prop ) => {
            const map = material[ prop ];
            if ( ! map ) return 0;
            return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;
    
        } )
        .join`
    
    • `[ 'transparent', 'opacity', 'alphaTest', 'alphaToCoverage', 'side', 'vertexColors', 'visible', 'blending', 'wireframe', 'flatShading', 'premultipliedAlpha', 'dithering', 'toneMapped', 'depthTest', 'depthWrite', 'metalness', 'roughness', 'clearcoat', 'clearcoatRoughness', 'sheen', 'sheenRoughness', 'transmission', 'thickness', 'attenuationDistance', 'ior', 'iridescence', 'iridescenceIOR', 'iridescenceThicknessRange', 'reflectivity', ] .map( ( prop ) => {
          if ( typeof material[ prop ] === 'undefined' ) return 0;
          if ( material[ prop ] === null ) return 0;
          return material[ prop ].toString();
      
      } )
      .join`
      
      • material.emissive.getHexString
      • material.attenuationColor.getHexString
      • material.sheenColor.getHexString
      • [ material.type, physicalProps, mapHash, emissiveHash, attenuationHash, sheenColorHash, ].join
Code
_getMaterialPropertiesHash( material ) {

        const mapProps = [
            'map',
            'alphaMap',
            'aoMap',
            'bumpMap',
            'displacementMap',
            'emissiveMap',
            'envMap',
            'lightMap',
            'metalnessMap',
            'normalMap',
            'roughnessMap',
        ];

        const mapHash = mapProps
            .map( ( prop ) => {

                const map = material[ prop ];
                if ( ! map ) return 0;
                return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;

            } )
            .join( '|' );

        const physicalProps = [
            'transparent',
            'opacity',
            'alphaTest',
            'alphaToCoverage',
            'side',
            'vertexColors',
            'visible',
            'blending',
            'wireframe',
            'flatShading',
            'premultipliedAlpha',
            'dithering',
            'toneMapped',
            'depthTest',
            'depthWrite',
            'metalness',
            'roughness',
            'clearcoat',
            'clearcoatRoughness',
            'sheen',
            'sheenRoughness',
            'transmission',
            'thickness',
            'attenuationDistance',
            'ior',
            'iridescence',
            'iridescenceIOR',
            'iridescenceThicknessRange',
            'reflectivity',
        ]
            .map( ( prop ) => {

                if ( typeof material[ prop ] === 'undefined' ) return 0;
                if ( material[ prop ] === null ) return 0;
                return material[ prop ].toString();

            } )
            .join( '|' );

        const emissiveHash = material.emissive ? material.emissive.getHexString() : 0;
        const attenuationHash = material.attenuationColor
            ? material.attenuationColor.getHexString()
            : 0;
        const sheenColorHash = material.sheenColor
            ? material.sheenColor.getHexString()
            : 0;

        return [
            material.type,
            physicalProps,
            mapHash,
            emissiveHash,
            attenuationHash,
            sheenColorHash,
        ].join( '_' );

    }

SceneOptimizer._getAttributesSignature(geometry: any): string

Parameters:

  • geometry any

Returns: string

Calls:

  • `Object.keys( geometry.attributes ) .sort() .map( ( name ) => {
            const attribute = geometry.attributes[ name ];
            return `${name}_${attribute.itemSize}_${attribute.normalized}`;
    
        } )
        .join`
    
Code
_getAttributesSignature( geometry ) {

        return Object.keys( geometry.attributes )
            .sort()
            .map( ( name ) => {

                const attribute = geometry.attributes[ name ];
                return `${name}_${attribute.itemSize}_${attribute.normalized}`;

            } )
            .join( '|' );

    }

SceneOptimizer._getGeometryHash(geometry: any): string

Parameters:

  • geometry any

Returns: string

Calls:

  • this._bufferToHash
  • this._getAttributesSignature
Code
_getGeometryHash( geometry ) {

        const indexHash = geometry.index
            ? this._bufferToHash( geometry.index.array )
            : 'noIndex';
        const positionHash = this._bufferToHash( geometry.attributes.position.array );
        const attributesSignature = this._getAttributesSignature( geometry );
        return `${indexHash}_${positionHash}_${attributesSignature}`;

    }

SceneOptimizer._getBatchKey(materialProps: any, attributesSignature: any): string

Parameters:

  • materialProps any
  • attributesSignature any

Returns: string

Code
_getBatchKey( materialProps, attributesSignature ) {

        return `${materialProps}_${attributesSignature}`;

    }

SceneOptimizer._analyzeModel(): { batchGroups: Map<any, any>; singleGroups: Map<any, any>; uniqueGeometries: number; }

Returns: { batchGroups: Map<any, any>; singleGroups: Map<any, any>; uniqueGeometries: number; }

Calls:

  • this.scene.updateMatrixWorld
  • this.scene.traverse
  • this._getMaterialPropertiesHash
  • this._getAttributesSignature
  • this._getBatchKey
  • this._getGeometryHash
  • uniqueGeometries.add
  • batchGroups.has
  • batchGroups.set
  • node.material.clone
  • batchGroups.get
  • group.meshes.push
  • group.geometryStats.has
  • group.geometryStats.set
  • group.geometryStats.get
  • singleGroups.set
  • batchGroups.delete

Internal Comments:

// Move single instance groups to singleGroups

Code
_analyzeModel() {

        const batchGroups = new Map();
        const singleGroups = new Map();
        const uniqueGeometries = new Set();

        this.scene.updateMatrixWorld( true );
        this.scene.traverse( ( node ) => {

            if ( ! node.isMesh ) return;

            const materialProps = this._getMaterialPropertiesHash( node.material );
            const attributesSignature = this._getAttributesSignature( node.geometry );
            const batchKey = this._getBatchKey( materialProps, attributesSignature );
            const geometryHash = this._getGeometryHash( node.geometry );
            uniqueGeometries.add( geometryHash );

            if ( ! batchGroups.has( batchKey ) ) {

                batchGroups.set( batchKey, {
                    meshes: [],
                    geometryStats: new Map(),
                    totalInstances: 0,
                    materialProps: node.material.clone(),
                } );

            }

            const group = batchGroups.get( batchKey );
            group.meshes.push( node );
            group.totalInstances ++;

            if ( ! group.geometryStats.has( geometryHash ) ) {

                group.geometryStats.set( geometryHash, {
                    count: 0,
                    vertices: node.geometry.attributes.position.count,
                    indices: node.geometry.index ? node.geometry.index.count : 0,
                    geometry: node.geometry,
                } );

            }

            group.geometryStats.get( geometryHash ).count ++;

        } );

        // Move single instance groups to singleGroups
        for ( const [ batchKey, group ] of batchGroups ) {

            if ( group.totalInstances === 1 ) {

                singleGroups.set( batchKey, group );
                batchGroups.delete( batchKey );

            }

        }

        return { batchGroups, singleGroups, uniqueGeometries: uniqueGeometries.size };

    }

SceneOptimizer._createBatchedMeshes(batchGroups: any): Set<any>

Parameters:

  • batchGroups any

Returns: Set<any>

Calls:

  • Array.from( group.geometryStats.values() ).reduce
  • batchedMaterial.color.set
  • referenceMesh.parent.updateWorldMatrix
  • inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert
  • this._getGeometryHash
  • geometryIds.has
  • geometryIds.set
  • batchedMesh.addGeometry
  • geometryIds.get
  • batchedMesh.addInstance
  • mesh.updateWorldMatrix
  • localMatrix.copy
  • localMatrix.premultiply
  • batchedMesh.setMatrixAt
  • batchedMesh.setColorAt
  • meshesToRemove.add
  • referenceMesh.parent.add

Internal Comments:

// Reset color to white, color will be set per instance (x5)

Code
_createBatchedMeshes( batchGroups ) {

        const meshesToRemove = new Set();

        for ( const [ , group ] of batchGroups ) {

            const maxGeometries = group.totalInstances;
            const maxVertices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.vertices,
                0
            );
            const maxIndices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.indices,
                0
            );

            const batchedMaterial = new group.materialProps.constructor( group.materialProps );

            if ( batchedMaterial.color !== undefined ) {

                // Reset color to white, color will be set per instance
                batchedMaterial.color.set( 1, 1, 1 );

            }

            const batchedMesh = new THREE.BatchedMesh(
                maxGeometries,
                maxVertices,
                maxIndices,
                batchedMaterial
            );

            const referenceMesh = group.meshes[ 0 ];
            batchedMesh.name = `${referenceMesh.name}_batch`;

            const geometryIds = new Map();
            const inverseParentMatrix = new THREE.Matrix4();

            if ( referenceMesh.parent ) {

                referenceMesh.parent.updateWorldMatrix( true, false );
                inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert();

            }

            for ( const mesh of group.meshes ) {

                const geometryHash = this._getGeometryHash( mesh.geometry );

                if ( ! geometryIds.has( geometryHash ) ) {

                    geometryIds.set( geometryHash, batchedMesh.addGeometry( mesh.geometry ) );

                }

                const geometryId = geometryIds.get( geometryHash );
                const instanceId = batchedMesh.addInstance( geometryId );

                const localMatrix = new THREE.Matrix4();
                mesh.updateWorldMatrix( true, false );
                localMatrix.copy( mesh.matrixWorld );
                if ( referenceMesh.parent ) {

                    localMatrix.premultiply( inverseParentMatrix );

                }

                batchedMesh.setMatrixAt( instanceId, localMatrix );
                batchedMesh.setColorAt( instanceId, mesh.material.color );

                meshesToRemove.add( mesh );

            }

            if ( referenceMesh.parent ) {

                referenceMesh.parent.add( batchedMesh );

            }

        }

        return meshesToRemove;

    }

SceneOptimizer.removeEmptyNodes(object: Object3D): void

JSDoc:

/**
     * Removes empty nodes from all descendants of the given 3D object.
     *
     * @param {Object3D} object - The 3D object to process.
     */

Parameters:

  • object Object3D

Returns: void

Calls:

  • this.removeEmptyNodes
  • object.remove
Code
removeEmptyNodes( object ) {

        const children = [ ...object.children ];

        for ( const child of children ) {

            this.removeEmptyNodes( child );

            if ( ( child instanceof THREE.Group || child.constructor === THREE.Object3D )
                && child.children.length === 0 ) {

                object.remove( child );

            }

        }

    }

SceneOptimizer.disposeMeshes(meshesToRemove: Set<Mesh>): void

JSDoc:

/**
     * Removes the given array of meshes from the scene.
     *
     * @param {Set<Mesh>} meshesToRemove - The meshes to remove.
     */

Parameters:

  • meshesToRemove Set<Mesh>

Returns: void

Calls:

  • meshesToRemove.forEach
  • mesh.parent.remove
  • mesh.geometry.dispose
  • Array.isArray
  • mesh.material.forEach
  • m.dispose
  • mesh.material.dispose
Code
disposeMeshes( meshesToRemove ) {

        meshesToRemove.forEach( ( mesh ) => {

            if ( mesh.parent ) {

                mesh.parent.remove( mesh );

            }

            if ( mesh.geometry ) mesh.geometry.dispose();
            if ( mesh.material ) {

                if ( Array.isArray( mesh.material ) ) {

                    mesh.material.forEach( ( m ) => m.dispose() );

                } else {

                    mesh.material.dispose();

                }

            }

        } );

    }

SceneOptimizer._logDebugInfo(stats: any): void

Parameters:

  • stats any

Returns: void

Calls:

  • console.group
  • console.log
  • console.groupEnd
Code
_logDebugInfo( stats ) {

        console.group( 'Scene Optimization Results' );
        console.log( `Original meshes: ${stats.originalMeshes}` );
        console.log( `Batched into: ${stats.batchedMeshes} BatchedMesh` );
        console.log( `Single meshes: ${stats.singleMeshes} Mesh` );
        console.log( `Total draw calls: ${stats.drawCalls}` );
        console.log( `Reduction Ratio: ${stats.reductionRatio}% fewer draw calls` );
        console.groupEnd();

    }

SceneOptimizer.toBatchedMesh(): Scene

JSDoc:

/**
     * Performs the auto-baching by identifying groups of meshes in the scene
     * that can be represented as a single {@link BatchedMesh}. The method modifies
     * the scene by adding instances of `BatchedMesh` and removing the now redundant
     * individual meshes.
     *
     * @return {Scene} The optimized scene.
     */

Returns: Scene

Calls:

  • this._analyzeModel
  • this._createBatchedMeshes
  • this.disposeMeshes
  • this.removeEmptyNodes
  • ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed
  • this._logDebugInfo
Code
toBatchedMesh() {

        const { batchGroups, singleGroups, uniqueGeometries } = this._analyzeModel();
        const meshesToRemove = this._createBatchedMeshes( batchGroups );

        this.disposeMeshes( meshesToRemove );
        this.removeEmptyNodes( this.scene );

        if ( this.debug ) {

            const totalOriginalMeshes = meshesToRemove.size + singleGroups.size;
            const totalFinalMeshes = batchGroups.size + singleGroups.size;

            const stats = {
                originalMeshes: totalOriginalMeshes,
                batchedMeshes: batchGroups.size,
                singleMeshes: singleGroups.size,
                drawCalls: totalFinalMeshes,
                uniqueGeometries: uniqueGeometries,
                reductionRatio: ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed( 1 ),
            };

            this._logDebugInfo( stats );

        }

        return this.scene;

    }

SceneOptimizer.toInstancingMesh(): Scene

JSDoc:

/**
     * Performs the auto-instancing by identifying groups of meshes in the scene
     * that can be represented as a single {@link InstancedMesh}. The method modifies
     * the scene by adding instances of `InstancedMesh` and removing the now redundant
     * individual meshes.
     *
     * This method is not yet implemented.
     *
     * @abstract
     * @return {Scene} The optimized scene.
     */

Returns: Scene

Code
toInstancingMesh() {

        throw new Error( 'InstancedMesh optimization not implemented yet' );

    }

Classes

SceneOptimizer

Class Code
class SceneOptimizer {

    /**
     * Constructs a new scene optimizer.
     *
     * @param {Scene} scene - The scene to optimize.
     * @param {SceneOptimizer~Options} options - The configuration options.
     */
    constructor( scene, options = {} ) {

        this.scene = scene;
        this.debug = options.debug || false;

    }

    _bufferToHash( buffer ) {

        let hash = 0;
        if ( buffer.byteLength !== 0 ) {

            let uintArray;
            if ( buffer.buffer ) {

                uintArray = new Uint8Array(
                    buffer.buffer,
                    buffer.byteOffset,
                    buffer.byteLength
                );

            } else {

                uintArray = new Uint8Array( buffer );

            }

            for ( let i = 0; i < buffer.byteLength; i ++ ) {

                const byte = uintArray[ i ];
                hash = ( hash << 5 ) - hash + byte;
                hash |= 0;

            }

        }

        return hash;

    }

    _getMaterialPropertiesHash( material ) {

        const mapProps = [
            'map',
            'alphaMap',
            'aoMap',
            'bumpMap',
            'displacementMap',
            'emissiveMap',
            'envMap',
            'lightMap',
            'metalnessMap',
            'normalMap',
            'roughnessMap',
        ];

        const mapHash = mapProps
            .map( ( prop ) => {

                const map = material[ prop ];
                if ( ! map ) return 0;
                return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;

            } )
            .join( '|' );

        const physicalProps = [
            'transparent',
            'opacity',
            'alphaTest',
            'alphaToCoverage',
            'side',
            'vertexColors',
            'visible',
            'blending',
            'wireframe',
            'flatShading',
            'premultipliedAlpha',
            'dithering',
            'toneMapped',
            'depthTest',
            'depthWrite',
            'metalness',
            'roughness',
            'clearcoat',
            'clearcoatRoughness',
            'sheen',
            'sheenRoughness',
            'transmission',
            'thickness',
            'attenuationDistance',
            'ior',
            'iridescence',
            'iridescenceIOR',
            'iridescenceThicknessRange',
            'reflectivity',
        ]
            .map( ( prop ) => {

                if ( typeof material[ prop ] === 'undefined' ) return 0;
                if ( material[ prop ] === null ) return 0;
                return material[ prop ].toString();

            } )
            .join( '|' );

        const emissiveHash = material.emissive ? material.emissive.getHexString() : 0;
        const attenuationHash = material.attenuationColor
            ? material.attenuationColor.getHexString()
            : 0;
        const sheenColorHash = material.sheenColor
            ? material.sheenColor.getHexString()
            : 0;

        return [
            material.type,
            physicalProps,
            mapHash,
            emissiveHash,
            attenuationHash,
            sheenColorHash,
        ].join( '_' );

    }

    _getAttributesSignature( geometry ) {

        return Object.keys( geometry.attributes )
            .sort()
            .map( ( name ) => {

                const attribute = geometry.attributes[ name ];
                return `${name}_${attribute.itemSize}_${attribute.normalized}`;

            } )
            .join( '|' );

    }

    _getGeometryHash( geometry ) {

        const indexHash = geometry.index
            ? this._bufferToHash( geometry.index.array )
            : 'noIndex';
        const positionHash = this._bufferToHash( geometry.attributes.position.array );
        const attributesSignature = this._getAttributesSignature( geometry );
        return `${indexHash}_${positionHash}_${attributesSignature}`;

    }

    _getBatchKey( materialProps, attributesSignature ) {

        return `${materialProps}_${attributesSignature}`;

    }

    _analyzeModel() {

        const batchGroups = new Map();
        const singleGroups = new Map();
        const uniqueGeometries = new Set();

        this.scene.updateMatrixWorld( true );
        this.scene.traverse( ( node ) => {

            if ( ! node.isMesh ) return;

            const materialProps = this._getMaterialPropertiesHash( node.material );
            const attributesSignature = this._getAttributesSignature( node.geometry );
            const batchKey = this._getBatchKey( materialProps, attributesSignature );
            const geometryHash = this._getGeometryHash( node.geometry );
            uniqueGeometries.add( geometryHash );

            if ( ! batchGroups.has( batchKey ) ) {

                batchGroups.set( batchKey, {
                    meshes: [],
                    geometryStats: new Map(),
                    totalInstances: 0,
                    materialProps: node.material.clone(),
                } );

            }

            const group = batchGroups.get( batchKey );
            group.meshes.push( node );
            group.totalInstances ++;

            if ( ! group.geometryStats.has( geometryHash ) ) {

                group.geometryStats.set( geometryHash, {
                    count: 0,
                    vertices: node.geometry.attributes.position.count,
                    indices: node.geometry.index ? node.geometry.index.count : 0,
                    geometry: node.geometry,
                } );

            }

            group.geometryStats.get( geometryHash ).count ++;

        } );

        // Move single instance groups to singleGroups
        for ( const [ batchKey, group ] of batchGroups ) {

            if ( group.totalInstances === 1 ) {

                singleGroups.set( batchKey, group );
                batchGroups.delete( batchKey );

            }

        }

        return { batchGroups, singleGroups, uniqueGeometries: uniqueGeometries.size };

    }

    _createBatchedMeshes( batchGroups ) {

        const meshesToRemove = new Set();

        for ( const [ , group ] of batchGroups ) {

            const maxGeometries = group.totalInstances;
            const maxVertices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.vertices,
                0
            );
            const maxIndices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.indices,
                0
            );

            const batchedMaterial = new group.materialProps.constructor( group.materialProps );

            if ( batchedMaterial.color !== undefined ) {

                // Reset color to white, color will be set per instance
                batchedMaterial.color.set( 1, 1, 1 );

            }

            const batchedMesh = new THREE.BatchedMesh(
                maxGeometries,
                maxVertices,
                maxIndices,
                batchedMaterial
            );

            const referenceMesh = group.meshes[ 0 ];
            batchedMesh.name = `${referenceMesh.name}_batch`;

            const geometryIds = new Map();
            const inverseParentMatrix = new THREE.Matrix4();

            if ( referenceMesh.parent ) {

                referenceMesh.parent.updateWorldMatrix( true, false );
                inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert();

            }

            for ( const mesh of group.meshes ) {

                const geometryHash = this._getGeometryHash( mesh.geometry );

                if ( ! geometryIds.has( geometryHash ) ) {

                    geometryIds.set( geometryHash, batchedMesh.addGeometry( mesh.geometry ) );

                }

                const geometryId = geometryIds.get( geometryHash );
                const instanceId = batchedMesh.addInstance( geometryId );

                const localMatrix = new THREE.Matrix4();
                mesh.updateWorldMatrix( true, false );
                localMatrix.copy( mesh.matrixWorld );
                if ( referenceMesh.parent ) {

                    localMatrix.premultiply( inverseParentMatrix );

                }

                batchedMesh.setMatrixAt( instanceId, localMatrix );
                batchedMesh.setColorAt( instanceId, mesh.material.color );

                meshesToRemove.add( mesh );

            }

            if ( referenceMesh.parent ) {

                referenceMesh.parent.add( batchedMesh );

            }

        }

        return meshesToRemove;

    }

    /**
     * Removes empty nodes from all descendants of the given 3D object.
     *
     * @param {Object3D} object - The 3D object to process.
     */
    removeEmptyNodes( object ) {

        const children = [ ...object.children ];

        for ( const child of children ) {

            this.removeEmptyNodes( child );

            if ( ( child instanceof THREE.Group || child.constructor === THREE.Object3D )
                && child.children.length === 0 ) {

                object.remove( child );

            }

        }

    }

    /**
     * Removes the given array of meshes from the scene.
     *
     * @param {Set<Mesh>} meshesToRemove - The meshes to remove.
     */
    disposeMeshes( meshesToRemove ) {

        meshesToRemove.forEach( ( mesh ) => {

            if ( mesh.parent ) {

                mesh.parent.remove( mesh );

            }

            if ( mesh.geometry ) mesh.geometry.dispose();
            if ( mesh.material ) {

                if ( Array.isArray( mesh.material ) ) {

                    mesh.material.forEach( ( m ) => m.dispose() );

                } else {

                    mesh.material.dispose();

                }

            }

        } );

    }

    _logDebugInfo( stats ) {

        console.group( 'Scene Optimization Results' );
        console.log( `Original meshes: ${stats.originalMeshes}` );
        console.log( `Batched into: ${stats.batchedMeshes} BatchedMesh` );
        console.log( `Single meshes: ${stats.singleMeshes} Mesh` );
        console.log( `Total draw calls: ${stats.drawCalls}` );
        console.log( `Reduction Ratio: ${stats.reductionRatio}% fewer draw calls` );
        console.groupEnd();

    }

    /**
     * Performs the auto-baching by identifying groups of meshes in the scene
     * that can be represented as a single {@link BatchedMesh}. The method modifies
     * the scene by adding instances of `BatchedMesh` and removing the now redundant
     * individual meshes.
     *
     * @return {Scene} The optimized scene.
     */
    toBatchedMesh() {

        const { batchGroups, singleGroups, uniqueGeometries } = this._analyzeModel();
        const meshesToRemove = this._createBatchedMeshes( batchGroups );

        this.disposeMeshes( meshesToRemove );
        this.removeEmptyNodes( this.scene );

        if ( this.debug ) {

            const totalOriginalMeshes = meshesToRemove.size + singleGroups.size;
            const totalFinalMeshes = batchGroups.size + singleGroups.size;

            const stats = {
                originalMeshes: totalOriginalMeshes,
                batchedMeshes: batchGroups.size,
                singleMeshes: singleGroups.size,
                drawCalls: totalFinalMeshes,
                uniqueGeometries: uniqueGeometries,
                reductionRatio: ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed( 1 ),
            };

            this._logDebugInfo( stats );

        }

        return this.scene;

    }

    /**
     * Performs the auto-instancing by identifying groups of meshes in the scene
     * that can be represented as a single {@link InstancedMesh}. The method modifies
     * the scene by adding instances of `InstancedMesh` and removing the now redundant
     * individual meshes.
     *
     * This method is not yet implemented.
     *
     * @abstract
     * @return {Scene} The optimized scene.
     */
    toInstancingMesh() {

        throw new Error( 'InstancedMesh optimization not implemented yet' );

    }

}

Methods

_bufferToHash(buffer: any): number
Code
_bufferToHash( buffer ) {

        let hash = 0;
        if ( buffer.byteLength !== 0 ) {

            let uintArray;
            if ( buffer.buffer ) {

                uintArray = new Uint8Array(
                    buffer.buffer,
                    buffer.byteOffset,
                    buffer.byteLength
                );

            } else {

                uintArray = new Uint8Array( buffer );

            }

            for ( let i = 0; i < buffer.byteLength; i ++ ) {

                const byte = uintArray[ i ];
                hash = ( hash << 5 ) - hash + byte;
                hash |= 0;

            }

        }

        return hash;

    }
_getMaterialPropertiesHash(material: any): string
Code
_getMaterialPropertiesHash( material ) {

        const mapProps = [
            'map',
            'alphaMap',
            'aoMap',
            'bumpMap',
            'displacementMap',
            'emissiveMap',
            'envMap',
            'lightMap',
            'metalnessMap',
            'normalMap',
            'roughnessMap',
        ];

        const mapHash = mapProps
            .map( ( prop ) => {

                const map = material[ prop ];
                if ( ! map ) return 0;
                return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;

            } )
            .join( '|' );

        const physicalProps = [
            'transparent',
            'opacity',
            'alphaTest',
            'alphaToCoverage',
            'side',
            'vertexColors',
            'visible',
            'blending',
            'wireframe',
            'flatShading',
            'premultipliedAlpha',
            'dithering',
            'toneMapped',
            'depthTest',
            'depthWrite',
            'metalness',
            'roughness',
            'clearcoat',
            'clearcoatRoughness',
            'sheen',
            'sheenRoughness',
            'transmission',
            'thickness',
            'attenuationDistance',
            'ior',
            'iridescence',
            'iridescenceIOR',
            'iridescenceThicknessRange',
            'reflectivity',
        ]
            .map( ( prop ) => {

                if ( typeof material[ prop ] === 'undefined' ) return 0;
                if ( material[ prop ] === null ) return 0;
                return material[ prop ].toString();

            } )
            .join( '|' );

        const emissiveHash = material.emissive ? material.emissive.getHexString() : 0;
        const attenuationHash = material.attenuationColor
            ? material.attenuationColor.getHexString()
            : 0;
        const sheenColorHash = material.sheenColor
            ? material.sheenColor.getHexString()
            : 0;

        return [
            material.type,
            physicalProps,
            mapHash,
            emissiveHash,
            attenuationHash,
            sheenColorHash,
        ].join( '_' );

    }
_getAttributesSignature(geometry: any): string
Code
_getAttributesSignature( geometry ) {

        return Object.keys( geometry.attributes )
            .sort()
            .map( ( name ) => {

                const attribute = geometry.attributes[ name ];
                return `${name}_${attribute.itemSize}_${attribute.normalized}`;

            } )
            .join( '|' );

    }
_getGeometryHash(geometry: any): string
Code
_getGeometryHash( geometry ) {

        const indexHash = geometry.index
            ? this._bufferToHash( geometry.index.array )
            : 'noIndex';
        const positionHash = this._bufferToHash( geometry.attributes.position.array );
        const attributesSignature = this._getAttributesSignature( geometry );
        return `${indexHash}_${positionHash}_${attributesSignature}`;

    }
_getBatchKey(materialProps: any, attributesSignature: any): string
Code
_getBatchKey( materialProps, attributesSignature ) {

        return `${materialProps}_${attributesSignature}`;

    }
_analyzeModel(): { batchGroups: Map<any, any>; singleGroups: Map<any, any>; uniqueGeometries: number; }
Code
_analyzeModel() {

        const batchGroups = new Map();
        const singleGroups = new Map();
        const uniqueGeometries = new Set();

        this.scene.updateMatrixWorld( true );
        this.scene.traverse( ( node ) => {

            if ( ! node.isMesh ) return;

            const materialProps = this._getMaterialPropertiesHash( node.material );
            const attributesSignature = this._getAttributesSignature( node.geometry );
            const batchKey = this._getBatchKey( materialProps, attributesSignature );
            const geometryHash = this._getGeometryHash( node.geometry );
            uniqueGeometries.add( geometryHash );

            if ( ! batchGroups.has( batchKey ) ) {

                batchGroups.set( batchKey, {
                    meshes: [],
                    geometryStats: new Map(),
                    totalInstances: 0,
                    materialProps: node.material.clone(),
                } );

            }

            const group = batchGroups.get( batchKey );
            group.meshes.push( node );
            group.totalInstances ++;

            if ( ! group.geometryStats.has( geometryHash ) ) {

                group.geometryStats.set( geometryHash, {
                    count: 0,
                    vertices: node.geometry.attributes.position.count,
                    indices: node.geometry.index ? node.geometry.index.count : 0,
                    geometry: node.geometry,
                } );

            }

            group.geometryStats.get( geometryHash ).count ++;

        } );

        // Move single instance groups to singleGroups
        for ( const [ batchKey, group ] of batchGroups ) {

            if ( group.totalInstances === 1 ) {

                singleGroups.set( batchKey, group );
                batchGroups.delete( batchKey );

            }

        }

        return { batchGroups, singleGroups, uniqueGeometries: uniqueGeometries.size };

    }
_createBatchedMeshes(batchGroups: any): Set<any>
Code
_createBatchedMeshes( batchGroups ) {

        const meshesToRemove = new Set();

        for ( const [ , group ] of batchGroups ) {

            const maxGeometries = group.totalInstances;
            const maxVertices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.vertices,
                0
            );
            const maxIndices = Array.from( group.geometryStats.values() ).reduce(
                ( sum, stats ) => sum + stats.indices,
                0
            );

            const batchedMaterial = new group.materialProps.constructor( group.materialProps );

            if ( batchedMaterial.color !== undefined ) {

                // Reset color to white, color will be set per instance
                batchedMaterial.color.set( 1, 1, 1 );

            }

            const batchedMesh = new THREE.BatchedMesh(
                maxGeometries,
                maxVertices,
                maxIndices,
                batchedMaterial
            );

            const referenceMesh = group.meshes[ 0 ];
            batchedMesh.name = `${referenceMesh.name}_batch`;

            const geometryIds = new Map();
            const inverseParentMatrix = new THREE.Matrix4();

            if ( referenceMesh.parent ) {

                referenceMesh.parent.updateWorldMatrix( true, false );
                inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert();

            }

            for ( const mesh of group.meshes ) {

                const geometryHash = this._getGeometryHash( mesh.geometry );

                if ( ! geometryIds.has( geometryHash ) ) {

                    geometryIds.set( geometryHash, batchedMesh.addGeometry( mesh.geometry ) );

                }

                const geometryId = geometryIds.get( geometryHash );
                const instanceId = batchedMesh.addInstance( geometryId );

                const localMatrix = new THREE.Matrix4();
                mesh.updateWorldMatrix( true, false );
                localMatrix.copy( mesh.matrixWorld );
                if ( referenceMesh.parent ) {

                    localMatrix.premultiply( inverseParentMatrix );

                }

                batchedMesh.setMatrixAt( instanceId, localMatrix );
                batchedMesh.setColorAt( instanceId, mesh.material.color );

                meshesToRemove.add( mesh );

            }

            if ( referenceMesh.parent ) {

                referenceMesh.parent.add( batchedMesh );

            }

        }

        return meshesToRemove;

    }
removeEmptyNodes(object: Object3D): void
Code
removeEmptyNodes( object ) {

        const children = [ ...object.children ];

        for ( const child of children ) {

            this.removeEmptyNodes( child );

            if ( ( child instanceof THREE.Group || child.constructor === THREE.Object3D )
                && child.children.length === 0 ) {

                object.remove( child );

            }

        }

    }
disposeMeshes(meshesToRemove: Set<Mesh>): void
Code
disposeMeshes( meshesToRemove ) {

        meshesToRemove.forEach( ( mesh ) => {

            if ( mesh.parent ) {

                mesh.parent.remove( mesh );

            }

            if ( mesh.geometry ) mesh.geometry.dispose();
            if ( mesh.material ) {

                if ( Array.isArray( mesh.material ) ) {

                    mesh.material.forEach( ( m ) => m.dispose() );

                } else {

                    mesh.material.dispose();

                }

            }

        } );

    }
_logDebugInfo(stats: any): void
Code
_logDebugInfo( stats ) {

        console.group( 'Scene Optimization Results' );
        console.log( `Original meshes: ${stats.originalMeshes}` );
        console.log( `Batched into: ${stats.batchedMeshes} BatchedMesh` );
        console.log( `Single meshes: ${stats.singleMeshes} Mesh` );
        console.log( `Total draw calls: ${stats.drawCalls}` );
        console.log( `Reduction Ratio: ${stats.reductionRatio}% fewer draw calls` );
        console.groupEnd();

    }
toBatchedMesh(): Scene
Code
toBatchedMesh() {

        const { batchGroups, singleGroups, uniqueGeometries } = this._analyzeModel();
        const meshesToRemove = this._createBatchedMeshes( batchGroups );

        this.disposeMeshes( meshesToRemove );
        this.removeEmptyNodes( this.scene );

        if ( this.debug ) {

            const totalOriginalMeshes = meshesToRemove.size + singleGroups.size;
            const totalFinalMeshes = batchGroups.size + singleGroups.size;

            const stats = {
                originalMeshes: totalOriginalMeshes,
                batchedMeshes: batchGroups.size,
                singleMeshes: singleGroups.size,
                drawCalls: totalFinalMeshes,
                uniqueGeometries: uniqueGeometries,
                reductionRatio: ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed( 1 ),
            };

            this._logDebugInfo( stats );

        }

        return this.scene;

    }
toInstancingMesh(): Scene
Code
toInstancingMesh() {

        throw new Error( 'InstancedMesh optimization not implemented yet' );

    }