BatchedMesh.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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. // for debug
  135. // console.log( parameters.vertexShader, parameters.uniforms );
  136. currentOnBeforeCompile.call( this, parameters, renderer );
  137. };
  138. material.defines = material.defines || {};
  139. material.defines.BATCHING = false;
  140. }
  141. getGeometryCount() {
  142. return this._geometryCount;
  143. }
  144. getVertexCount() {
  145. return this._vertexCount;
  146. }
  147. getIndexCount() {
  148. return this._indexCount;
  149. }
  150. applyGeometry( geometry ) {
  151. // @TODO: geometry.groups support?
  152. // @TODO: geometry.drawRange support?
  153. // @TODO: geometry.mortphAttributes support?
  154. if ( this._geometryCount >= this._maxGeometryCount ) {
  155. // @TODO: Error handling
  156. }
  157. if ( this._geometryInitialized === false ) {
  158. for ( const attributeName in geometry.attributes ) {
  159. const srcAttribute = geometry.getAttribute( attributeName );
  160. const { array, itemSize, normalized } = srcAttribute;
  161. const dstArray = new array.constructor( this._maxVertexCount * itemSize );
  162. const dstAttribute = new srcAttribute.constructor( dstArray, itemSize, normalized );
  163. dstAttribute.setUsage( srcAttribute.usage );
  164. this.geometry.setAttribute( attributeName, dstAttribute );
  165. }
  166. if ( geometry.getIndex() !== null ) {
  167. const indexArray = this._maxVertexCount > 65536
  168. ? new Uint32Array( this._maxIndexCount )
  169. : new Uint16Array( this._maxIndexCount );
  170. this.geometry.setIndex( new BufferAttribute( indexArray, 1 ) );
  171. }
  172. const idArray = this._maxGeometryCount > 65536
  173. ? new Uint32Array( this._maxVertexCount )
  174. : new Uint16Array( this._maxVertexCount );
  175. // @TODO: What if attribute name 'id' is already used?
  176. this.geometry.setAttribute( 'id', new BufferAttribute( idArray, 1 ) );
  177. this._geometryInitialized = true;
  178. } else {
  179. // @TODO: Check if geometry has the same attributes set
  180. }
  181. const hasIndex = this.geometry.getIndex() !== null;
  182. const dstIndex = this.geometry.getIndex();
  183. const srcIndex = geometry.getIndex();
  184. // Assuming geometry has position attribute
  185. const srcPositionAttribute = geometry.getAttribute( 'position' );
  186. this._vertexStarts.push( this._vertexCount );
  187. this._vertexCounts.push( srcPositionAttribute.count );
  188. if ( hasIndex ) {
  189. this._indexStarts.push( this._indexCount );
  190. this._indexCounts.push( srcIndex.count );
  191. }
  192. this._visibles.push( true );
  193. this._alives.push( true );
  194. // @TODO: Error handling if exceeding maxVertexCount or maxIndexCount
  195. for ( const attributeName in geometry.attributes ) {
  196. const srcAttribute = geometry.getAttribute( attributeName );
  197. const dstAttribute = this.geometry.getAttribute( attributeName );
  198. dstAttribute.array.set( srcAttribute.array, this._vertexCount * dstAttribute.itemSize );
  199. dstAttribute.needsUpdate = true;
  200. }
  201. if ( hasIndex ) {
  202. for ( let i = 0; i < srcIndex.count; i ++ ) {
  203. dstIndex.setX( this._indexCount + i, this._vertexCount + srcIndex.getX( i ) );
  204. }
  205. this._indexCount += srcIndex.count;
  206. dstIndex.needsUpdate = true;
  207. }
  208. const geometryId = this._geometryCount;
  209. this._geometryCount ++;
  210. const idAttribute = this.geometry.getAttribute( 'id' );
  211. for ( let i = 0; i < srcPositionAttribute.count; i ++ ) {
  212. idAttribute.setX( this._vertexCount + i, geometryId );
  213. }
  214. idAttribute.needsUpdate = true;
  215. this._vertexCount += srcPositionAttribute.count;
  216. this._matrices.push( new Matrix4() );
  217. _identityMatrix.toArray( this._matricesArray, geometryId * 16 );
  218. this._matricesTexture.needsUpdate = true;
  219. return geometryId;
  220. }
  221. deleteGeometry( geometryId ) {
  222. if ( geometryId >= this._alives.length || this._alives[ geometryId ] === false ) {
  223. return this;
  224. }
  225. this._alives[ geometryId ] = false;
  226. _zeroScaleMatrix.toArray( this._matricesArray, geometryId * 16 );
  227. this._matricesTexture.needsUpdate = true;
  228. // User needs to call optimize() to pack the data.
  229. return this;
  230. }
  231. optimize() {
  232. // @TODO: Implement
  233. return this;
  234. }
  235. setMatrixAt( geometryId, matrix ) {
  236. // @TODO: Map geometryId to index of the arrays because
  237. // optimize() can make geometryId mismatch the index
  238. if ( geometryId >= this._matrices.length || this._alives[ geometryId ] === false ) {
  239. return this;
  240. }
  241. this._matrices[ geometryId ].copy( matrix );
  242. if ( this._visibles[ geometryId ] === true ) {
  243. matrix.toArray( this._matricesArray, geometryId * 16 );
  244. this._matricesTexture.needsUpdate = true;
  245. }
  246. return this;
  247. }
  248. getMatrixAt( geometryId, matrix ) {
  249. if ( geometryId >= this._matrices.length || this._alives[ geometryId ] === false ) {
  250. return matrix;
  251. }
  252. return matrix.copy( this._matrices[ geometryId ] );
  253. }
  254. setVisibleAt( geometryId, visible ) {
  255. if ( geometryId >= this._visibles.length || this._alives[ geometryId ] === false ) {
  256. return this;
  257. }
  258. if ( this._visibles[ geometryId ] === visible ) {
  259. return this;
  260. }
  261. if ( visible === true ) {
  262. this._matrices[ geometryId ].toArray( this._matricesArray, geometryId * 16 );
  263. } else {
  264. _zeroScaleMatrix.toArray( this._matricesArray, geometryId * 16 );
  265. }
  266. this._matricesTexture.needsUpdate = true;
  267. this._visibles[ geometryId ] = visible;
  268. return this;
  269. }
  270. getVisibleAt( geometryId ) {
  271. if ( geometryId >= this._visibles.length || this._alives[ geometryId ] === false ) {
  272. return false;
  273. }
  274. return this._visibles[ geometryId ];
  275. }
  276. copy( source ) {
  277. super.copy( source );
  278. // @TODO: Implement
  279. return this;
  280. }
  281. toJSON( meta ) {
  282. // @TODO: Implement
  283. return super.toJSON( meta );
  284. }
  285. dispose() {
  286. // Assuming the geometry is not shared with other meshes
  287. this.geometry.dispose();
  288. this._matricesTexture.dispose();
  289. this._matricesTexture = null;
  290. return this;
  291. }
  292. onBeforeRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) {
  293. material.defines.BATCHING = true;
  294. // @TODO: Implement frustum culling for each geometry
  295. }
  296. onAfterRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) {
  297. material.defines.BATCHING = false;
  298. }
  299. }
  300. export { BatchedMesh };