123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- import * as THREE from 'three';
- /**
- * This class can be used to optimized scenes by converting
- * individual meshes into {@link BatchedMesh}. This component
- * is an experimental attempt to implement auto-batching in three.js.
- *
- * @three_import import { SceneOptimizer } from 'three/addons/utils/SceneOptimizer.js';
- */
- 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' );
- }
- }
- /**
- * Constructor options of `SceneOptimizer`.
- *
- * @typedef {Object} SceneOptimizer~Options
- * @property {boolean} [debug=false] - Whether to enable debug mode or not.
- **/
- export { SceneOptimizer };
|