BatchedMesh.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import {
  2. BufferAttribute,
  3. BufferGeometry,
  4. DataTexture,
  5. FloatType,
  6. MathUtils,
  7. Matrix4,
  8. Mesh,
  9. RGBAFormat
  10. } from 'three';
  11. const _identityMatrix = new Matrix4();
  12. const _zeroScaleMatrix = new Matrix4().set(
  13. 0, 0, 0, 0,
  14. 0, 0, 0, 0,
  15. 0, 0, 0, 0,
  16. 0, 0, 0, 1,
  17. );
  18. // Custom shaders
  19. const batchingParsVertex = `
  20. #ifdef BATCHING
  21. attribute float id;
  22. uniform highp sampler2D batchingTexture;
  23. uniform int batchingTextureSize;
  24. mat4 getBatchingMatrix( const in float i ) {
  25. float j = i * 4.0;
  26. float x = mod( j, float( batchingTextureSize ) );
  27. float y = floor( j / float( batchingTextureSize ) );
  28. float dx = 1.0 / float( batchingTextureSize );
  29. float dy = 1.0 / float( batchingTextureSize );
  30. y = dy * ( y + 0.5 );
  31. vec4 v1 = texture2D( batchingTexture, vec2( dx * ( x + 0.5 ), y ) );
  32. vec4 v2 = texture2D( batchingTexture, vec2( dx * ( x + 1.5 ), y ) );
  33. vec4 v3 = texture2D( batchingTexture, vec2( dx * ( x + 2.5 ), y ) );
  34. vec4 v4 = texture2D( batchingTexture, vec2( dx * ( x + 3.5 ), y ) );
  35. return mat4( v1, v2, v3, v4 );
  36. }
  37. #endif
  38. `;
  39. const batchingbaseVertex = `
  40. #ifdef BATCHING
  41. mat4 batchingMatrix = getBatchingMatrix( id );
  42. #endif
  43. `;
  44. const batchingnormalVertex = `
  45. #ifdef BATCHING
  46. objectNormal = vec4( batchingMatrix * vec4( objectNormal, 0.0 ) ).xyz;
  47. #ifdef USE_TANGENT
  48. objectTangent = vec4( batchingMatrix * vec4( objectTangent, 0.0 ) ).xyz;
  49. #endif
  50. #endif
  51. `;
  52. const batchingVertex = `
  53. #ifdef BATCHING
  54. transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz;
  55. #endif
  56. `;
  57. // @TODO: SkinnedMesh support?
  58. // @TODO: Future work if needed. Move into the core. Can be optimized more with WEBGL_multi_draw.
  59. class BatchedMesh extends Mesh {
  60. constructor( maxGeometryCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material ) {
  61. super( new BufferGeometry(), material );
  62. this._vertexStarts = [];
  63. this._vertexCounts = [];
  64. this._indexStarts = [];
  65. this._indexCounts = [];
  66. this._visibles = [];
  67. this._alives = [];
  68. this._maxGeometryCount = maxGeometryCount;
  69. this._maxVertexCount = maxVertexCount;
  70. this._maxIndexCount = maxIndexCount;
  71. this._geometryInitialized = false;
  72. this._geometryCount = 0;
  73. this._vertexCount = 0;
  74. this._indexCount = 0;
  75. // Local matrix per geometry by using data texture
  76. // @TODO: Support uniform parameter per geometry
  77. this._matrices = [];
  78. this._matricesArray = null;
  79. this._matricesTexture = null;
  80. this._matricesTextureSize = null;
  81. // @TODO: Calculate the entire binding box and make frustumCulled true
  82. this.frustumCulled = false;
  83. this._customUniforms = {
  84. batchingTexture: { value: null },
  85. batchingTextureSize: { value: 0 }
  86. };
  87. this._initMatricesTexture();
  88. this._initShader();
  89. }
  90. _initMatricesTexture() {
  91. // layout (1 matrix = 4 pixels)
  92. // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
  93. // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
  94. // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
  95. // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
  96. // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
  97. let size = Math.sqrt( this._maxGeometryCount * 4 ); // 4 pixels needed for 1 matrix
  98. size = MathUtils.ceilPowerOfTwo( size );
  99. size = Math.max( size, 4 );
  100. const matricesArray = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel
  101. const matricesTexture = new DataTexture( matricesArray, size, size, RGBAFormat, FloatType );
  102. this._matricesArray = matricesArray;
  103. this._matricesTexture = matricesTexture;
  104. this._matricesTextureSize = size;
  105. this._customUniforms.batchingTexture.value = this._matricesTexture;
  106. this._customUniforms.batchingTextureSize.value = this._matricesTextureSize;
  107. }
  108. _initShader() {
  109. const material = this.material;
  110. const currentOnBeforeCompile = material.onBeforeCompile;
  111. const customUniforms = this._customUniforms;
  112. material.onBeforeCompile = function onBeforeCompile( parameters, renderer ) {
  113. // Is this replacement stable across any materials?
  114. parameters.vertexShader = parameters.vertexShader
  115. .replace(
  116. '#include <skinning_pars_vertex>',
  117. '#include <skinning_pars_vertex>\n'
  118. + batchingParsVertex
  119. )
  120. .replace(
  121. '#include <skinnormal_vertex>',
  122. '#include <skinnormal_vertex>\n'
  123. + batchingbaseVertex
  124. + batchingnormalVertex
  125. )
  126. .replace(
  127. '#include <skinning_vertex>',
  128. '#include <skinning_vertex>\n'
  129. + batchingVertex
  130. );
  131. for ( const uniformName in customUniforms ) {
  132. parameters.uniforms[ uniformName ] = customUniforms[ uniformName ];
  133. }
  134. currentOnBeforeCompile.call( this, parameters, renderer );
  135. };
  136. material.defines = material.defines || {};
  137. material.defines.BATCHING = false;
  138. }
  139. _initializeGeometry( reference ) {
  140. // @TODO: geometry.groups support?
  141. // @TODO: geometry.drawRange support?
  142. // @TODO: geometry.morphAttributes support?
  143. const geometry = this.geometry;
  144. const maxVertexCount = this._maxVertexCount;
  145. const maxGeometryCount = this._maxGeometryCount;
  146. const maxIndexCount = this._maxIndexCount;
  147. if ( this._geometryInitialized === false ) {
  148. for ( const attributeName in reference.attributes ) {
  149. const srcAttribute = reference.getAttribute( attributeName );
  150. const { array, itemSize, normalized } = srcAttribute;
  151. const dstArray = new array.constructor( maxVertexCount * itemSize );
  152. const dstAttribute = new srcAttribute.constructor( dstArray, itemSize, normalized );
  153. dstAttribute.setUsage( srcAttribute.usage );
  154. geometry.setAttribute( attributeName, dstAttribute );
  155. }
  156. if ( reference.getIndex() !== null ) {
  157. const indexArray = maxVertexCount > 65536
  158. ? new Uint32Array( maxIndexCount )
  159. : new Uint16Array( maxIndexCount );
  160. geometry.setIndex( new BufferAttribute( indexArray, 1 ) );
  161. }
  162. const idArray = maxGeometryCount > 65536
  163. ? new Uint32Array( maxVertexCount )
  164. : new Uint16Array( maxVertexCount );
  165. // @TODO: What if attribute name 'id' is already used?
  166. geometry.setAttribute( 'id', new BufferAttribute( idArray, 1 ) );
  167. this._geometryInitialized = true;
  168. } else {
  169. // @TODO: Check if geometry has the same attributes set
  170. }
  171. }
  172. getGeometryCount() {
  173. return this._geometryCount;
  174. }
  175. getVertexCount() {
  176. return this._vertexCount;
  177. }
  178. getIndexCount() {
  179. return this._indexCount;
  180. }
  181. applyGeometry( geometry ) {
  182. this._initializeGeometry( geometry );
  183. // @TODO: Error handling if exceeding maxVertexCount or maxIndexCount
  184. if ( this._geometryCount >= this._maxGeometryCount ) {
  185. // @TODO: Error handling
  186. }
  187. const batchGeometry = this.geometry;
  188. const visibles = this._visibles;
  189. const alives = this._alives;
  190. const matricesTexture = this._matricesTexture;
  191. const matrices = this._matrices;
  192. const matricesArray = this._matricesArray;
  193. const vertexCount = this._vertexCount;
  194. const indexCount = this._indexCount;
  195. const indexCounts = this._indexCounts;
  196. const indexStarts = this._indexStarts;
  197. const vertexCounts = this._vertexCounts;
  198. const vertexStarts = this._vertexStarts;
  199. const hasIndex = batchGeometry.getIndex() !== null;
  200. const dstIndex = batchGeometry.getIndex();
  201. const srcIndex = geometry.getIndex();
  202. // Assuming geometry has position attribute
  203. const srcPositionAttribute = geometry.getAttribute( 'position' );
  204. // push new geometry data range
  205. vertexStarts.push( vertexCount );
  206. vertexCounts.push( srcPositionAttribute.count );
  207. // copy attribute data over
  208. // @TODO: Handle case where geometry does not have common attributes
  209. for ( const attributeName in geometry.attributes ) {
  210. const srcAttribute = geometry.getAttribute( attributeName );
  211. const dstAttribute = batchGeometry.getAttribute( attributeName );
  212. dstAttribute.array.set( srcAttribute.array, vertexCount * dstAttribute.itemSize );
  213. dstAttribute.needsUpdate = true;
  214. }
  215. if ( hasIndex ) {
  216. // push new index range
  217. indexStarts.push( indexCount );
  218. indexCounts.push( srcIndex.count );
  219. // copy index data over
  220. for ( let i = 0; i < srcIndex.count; i ++ ) {
  221. dstIndex.setX( indexCount + i, vertexCount + srcIndex.getX( i ) );
  222. }
  223. this._indexCount += srcIndex.count;
  224. dstIndex.needsUpdate = true;
  225. }
  226. // fill in the geometry ids
  227. const geometryId = this._geometryCount;
  228. this._geometryCount ++;
  229. const idAttribute = batchGeometry.getAttribute( 'id' );
  230. for ( let i = 0; i < srcPositionAttribute.count; i ++ ) {
  231. idAttribute.setX( this._vertexCount + i, geometryId );
  232. }
  233. idAttribute.needsUpdate = true;
  234. // extend new range
  235. this._vertexCount += srcPositionAttribute.count;
  236. // push new visibility states
  237. visibles.push( true );
  238. alives.push( true );
  239. // initialize matrix information
  240. matrices.push( new Matrix4() );
  241. _identityMatrix.toArray( matricesArray, geometryId * 16 );
  242. matricesTexture.needsUpdate = true;
  243. return geometryId;
  244. }
  245. deleteGeometry( geometryId ) {
  246. // Note: User needs to call optimize() afterward to pack the data.
  247. const alives = this._alives;
  248. const matricesArray = this._matricesArray;
  249. const matricesTexture = this._matricesTexture;
  250. if ( geometryId >= alives.length || alives[ geometryId ] === false ) {
  251. return this;
  252. }
  253. alives[ geometryId ] = false;
  254. _zeroScaleMatrix.toArray( matricesArray, geometryId * 16 );
  255. matricesTexture.needsUpdate = true;
  256. return this;
  257. }
  258. optimize() {
  259. // @TODO: Implement
  260. return this;
  261. }
  262. setMatrixAt( geometryId, matrix ) {
  263. // @TODO: Map geometryId to index of the arrays because
  264. // optimize() can make geometryId mismatch the index
  265. const visibles = this._visibles;
  266. const alives = this._alives;
  267. const matricesTexture = this._matricesTexture;
  268. const matrices = this._matrices;
  269. const matricesArray = this._matricesArray;
  270. if ( geometryId >= matrices.length || alives[ geometryId ] === false ) {
  271. return this;
  272. }
  273. if ( visibles[ geometryId ] === true ) {
  274. matrix.toArray( matricesArray, geometryId * 16 );
  275. matricesTexture.needsUpdate = true;
  276. }
  277. matrices[ geometryId ].copy( matrix );
  278. return this;
  279. }
  280. getMatrixAt( geometryId, matrix ) {
  281. const matrices = this._matrices;
  282. const alives = this._alives;
  283. if ( geometryId >= matrices.length || alives[ geometryId ] === false ) {
  284. return matrix;
  285. }
  286. return matrix.copy( matrices[ geometryId ] );
  287. }
  288. setVisibleAt( geometryId, visible ) {
  289. const visibles = this._visibles;
  290. const alives = this._alives;
  291. const matricesTexture = this._matricesTexture;
  292. const matrices = this._matrices;
  293. const matricesArray = this._matricesArray;
  294. // if the geometry is out of range, not active, or visibility state
  295. // does not change then return early
  296. if (
  297. geometryId >= visibles.length ||
  298. alives[ geometryId ] === false ||
  299. visibles[ geometryId ] === visible
  300. ) {
  301. return this;
  302. }
  303. // scale the matrix to zero if it's hidden
  304. if ( visible === true ) {
  305. matrices[ geometryId ].toArray( matricesArray, geometryId * 16 );
  306. } else {
  307. _zeroScaleMatrix.toArray( matricesArray, geometryId * 16 );
  308. }
  309. matricesTexture.needsUpdate = true;
  310. visibles[ geometryId ] = visible;
  311. return this;
  312. }
  313. getVisibleAt( geometryId ) {
  314. const visibles = this._visibles;
  315. const alives = this._alives;
  316. // return early if the geometry is out of range or not active
  317. if ( geometryId >= visibles.length || alives[ geometryId ] === false ) {
  318. return false;
  319. }
  320. return visibles[ geometryId ];
  321. }
  322. copy( source ) {
  323. super.copy( source );
  324. // @TODO: Implement
  325. return this;
  326. }
  327. toJSON( meta ) {
  328. // @TODO: Implement
  329. return super.toJSON( meta );
  330. }
  331. dispose() {
  332. // Assuming the geometry is not shared with other meshes
  333. this.geometry.dispose();
  334. this._matricesTexture.dispose();
  335. this._matricesTexture = null;
  336. return this;
  337. }
  338. onBeforeRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) {
  339. material.defines.BATCHING = true;
  340. // @TODO: Implement frustum culling for each geometry
  341. }
  342. onAfterRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) {
  343. material.defines.BATCHING = false;
  344. }
  345. }
  346. export { BatchedMesh };