SceneOptimizer.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import * as THREE from 'three';
  2. class SceneOptimizer {
  3. constructor( scene, options = {} ) {
  4. this.scene = scene;
  5. this.debug = options.debug || false;
  6. }
  7. bufferToHash( buffer ) {
  8. let hash = 0;
  9. if ( buffer.byteLength !== 0 ) {
  10. let uintArray;
  11. if ( buffer.buffer ) {
  12. uintArray = new Uint8Array(
  13. buffer.buffer,
  14. buffer.byteOffset,
  15. buffer.byteLength
  16. );
  17. } else {
  18. uintArray = new Uint8Array( buffer );
  19. }
  20. for ( let i = 0; i < buffer.byteLength; i ++ ) {
  21. const byte = uintArray[ i ];
  22. hash = ( hash << 5 ) - hash + byte;
  23. hash |= 0;
  24. }
  25. }
  26. return hash;
  27. }
  28. getMaterialPropertiesHash( material ) {
  29. const mapProps = [
  30. 'alphaMap',
  31. 'aoMap',
  32. 'bumpMap',
  33. 'displacementMap',
  34. 'emissiveMap',
  35. 'envMap',
  36. 'lightMap',
  37. 'metalnessMap',
  38. 'normalMap',
  39. 'roughnessMap',
  40. ];
  41. const mapHash = mapProps
  42. .map( ( prop ) => {
  43. const map = material[ prop ];
  44. if ( ! map ) return 0;
  45. return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;
  46. } )
  47. .join( '|' );
  48. const physicalProps = [
  49. 'transparent',
  50. 'opacity',
  51. 'alphaTest',
  52. 'alphaToCoverage',
  53. 'side',
  54. 'vertexColors',
  55. 'visible',
  56. 'blending',
  57. 'wireframe',
  58. 'flatShading',
  59. 'premultipliedAlpha',
  60. 'dithering',
  61. 'toneMapped',
  62. 'depthTest',
  63. 'depthWrite',
  64. 'metalness',
  65. 'roughness',
  66. 'clearcoat',
  67. 'clearcoatRoughness',
  68. 'sheen',
  69. 'sheenRoughness',
  70. 'transmission',
  71. 'thickness',
  72. 'attenuationDistance',
  73. 'ior',
  74. 'iridescence',
  75. 'iridescenceIOR',
  76. 'iridescenceThicknessRange',
  77. 'reflectivity',
  78. ]
  79. .map( ( prop ) => {
  80. if ( typeof material[ prop ] === 'undefined' ) return 0;
  81. if ( material[ prop ] === null ) return 0;
  82. return material[ prop ].toString();
  83. } )
  84. .join( '|' );
  85. const emissiveHash = material.emissive ? material.emissive.getHexString() : 0;
  86. const attenuationHash = material.attenuationColor
  87. ? material.attenuationColor.getHexString()
  88. : 0;
  89. const sheenColorHash = material.sheenColor
  90. ? material.sheenColor.getHexString()
  91. : 0;
  92. return [
  93. material.type,
  94. physicalProps,
  95. mapHash,
  96. emissiveHash,
  97. attenuationHash,
  98. sheenColorHash,
  99. ].join( '_' );
  100. }
  101. getAttributesSignature( geometry ) {
  102. return Object.keys( geometry.attributes )
  103. .sort()
  104. .map( ( name ) => {
  105. const attribute = geometry.attributes[ name ];
  106. return `${name}_${attribute.itemSize}_${attribute.normalized}`;
  107. } )
  108. .join( '|' );
  109. }
  110. getGeometryHash( geometry ) {
  111. const indexHash = geometry.index
  112. ? this.bufferToHash( geometry.index.array )
  113. : 'noIndex';
  114. const positionHash = this.bufferToHash( geometry.attributes.position.array );
  115. const attributesSignature = this.getAttributesSignature( geometry );
  116. return `${indexHash}_${positionHash}_${attributesSignature}`;
  117. }
  118. getBatchKey( materialProps, attributesSignature ) {
  119. return `${materialProps}_${attributesSignature}`;
  120. }
  121. analyzeModel() {
  122. const batchGroups = new Map();
  123. const singleGroups = new Map();
  124. const uniqueGeometries = new Set();
  125. this.scene.updateMatrixWorld( true );
  126. this.scene.traverse( ( node ) => {
  127. if ( ! node.isMesh ) return;
  128. const materialProps = this.getMaterialPropertiesHash( node.material );
  129. const attributesSignature = this.getAttributesSignature( node.geometry );
  130. const batchKey = this.getBatchKey( materialProps, attributesSignature );
  131. const geometryHash = this.getGeometryHash( node.geometry );
  132. uniqueGeometries.add( geometryHash );
  133. if ( ! batchGroups.has( batchKey ) ) {
  134. batchGroups.set( batchKey, {
  135. meshes: [],
  136. geometryStats: new Map(),
  137. totalInstances: 0,
  138. materialProps: node.material.clone(),
  139. } );
  140. }
  141. const group = batchGroups.get( batchKey );
  142. group.meshes.push( node );
  143. group.totalInstances ++;
  144. if ( ! group.geometryStats.has( geometryHash ) ) {
  145. group.geometryStats.set( geometryHash, {
  146. count: 0,
  147. vertices: node.geometry.attributes.position.count,
  148. indices: node.geometry.index ? node.geometry.index.count : 0,
  149. geometry: node.geometry,
  150. } );
  151. }
  152. group.geometryStats.get( geometryHash ).count ++;
  153. } );
  154. // Move single instance groups to singleGroups
  155. for ( const [ batchKey, group ] of batchGroups ) {
  156. if ( group.totalInstances === 1 ) {
  157. singleGroups.set( batchKey, group );
  158. batchGroups.delete( batchKey );
  159. }
  160. }
  161. return { batchGroups, singleGroups, uniqueGeometries: uniqueGeometries.size };
  162. }
  163. createBatchedMeshes( batchGroups ) {
  164. const meshesToRemove = new Set();
  165. for ( const [ , group ] of batchGroups ) {
  166. const maxGeometries = group.totalInstances;
  167. const maxVertices = Array.from( group.geometryStats.values() ).reduce(
  168. ( sum, stats ) => sum + stats.vertices,
  169. 0
  170. );
  171. const maxIndices = Array.from( group.geometryStats.values() ).reduce(
  172. ( sum, stats ) => sum + stats.indices,
  173. 0
  174. );
  175. const batchedMaterial = new THREE.MeshPhysicalMaterial( group.materialProps );
  176. const batchedMesh = new THREE.BatchedMesh(
  177. maxGeometries,
  178. maxVertices,
  179. maxIndices,
  180. batchedMaterial
  181. );
  182. const referenceMesh = group.meshes[ 0 ];
  183. batchedMesh.name = `${referenceMesh.name}_batch`;
  184. const geometryIds = new Map();
  185. const inverseParentMatrix = new THREE.Matrix4();
  186. if ( referenceMesh.parent ) {
  187. referenceMesh.parent.updateWorldMatrix( true, false );
  188. inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert();
  189. }
  190. for ( const mesh of group.meshes ) {
  191. const geometryHash = this.getGeometryHash( mesh.geometry );
  192. if ( ! geometryIds.has( geometryHash ) ) {
  193. geometryIds.set( geometryHash, batchedMesh.addGeometry( mesh.geometry ) );
  194. }
  195. const geometryId = geometryIds.get( geometryHash );
  196. const instanceId = batchedMesh.addInstance( geometryId );
  197. const localMatrix = new THREE.Matrix4();
  198. mesh.updateWorldMatrix( true, false );
  199. localMatrix.copy( mesh.matrixWorld );
  200. if ( referenceMesh.parent ) {
  201. localMatrix.premultiply( inverseParentMatrix );
  202. }
  203. batchedMesh.setMatrixAt( instanceId, localMatrix );
  204. batchedMesh.setColorAt( instanceId, mesh.material.color );
  205. meshesToRemove.add( mesh );
  206. }
  207. if ( referenceMesh.parent ) {
  208. referenceMesh.parent.add( batchedMesh );
  209. }
  210. }
  211. return meshesToRemove;
  212. }
  213. removeEmptyNodes( object ) {
  214. const children = [ ...object.children ];
  215. for ( const child of children ) {
  216. this.removeEmptyNodes( child );
  217. if ( ( child instanceof THREE.Group || child.constructor === THREE.Object3D )
  218. && child.children.length === 0 ) {
  219. object.remove( child );
  220. }
  221. }
  222. }
  223. disposeMeshes( meshesToRemove ) {
  224. meshesToRemove.forEach( ( mesh ) => {
  225. if ( mesh.parent ) {
  226. mesh.parent.remove( mesh );
  227. }
  228. if ( mesh.geometry ) mesh.geometry.dispose();
  229. if ( mesh.material ) {
  230. if ( Array.isArray( mesh.material ) ) {
  231. mesh.material.forEach( ( m ) => m.dispose() );
  232. } else {
  233. mesh.material.dispose();
  234. }
  235. }
  236. } );
  237. }
  238. logDebugInfo( stats ) {
  239. console.group( 'Scene Optimization Results' );
  240. console.log( `Original meshes: ${stats.originalMeshes}` );
  241. console.log( `Batched into: ${stats.batchedMeshes} BatchedMesh` );
  242. console.log( `Single meshes: ${stats.singleMeshes} Mesh` );
  243. console.log( `Total draw calls: ${stats.drawCalls}` );
  244. console.log( `Reduction Ratio: ${stats.reductionRatio}% fewer draw calls` );
  245. console.groupEnd();
  246. }
  247. toBatchedMesh() {
  248. const { batchGroups, singleGroups, uniqueGeometries } = this.analyzeModel();
  249. const meshesToRemove = this.createBatchedMeshes( batchGroups );
  250. this.disposeMeshes( meshesToRemove );
  251. this.removeEmptyNodes( this.scene );
  252. if ( this.debug ) {
  253. const totalOriginalMeshes = meshesToRemove.size + singleGroups.size;
  254. const totalFinalMeshes = batchGroups.size + singleGroups.size;
  255. const stats = {
  256. originalMeshes: totalOriginalMeshes,
  257. batchedMeshes: batchGroups.size,
  258. singleMeshes: singleGroups.size,
  259. drawCalls: totalFinalMeshes,
  260. uniqueGeometries: uniqueGeometries,
  261. reductionRatio: ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed( 1 ),
  262. };
  263. this.logDebugInfo( stats );
  264. }
  265. return this.scene;
  266. }
  267. // Placeholder for future implementation
  268. toInstancingMesh() {
  269. throw new Error( 'InstancedMesh optimization not implemented yet' );
  270. }
  271. }
  272. export { SceneOptimizer };
粤ICP备19079148号