Skip to content

Commit

Permalink
Fixed TAS Shapecast (#3661)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AlexandruPopovici authored Dec 10, 2024
1 parent 8d0cbad commit e7613a6
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 32 deletions.
42 changes: 31 additions & 11 deletions packages/viewer-sandbox/src/Extensions/BoxSelection.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -27,8 +26,9 @@ export class BoxSelection extends Extension {

private dragging = false
private frameLock = false
private _realTimeSelection = true

private idsToSelect: Array<string> | null = []
private idsToSelect: Set<string> | null = new Set()

get enabled(): boolean {
return this._enabled
Expand All @@ -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 */
Expand All @@ -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()
}
Expand All @@ -72,14 +76,22 @@ 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 */
this.dragBoxMaterial.uniforms.transform.value = new Matrix4().makeScale(0, 0, 0)
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()
}

Expand All @@ -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 */
Expand All @@ -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<NodeRenderView> = []
const selection: Set<string> = 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
},
Expand All @@ -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 */
Expand All @@ -153,7 +173,7 @@ export class BoxSelection extends Extension {
}
})
}
return selectionRvs.map((rv: NodeRenderView) => rv.renderData.id)
return selection
}

/** Buffers for reading/writing */
Expand Down
10 changes: 7 additions & 3 deletions packages/viewer-sandbox/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>(containerName)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -450,6 +452,8 @@ const getStream = () => {

// Perfectly flat
// 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e'

// 'https://speckle.xyz/streams/27e89d0ad6/commits/5ed4b74252'
)
}

Expand Down
16 changes: 14 additions & 2 deletions packages/viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -165,6 +172,8 @@ export {
LineBatch,
PointBatch,
TextBatch,
AccelerationStructure,
TopLevelAccelerationStructure,
SpeckleStandardMaterial,
SpeckleBasicMaterial,
SpeckleTextMaterial,
Expand Down Expand Up @@ -209,7 +218,10 @@ export {
ViewMode,
FilterMaterial,
FilterMaterialType,
FilterMaterialOptions
FilterMaterialOptions,
NOT_INTERSECTED,
INTERSECTED,
CONTAINED
}

export type {
Expand Down
5 changes: 4 additions & 1 deletion packages/viewer/src/modules/objects/SpeckleRaycaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<BatchObject>()
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
}
})
Expand Down

0 comments on commit e7613a6

Please sign in to comment.