import { BufferAttribute, BufferGeometry, DataTexture, FloatType, MathUtils, Matrix4, Mesh, RGBAFormat } from 'three'; const _identityMatrix = new Matrix4(); const _zeroMatrix = new Matrix4().set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ); // Custom shaders const batchingParsVertex = ` #ifdef BATCHING attribute float id; uniform highp sampler2D batchingTexture; uniform int batchingTextureSize; mat4 getBatchingMatrix( const in float i ) { float j = i * 4.0; float x = mod( j, float( batchingTextureSize ) ); float y = floor( j / float( batchingTextureSize ) ); float dx = 1.0 / float( batchingTextureSize ); float dy = 1.0 / float( batchingTextureSize ); y = dy * ( y + 0.5 ); vec4 v1 = texture2D( batchingTexture, vec2( dx * ( x + 0.5 ), y ) ); vec4 v2 = texture2D( batchingTexture, vec2( dx * ( x + 1.5 ), y ) ); vec4 v3 = texture2D( batchingTexture, vec2( dx * ( x + 2.5 ), y ) ); vec4 v4 = texture2D( batchingTexture, vec2( dx * ( x + 3.5 ), y ) ); return mat4( v1, v2, v3, v4 ); } #endif `; const batchingbaseVertex = ` #ifdef BATCHING mat4 batchingMatrix = getBatchingMatrix( id ); #endif `; const batchingnormalVertex = ` #ifdef BATCHING objectNormal = vec4( batchingMatrix * vec4( objectNormal, 0.0 ) ).xyz; #ifdef USE_TANGENT objectTangent = vec4( batchingMatrix * vec4( objectTangent, 0.0 ) ).xyz; #endif #endif `; const batchingVertex = ` #ifdef BATCHING transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz; #endif `; // @TODO: SkinnedMesh support? // @TODO: Future work if needed. Move into the core. Can be optimized more with WEBGL_multi_draw. class BatchedMesh extends Mesh { constructor( maxGeometryCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material ) { super( new BufferGeometry(), material ); this._vertexStarts = []; this._vertexCounts = []; this._indexStarts = []; this._indexCounts = []; this._visibles = []; this._alives = []; this._maxGeometryCount = maxGeometryCount; this._maxVertexCount = maxVertexCount; this._maxIndexCount = maxIndexCount; this._geometryInitialized = false; this._geometryCount = 0; this._vertexCount = 0; this._indexCount = 0; // Local matrix per geometry by using data texture // @TODO: Support uniform parameter per geometry this._matrices = []; this._matricesArray = null; this._matricesTexture = null; this._matricesTextureSize = null; // @TODO: Calculate the entire binding box and make frustumCulled true this.frustumCulled = false; this._customUniforms = { batchingTexture: { value: null }, batchingTextureSize: { value: 0 } }; this._initMatricesTexture(); this._initShader(); } _initMatricesTexture() { // layout (1 matrix = 4 pixels) // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4) // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8) // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16) // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32) // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64) let size = Math.sqrt( this._maxGeometryCount * 4 ); // 4 pixels needed for 1 matrix size = MathUtils.ceilPowerOfTwo( size ); size = Math.max( size, 4 ); const matricesArray = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel const matricesTexture = new DataTexture( matricesArray, size, size, RGBAFormat, FloatType ); this._matricesArray = matricesArray; this._matricesTexture = matricesTexture; this._matricesTextureSize = size; this._customUniforms.batchingTexture.value = this._matricesTexture; this._customUniforms.batchingTextureSize.value = this._matricesTextureSize; } _initShader() { const material = this.material; const currentOnBeforeCompile = material.onBeforeCompile; const customUniforms = this._customUniforms; material.onBeforeCompile = function onBeforeCompile( parameters, renderer ) { // Is this replacement stable across any materials? parameters.vertexShader = parameters.vertexShader .replace( '#include ', '#include \n' + batchingParsVertex ) .replace( '#include ', '#include \n' + batchingbaseVertex + batchingnormalVertex ) .replace( '#include ', '#include \n' + batchingVertex ); for ( const uniformName in customUniforms ) { parameters.uniforms[ uniformName ] = customUniforms[ uniformName ]; } // for debug // console.log( parameters.vertexShader, parameters.uniforms ); currentOnBeforeCompile.call( this, parameters, renderer ); }; material.defines = material.defines || {}; material.defines.BATCHING = false; } getGeometryCount() { return this._geometryCount; } getVertexCount() { return this._vertexCount; } getIndexCount() { return this._indexCount; } applyGeometry( geometry ) { // @TODO: geometry.groups support? // @TODO: geometry.drawRange support? // @TODO: geometry.mortphAttributes support? if ( this._geometryCount >= this._maxGeometryCount ) { // @TODO: Error handling } if ( this._geometryInitialized === false ) { for ( const attributeName in geometry.attributes ) { const srcAttribute = geometry.getAttribute( attributeName ); const { array, itemSize, normalized } = srcAttribute; const dstArray = new array.constructor( this._maxVertexCount * itemSize ); const dstAttribute = new srcAttribute.constructor( dstArray, itemSize, normalized ); dstAttribute.setUsage( srcAttribute.usage ); this.geometry.setAttribute( attributeName, dstAttribute ); } if ( geometry.getIndex() !== null ) { const indexArray = this._maxVertexCount > 65536 ? new Uint32Array( this._maxIndexCount ) : new Uint16Array( this._maxIndexCount ); this.geometry.setIndex( new BufferAttribute( indexArray, 1 ) ); } const idArray = this._maxGeometryCount > 65536 ? new Uint32Array( this._maxVertexCount ) : new Uint16Array( this._maxVertexCount ); // @TODO: What if attribute name 'id' is already used? this.geometry.setAttribute( 'id', new BufferAttribute( idArray, 1 ) ); this._geometryInitialized = true; } else { // @TODO: Check if geometry has the same attributes set } const hasIndex = this.geometry.getIndex() !== null; const dstIndex = this.geometry.getIndex(); const srcIndex = geometry.getIndex(); // Assuming geometry has position attribute const srcPositionAttribute = geometry.getAttribute( 'position' ); this._vertexStarts.push( this._vertexCount ); this._vertexCounts.push( srcPositionAttribute.count ); if ( hasIndex ) { this._indexStarts.push( this._indexCount ); this._indexCounts.push( srcIndex.count ); } this._visibles.push( true ); this._alives.push( true ); // @TODO: Error handling if exceeding maxVertexCount or maxIndexCount for ( const attributeName in geometry.attributes ) { const srcAttribute = geometry.getAttribute( attributeName ); const dstAttribute = this.geometry.getAttribute( attributeName ); dstAttribute.array.set( srcAttribute.array, this._vertexCount * dstAttribute.itemSize ); dstAttribute.needsUpdate = true; } if ( hasIndex ) { for ( let i = 0; i < srcIndex.count; i ++ ) { dstIndex.setX( this._indexCount + i, this._vertexCount + srcIndex.getX( i ) ); } this._indexCount += srcIndex.count; dstIndex.needsUpdate = true; } const geometryId = this._geometryCount; this._geometryCount ++; const idAttribute = this.geometry.getAttribute( 'id' ); for ( let i = 0; i < srcPositionAttribute.count; i ++ ) { idAttribute.setX( this._vertexCount + i, geometryId ); } idAttribute.needsUpdate = true; this._vertexCount += srcPositionAttribute.count; this._matrices.push( new Matrix4() ); _identityMatrix.toArray( this._matricesArray, geometryId * 16 ); this._matricesTexture.needsUpdate = true; return geometryId; } deleteGeometry( geometryId ) { if ( geometryId >= this._alives.length || this._alives[ geometryId ] === false ) { return this; } this._alives[ geometryId ] = false; _zeroMatrix.toArray( this._matricesArray, geometryId * 16 ); this._matricesTexture.needsUpdate = true; // User needs to call optimize() to pack the data. return this; } optimize() { // @TODO: Implement return this; } setMatrixAt( geometryId, matrix ) { // @TODO: Map geometryId to index of the arrays because // optimize() can make geometryId mismatch the index if ( geometryId >= this._matrices.length || this._alives[ geometryId ] === false ) { return this; } this._matrices[ geometryId ].copy( matrix ); if ( this._visibles[ geometryId ] === true ) { matrix.toArray( this._matricesArray, geometryId * 16 ); this._matricesTexture.needsUpdate = true; } return this; } getMatrixAt( geometryId, matrix ) { if ( geometryId >= this._matrices.length || this._alives[ geometryId ] === false ) { return matrix; } return matrix.copy( this._matrices[ geometryId ] ); } setVisibleAt( geometryId, visible ) { if ( geometryId >= this._visibles.length || this._alives[ geometryId ] === false ) { return this; } if ( this._visibles[ geometryId ] === visible ) { return this; } if ( visible === true ) { this._matrices[ geometryId ].toArray( this._matricesArray, geometryId * 16 ); } else { _zeroMatrix.toArray( this._matricesArray, geometryId * 16 ); } this._matricesTexture.needsUpdate = true; this._visibles[ geometryId ] = visible; return this; } getVisibleAt( geometryId ) { if ( geometryId >= this._visibles.length || this._alives[ geometryId ] === false ) { return false; } return this._visibles[ geometryId ]; } copy( source ) { super.copy( source ); // @TODO: Implement return this; } toJSON( meta ) { // @TODO: Implement return super.toJSON( meta ); } dispose() { // Assuming the geometry is not shared with other meshes this.geometry.dispose(); this._matricesTexture.dispose(); this._matricesTexture = null; return this; } onBeforeRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) { material.defines.BATCHING = true; // @TODO: Implement frustum culling for each geometry } onAfterRender( _renderer, _scene, _camera, _geometry, material/*, _group*/ ) { material.defines.BATCHING = false; } } export { BatchedMesh };