From e7613a60d7e017317ba4415b87b139170eafaa2c Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Tue, 10 Dec 2024 11:29:05 +0200 Subject: [PATCH] Fixed TAS Shapecast (#3661) * Fixed the issue where TAS only shapecasts were not running properly on all batch objects, missing some of them. Some small updates to BoxSelection * Improved shapecast travesal time 4X. Only passing batch objects only once to intersectTASRange and also added the builtin CONTAINED acceleration to bounds testing. * Some critical typescript errors * Added missing types from export --- .../src/Extensions/BoxSelection.ts | 42 +++++++++++----- packages/viewer-sandbox/src/main.ts | 10 ++-- packages/viewer/src/index.ts | 16 +++++- .../src/modules/objects/SpeckleRaycaster.ts | 5 +- .../objects/TopLevelAccelerationStructure.ts | 49 +++++++++++++------ 5 files changed, 90 insertions(+), 32 deletions(-) diff --git a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts index b0fa82b7ad..e2d57f883d 100644 --- a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { InputEvent } from '@speckle/viewer' +import { CONTAINED, InputEvent } from '@speckle/viewer' import { ObjectLayers } from '@speckle/viewer' -import { NodeRenderView } from '@speckle/viewer' import { SelectionExtension } from '@speckle/viewer' import { BatchObject } from '@speckle/viewer' import { Extension, IViewer, GeometryType, CameraController } from '@speckle/viewer' @@ -27,8 +26,9 @@ export class BoxSelection extends Extension { private dragging = false private frameLock = false + private _realTimeSelection = true - private idsToSelect: Array | null = [] + private idsToSelect: Set | null = new Set() get enabled(): boolean { return this._enabled @@ -37,6 +37,10 @@ export class BoxSelection extends Extension { this._enabled = value } + set realtimeSelection(value: boolean) { + this._realTimeSelection = value + } + public constructor(viewer: IViewer, private cameraController: CameraController) { super(viewer) /** Get the SelectionExtension. We'll need it to remotely enable/disable it */ @@ -53,10 +57,10 @@ export class BoxSelection extends Extension { } public onEarlyUpdate() { - if (this.idsToSelect) { + if (this.idsToSelect?.size) { /** Send the ids to the selection extension to be selected */ this.selectionExtension.clearSelection() - this.selectionExtension.selectObjects(this.idsToSelect, true) + this.selectionExtension.selectObjects(Array.from(this.idsToSelect), true) this.idsToSelect = null this.viewer.requestRender() } @@ -72,7 +76,7 @@ export class BoxSelection extends Extension { } } - private onPointerUp() { + private onPointerUp(e: Vector2 & { event: PointerEvent }) { /** Re-enable the camera controller */ this.cameraController.enabled = true /** Hide the selection box */ @@ -80,6 +84,14 @@ export class BoxSelection extends Extension { this.dragBoxMaterial.needsUpdate = true this.dragging = false + + if (!this._realTimeSelection && e.event.altKey) { + /** Get the ids of objects that fall withing the selection box */ + this.idsToSelect = this.getSelectionIds(this.ndcBox) + } + + this.ndcBox.makeEmpty() + this.viewer.requestRender() } @@ -101,9 +113,13 @@ export class BoxSelection extends Extension { this.ndcBox.max.set(1, 1, 0) this.ndcBox.applyMatrix4(ndcTransform) - /** Get the ids of objects that fall withing the selection box */ - this.idsToSelect = this.getSelectionIds(this.ndcBox) + if (this._realTimeSelection) { + /** Get the ids of objects that fall withing the selection box */ + this.idsToSelect = this.getSelectionIds(this.ndcBox) + } + this.frameLock = true + this.viewer.requestRender() } /** Gets the object ids that fall withing the provided selection box */ @@ -124,13 +140,16 @@ export class BoxSelection extends Extension { /** We're using three-mesh-bvh library for out BVH * Go over each batch and test it against the TAS only. **/ - const selectionRvs: Array = [] + const selection: Set = new Set() for (let b = 0; b < batches.length; b++) { batches[b].mesh.TAS.shapecast({ /** This is the callback from the TAS's bounds internal nodes */ intersectsTAS: (box: Box3) => { /** We continue traversion only if the selection box intersects an internal node */ const ndcBox = this.worldBoxToNDC(box, clipMatrix) + if (selectionBox.containsBox(ndcBox)) { + return CONTAINED + } const ret = selectionBox.intersectsBox(ndcBox) return ret }, @@ -140,7 +159,8 @@ export class BoxSelection extends Extension { const ndcBox = this.worldBoxToNDC(objectBox, clipMatrix) /** We consider an object selected only it's NDC AABB is contained in the selection box */ if (selectionBox.containsBox(ndcBox)) - selectionRvs.push(batchObject.renderView) + selection.add(batchObject.renderView.renderData.id) + /** We always return false here because we don't want to continue intersecting batch object triangles. */ return false }, /** This is the callback from the BAS bounds internal nodes */ @@ -153,7 +173,7 @@ export class BoxSelection extends Extension { } }) } - return selectionRvs.map((rv: NodeRenderView) => rv.renderData.id) + return selection } /** Buffers for reading/writing */ diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index b8e50f6775..a473e0716f 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -20,6 +20,7 @@ import { import { SectionTool } from '@speckle/viewer' import { SectionOutlines } from '@speckle/viewer' import { ViewModesKeys } from './Extensions/ViewModesKeys' +import { BoxSelection } from './Extensions/BoxSelection' const createViewer = async (containerName: string, stream: string) => { const container = document.querySelector(containerName) @@ -53,7 +54,8 @@ const createViewer = async (containerName: string, stream: string) => { const diff = viewer.createExtension(DiffExtension) viewer.createExtension(ViewModes) viewer.createExtension(ViewModesKeys) - // const boxSelect = viewer.createExtension(BoxSelection) + const boxSelect = viewer.createExtension(BoxSelection) + boxSelect.realtimeSelection = false // const rotateCamera = viewer.createExtension(RotateCamera) cameraController // use it selection // use it @@ -108,12 +110,12 @@ const getStream = () => { // prettier-ignore // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) - 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' + // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d' // 'Super' heavy revit shit - // 'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5' + 'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5' // IFC building (good for a tree based structure) // 'https://latest.speckle.systems/streams/92b620fb17/commits/2ebd336223' // IFC story, a subtree of the above @@ -450,6 +452,8 @@ const getStream = () => { // Perfectly flat // 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e' + + // 'https://speckle.xyz/streams/27e89d0ad6/commits/5ed4b74252' ) } diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 182a7bd2b7..0a558915b5 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -77,7 +77,12 @@ import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial.js' import { SpeckleText } from './modules/objects/SpeckleText.js' import { NodeRenderView } from './modules/tree/NodeRenderView.js' -import { type ExtendedIntersection } from './modules/objects/SpeckleRaycaster.js' +import { + CONTAINED, + INTERSECTED, + NOT_INTERSECTED, + type ExtendedIntersection +} from './modules/objects/SpeckleRaycaster.js' import { SpeckleGeometryConverter } from './modules/loaders/Speckle/SpeckleGeometryConverter.js' import { Assets } from './modules/Assets.js' import { InstancedBatchObject } from './modules/batching/InstancedBatchObject.js' @@ -124,6 +129,8 @@ import { FilterMaterialOptions, FilterMaterialType } from './modules/materials/Materials.js' +import { AccelerationStructure } from './modules/objects/AccelerationStructure.js' +import { TopLevelAccelerationStructure } from './modules/objects/TopLevelAccelerationStructure.js' export { Viewer, @@ -165,6 +172,8 @@ export { LineBatch, PointBatch, TextBatch, + AccelerationStructure, + TopLevelAccelerationStructure, SpeckleStandardMaterial, SpeckleBasicMaterial, SpeckleTextMaterial, @@ -209,7 +218,10 @@ export { ViewMode, FilterMaterial, FilterMaterialType, - FilterMaterialOptions + FilterMaterialOptions, + NOT_INTERSECTED, + INTERSECTED, + CONTAINED } export type { diff --git a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts index f45108b4f9..46f9a10657 100644 --- a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts +++ b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts @@ -13,6 +13,9 @@ import { ObjectLayers } from '../../IViewer.js' import SpeckleMesh from './SpeckleMesh.js' import SpeckleInstancedMesh from './SpeckleInstancedMesh.js' +export const NOT_INTERSECTED: ShapecastIntersection = 0 +export const INTERSECTED: ShapecastIntersection = 1 +export const CONTAINED: ShapecastIntersection = 2 export type ExtendedShapeCastCallbacks = { intersectsTAS?: ( box: Box3, @@ -21,7 +24,7 @@ export type ExtendedShapeCastCallbacks = { depth: number, nodeIndex: number ) => ShapecastIntersection | boolean - intersectTASRange?: (batchObject: BatchObject) => ShapecastIntersection | boolean + intersectTASRange?: (batchObjects: BatchObject) => ShapecastIntersection | boolean intersectsBounds: ( box: Box3, isLeaf: boolean, diff --git a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts index 9f1f0be353..76391b5dc0 100644 --- a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts @@ -9,7 +9,7 @@ import { Side, Vector3 } from 'three' -import { MeshBVHVisualizer } from 'three-mesh-bvh' +import { MeshBVHVisualizer, ShapecastIntersection } from 'three-mesh-bvh' import { BatchObject } from '../batching/BatchObject.js' import { ExtendedTriangle, HitPointInfo } from 'three-mesh-bvh' import type { @@ -294,28 +294,47 @@ export class TopLevelAccelerationStructure { } let ret = false + /** We only call intersectTASRange once for each batch object. */ + const visitedObjects: { [id: string]: boolean | ShapecastIntersection } = {} this.accelerationStructure.shapecast({ intersectsBounds: (box, isLeaf, score, depth, nodeIndex) => { - if (callbacks.intersectsTAS) + if (callbacks.intersectsTAS) { return callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex) + } return false }, - intersectsRange: (triangleOffset: number) => { + intersectsRange: (triangleOffset: number, triangleCount: number) => { /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ - const indexBufferAttribute: BufferAttribute = this.accelerationStructure - .geometry.index as BufferAttribute - const vertIndex = indexBufferAttribute.array[triangleOffset * 3] - const batchObjectIndex = Math.trunc( - vertIndex / TopLevelAccelerationStructure.CUBE_VERTS - ) - if (callbacks.intersectTASRange) { - const ret = callbacks.intersectTASRange(this.batchObjects[batchObjectIndex]) - if (!ret) return false + const batchObjects = new Set() + for (let k = 0; k < triangleCount; k++) { + const indexBufferAttribute: BufferAttribute = this.accelerationStructure + .geometry.index as BufferAttribute + const vertIndex = indexBufferAttribute.array[triangleOffset * 3 + k * 3] + const batchObjectIndex = Math.trunc( + vertIndex / TopLevelAccelerationStructure.CUBE_VERTS + ) + const batchObject = this.batchObjects[batchObjectIndex] + if (callbacks.intersectTASRange) { + if (visitedObjects[batchObject.renderView.renderData.id] !== undefined) + continue + + const ret = callbacks.intersectTASRange(batchObject) + visitedObjects[batchObject.renderView.renderData.id] = ret + if (ret) batchObjects.add(batchObject) + } else { + batchObjects.add(batchObject) + } + } + /** No batch object selected, stop here */ + if (!batchObjects.size) return false + + for (const batchObject of batchObjects) { + ret ||= batchObject.accelerationStructure.shapecast( + wrapCallbacks(batchObject) + ) } - ret ||= this.batchObjects[batchObjectIndex].accelerationStructure.shapecast( - wrapCallbacks(this.batchObjects[batchObjectIndex]) - ) + /** We never test agains the TAS triangles because there is no point. Traversal stops here */ return false } })