SceneOptimizer.js 8.5 KB

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