SceneOptimizer.js 10 KB

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