|
|
@@ -0,0 +1,401 @@
|
|
|
+import * as THREE from 'three';
|
|
|
+
|
|
|
+class SceneOptimizer {
|
|
|
+
|
|
|
+ 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 = [
|
|
|
+ '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 THREE.MeshPhysicalMaterial( group.materialProps );
|
|
|
+ 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 ) {
|
|
|
+
|
|
|
+ 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 ) {
|
|
|
+
|
|
|
+ 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();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Placeholder for future implementation
|
|
|
+ toInstancingMesh() {
|
|
|
+
|
|
|
+ throw new Error( 'InstancedMesh optimization not implemented yet' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export { SceneOptimizer };
|