Explorar el Código

SceneOptimizer: Introduce `optimizer.toBatchedMesh()` (#29782)

* BatchedMeshUtils: Introduce convertGLTFToBatchedMeshes()

* improve debug

* cleanup and fix wrong instance count

* refactor to optimize original scene instead

* fix

* keep structure of graph, name and update the original scene instead

* remove empty nodes post transform

* Refactor to SceneOptimizer
Renaud Rohlinger hace 1 año
padre
commit
9853674a64
Se han modificado 1 ficheros con 401 adiciones y 0 borrados
  1. 401 0
      examples/jsm/utils/SceneOptimizer.js

+ 401 - 0
examples/jsm/utils/SceneOptimizer.js

@@ -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 };

粤ICP备19079148号