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)