diff --git a/examples/files.json b/examples/files.json index 51248d6713b316..5cc57d9eb99dbf 100644 --- a/examples/files.json +++ b/examples/files.json @@ -173,6 +173,7 @@ "webgl_materials_wireframe", "webgl_math_obb", "webgl_math_orientation_transform", + "webgl_mesh_batch", "webgl_mirror", "webgl_modifier_curve", "webgl_modifier_curve_instanced", diff --git a/examples/jsm/objects/BatchedMesh.js b/examples/jsm/objects/BatchedMesh.js new file mode 100644 index 00000000000000..4d1ced7da9d097 --- /dev/null +++ b/examples/jsm/objects/BatchedMesh.js @@ -0,0 +1,466 @@ +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 }; diff --git a/examples/screenshots/webgl_mesh_batch.jpg b/examples/screenshots/webgl_mesh_batch.jpg new file mode 100644 index 00000000000000..dfa382b47e0c03 Binary files /dev/null and b/examples/screenshots/webgl_mesh_batch.jpg differ diff --git a/examples/webgl_mesh_batch.html b/examples/webgl_mesh_batch.html new file mode 100644 index 00000000000000..090f24b290ae0b --- /dev/null +++ b/examples/webgl_mesh_batch.html @@ -0,0 +1,320 @@ + + + + three.js webgl - mesh - batch + + + + + + + +
+ + three.js webgl - mesh - batch + +
+ + + + + + +