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
This commit is contained in:
Alexandru Popovici
2025-10-07 09:46:53 +03:00
committed by GitHub
parent 87f3096892
commit ac6d0d892e
10 changed files with 186 additions and 41 deletions
+1 -1
View File
@@ -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()
-1
View File
@@ -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)
+44 -2
View File
@@ -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
@@ -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))
)
}
@@ -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(),
@@ -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()
}
@@ -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
}
}
}
}
@@ -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
@@ -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
@@ -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)