SceneOptimizer.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import * as THREE from 'three';
  2. /**
  3. * This class can be used to optimized scenes by converting
  4. * individual meshes into {@link BatchedMesh}. This component
  5. * is an experimental attempt to implement auto-batching in three.js.
  6. */
  7. class SceneOptimizer {
  8. /**
  9. * Constructs a new scene optimizer.
  10. *
  11. * @param {Scene} scene - The scene to optimize.
  12. * @param {SceneOptimizer~Options} options - The configuration options.
  13. */
  14. constructor( scene, options = {} ) {
  15. this.scene = scene;
  16. this.debug = options.debug || false;
  17. }
  18. _bufferToHash( buffer ) {
  19. let hash = 0;
  20. if ( buffer.byteLength !== 0 ) {
  21. let uintArray;
  22. if ( buffer.buffer ) {
  23. uintArray = new Uint8Array(
  24. buffer.buffer,
  25. buffer.byteOffset,
  26. buffer.byteLength
  27. );
  28. } else {
  29. uintArray = new Uint8Array( buffer );
  30. }
  31. for ( let i = 0; i < buffer.byteLength; i ++ ) {
  32. const byte = uintArray[ i ];
  33. hash = ( hash << 5 ) - hash + byte;
  34. hash |= 0;
  35. }
  36. }
  37. return hash;
  38. }
  39. _getMaterialPropertiesHash( material ) {
  40. const mapProps = [
  41. 'map',
  42. 'alphaMap',
  43. 'aoMap',
  44. 'bumpMap',
  45. 'displacementMap',
  46. 'emissiveMap',
  47. 'envMap',
  48. 'lightMap',
  49. 'metalnessMap',
  50. 'normalMap',
  51. 'roughnessMap',
  52. ];
  53. const mapHash = mapProps
  54. .map( ( prop ) => {
  55. const map = material[ prop ];
  56. if ( ! map ) return 0;
  57. return `${map.uuid}_${map.offset.x}_${map.offset.y}_${map.repeat.x}_${map.repeat.y}_${map.rotation}`;
  58. } )
  59. .join( '|' );
  60. const physicalProps = [
  61. 'transparent',
  62. 'opacity',
  63. 'alphaTest',
  64. 'alphaToCoverage',
  65. 'side',
  66. 'vertexColors',
  67. 'visible',
  68. 'blending',
  69. 'wireframe',
  70. 'flatShading',
  71. 'premultipliedAlpha',
  72. 'dithering',
  73. 'toneMapped',
  74. 'depthTest',
  75. 'depthWrite',
  76. 'metalness',
  77. 'roughness',
  78. 'clearcoat',
  79. 'clearcoatRoughness',
  80. 'sheen',
  81. 'sheenRoughness',
  82. 'transmission',
  83. 'thickness',
  84. 'attenuationDistance',
  85. 'ior',
  86. 'iridescence',
  87. 'iridescenceIOR',
  88. 'iridescenceThicknessRange',
  89. 'reflectivity',
  90. ]
  91. .map( ( prop ) => {
  92. if ( typeof material[ prop ] === 'undefined' ) return 0;
  93. if ( material[ prop ] === null ) return 0;
  94. return material[ prop ].toString();
  95. } )
  96. .join( '|' );
  97. const emissiveHash = material.emissive ? material.emissive.getHexString() : 0;
  98. const attenuationHash = material.attenuationColor
  99. ? material.attenuationColor.getHexString()
  100. : 0;
  101. const sheenColorHash = material.sheenColor
  102. ? material.sheenColor.getHexString()
  103. : 0;
  104. return [
  105. material.type,
  106. physicalProps,
  107. mapHash,
  108. emissiveHash,
  109. attenuationHash,
  110. sheenColorHash,
  111. ].join( '_' );
  112. }
  113. _getAttributesSignature( geometry ) {
  114. return Object.keys( geometry.attributes )
  115. .sort()
  116. .map( ( name ) => {
  117. const attribute = geometry.attributes[ name ];
  118. return `${name}_${attribute.itemSize}_${attribute.normalized}`;
  119. } )
  120. .join( '|' );
  121. }
  122. _getGeometryHash( geometry ) {
  123. const indexHash = geometry.index
  124. ? this._bufferToHash( geometry.index.array )
  125. : 'noIndex';
  126. const positionHash = this._bufferToHash( geometry.attributes.position.array );
  127. const attributesSignature = this._getAttributesSignature( geometry );
  128. return `${indexHash}_${positionHash}_${attributesSignature}`;
  129. }
  130. _getBatchKey( materialProps, attributesSignature ) {
  131. return `${materialProps}_${attributesSignature}`;
  132. }
  133. _analyzeModel() {
  134. const batchGroups = new Map();
  135. const singleGroups = new Map();
  136. const uniqueGeometries = new Set();
  137. this.scene.updateMatrixWorld( true );
  138. this.scene.traverse( ( node ) => {
  139. if ( ! node.isMesh ) return;
  140. const materialProps = this._getMaterialPropertiesHash( node.material );
  141. const attributesSignature = this._getAttributesSignature( node.geometry );
  142. const batchKey = this._getBatchKey( materialProps, attributesSignature );
  143. const geometryHash = this._getGeometryHash( node.geometry );
  144. uniqueGeometries.add( geometryHash );
  145. if ( ! batchGroups.has( batchKey ) ) {
  146. batchGroups.set( batchKey, {
  147. meshes: [],
  148. geometryStats: new Map(),
  149. totalInstances: 0,
  150. materialProps: node.material.clone(),
  151. } );
  152. }
  153. const group = batchGroups.get( batchKey );
  154. group.meshes.push( node );
  155. group.totalInstances ++;
  156. if ( ! group.geometryStats.has( geometryHash ) ) {
  157. group.geometryStats.set( geometryHash, {
  158. count: 0,
  159. vertices: node.geometry.attributes.position.count,
  160. indices: node.geometry.index ? node.geometry.index.count : 0,
  161. geometry: node.geometry,
  162. } );
  163. }
  164. group.geometryStats.get( geometryHash ).count ++;
  165. } );
  166. // Move single instance groups to singleGroups
  167. for ( const [ batchKey, group ] of batchGroups ) {
  168. if ( group.totalInstances === 1 ) {
  169. singleGroups.set( batchKey, group );
  170. batchGroups.delete( batchKey );
  171. }
  172. }
  173. return { batchGroups, singleGroups, uniqueGeometries: uniqueGeometries.size };
  174. }
  175. _createBatchedMeshes( batchGroups ) {
  176. const meshesToRemove = new Set();
  177. for ( const [ , group ] of batchGroups ) {
  178. const maxGeometries = group.totalInstances;
  179. const maxVertices = Array.from( group.geometryStats.values() ).reduce(
  180. ( sum, stats ) => sum + stats.vertices,
  181. 0
  182. );
  183. const maxIndices = Array.from( group.geometryStats.values() ).reduce(
  184. ( sum, stats ) => sum + stats.indices,
  185. 0
  186. );
  187. const batchedMaterial = new group.materialProps.constructor( group.materialProps );
  188. if ( batchedMaterial.color !== undefined ) {
  189. // Reset color to white, color will be set per instance
  190. batchedMaterial.color.set( 1, 1, 1 );
  191. }
  192. const batchedMesh = new THREE.BatchedMesh(
  193. maxGeometries,
  194. maxVertices,
  195. maxIndices,
  196. batchedMaterial
  197. );
  198. const referenceMesh = group.meshes[ 0 ];
  199. batchedMesh.name = `${referenceMesh.name}_batch`;
  200. const geometryIds = new Map();
  201. const inverseParentMatrix = new THREE.Matrix4();
  202. if ( referenceMesh.parent ) {
  203. referenceMesh.parent.updateWorldMatrix( true, false );
  204. inverseParentMatrix.copy( referenceMesh.parent.matrixWorld ).invert();
  205. }
  206. for ( const mesh of group.meshes ) {
  207. const geometryHash = this._getGeometryHash( mesh.geometry );
  208. if ( ! geometryIds.has( geometryHash ) ) {
  209. geometryIds.set( geometryHash, batchedMesh.addGeometry( mesh.geometry ) );
  210. }
  211. const geometryId = geometryIds.get( geometryHash );
  212. const instanceId = batchedMesh.addInstance( geometryId );
  213. const localMatrix = new THREE.Matrix4();
  214. mesh.updateWorldMatrix( true, false );
  215. localMatrix.copy( mesh.matrixWorld );
  216. if ( referenceMesh.parent ) {
  217. localMatrix.premultiply( inverseParentMatrix );
  218. }
  219. batchedMesh.setMatrixAt( instanceId, localMatrix );
  220. batchedMesh.setColorAt( instanceId, mesh.material.color );
  221. meshesToRemove.add( mesh );
  222. }
  223. if ( referenceMesh.parent ) {
  224. referenceMesh.parent.add( batchedMesh );
  225. }
  226. }
  227. return meshesToRemove;
  228. }
  229. /**
  230. * Removes empty nodes from all descendants of the given 3D object.
  231. *
  232. * @param {Object3D} object - The 3D object to process.
  233. */
  234. removeEmptyNodes( object ) {
  235. const children = [ ...object.children ];
  236. for ( const child of children ) {
  237. this.removeEmptyNodes( child );
  238. if ( ( child instanceof THREE.Group || child.constructor === THREE.Object3D )
  239. && child.children.length === 0 ) {
  240. object.remove( child );
  241. }
  242. }
  243. }
  244. /**
  245. * Removes the given array of meshes from the scene.
  246. *
  247. * @param {Set<Mesh>} meshesToRemove - The meshes to remove.
  248. */
  249. disposeMeshes( meshesToRemove ) {
  250. meshesToRemove.forEach( ( mesh ) => {
  251. if ( mesh.parent ) {
  252. mesh.parent.remove( mesh );
  253. }
  254. if ( mesh.geometry ) mesh.geometry.dispose();
  255. if ( mesh.material ) {
  256. if ( Array.isArray( mesh.material ) ) {
  257. mesh.material.forEach( ( m ) => m.dispose() );
  258. } else {
  259. mesh.material.dispose();
  260. }
  261. }
  262. } );
  263. }
  264. _logDebugInfo( stats ) {
  265. console.group( 'Scene Optimization Results' );
  266. console.log( `Original meshes: ${stats.originalMeshes}` );
  267. console.log( `Batched into: ${stats.batchedMeshes} BatchedMesh` );
  268. console.log( `Single meshes: ${stats.singleMeshes} Mesh` );
  269. console.log( `Total draw calls: ${stats.drawCalls}` );
  270. console.log( `Reduction Ratio: ${stats.reductionRatio}% fewer draw calls` );
  271. console.groupEnd();
  272. }
  273. /**
  274. * Performs the auto-baching by identifying groups of meshes in the scene
  275. * that can be represented as a single {@link BatchedMesh}. The method modifies
  276. * the scene by adding instances of `BatchedMesh` and removing the now redundant
  277. * individual meshes.
  278. *
  279. * @return {Scene} The optimized scene.
  280. */
  281. toBatchedMesh() {
  282. const { batchGroups, singleGroups, uniqueGeometries } = this._analyzeModel();
  283. const meshesToRemove = this._createBatchedMeshes( batchGroups );
  284. this.disposeMeshes( meshesToRemove );
  285. this.removeEmptyNodes( this.scene );
  286. if ( this.debug ) {
  287. const totalOriginalMeshes = meshesToRemove.size + singleGroups.size;
  288. const totalFinalMeshes = batchGroups.size + singleGroups.size;
  289. const stats = {
  290. originalMeshes: totalOriginalMeshes,
  291. batchedMeshes: batchGroups.size,
  292. singleMeshes: singleGroups.size,
  293. drawCalls: totalFinalMeshes,
  294. uniqueGeometries: uniqueGeometries,
  295. reductionRatio: ( ( 1 - totalFinalMeshes / totalOriginalMeshes ) * 100 ).toFixed( 1 ),
  296. };
  297. this._logDebugInfo( stats );
  298. }
  299. return this.scene;
  300. }
  301. /**
  302. * Performs the auto-instancing by identifying groups of meshes in the scene
  303. * that can be represented as a single {@link InstancedMesh}. The method modifies
  304. * the scene by adding instances of `InstancedMesh` and removing the now redundant
  305. * individual meshes.
  306. *
  307. * This method is not yet implemented.
  308. *
  309. * @abstract
  310. * @return {Scene} The optimized scene.
  311. */
  312. toInstancingMesh() {
  313. throw new Error( 'InstancedMesh optimization not implemented yet' );
  314. }
  315. }
  316. /**
  317. * Constructor options of `SceneOptimizer`.
  318. *
  319. * @typedef {Object} SceneOptimizer~Options
  320. * @property {boolean} [debug=false] - Whether to enable debug mode or not.
  321. **/
  322. export { SceneOptimizer };
粤ICP备19079148号