Files
speckle-server/packages/viewer/src/modules/batching/MeshBatch.ts
T
Alexandru Popovici 6b7dbc5d70 Reduced Viewer Memory Footprint (#1406)
* The ObjectLoader and ViewerObjectsLoaders are disposed upon loading complete.

* Added viewer parameter for keeping or discarding original geometry data. Defaults to discarding it. WIP on mesh and point cloud implementation for actual discarding it

* Geometry data from raw nodes is now disposed after building their reder views

* Removed raw geometry data from points and lines

* Removed the float64 array which was persisted pointlessly by the section plane outlines feature. Also fixed a bug for this feature regarding resizing the point buffer and RTE

* Implemented BVHs in local space, relative to world origin. This cuts down on it's redundant memory footprint by half

* Implemented SpeckleMeshBVH which completely hides the RTE-related transformations that we're making to reduce memory footprint.

* Added some clarifications to the SpeckleMeshBVH class
2023-02-28 13:07:33 +02:00

602 lines
19 KiB
TypeScript

import {
Box3,
BufferAttribute,
BufferGeometry,
DynamicDrawUsage,
Float32BufferAttribute,
Material,
Object3D,
Uint16BufferAttribute,
Uint32BufferAttribute,
WebGLRenderer
} from 'three'
import { Geometry } from '../converter/Geometry'
import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial'
import SpeckleMesh from '../objects/SpeckleMesh'
import { NodeRenderView } from '../tree/NodeRenderView'
import { Viewer } from '../Viewer'
import {
AllBatchUpdateRange,
Batch,
BatchUpdateRange,
GeometryType,
HideAllBatchUpdateRange
} from './Batch'
import Logger from 'js-logger'
import { GeometryConverter } from '../converter/GeometryConverter'
import { WorldTree } from '../tree/WorldTree'
import { SpeckleMeshBVH } from '../objects/SpeckleMeshBVH'
import { ObjectLayers } from '../SpeckleRenderer'
export default class MeshBatch implements Batch {
public id: string
public subtreeId: string
public renderViews: NodeRenderView[]
private geometry: BufferGeometry
public batchMaterial: Material
public mesh: SpeckleMesh
public boundsTree: SpeckleMeshBVH
public bounds: Box3 = new Box3()
private gradientIndexBuffer: BufferAttribute
private indexBuffer0: BufferAttribute
private indexBuffer1: BufferAttribute
private indexBufferIndex = 0
public constructor(id: string, subtreeId: string, renderViews: NodeRenderView[]) {
this.id = id
this.subtreeId = subtreeId
this.renderViews = renderViews
}
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 setBatchMaterial(material: Material) {
this.batchMaterial = material
}
public onUpdate(deltaTime: number) {
deltaTime
}
public onRender(renderer: WebGLRenderer) {
renderer
}
public setVisibleRange(...ranges: BatchUpdateRange[]) {
if (ranges.length === 1 && ranges[0] === HideAllBatchUpdateRange) {
this.geometry.setDrawRange(0, 0)
this.mesh.visible = false
return
}
if (
ranges.length === 1 &&
ranges[0].offset === AllBatchUpdateRange.offset &&
ranges[0].count === AllBatchUpdateRange.count
) {
this.geometry.setDrawRange(0, this.getCount())
this.mesh.visible = true
return
}
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 {
if (this.geometry.groups.length === 0) return AllBatchUpdateRange
return {
offset: this.geometry.drawRange.start,
count: this.geometry.drawRange.count
}
}
public setDrawRanges(...ranges: BatchUpdateRange[]) {
const materials = ranges.map((val) => val.material)
const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))]
if (!Array.isArray(this.mesh.material)) {
this.mesh.material = [this.mesh.material]
}
for (let k = 0; k < uniqueMaterials.length; k++) {
if (!this.mesh.material.includes(uniqueMaterials[k]))
this.mesh.material.push(uniqueMaterials[k])
}
const sortedRanges = ranges.sort((a, b) => {
return a.offset - b.offset
})
const newGroups = []
let minGradientIndex = Infinity
let maxGradientIndex = 0
for (let k = 0; k < sortedRanges.length; k++) {
if (sortedRanges[k].materialOptions) {
if (sortedRanges[k].materialOptions.rampIndex !== undefined) {
const start = sortedRanges[k].offset
const len = sortedRanges[k].offset + sortedRanges[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 =
sortedRanges[k].materialOptions.rampIndex +
0.5 / sortedRanges[k].materialOptions.rampWidth
const minMaxIndices = this.updateGradientIndexBufferData(
start,
sortedRanges[k].count === Infinity
? this.geometry.attributes['gradientIndex'].array.length
: len,
shiftedIndex
)
minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex)
maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex)
}
if (sortedRanges[k].materialOptions.rampTexture) {
;(
sortedRanges[k].material as SpeckleStandardColoredMaterial
).setGradientTexture(sortedRanges[k].materialOptions.rampTexture)
}
}
const collidingGroup = this.getDrawRangeCollision(sortedRanges[k])
if (collidingGroup) {
// console.warn(`Draw range collision @ ${this.id} overwritting...`)
collidingGroup.materialIndex = this.mesh.material.indexOf(
sortedRanges[k].material
)
continue
}
newGroups.push(sortedRanges[k])
}
/** Should properly compute min, max buffer range. Current minGradientIndex and maxGradientIndex are incorrect */
this.updateGradientIndexBuffer()
for (let i = 0; i < newGroups.length; i++) {
this.geometry.addGroup(
newGroups[i].offset,
newGroups[i].count,
this.mesh.material.indexOf(newGroups[i].material)
)
}
}
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) {
return this.geometry.groups[i]
}
}
return null
}
return null
}
private getCurrentIndexBuffer(): BufferAttribute {
return this.indexBufferIndex % 2 === 0 ? this.indexBuffer0 : this.indexBuffer1
}
private getNextIndexBuffer(): BufferAttribute {
return ++this.indexBufferIndex % 2 === 0 ? this.indexBuffer0 : this.indexBuffer1
}
public autoFillDrawRanges() {
// this.autoFillDrawRangesBasic()
this.autoFillDrawRangesShuffleIBO()
}
private autoFillDrawRangesShuffleIBO() {
const groups = this.geometry.groups
.sort((a, b) => {
return a.start - b.start
})
.slice()
const groupsStart = groups.map((val) => {
return val.start
})
for (let k = 0; k < this.renderViews.length; k++) {
if (!groupsStart.includes(this.renderViews[k].batchStart)) {
groups.push({
start: this.renderViews[k].batchStart,
count: this.renderViews[k].batchCount,
materialIndex: 0
})
}
}
groups.sort((a, b) => {
const materialA: Material = (this.mesh.material as Array<Material>)[
a.materialIndex
]
const materialB: Material = (this.mesh.material as Array<Material>)[
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)
// if (materialOrder.length > 1) {
// for (let m = 0; m < materialOrder.length; m++) {
// if (!this.mesh.material[materialOrder[m]].visible)
// console.log(
// `Batch ${this.id} material: Hidden -> ${!this.mesh.material[
// materialOrder[m]
// ].visible}, Transparent -> ${
// this.mesh.material[materialOrder[m]].transparent
// }`
// )
// }
// }
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 newMapping: { [index: number]: number } = {}
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)
newMapping[start] = targetIBOOffset
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
)
}
const oldMapping = this.createRenderViewMapping()
for (const index in oldMapping) {
const rv = oldMapping[index]
rv.setBatchData(rv.batchId, newMapping[index], rv.batchCount)
}
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
})
}
}
/** This is the initial basic way of dealing with auto-completing draw groups
* It's simpler, but it cand add a lot of redundant draw calls. I'm keeping it
* for now
*/
/*
private autoFillDrawRangesBasic() {
const sortedRanges = this.geometry.groups
.sort((a, b) => {
return a.start - b.start
})
.slice()
// console.warn(`Batch ID ${this.id} Group count ${sortedRanges.length}`)
for (let k = 0; k < sortedRanges.length; k++) {
if (k === 0) {
if (sortedRanges[k].start > 0) {
this.geometry.addGroup(0, sortedRanges[k].start, 0)
}
if (
sortedRanges.length === 1 &&
sortedRanges[k].start + sortedRanges[k].count < this.getCount()
) {
this.geometry.addGroup(
sortedRanges[k].start + sortedRanges[k].count,
this.getCount() - sortedRanges[k].start + sortedRanges[k].count,
0
)
}
} else if (k === sortedRanges.length - 1) {
if (sortedRanges[k].start + sortedRanges[k].count < this.getCount()) {
this.geometry.addGroup(
sortedRanges[k].start + sortedRanges[k].count,
this.getCount() - sortedRanges[k].start + sortedRanges[k].count,
0
)
}
if (
sortedRanges[k - 1].start + sortedRanges[k - 1].count <
sortedRanges[k].start
) {
this.geometry.addGroup(
sortedRanges[k - 1].start + sortedRanges[k - 1].count,
sortedRanges[k].start -
(sortedRanges[k - 1].start + sortedRanges[k - 1].count),
0
)
}
continue
} else {
if (
sortedRanges[k - 1].start + sortedRanges[k - 1].count <
sortedRanges[k].start
) {
this.geometry.addGroup(
sortedRanges[k - 1].start + sortedRanges[k - 1].count,
sortedRanges[k].start -
(sortedRanges[k - 1].start + sortedRanges[k - 1].count),
0
)
}
}
}
this.geometry.groups.sort((a, b) => {
return a.start - b.start
})
let count = 0
this.geometry.groups.forEach((val) => {
count += val.count
})
if (count < this.getCount()) {
// Shouldn't happen
console.error(`DrawRange MESH autocomplete failed! ${count}vs${this.getCount()}`)
}
}
*/
private createRenderViewMapping(): { [index: number]: NodeRenderView } {
const mapping: { [index: number]: NodeRenderView } = {}
for (let k = 0; k < this.renderViews.length; k++) {
mapping[this.renderViews[k].batchStart] = this.renderViews[k]
}
return mapping
}
public resetDrawRanges() {
this.mesh.material = this.batchMaterial
this.mesh.visible = true
this.geometry.clearGroups()
this.geometry.setDrawRange(0, Infinity)
}
public buildBatch() {
const indicesCount = this.renderViews.flatMap(
(val: NodeRenderView) => val.renderData.geometry.attributes.INDEX
).length
const attributeCount = this.renderViews.flatMap(
(val: NodeRenderView) => val.renderData.geometry.attributes.POSITION
).length
const indices = new Uint32Array(indicesCount)
const position = new Float64Array(attributeCount)
const color = new Float32Array(this.batchMaterial.vertexColors ? attributeCount : 0)
color.fill(1)
let offset = 0
let arrayOffset = 0
for (let k = 0; k < this.renderViews.length; k++) {
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)
this.renderViews[k].setBatchData(
this.id,
arrayOffset,
geometry.attributes.INDEX.length,
offset / 3,
offset / 3 + geometry.attributes.POSITION.length / 3
)
offset += geometry.attributes.POSITION.length
arrayOffset += geometry.attributes.INDEX.length
if (!GeometryConverter.keepGeometryData) {
this.renderViews[k].disposeGeometry()
}
}
this.makeMeshGeometry(
indices,
position,
this.batchMaterial.vertexColors ? color : null
)
this.boundsTree = Geometry.buildBVH(
indices,
position,
WorldTree.getRenderTree(this.subtreeId).treeBounds
)
this.boundsTree.getBoundingBox(this.bounds)
this.mesh = new SpeckleMesh(this.geometry, this.batchMaterial, this.boundsTree)
this.mesh.uuid = this.id
this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_MESH)
}
public getRenderView(index: number): NodeRenderView {
for (let k = 0; k < this.renderViews.length; k++) {
// if (
// index * 3 >= this.renderViews[k].batchStart &&
// index * 3 < this.renderViews[k].batchEnd
// ) {
// return this.renderViews[k]
// }
const vertIndex = this.boundsTree.geometry.index.array[index * 3]
if (
vertIndex >= this.renderViews[k].vertStart &&
vertIndex < this.renderViews[k].vertEnd
)
return this.renderViews[k]
}
}
public getMaterialAtIndex(index: number): Material {
for (let k = 0; k < this.renderViews.length; k++) {
const vertIndex = this.boundsTree.geometry.index.array[index * 3]
if (
vertIndex >= this.renderViews[k].vertStart &&
vertIndex < this.renderViews[k].vertEnd
) {
const rv = this.renderViews[k]
const group = this.geometry.groups.find((value) => {
return (
rv.batchStart >= value.start &&
rv.batchStart + rv.batchCount <= value.count + value.start
)
})
if (!Array.isArray(this.mesh.material)) {
return this.mesh.material
} else {
if (!group) {
Logger.warn(`Malformed material index!`)
return null
}
return this.mesh.material[group.materialIndex]
}
}
}
}
private makeMeshGeometry(
indices: Uint32Array | Uint16Array,
position: Float64Array,
color?: Float32Array
): BufferGeometry {
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 (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()
Geometry.computeVertexNormals(this.geometry, position)
this.geometry.computeBoundingSphere()
this.geometry.computeBoundingBox()
Viewer.World.expandWorld(this.geometry.boundingBox)
Geometry.updateRTEGeometry(this.geometry, position)
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
}
}