import { Box3, BufferAttribute, BufferGeometry, DynamicDrawUsage, Float32BufferAttribute, Material, Object3D, Sphere, Uint16BufferAttribute, Uint32BufferAttribute, WebGLRenderer } from 'three' import { Geometry } from '../converter/Geometry' import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, Batch, BatchUpdateRange, GeometryType, NoneBatchUpdateRange } from './Batch' import { BatchObject } from './BatchObject' import Logger from 'js-logger' import { ObjectLayers } from '../../IViewer' import { DrawGroup } from './InstancedMeshBatch' import Materials from '../materials/Materials' import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial' export default class MeshBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] private transformStorage: TransformStorage private geometry: BufferGeometry public batchMaterial: Material public mesh: SpeckleMesh private gradientIndexBuffer: BufferAttribute private indexBuffer0: BufferAttribute private indexBuffer1: BufferAttribute private indexBufferIndex = 0 private needsShuffle = false private needsFlatten = false public get bounds(): Box3 { return this.mesh.TAS.getBoundingBox(new Box3()) } public get drawCalls(): number { return this.geometry.groups.length } public get minDrawCalls(): number { return [ ...Array.from(new Set(this.geometry.groups.map((value) => value.materialIndex))) ].length } public get triCount(): number { return this.getCount() / 3 } public get vertCount(): number { return this.geometry.attributes.position.count } public constructor( id: string, subtreeId: string, renderViews: NodeRenderView[], transformStorage: TransformStorage ) { this.id = id this.subtreeId = subtreeId this.renderViews = renderViews this.transformStorage = transformStorage } public get geometryType(): GeometryType { return this.renderViews[0].geometryType } public get renderObject(): Object3D { return this.mesh } public getCount(): number { return this.geometry.index.count } public get materials(): Material[] { return this.mesh.material as Material[] } public get groups(): DrawGroup[] { return this.geometry.groups } public setBatchMaterial(material: Material) { this.batchMaterial = material } public onUpdate(deltaTime: number) { deltaTime if (this.needsFlatten) { this.flattenDrawGroups() this.needsFlatten = false } if (this.needsShuffle) { this.shuffleDrawGroups() this.needsShuffle = false } } public onRender(renderer: WebGLRenderer) { renderer } public setVisibleRange(...ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { this.geometry.setDrawRange(0, 0) /** We unset the 'visible' flag, otherwise three.js will still run pointless buffer binding commands*/ this.mesh.visible = false return } /** Entire batch needs to BE drawn */ if (ranges.length === 1 && ranges[0] === AllBatchUpdateRange) { this.geometry.setDrawRange(0, this.getCount()) this.mesh.visible = true return } /** Parts of the batch need to be visible. We get the min/max offset and total count */ let minOffset = Infinity let maxOffset = 0 ranges.forEach((range) => { minOffset = Math.min(minOffset, range.offset) maxOffset = Math.max(maxOffset, range.offset) }) this.geometry.setDrawRange( minOffset, maxOffset - minOffset + ranges.find((val) => val.offset === maxOffset).count ) this.mesh.visible = true } public getVisibleRange(): BatchUpdateRange { /** Entire batch is visible */ if (this.geometry.groups.length === 1 && this.mesh.visible) return AllBatchUpdateRange /** Entire batch is hidden */ if (!this.mesh.visible) return NoneBatchUpdateRange /** Parts of the batch are visible */ return { offset: this.geometry.drawRange.start, count: this.geometry.drawRange.count } } public getOpaque(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false ) }) if (transparentOrHiddenGroup) { return { offset: 0, count: transparentOrHiddenGroup.start } } /** Entire batch is opaque */ return AllBatchUpdateRange } public getTransparent(): BatchUpdateRange { /** Look for a transparent group */ const transparentGroup = this.groups.find((value) => { return Materials.isTransparent(this.materials[value.materialIndex]) }) /** Look for a hidden group */ const hiddenGroup = this.groups.find((value) => { return this.materials[value.materialIndex].visible === false }) /** If there is a transparent group return it's range */ if (transparentGroup) { return { offset: transparentGroup.start, count: hiddenGroup !== undefined ? hiddenGroup.start : this.getCount() - transparentGroup.start } } /** Entire batch is not transparent */ return NoneBatchUpdateRange } public getStencil(): BatchUpdateRange { /** If there is a single group and it's material writes to stencil, return all */ if (this.groups.length === 1) { if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange } const stencilGroup = this.groups.find((value) => { return this.materials[value.materialIndex].stencilWrite === true }) if (stencilGroup) { return { offset: stencilGroup.start, count: stencilGroup.count } } /** No stencil group */ return NoneBatchUpdateRange } public setBatchBuffers(...range: BatchUpdateRange[]): void { let minGradientIndex = Infinity let maxGradientIndex = 0 for (let k = 0; k < range.length; k++) { if (range[k].materialOptions) { if (range[k].materialOptions.rampIndex !== undefined) { const start = range[k].offset const len = range[k].offset + range[k].count /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent * sampling can occur. */ const shiftedIndex = range[k].materialOptions.rampIndex + 0.5 / range[k].materialOptions.rampWidth const minMaxIndices = this.updateGradientIndexBufferData( start, range[k].count === Infinity ? this.geometry.attributes['gradientIndex'].array.length : len, shiftedIndex ) minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex) maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex) } /** We need to update the texture here, because each batch uses it's own clone for any material we use on it * because otherwise three.js won't properly update our custom uniforms */ if (range[k].materialOptions.rampTexture !== undefined) { if (range[k].material instanceof SpeckleStandardColoredMaterial) { ;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture( range[k].materialOptions.rampTexture ) } } } } if (minGradientIndex < Infinity && maxGradientIndex > 0) this.updateGradientIndexBuffer() } public setDrawRanges(...ranges: BatchUpdateRange[]) { ranges.forEach((value: BatchUpdateRange) => { if (value.material) { value.material = this.mesh.getCachedMaterial(value.material) } }) const materials = ranges.map((val) => { return val.material }) const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] for (let k = 0; k < uniqueMaterials.length; k++) { if (!this.materials.includes(uniqueMaterials[k])) this.materials.push(uniqueMaterials[k]) } const sortedRanges = ranges.sort((a, b) => { return a.offset - b.offset }) for (let i = 0; i < sortedRanges.length; i++) { const materialIndex = this.materials.indexOf(sortedRanges[i].material) const collidingGroup = this.getDrawRangeCollision(sortedRanges[i]) if (collidingGroup) { collidingGroup.materialIndex = this.materials.indexOf(sortedRanges[i].material) } else { const includingGroup = this.geDrawRangeInclusion(sortedRanges[i]) if (includingGroup && includingGroup.materialIndex !== materialIndex) { this.geometry.groups.splice(this.geometry.groups.indexOf(includingGroup), 1) if (includingGroup.start === sortedRanges[i].offset) { this.geometry.addGroup( sortedRanges[i].offset, sortedRanges[i].count, materialIndex ) this.geometry.addGroup( sortedRanges[i].offset + sortedRanges[i].count, includingGroup.count - sortedRanges[i].count, includingGroup.materialIndex ) } else if ( sortedRanges[i].offset + sortedRanges[i].count === includingGroup.start + includingGroup.count ) { this.geometry.addGroup( includingGroup.start, includingGroup.count - sortedRanges[i].count, includingGroup.materialIndex ) this.geometry.addGroup( sortedRanges[i].offset, sortedRanges[i].count, materialIndex ) } else { this.geometry.addGroup( includingGroup.start, sortedRanges[i].offset - includingGroup.start, includingGroup.materialIndex ) this.geometry.addGroup( sortedRanges[i].offset, sortedRanges[i].count, materialIndex ) this.geometry.addGroup( sortedRanges[i].offset + sortedRanges[i].count, includingGroup.count - (sortedRanges[i].count + sortedRanges[i].offset - includingGroup.start), includingGroup.materialIndex ) } } } } let count = 0 this.geometry.groups.forEach((value) => (count += value.count)) if (count !== this.getCount()) { Logger.error(`Draw groups invalid on ${this.id}`) } this.setBatchBuffers(...ranges) this.needsFlatten = true } private getDrawRangeCollision(range: BatchUpdateRange): { start: number count: number materialIndex?: number } { if (this.geometry.groups.length > 0) { for (let i = 0; i < this.geometry.groups.length; i++) { if ( range.offset === this.geometry.groups[i].start && range.count === this.geometry.groups[i].count ) { return this.geometry.groups[i] } } return null } return null } private geDrawRangeInclusion(range: BatchUpdateRange): { start: number count: number materialIndex?: number } { if (this.geometry.groups.length > 0) { for (let i = 0; i < this.geometry.groups.length; i++) { if ( range.offset >= this.geometry.groups[i].start && range.offset + range.count <= this.geometry.groups[i].start + this.geometry.groups[i].count ) { return this.geometry.groups[i] } } return null } return null } private sortGroups() { this.geometry.groups.sort((a, b) => { const materialA: Material = (this.mesh.material as Array)[ a.materialIndex ] const materialB: Material = (this.mesh.material as Array)[ b.materialIndex ] const visibleOrder = +materialB.visible - +materialA.visible const transparentOrder = +materialA.transparent - +materialB.transparent if (visibleOrder !== 0) return visibleOrder return transparentOrder }) } private flattenDrawGroups() { const materialOrder = [] this.geometry.groups.reduce((previousValue, currentValue) => { if (previousValue.indexOf(currentValue.materialIndex) === -1) { previousValue.push(currentValue.materialIndex) } return previousValue }, materialOrder) const grouped = [] for (let k = 0; k < materialOrder.length; k++) { grouped.push( this.geometry.groups.filter((val) => { return val.materialIndex === materialOrder[k] }) ) } this.geometry.groups = [] for (let matIndex = 0; matIndex < grouped.length; matIndex++) { const matGroup = grouped[matIndex].sort((a, b) => { return a.start - b.start }) for (let k = 0; k < matGroup.length; ) { let offset = matGroup[k].start let count = matGroup[k].count let runningCount = matGroup[k].count let n = k + 1 for (; n < matGroup.length; n++) { if (offset + count === matGroup[n].start) { offset = matGroup[n].start count = matGroup[n].count runningCount += matGroup[n].count } else { const group = { start: matGroup[k].start, count: runningCount, materialIndex: matGroup[k].materialIndex } this.geometry.groups.push(group) break } } if (n === matGroup.length) { const group = { start: matGroup[k].start, count: runningCount, materialIndex: matGroup[k].materialIndex } this.geometry.groups.push(group) } k = n } } if (this.drawCalls > this.minDrawCalls + 2) { this.needsShuffle = true } else { this.geometry.groups.sort((a, b) => { return a.start - b.start }) const transparentOrHiddenGroup = this.geometry.groups.find( (value) => this.materials[value.materialIndex].transparent === true || this.materials[value.materialIndex].visible === false ) if (transparentOrHiddenGroup) { for ( let k = this.geometry.groups.indexOf(transparentOrHiddenGroup); k < this.geometry.groups.length; k++ ) { const material = this.materials[this.geometry.groups[k].materialIndex] if (material.transparent !== true && material.visible !== false) { this.needsShuffle = true break } } } } } private getCurrentIndexBuffer(): BufferAttribute { return this.indexBufferIndex % 2 === 0 ? this.indexBuffer0 : this.indexBuffer1 } private getNextIndexBuffer(): BufferAttribute { return ++this.indexBufferIndex % 2 === 0 ? this.indexBuffer0 : this.indexBuffer1 } private shuffleDrawGroups() { const groups = this.geometry.groups .sort((a, b) => { return a.start - b.start }) .slice() groups.sort((a, b) => { const materialA: Material = (this.mesh.material as Array)[ a.materialIndex ] const materialB: Material = (this.mesh.material as Array)[ b.materialIndex ] const visibleOrder = +materialB.visible - +materialA.visible const transparentOrder = +materialA.transparent - +materialB.transparent if (visibleOrder !== 0) return visibleOrder return transparentOrder }) const materialOrder = [] groups.reduce((previousValue, currentValue) => { if (previousValue.indexOf(currentValue.materialIndex) === -1) { previousValue.push(currentValue.materialIndex) } return previousValue }, materialOrder) const grouped = [] for (let k = 0; k < materialOrder.length; k++) { grouped.push( groups.filter((val) => { return val.materialIndex === materialOrder[k] }) ) } const sourceIBO: BufferAttribute = this.getCurrentIndexBuffer() const targetIBO: BufferAttribute = this.getNextIndexBuffer() const newGroups = [] const scratchRvs = this.renderViews.slice() scratchRvs.sort((a, b) => { return a.batchStart - b.batchStart }) let targetIBOOffset = 0 for (let k = 0; k < grouped.length; k++) { const materialGroup = grouped[k] const materialGroupStart = targetIBOOffset let materialGroupCount = 0 for (let i = 0; i < (materialGroup as []).length; i++) { const start = materialGroup[i].start const count = materialGroup[i].count const subArray = (sourceIBO.array as Uint16Array).subarray(start, start + count) ;(targetIBO.array as Uint16Array).set(subArray, targetIBOOffset) let rvTrisCount = 0 for (let m = 0; m < scratchRvs.length; m++) { if ( scratchRvs[m].batchStart >= start && scratchRvs[m].batchEnd <= start + count ) { scratchRvs[m].setBatchData( this.id, targetIBOOffset + rvTrisCount, scratchRvs[m].batchCount ) rvTrisCount += scratchRvs[m].batchCount scratchRvs.splice(m, 1) m-- } } targetIBOOffset += count materialGroupCount += count } newGroups.push({ offset: materialGroupStart, count: materialGroupCount, materialIndex: materialGroup[0].materialIndex }) } this.geometry.groups = [] for (let i = 0; i < newGroups.length; i++) { this.geometry.addGroup( newGroups[i].offset, newGroups[i].count, newGroups[i].materialIndex ) } this.geometry.setIndex(targetIBO) this.geometry.index.needsUpdate = true const hiddenGroup = this.geometry.groups.find((value) => { return this.mesh.material[value.materialIndex].visible === false }) if (hiddenGroup) { this.setVisibleRange({ offset: 0, count: hiddenGroup.start }) } } public resetDrawRanges() { this.mesh.setBatchMaterial(this.batchMaterial) this.mesh.visible = true this.geometry.clearGroups() this.geometry.addGroup(0, this.getCount(), 0) this.geometry.setDrawRange(0, Infinity) } public static bufferSetup = 0 public static arrayWork = 0 public static objectBvh = 0 public static computeNormals = 0 public static computeBoxAndSphere = 0 public static computeRTE = 0 public static batchBVH = 0 public buildBatch() { const start = performance.now() let indicesCount = 0 let attributeCount = 0 for (let k = 0; k < this.renderViews.length; k++) { indicesCount += this.renderViews[k].renderData.geometry.attributes.INDEX.length attributeCount += this.renderViews[k].renderData.geometry.attributes.POSITION.length } const hasVertexColors = this.renderViews[0].renderData.geometry.attributes.COLOR !== undefined const indices = new Uint32Array(indicesCount) const position = new Float64Array(attributeCount) const color = new Float32Array(hasVertexColors ? attributeCount : 0) color.fill(1) const batchIndices = new Float32Array(attributeCount / 3) MeshBatch.bufferSetup += performance.now() - start let offset = 0 let arrayOffset = 0 const batchObjects = [] for (let k = 0; k < this.renderViews.length; k++) { const start2 = performance.now() const geometry = this.renderViews[k].renderData.geometry indices.set( geometry.attributes.INDEX.map((val) => val + offset / 3), arrayOffset ) position.set(geometry.attributes.POSITION, offset) if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset) batchIndices.fill( k, offset / 3, offset / 3 + geometry.attributes.POSITION.length / 3 ) MeshBatch.arrayWork += performance.now() - start2 this.renderViews[k].setBatchData( this.id, arrayOffset, geometry.attributes.INDEX.length, offset / 3, offset / 3 + geometry.attributes.POSITION.length / 3 ) const batchObject = new BatchObject(this.renderViews[k], k) const start3 = performance.now() batchObject.buildAccelerationStructure() MeshBatch.objectBvh += performance.now() - start3 batchObjects.push(batchObject) offset += geometry.attributes.POSITION.length arrayOffset += geometry.attributes.INDEX.length } this.makeMeshGeometry( indices, position, batchIndices, hasVertexColors ? color : null ) this.mesh = new SpeckleMesh(this.geometry) this.mesh.setBatchObjects(batchObjects, this.transformStorage) this.mesh.setBatchMaterial(this.batchMaterial) const start6 = performance.now() this.mesh.buildTAS() MeshBatch.batchBVH += performance.now() - start6 this.geometry.boundingBox = this.mesh.TAS.getBoundingBox(new Box3()) this.geometry.boundingSphere = this.geometry.boundingBox.getBoundingSphere( new Sphere() ) this.mesh.uuid = this.id this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_MESH) this.mesh.frustumCulled = false this.mesh.geometry.addGroup(0, this.getCount(), 0) batchObjects.forEach((element: BatchObject) => { element.renderView.disposeGeometry() }) } public getRenderView(index: number): NodeRenderView { index Logger.warn('Deprecated! Do not call this anymore') return null } public getMaterialAtIndex(index: number): Material { index Logger.warn('Deprecated! Do not call this anymore') return null } public getMaterial(rv: NodeRenderView): Material { for (let k = 0; k < this.geometry.groups.length; k++) { try { if ( rv.batchStart >= this.geometry.groups[k].start && rv.batchEnd <= this.geometry.groups[k].start + this.geometry.groups[k].count ) { return this.materials[this.geometry.groups[k].materialIndex] } } catch (e) { Logger.error('Failed to get material') } } } private makeMeshGeometry( indices: Uint32Array | Uint16Array, position: Float64Array, batchIndices: Float32Array, color?: Float32Array ): BufferGeometry { // const start5 = performance.now() this.geometry = new BufferGeometry() if (position.length >= 65535 || indices.length >= 65535) { this.indexBuffer0 = new Uint32BufferAttribute(indices, 1) this.indexBuffer1 = new Uint32BufferAttribute(new Uint32Array(indices.length), 1) } else { this.indexBuffer0 = new Uint16BufferAttribute(indices, 1) this.indexBuffer1 = new Uint16BufferAttribute(new Uint16Array(indices.length), 1) } this.geometry.setIndex(this.indexBuffer0) if (position) { /** When RTE enabled, we'll be storing the high component of the encoding here, * which considering our current encoding method is actually the original casted * down float32 position! */ this.geometry.setAttribute('position', new Float32BufferAttribute(position, 3)) } if (batchIndices) { this.geometry.setAttribute( 'objIndex', new Float32BufferAttribute(batchIndices, 1) ) } if (color) { this.geometry.setAttribute('color', new Float32BufferAttribute(color, 3)) } const buffer = new Float32Array(position.length / 3) this.gradientIndexBuffer = new Float32BufferAttribute(buffer, 1) this.gradientIndexBuffer.setUsage(DynamicDrawUsage) this.geometry.setAttribute('gradientIndex', this.gradientIndexBuffer) this.updateGradientIndexBufferData(0, buffer.length, 0) this.updateGradientIndexBuffer() const start2 = performance.now() Geometry.computeVertexNormals(this.geometry, position) MeshBatch.computeNormals += performance.now() - start2 const start4 = performance.now() Geometry.updateRTEGeometry(this.geometry, position) MeshBatch.computeRTE += performance.now() - start4 return this.geometry } private updateGradientIndexBufferData( start: number, end: number, value: number ): { minIndex: number; maxIndex: number } { const index = this.geometry.index.array as number[] const data = this.gradientIndexBuffer.array as number[] let minVertexIndex = Infinity let maxVertexIndex = 0 for (let k = start; k < end; k++) { const vIndex = index[k] minVertexIndex = Math.min(minVertexIndex, vIndex) maxVertexIndex = Math.max(maxVertexIndex, vIndex) data[vIndex] = value } this.gradientIndexBuffer.updateRange = { offset: minVertexIndex, count: maxVertexIndex - minVertexIndex + 1 } this.gradientIndexBuffer.needsUpdate = true this.geometry.attributes['gradientIndex'].needsUpdate = true return { minIndex: minVertexIndex, maxIndex: maxVertexIndex } } private updateGradientIndexBuffer(rangeMin?: number, rangeMax?: number) { this.gradientIndexBuffer.updateRange = { offset: rangeMin !== undefined ? rangeMin : 0, count: rangeMin !== undefined && rangeMax !== undefined ? rangeMax - rangeMin + 1 : -1 } this.gradientIndexBuffer.needsUpdate = true this.geometry.attributes['gradientIndex'].needsUpdate = true } public purge() { this.renderViews.length = 0 this.geometry.dispose() this.batchMaterial.dispose() this.mesh = null } }