From ac6d0d892eb11540f57ce4aca0217df399e3f3cd Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Tue, 7 Oct 2025 09:46:53 +0300 Subject: [PATCH] Support for visible bounds (#5466) * feat(viewer-lib): Added visible bounds for mesh batches * feat(viewer-lib): Single visible scene box implementation and renderer level. Updated some internals related to use the visible box instead of the whole box. ExplodeExtension and default zooming to extents now uses the visible box * Updates regarding batches and visible range management and reporting - Added stardard functions for All and None batch update ranges for visibility - Fixed an issue with mesh batch where draw ranges where not reshuffled when there was onyl a single hidden draw group - Fixed an issue with instanced mesh batch where hidden groups were not properly handled all the time - Line batch now holds a range visibility map and can now properly report if the entire batch is visible or not. Still no per object visibility reporting as it's less feasible with the current line batch rendering approach but posssible if really required * feat(viewer-lib): Text batches now also report visible ranges unitarily * chore(viewer-lib): Fixed compiler error --- packages/viewer-sandbox/src/Sandbox.ts | 2 +- packages/viewer-sandbox/src/main.ts | 1 - .../viewer/src/modules/SpeckleRenderer.ts | 46 +++++++++++++++- packages/viewer/src/modules/batching/Batch.ts | 15 ++++++ .../modules/batching/InstancedMeshBatch.ts | 26 ++++++++- .../viewer/src/modules/batching/LineBatch.ts | 54 ++++++++++++------- .../viewer/src/modules/batching/MeshBatch.ts | 23 ++++---- .../src/modules/batching/PrimitiveBatch.ts | 3 +- .../viewer/src/modules/batching/TextBatch.ts | 13 ++++- .../modules/extensions/ExplodeExtension.ts | 44 +++++++++++++-- 10 files changed, 186 insertions(+), 41 deletions(-) diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index e38feee51..7723c308c 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -429,7 +429,7 @@ export default class Sandbox { this.selectionList.map((val) => val.hits[0].node.model.raw.id) as string[] ) if (!box) { - box = this.viewer.getRenderer().sceneBox + box = this.viewer.getRenderer().visibleSceneBox } this.viewer.getExtension(SectionTool).setBox(box) this.viewer.getExtension(SectionTool).toggle() diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index d76656d57..771196e50 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -56,7 +56,6 @@ const createViewer = async (containerName: string, _stream: string) => { const boxSelect = viewer.createExtension(BoxSelection) boxSelect.realtimeSelection = false viewer.createExtension(PassReader) - // viewer.createExtension(SectionCaps) const sandbox = new Sandbox(controlsContainer, viewer, multiSelectList) diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index e72bfc8aa..e0b543944 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -188,6 +188,48 @@ export default class SpeckleRenderer { return bounds } + public get visibleSceneBox(): Box3 { + const bounds: Box3 = new Box3() + const batches = this.batcher.getBatches() + for (let k = 0; k < batches.length; k++) { + const batch = batches[k] + const rvs = batch.renderViews.slice() + rvs.sort((a, b) => { + return a.batchStart - b.batchStart + }) + let batchObjects = null + if (isAcceleratedBatchType(batch)) { + batchObjects = batch.mesh.batchObjects.slice() + batchObjects.sort((a, b) => { + return a.renderView.batchStart - b.renderView.batchStart + }) + } + + const visibleRange = batch.getVisibleRange() + let lo = 0, + hi = rvs.length + while (lo < hi) { + const mid = (lo + hi) >>> 1 + if (rvs[mid].batchStart < visibleRange.offset) lo = mid + 1 + else hi = mid + } + + const qStart = visibleRange.offset + const qEnd = visibleRange.offset + visibleRange.count + + for (; lo < rvs.length; lo++) { + const s = rvs[lo] + const b = batchObjects ? batchObjects[lo] : null + if (s.batchStart >= qEnd) break + const sEnd = s.batchStart + s.batchCount + if (s.batchStart >= qStart && sEnd <= qEnd) { + bounds.union(b ? b.aabb : s.aabb) + } + } + } + return bounds + } + public get sceneSphere(): Sphere { return this.sceneBox.getBoundingSphere(new Sphere()) } @@ -199,7 +241,7 @@ export default class SpeckleRenderer { public get clippingVolume(): OBB { return !this._clippingVolume.isEmpty() && this._renderer.localClippingEnabled ? this._clippingVolume - : new OBB().fromBox3(this.sceneBox) + : new OBB().fromBox3(this.visibleSceneBox) } public set clippingVolume(box: Box3 | OBB) { @@ -1215,7 +1257,7 @@ export default class SpeckleRenderer { rvs.push(...this.tree.getRenderTree().getRenderViewsForNode(node)) }) } - } else box = this.sceneBox + } else box = this.visibleSceneBox for (let k = 0; k < rvs.length; k++) { const object = this.getObject(rvs[k]) const aabb = object ? object.aabb : rvs[k].aabb diff --git a/packages/viewer/src/modules/batching/Batch.ts b/packages/viewer/src/modules/batching/Batch.ts index e0b4ba006..dbeba473e 100644 --- a/packages/viewer/src/modules/batching/Batch.ts +++ b/packages/viewer/src/modules/batching/Batch.ts @@ -91,3 +91,18 @@ export function isAcceleratedBatchType(batch: Batch): batch is AcceleratedBatchT batch.geometryType === GeometryType.TEXT) ) } + +export function isNoneBatchUpdateRange(range: BatchUpdateRange) { + return ( + range.offset === NoneBatchUpdateRange.offset && + range.count === NoneBatchUpdateRange.count + ) +} + +export function isAllBatchUpdateRange(range: BatchUpdateRange, totalCount?: number) { + return ( + range.offset === AllBatchUpdateRange.offset && + (range.count === AllBatchUpdateRange.count || + (totalCount ? range.count === totalCount : true)) + ) +} diff --git a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts index 1f0de3a0c..40b777cc4 100644 --- a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts +++ b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts @@ -19,6 +19,7 @@ import { type DrawGroup, GeometryType, INSTANCE_TRANSFORM_BUFFER_STRIDE, + isNoneBatchUpdateRange, NoneBatchUpdateRange } from './Batch.js' import SpeckleInstancedMesh from '../objects/SpeckleInstancedMesh.js' @@ -139,7 +140,7 @@ export class InstancedMeshBatch implements Batch { /** Note: You can only set visibility on ranges that exist as draw groups! */ public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ - if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { + if (ranges.length === 1 && isNoneBatchUpdateRange(ranges[0])) { this.mesh.children.forEach((instance) => (instance.visible = false)) return } @@ -324,9 +325,32 @@ export class InstancedMeshBatch implements Batch { } this.setBatchBuffers(ranges) this.cleanMaterials() + const transparentDepthHiddenGroup = this.groups.find( + (value) => + this.materials[value.materialIndex].transparent === true || + this.materials[value.materialIndex].visible === false || + this.materials[value.materialIndex].colorWrite === false + ) /** We shuffle only when above a certain fragmentation threshold. We don't want to be shuffling every single time */ if (this.drawCalls > this.maxDrawCalls) { this.needsShuffle = true + } else if (transparentDepthHiddenGroup) { + if (this.groups.length === 1) this.needsShuffle = true + else { + for ( + let k = this.groups.indexOf(transparentDepthHiddenGroup); + k < this.groups.length; + k++ + ) { + const material = this.materials[this.groups[k].materialIndex] + if (material.visible) { + if (!material.transparent || material.colorWrite) { + this.needsShuffle = true + break + } + } + } + } } else this.mesh.updateDrawGroups( this.getCurrentTransformBuffer(), diff --git a/packages/viewer/src/modules/batching/LineBatch.ts b/packages/viewer/src/modules/batching/LineBatch.ts index 74bfa3a2e..8c0557644 100644 --- a/packages/viewer/src/modules/batching/LineBatch.ts +++ b/packages/viewer/src/modules/batching/LineBatch.ts @@ -18,14 +18,18 @@ import { AllBatchUpdateRange, type Batch, type BatchUpdateRange, - type DrawGroup, + DrawGroup, GeometryType, + isAllBatchUpdateRange, + isNoneBatchUpdateRange, NoneBatchUpdateRange } from './Batch.js' import { ObjectLayers } from '../../IViewer.js' import Materials from '../materials/Materials.js' import { ChunkArray } from '../converter/VirtualArray.js' +const vec4Buffer = new Vector4() + export default class LineBatch implements Batch { public id: string public subtreeId: string @@ -37,7 +41,7 @@ export default class LineBatch implements Batch { protected mesh: LineSegments2 public colorBuffer: InstancedInterleavedBuffer - private static readonly vector4Buffer: Vector4 = new Vector4() + protected visibilityRanges: { [offset: number]: boolean } = {} public get bounds(): Box3 { if (!this.geometry.boundingBox) this.geometry.computeBoundingBox() @@ -65,6 +69,11 @@ export default class LineBatch implements Batch { this.subtreeId = subtreeId this.renderViews = renderViews } + + get groups(): DrawGroup[] { + return [] + } + public get pointCount(): number { return 0 } @@ -88,10 +97,6 @@ export default class LineBatch implements Batch { return this.mesh.material as unknown as Material[] } - public get groups(): DrawGroup[] { - return [] - } - public getCount(): number { return this.geometry.attributes.position.array.length / 6 } @@ -112,23 +117,16 @@ export default class LineBatch implements Batch { } public setVisibleRange(ranges: BatchUpdateRange[]) { - if ( - ranges.length === 1 && - ranges[0].offset === NoneBatchUpdateRange.offset && - ranges[0].count === NoneBatchUpdateRange.count - ) { + if (ranges.length === 1 && isNoneBatchUpdateRange(ranges[0])) { this.mesh.visible = false return } - if ( - ranges.length === 1 && - ranges[0].offset === AllBatchUpdateRange.offset && - ranges[0].count === AllBatchUpdateRange.count - ) { + if (ranges.length === 1 && isAllBatchUpdateRange(ranges[0], this.getCount())) { this.mesh.visible = true return } + this.mesh.visible = true const data = this.colorBuffer.array as number[] for (let k = 0; k < data.length; k += 4) { @@ -150,9 +148,11 @@ export default class LineBatch implements Batch { this.geometry.attributes['instanceColorEnd'].needsUpdate = true } + /** Line batches do not sort their ranges. This means we can have hidden/transparent objects anywhere inside the batch. + */ public getVisibleRange() { + if (!this.mesh.visible) return NoneBatchUpdateRange return AllBatchUpdateRange - // TO DO if required } public getOpaque(): BatchUpdateRange { @@ -199,17 +199,26 @@ export default class LineBatch implements Batch { ranges[i].offset * this.colorBuffer.stride + ranges[i].count * this.colorBuffer.stride - LineBatch.vector4Buffer.set(color.r, color.g, color.b, alpha) + vec4Buffer.set(color.r, color.g, color.b, alpha) this.updateColorBuffer( start, ranges[i].count === Infinity ? this.colorBuffer.array.length : len, - LineBatch.vector4Buffer + vec4Buffer ) + this.visibilityRanges[ranges[i].offset] = material.visible } this.colorBuffer.updateRange = { offset: 0, count: data.length } this.colorBuffer.needsUpdate = true this.geometry.attributes['instanceColorStart'].needsUpdate = true this.geometry.attributes['instanceColorEnd'].needsUpdate = true + + const visibility = Object.values(this.visibilityRanges) + let anyVisible = false + for (let k = 0; k < visibility.length; k++) { + anyVisible ||= visibility[k] + } + if (anyVisible) this.setVisibleRange([AllBatchUpdateRange]) + else this.setVisibleRange([NoneBatchUpdateRange]) } public setDrawRanges(ranges: BatchUpdateRange[]) { @@ -220,14 +229,16 @@ export default class LineBatch implements Batch { this.setDrawRanges([ { offset: 0, - count: Infinity, + count: this.getCount(), material: this.batchMaterial } ]) + this.mesh.material = this.batchMaterial this.mesh.visible = true this.batchMaterial.transparent = this.batchTransparent this.batchMaterial.opacity = this.batchOpacity + this.visibilityRanges = { 0: this.batchMaterial.visible } } public buildBatch() { @@ -304,6 +315,9 @@ export default class LineBatch implements Batch { this.mesh.uuid = this.id this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_LINE) + + this.visibilityRanges = { 0: this.batchMaterial.visible } + return Promise.resolve() } diff --git a/packages/viewer/src/modules/batching/MeshBatch.ts b/packages/viewer/src/modules/batching/MeshBatch.ts index 261be8de9..e60473044 100644 --- a/packages/viewer/src/modules/batching/MeshBatch.ts +++ b/packages/viewer/src/modules/batching/MeshBatch.ts @@ -168,16 +168,19 @@ export class MeshBatch extends PrimitiveBatch { ) if (transparentDepthHiddenGroup) { - for ( - let k = this.groups.indexOf(transparentDepthHiddenGroup); - k < this.groups.length; - k++ - ) { - const material = this.materials[this.groups[k].materialIndex] - if (material.visible) { - if (!material.transparent || material.colorWrite) { - this.needsShuffle = true - break + if (this.groups.length === 1) this.needsShuffle = true + else { + for ( + let k = this.groups.indexOf(transparentDepthHiddenGroup); + k < this.groups.length; + k++ + ) { + const material = this.materials[this.groups[k].materialIndex] + if (material.visible) { + if (!material.transparent || material.colorWrite) { + this.needsShuffle = true + break + } } } } diff --git a/packages/viewer/src/modules/batching/PrimitiveBatch.ts b/packages/viewer/src/modules/batching/PrimitiveBatch.ts index 6e179f1be..70b933d73 100644 --- a/packages/viewer/src/modules/batching/PrimitiveBatch.ts +++ b/packages/viewer/src/modules/batching/PrimitiveBatch.ts @@ -5,6 +5,7 @@ import { type Batch, type BatchUpdateRange, GeometryType, + isNoneBatchUpdateRange, NoneBatchUpdateRange } from './Batch.js' import { type DrawGroup } from './Batch.js' @@ -85,7 +86,7 @@ export abstract class PrimitiveBatch implements Batch { public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ - if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { + if (ranges.length === 1 && isNoneBatchUpdateRange(ranges[0])) { this.primitive.geometry.setDrawRange(0, 0) /** We unset the 'visible' flag, otherwise three.js will still run pointless buffer binding commands*/ this.primitive.visible = false diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index 4e836c7ea..4450a7f42 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -112,10 +112,19 @@ export default class TextBatch implements Batch { public setVisibleRange(ranges: BatchUpdateRange[]) { ranges - // TO DO } + /* I hate how brittle Troika is. **Everything** you touch breaks shit + * We can't actually use the 'visible' property inherited from Mesh, because it just breaks the text batch + */ public getVisibleRange(): BatchUpdateRange { + if (this.mesh.groups.length === 1) { + const group = this.mesh.groups[0] + if (!this.materials[group.materialIndex].visible) { + return NoneBatchUpdateRange + } + } + return AllBatchUpdateRange } @@ -144,7 +153,7 @@ export default class TextBatch implements Batch { /** Text batches are mix between how mesh and line batches work. * - They still keep track of various draw groups each with it's material - * - However that material is not really being used, bur rather the properies are copied over to the batch fp32 data texture + * - However that material is not really being used, but rather the properies are copied over to the batch fp32 data texture * - For filtering we cheat and use `SpeckleTextColoredMaterial` only to store the gradient/ramp texture + gradient indices for each text in the batch * - The color from the gradient/ramp texture will be used only if the gradient index > 0, otherwise the regular color will be used * - The gradient index is stored in each text object in it's `userData` and written to the 27'th float in the batch data texture, where the shader reads if from diff --git a/packages/viewer/src/modules/extensions/ExplodeExtension.ts b/packages/viewer/src/modules/extensions/ExplodeExtension.ts index 782c0b739..92094a027 100644 --- a/packages/viewer/src/modules/extensions/ExplodeExtension.ts +++ b/packages/viewer/src/modules/extensions/ExplodeExtension.ts @@ -1,4 +1,4 @@ -import { Vector3 } from 'three' +import { Box3, Vector3 } from 'three' import { Extension } from './Extension.js' import { UpdateFlags } from '../../IViewer.js' @@ -20,8 +20,44 @@ export class ExplodeExtension extends Extension { this._enabled = value } + /** Similar to SpeckleRenderer's visibleSceneBox, but with static boxes from render views */ + public get visibleWorld(): Box3 { + const bounds: Box3 = new Box3() + const batches = this.viewer.getRenderer().batcher.getBatches() + for (let k = 0; k < batches.length; k++) { + const batch = batches[k] + const rvs = batch.renderViews.slice() + rvs.sort((a, b) => { + return a.batchStart - b.batchStart + }) + + const visibleRange = batch.getVisibleRange() + let lo = 0, + hi = rvs.length + while (lo < hi) { + const mid = (lo + hi) >>> 1 + if (rvs[mid].batchStart < visibleRange.offset) lo = mid + 1 + else hi = mid + } + + const qStart = visibleRange.offset + const qEnd = visibleRange.offset + visibleRange.count + + for (; lo < rvs.length; lo++) { + const s = rvs[lo] + if (s.batchStart >= qEnd) break + const sEnd = s.batchStart + s.batchCount + if (s.batchStart >= qStart && sEnd <= qEnd) { + bounds.union(s.aabb) + } + } + } + return bounds + } + private explodeTime = -1 private explodeRange = 0 + private explodeOrigin: Vector3 = new Vector3() public onEarlyUpdate() { if (!this._enabled) return @@ -32,9 +68,11 @@ export class ExplodeExtension extends Extension { } } public setExplode(time: number) { - const size = this.viewer.World.worldSize + const visibleWorld = this.visibleWorld + const size = visibleWorld.getSize(new Vector3()) this.explodeTime = time this.explodeRange = Math.sqrt(size.x * size.x + size.y * size.y + size.z * size.z) + visibleWorld.getCenter(this.explodeOrigin) } private explode(time: number, range: number) { @@ -42,7 +80,7 @@ export class ExplodeExtension extends Extension { const vecBuff = new Vector3() for (let i = 0; i < objects.length; i++) { const center = objects[i].aabb.getCenter(vecBuff) - const dir = center.sub(this.viewer.World.worldOrigin) + const dir = center.sub(this.explodeOrigin) dir.normalize().multiplyScalar(time * range) objects[i].transformTRS(dir, undefined, undefined, undefined)