import { BufferAttribute } from '../core/BufferAttribute.js'; import { BufferGeometry } from '../core/BufferGeometry.js'; import { DataTexture } from '../textures/DataTexture.js'; import { FloatType } from '../constants.js'; import { Matrix4 } from '../math/Matrix4.js'; import { Mesh } from './Mesh.js'; import { RGBAFormat } from '../constants.js'; import { Box3 } from '../math/Box3.js'; import { Sphere } from '../math/Sphere.js'; import { Frustum } from '../math/Frustum.js'; import { Vector3 } from '../math/Vector3.js'; function sortOpaque( a, b ) { return a.z - b.z; } function sortTransparent( a, b ) { return b.z - a.z; } class MultiDrawRenderList { constructor() { this.index = 0; this.pool = []; this.list = []; } push( drawRange, z ) { const pool = this.pool; const list = this.list; if ( this.index >= pool.length ) { pool.push( { start: - 1, count: - 1, z: - 1, } ); } const item = pool[ this.index ]; list.push( item ); this.index ++; item.start = drawRange.start; item.count = drawRange.count; item.z = z; } reset() { this.list.length = 0; this.index = 0; } } const ID_ATTR_NAME = 'batchId'; const _matrix = /*@__PURE__*/ new Matrix4(); const _invMatrixWorld = /*@__PURE__*/ new Matrix4(); const _identityMatrix = /*@__PURE__*/ new Matrix4(); const _projScreenMatrix = /*@__PURE__*/ new Matrix4(); const _frustum = /*@__PURE__*/ new Frustum(); const _box = /*@__PURE__*/ new Box3(); const _sphere = /*@__PURE__*/ new Sphere(); const _vector = /*@__PURE__*/ new Vector3(); const _renderList = /*@__PURE__*/ new MultiDrawRenderList(); const _mesh = /*@__PURE__*/ new Mesh(); const _batchIntersects = []; // @TODO: SkinnedMesh support? // @TODO: geometry.groups support? // @TODO: geometry.drawRange support? // @TODO: geometry.morphAttributes support? // @TODO: Support uniform parameter per geometry // @TODO: Add an "optimize" function to pack geometry and remove data gaps // copies data from attribute "src" into "target" starting at "targetOffset" function copyAttributeData( src, target, targetOffset = 0 ) { const itemSize = target.itemSize; if ( src.isInterleavedBufferAttribute || src.array.constructor !== target.array.constructor ) { // use the component getters and setters if the array data cannot // be copied directly const vertexCount = src.count; for ( let i = 0; i < vertexCount; i ++ ) { for ( let c = 0; c < itemSize; c ++ ) { target.setComponent( i + targetOffset, c, src.getComponent( i, c ) ); } } } else { // faster copy approach using typed array set function target.array.set( src.array, targetOffset * itemSize ); } target.needsUpdate = true; } class BatchedMesh extends Mesh { get maxGeometryCount() { return this._maxGeometryCount; } constructor( maxGeometryCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material ) { super( new BufferGeometry(), material ); this.isBatchedMesh = true; this.perObjectFrustumCulled = true; this.sortObjects = true; this.boundingBox = null; this.boundingSphere = null; this.customSort = null; this._drawRanges = []; this._reservedRanges = []; this._visibility = []; this._active = []; this._bounds = []; this._maxGeometryCount = maxGeometryCount; this._maxVertexCount = maxVertexCount; this._maxIndexCount = maxIndexCount; this._geometryInitialized = false; this._geometryCount = 0; this._multiDrawCounts = new Int32Array( maxGeometryCount ); this._multiDrawStarts = new Int32Array( maxGeometryCount ); this._multiDrawCount = 0; this._visibilityChanged = true; // Local matrix per geometry by using data texture this._matricesTexture = null; this._initMatricesTexture(); } _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 = Math.ceil( size / 4 ) * 4; 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._matricesTexture = matricesTexture; } _initializeGeometry( reference ) { const geometry = this.geometry; const maxVertexCount = this._maxVertexCount; const maxGeometryCount = this._maxGeometryCount; const maxIndexCount = this._maxIndexCount; if ( this._geometryInitialized === false ) { for ( const attributeName in reference.attributes ) { const srcAttribute = reference.getAttribute( attributeName ); const { array, itemSize, normalized } = srcAttribute; const dstArray = new array.constructor( maxVertexCount * itemSize ); const dstAttribute = new BufferAttribute( dstArray, itemSize, normalized ); geometry.setAttribute( attributeName, dstAttribute ); } if ( reference.getIndex() !== null ) { const indexArray = maxVertexCount > 65536 ? new Uint32Array( maxIndexCount ) : new Uint16Array( maxIndexCount ); geometry.setIndex( new BufferAttribute( indexArray, 1 ) ); } const idArray = maxGeometryCount > 65536 ? new Uint32Array( maxVertexCount ) : new Uint16Array( maxVertexCount ); geometry.setAttribute( ID_ATTR_NAME, new BufferAttribute( idArray, 1 ) ); this._geometryInitialized = true; } } // Make sure the geometry is compatible with the existing combined geometry attributes _validateGeometry( geometry ) { // check that the geometry doesn't have a version of our reserved id attribute if ( geometry.getAttribute( ID_ATTR_NAME ) ) { throw new Error( `BatchedMesh: Geometry cannot use attribute "${ ID_ATTR_NAME }"` ); } // check to ensure the geometries are using consistent attributes and indices const batchGeometry = this.geometry; if ( Boolean( geometry.getIndex() ) !== Boolean( batchGeometry.getIndex() ) ) { throw new Error( 'BatchedMesh: All geometries must consistently have "index".' ); } for ( const attributeName in batchGeometry.attributes ) { if ( attributeName === ID_ATTR_NAME ) { continue; } if ( ! geometry.hasAttribute( attributeName ) ) { throw new Error( `BatchedMesh: Added geometry missing "${ attributeName }". All geometries must have consistent attributes.` ); } const srcAttribute = geometry.getAttribute( attributeName ); const dstAttribute = batchGeometry.getAttribute( attributeName ); if ( srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized ) { throw new Error( 'BatchedMesh: All attributes must have a consistent itemSize and normalized value.' ); } } } setCustomSort( func ) { this.customSort = func; return this; } computeBoundingBox() { if ( this.boundingBox === null ) { this.boundingBox = new Box3(); } const geometryCount = this._geometryCount; const boundingBox = this.boundingBox; const active = this._active; boundingBox.makeEmpty(); for ( let i = 0; i < geometryCount; i ++ ) { if ( active[ i ] === false ) continue; this.getMatrixAt( i, _matrix ); this.getBoundingBoxAt( i, _box ).applyMatrix4( _matrix ); boundingBox.union( _box ); } } computeBoundingSphere() { if ( this.boundingSphere === null ) { this.boundingSphere = new Sphere(); } const geometryCount = this._geometryCount; const boundingSphere = this.boundingSphere; const active = this._active; boundingSphere.makeEmpty(); for ( let i = 0; i < geometryCount; i ++ ) { if ( active[ i ] === false ) continue; this.getMatrixAt( i, _matrix ); this.getBoundingSphereAt( i, _sphere ).applyMatrix4( _matrix ); boundingSphere.union( _sphere ); } } addGeometry( geometry, vertexCount = - 1, indexCount = - 1 ) { this._initializeGeometry( geometry ); this._validateGeometry( geometry ); // ensure we're not over geometry if ( this._geometryCount >= this._maxGeometryCount ) { throw new Error( 'BatchedMesh: Maximum geometry count reached.' ); } // get the necessary range fo the geometry const reservedRange = { vertexStart: - 1, vertexCount: - 1, indexStart: - 1, indexCount: - 1, }; let lastRange = null; const reservedRanges = this._reservedRanges; const drawRanges = this._drawRanges; const bounds = this._bounds; if ( this._geometryCount !== 0 ) { lastRange = reservedRanges[ reservedRanges.length - 1 ]; } if ( vertexCount === - 1 ) { reservedRange.vertexCount = geometry.getAttribute( 'position' ).count; } else { reservedRange.vertexCount = vertexCount; } if ( lastRange === null ) { reservedRange.vertexStart = 0; } else { reservedRange.vertexStart = lastRange.vertexStart + lastRange.vertexCount; } const index = geometry.getIndex(); const hasIndex = index !== null; if ( hasIndex ) { if ( indexCount === - 1 ) { reservedRange.indexCount = index.count; } else { reservedRange.indexCount = indexCount; } if ( lastRange === null ) { reservedRange.indexStart = 0; } else { reservedRange.indexStart = lastRange.indexStart + lastRange.indexCount; } } if ( reservedRange.indexStart !== - 1 && reservedRange.indexStart + reservedRange.indexCount > this._maxIndexCount || reservedRange.vertexStart + reservedRange.vertexCount > this._maxVertexCount ) { throw new Error( 'BatchedMesh: Reserved space request exceeds the maximum buffer size.' ); } const visibility = this._visibility; const active = this._active; const matricesTexture = this._matricesTexture; const matricesArray = this._matricesTexture.image.data; // push new visibility states visibility.push( true ); active.push( true ); // update id const geometryId = this._geometryCount; this._geometryCount ++; // initialize matrix information _identityMatrix.toArray( matricesArray, geometryId * 16 ); matricesTexture.needsUpdate = true; // add the reserved range and draw range objects reservedRanges.push( reservedRange ); drawRanges.push( { start: hasIndex ? reservedRange.indexStart : reservedRange.vertexStart, count: - 1 } ); bounds.push( { boxInitialized: false, box: new Box3(), sphereInitialized: false, sphere: new Sphere() } ); // set the id for the geometry const idAttribute = this.geometry.getAttribute( ID_ATTR_NAME ); for ( let i = 0; i < reservedRange.vertexCount; i ++ ) { idAttribute.setX( reservedRange.vertexStart + i, geometryId ); } idAttribute.needsUpdate = true; // update the geometry this.setGeometryAt( geometryId, geometry ); return geometryId; } setGeometryAt( id, geometry ) { if ( id >= this._geometryCount ) { throw new Error( 'BatchedMesh: Maximum geometry count reached.' ); } this._validateGeometry( geometry ); const batchGeometry = this.geometry; const hasIndex = batchGeometry.getIndex() !== null; const dstIndex = batchGeometry.getIndex(); const srcIndex = geometry.getIndex(); const reservedRange = this._reservedRanges[ id ]; if ( hasIndex && srcIndex.count > reservedRange.indexCount || geometry.attributes.position.count > reservedRange.vertexCount ) { throw new Error( 'BatchedMesh: Reserved space not large enough for provided geometry.' ); } // copy geometry over const vertexStart = reservedRange.vertexStart; const vertexCount = reservedRange.vertexCount; for ( const attributeName in batchGeometry.attributes ) { if ( attributeName === ID_ATTR_NAME ) { continue; } // copy attribute data const srcAttribute = geometry.getAttribute( attributeName ); const dstAttribute = batchGeometry.getAttribute( attributeName ); copyAttributeData( srcAttribute, dstAttribute, vertexStart ); // fill the rest in with zeroes const itemSize = srcAttribute.itemSize; for ( let i = srcAttribute.count, l = vertexCount; i < l; i ++ ) { const index = vertexStart + i; for ( let c = 0; c < itemSize; c ++ ) { dstAttribute.setComponent( index, c, 0 ); } } dstAttribute.needsUpdate = true; dstAttribute.addUpdateRange( vertexStart * itemSize, vertexCount * itemSize ); } // copy index if ( hasIndex ) { const indexStart = reservedRange.indexStart; // copy index data over for ( let i = 0; i < srcIndex.count; i ++ ) { dstIndex.setX( indexStart + i, vertexStart + srcIndex.getX( i ) ); } // fill the rest in with zeroes for ( let i = srcIndex.count, l = reservedRange.indexCount; i < l; i ++ ) { dstIndex.setX( indexStart + i, vertexStart ); } dstIndex.needsUpdate = true; dstIndex.addUpdateRange( indexStart, reservedRange.indexCount ); } // store the bounding boxes const bound = this._bounds[ id ]; if ( geometry.boundingBox !== null ) { bound.box.copy( geometry.boundingBox ); bound.boxInitialized = true; } else { bound.boxInitialized = false; } if ( geometry.boundingSphere !== null ) { bound.sphere.copy( geometry.boundingSphere ); bound.sphereInitialized = true; } else { bound.sphereInitialized = false; } // set drawRange count const drawRange = this._drawRanges[ id ]; const posAttr = geometry.getAttribute( 'position' ); drawRange.count = hasIndex ? srcIndex.count : posAttr.count; this._visibilityChanged = true; return id; } deleteGeometry( geometryId ) { // Note: User needs to call optimize() afterward to pack the data. const active = this._active; if ( geometryId >= active.length || active[ geometryId ] === false ) { return this; } active[ geometryId ] = false; this._visibilityChanged = true; return this; } // get bounding box and compute it if it doesn't exist getBoundingBoxAt( id, target ) { const active = this._active; if ( active[ id ] === false ) { return null; } // compute bounding box const bound = this._bounds[ id ]; const box = bound.box; const geometry = this.geometry; if ( bound.boxInitialized === false ) { box.makeEmpty(); const index = geometry.index; const position = geometry.attributes.position; const drawRange = this._drawRanges[ id ]; for ( let i = drawRange.start, l = drawRange.start + drawRange.count; i < l; i ++ ) { let iv = i; if ( index ) { iv = index.getX( iv ); } box.expandByPoint( _vector.fromBufferAttribute( position, iv ) ); } bound.boxInitialized = true; } target.copy( box ); return target; } // get bounding sphere and compute it if it doesn't exist getBoundingSphereAt( id, target ) { const active = this._active; if ( active[ id ] === false ) { return null; } // compute bounding sphere const bound = this._bounds[ id ]; const sphere = bound.sphere; const geometry = this.geometry; if ( bound.sphereInitialized === false ) { sphere.makeEmpty(); this.getBoundingBoxAt( id, _box ); _box.getCenter( sphere.center ); const index = geometry.index; const position = geometry.attributes.position; const drawRange = this._drawRanges[ id ]; let maxRadiusSq = 0; for ( let i = drawRange.start, l = drawRange.start + drawRange.count; i < l; i ++ ) { let iv = i; if ( index ) { iv = index.getX( iv ); } _vector.fromBufferAttribute( position, iv ); maxRadiusSq = Math.max( maxRadiusSq, sphere.center.distanceToSquared( _vector ) ); } sphere.radius = Math.sqrt( maxRadiusSq ); bound.sphereInitialized = true; } target.copy( sphere ); return target; } setMatrixAt( geometryId, matrix ) { // @TODO: Map geometryId to index of the arrays because // optimize() can make geometryId mismatch the index const active = this._active; const matricesTexture = this._matricesTexture; const matricesArray = this._matricesTexture.image.data; const geometryCount = this._geometryCount; if ( geometryId >= geometryCount || active[ geometryId ] === false ) { return this; } matrix.toArray( matricesArray, geometryId * 16 ); matricesTexture.needsUpdate = true; return this; } getMatrixAt( geometryId, matrix ) { const active = this._active; const matricesArray = this._matricesTexture.image.data; const geometryCount = this._geometryCount; if ( geometryId >= geometryCount || active[ geometryId ] === false ) { return null; } return matrix.fromArray( matricesArray, geometryId * 16 ); } setVisibleAt( geometryId, value ) { const visibility = this._visibility; const active = this._active; const geometryCount = this._geometryCount; // if the geometry is out of range, not active, or visibility state // does not change then return early if ( geometryId >= geometryCount || active[ geometryId ] === false || visibility[ geometryId ] === value ) { return this; } visibility[ geometryId ] = value; this._visibilityChanged = true; return this; } getVisibleAt( geometryId ) { const visibility = this._visibility; const active = this._active; const geometryCount = this._geometryCount; // return early if the geometry is out of range or not active if ( geometryId >= geometryCount || active[ geometryId ] === false ) { return false; } return visibility[ geometryId ]; } raycast( raycaster, intersects ) { const visibility = this._visibility; const active = this._active; const drawRanges = this._drawRanges; const geometryCount = this._geometryCount; const matrixWorld = this.matrixWorld; const batchGeometry = this.geometry; // iterate over each geometry _mesh.material = this.material; _mesh.geometry.index = batchGeometry.index; _mesh.geometry.attributes = batchGeometry.attributes; if ( _mesh.geometry.boundingBox === null ) { _mesh.geometry.boundingBox = new Box3(); } if ( _mesh.geometry.boundingSphere === null ) { _mesh.geometry.boundingSphere = new Sphere(); } for ( let i = 0; i < geometryCount; i ++ ) { if ( ! visibility[ i ] || ! active[ i ] ) { continue; } const drawRange = drawRanges[ i ]; _mesh.geometry.setDrawRange( drawRange.start, drawRange.count ); // ge the intersects this.getMatrixAt( i, _mesh.matrixWorld ).premultiply( matrixWorld ); this.getBoundingBoxAt( i, _mesh.geometry.boundingBox ); this.getBoundingSphereAt( i, _mesh.geometry.boundingSphere ); _mesh.raycast( raycaster, _batchIntersects ); // add batch id to the intersects for ( let j = 0, l = _batchIntersects.length; j < l; j ++ ) { const intersect = _batchIntersects[ j ]; intersect.object = this; intersect.batchId = i; intersects.push( intersect ); } _batchIntersects.length = 0; } _mesh.material = null; _mesh.geometry.index = null; _mesh.geometry.attributes = {}; _mesh.geometry.setDrawRange( 0, Infinity ); } copy( source ) { super.copy( source ); this.geometry = source.geometry.clone(); this.perObjectFrustumCulled = source.perObjectFrustumCulled; this.sortObjects = source.sortObjects; this.boundingBox = source.boundingBox !== null ? source.boundingBox.clone() : null; this.boundingSphere = source.boundingSphere !== null ? source.boundingSphere.clone() : null; this._drawRanges = source._drawRanges.map( range => ( { ...range } ) ); this._reservedRanges = source._reservedRanges.map( range => ( { ...range } ) ); this._visibility = source._visibility.slice(); this._active = source._active.slice(); this._bounds = source._bounds.map( bound => ( { boxInitialized: bound.boxInitialized, box: bound.box.clone(), sphereInitialized: bound.sphereInitialized, sphere: bound.sphere.clone() } ) ); this._maxGeometryCount = source._maxGeometryCount; this._maxVertexCount = source._maxVertexCount; this._maxIndexCount = source._maxIndexCount; this._geometryInitialized = source._geometryInitialized; this._geometryCount = source._geometryCount; this._multiDrawCounts = source._multiDrawCounts.slice(); this._multiDrawStarts = source._multiDrawStarts.slice(); this._matricesTexture = source._matricesTexture.clone(); this._matricesTexture.image.data = this._matricesTexture.image.slice(); return this; } 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*/ ) { // if visibility has not changed and frustum culling and object sorting is not required // then skip iterating over all items if ( ! this._visibilityChanged && ! this.perObjectFrustumCulled && ! this.sortObjects ) { return; } // the indexed version of the multi draw function requires specifying the start // offset in bytes. const index = geometry.getIndex(); const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT; const active = this._active; const visibility = this._visibility; const multiDrawStarts = this._multiDrawStarts; const multiDrawCounts = this._multiDrawCounts; const drawRanges = this._drawRanges; const perObjectFrustumCulled = this.perObjectFrustumCulled; // prepare the frustum in the local frame if ( perObjectFrustumCulled ) { _projScreenMatrix .multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) .multiply( this.matrixWorld ); _frustum.setFromProjectionMatrix( _projScreenMatrix, renderer.coordinateSystem ); } let count = 0; if ( this.sortObjects ) { // get the camera position in the local frame _invMatrixWorld.copy( this.matrixWorld ).invert(); _vector.setFromMatrixPosition( camera.matrixWorld ).applyMatrix4( _invMatrixWorld ); for ( let i = 0, l = visibility.length; i < l; i ++ ) { if ( visibility[ i ] && active[ i ] ) { // get the bounds in world space this.getMatrixAt( i, _matrix ); this.getBoundingSphereAt( i, _sphere ).applyMatrix4( _matrix ); // determine whether the batched geometry is within the frustum let culled = false; if ( perObjectFrustumCulled ) { culled = ! _frustum.intersectsSphere( _sphere ); } if ( ! culled ) { // get the distance from camera used for sorting const z = _vector.distanceTo( _sphere.center ); _renderList.push( drawRanges[ i ], z ); } } } // Sort the draw ranges and prep for rendering const list = _renderList.list; const customSort = this.customSort; if ( customSort === null ) { list.sort( material.transparent ? sortTransparent : sortOpaque ); } else { customSort.call( this, list, camera ); } for ( let i = 0, l = list.length; i < l; i ++ ) { const item = list[ i ]; multiDrawStarts[ count ] = item.start * bytesPerElement; multiDrawCounts[ count ] = item.count; count ++; } _renderList.reset(); } else { for ( let i = 0, l = visibility.length; i < l; i ++ ) { if ( visibility[ i ] && active[ i ] ) { // determine whether the batched geometry is within the frustum let culled = false; if ( perObjectFrustumCulled ) { // get the bounds in world space this.getMatrixAt( i, _matrix ); this.getBoundingSphereAt( i, _sphere ).applyMatrix4( _matrix ); culled = ! _frustum.intersectsSphere( _sphere ); } if ( ! culled ) { const range = drawRanges[ i ]; multiDrawStarts[ count ] = range.start * bytesPerElement; multiDrawCounts[ count ] = range.count; count ++; } } } } this._multiDrawCount = count; this._visibilityChanged = false; } onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial/* , group */ ) { this.onBeforeRender( renderer, null, shadowCamera, geometry, depthMaterial ); } } export { BatchedMesh };