Buckets:
| 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 }; | |
Xet Storage Details
- Size:
- 10.3 kB
- Xet hash:
- 8f3e7444e2546c97181ef1f69a48315ba3b1c2291fd308061ed5e46a2576e9c2
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.