Skip to content

Commit

Permalink
feat(scene-composer): enable accelerated raycasting for 3D Tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
hwandersman committed Feb 12, 2024
1 parent 1d797b1 commit 84d2ce4
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { ThreeEvent, useFrame } from '@react-three/fiber';
import { Object3D } from 'three';

import { MAX_CLICK_DISTANCE } from '../../../common/constants';
import useLifecycleLogging from '../../../logger/react-logger/hooks/useLifecycleLogging';
import { IModelRefComponentInternal, ISceneNodeInternal, useEditorState, useStore } from '../../../store';
import { getComponentGroupName } from '../../../utils/objectThreeUtils';
import { acceleratedRaycasting, getComponentGroupName } from '../../../utils/objectThreeUtils';
import {
findComponentByType,
createNodeWithPositionAndNormal,
Expand Down Expand Up @@ -33,6 +34,14 @@ export const TilesModelComponent: React.FC<TilesModelProps> = ({ node, component
// to clone the model like what we did in GLTFModelComponent. However, if we found this assumption is
// wrong in the future, let's optimize from here.
const tilesRenderer = useTiles(component.uri, uriModifier);

// Enable optimized raycasting
tilesRenderer.onLoadModel = (scene: Object3D) => {
scene.traverse((obj: Object3D) => {
acceleratedRaycasting(obj);
});
};

useFrame(() => {
tilesRenderer.update();
});
Expand Down
2 changes: 1 addition & 1 deletion packages/scene-composer/src/three/tiles3d/TilesRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { CMPTLoader } from '3d-tiles-renderer/src/three/CMPTLoader.js';
import { GLTFExtensionLoader } from '3d-tiles-renderer/src/three/GLTFExtensionLoader.js';
import { TilesGroup } from '3d-tiles-renderer/src/three/TilesGroup.js';
import { Matrix4, Box3, Sphere, Vector3, Vector2, Frustum, LoadingManager } from 'three';
import { raycastTraverse, raycastTraverseFirstHit } from '3d-tiles-renderer/src/three/raycastTraverse.js';
import { readMagicBytes } from '3d-tiles-renderer/src/utilities/readMagicBytes.js';

import { TilesRendererBase } from './TilesRendererBase.js';
import { raycastTraverse, raycastTraverseFirstHit } from './raycastTraverse.js';

const INITIAL_FRUSTUM_CULLED = Symbol('INITIAL_FRUSTUM_CULLED');
const tempMat = new Matrix4();
Expand Down
237 changes: 237 additions & 0 deletions packages/scene-composer/src/three/tiles3d/raycastTraverse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { Matrix4, Sphere, Ray, Vector3 } from 'three';

const _sphere = new Sphere();
const _mat = new Matrix4();
const _vec = new Vector3();
const _vec2 = new Vector3();
const _ray = new Ray();

const _hitArray = [];

function distanceSort( a, b ) {

return a.distance - b.distance;

}

function intersectTileScene( scene, raycaster, intersects ) {

// + Amazon --------
// Call the object's overridden raycast method
scene.traverse(c => c.raycast.call(c, raycaster, intersects));
// - Amazon --------

}

// Returns the closest hit when traversing the tree
export function raycastTraverseFirstHit( root, group, activeTiles, raycaster ) {

// If the root is active make sure we've checked it
if ( activeTiles.has( root ) ) {

intersectTileScene( root.cached.scene, raycaster, _hitArray );

if ( _hitArray.length > 0 ) {

if ( _hitArray.length > 1 ) {

_hitArray.sort( distanceSort );

}

const res = _hitArray[ 0 ];
_hitArray.length = 0;
return res;

} else {

return null;

}

}

// TODO: can we avoid creating a new array here every time to save on memory?
const array = [];
const children = root.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {

const tile = children[ i ];
const cached = tile.cached;
const groupMatrixWorld = group.matrixWorld;

_mat.copy( groupMatrixWorld );

// if we don't hit the sphere then early out
const sphere = cached.sphere;
if ( sphere ) {

_sphere.copy( sphere );
_sphere.applyMatrix4( _mat );
if ( ! raycaster.ray.intersectsSphere( _sphere ) ) {

continue;

}

}

// TODO: check region?

const boundingBox = cached.box;
const obbMat = cached.boxTransform;
if ( boundingBox ) {

_mat.multiply( obbMat ).invert();
_ray.copy( raycaster.ray );
_ray.applyMatrix4( _mat );
if ( _ray.intersectBox( boundingBox, _vec ) ) {

// account for tile scale
_vec2.setFromMatrixScale( _mat );
const invScale = _vec2.x;

if ( Math.abs( Math.max( _vec2.x - _vec2.y, _vec2.x - _vec2.z ) ) > 1e-6 ) {

console.warn( 'ThreeTilesRenderer : Non uniform scale used for tile which may cause issues when raycasting.' );

}

// if we intersect the box save the distance to the tile bounds
const data = {
distance: Infinity,
tile: null
};
array.push( data );

data.distance = _vec.distanceToSquared( _ray.origin ) * invScale * invScale;
data.tile = tile;

} else {

continue;

}

}

}

// sort them by ascending distance
array.sort( distanceSort );

// traverse until we find the best hit and early out if a tile bounds
// couldn't possible include a best hit
let bestDistanceSquared = Infinity;
let bestHit = null;
for ( let i = 0, l = array.length; i < l; i ++ ) {

const data = array[ i ];
const distanceSquared = data.distance;
if ( distanceSquared > bestDistanceSquared ) {

break;

} else {

const tile = data.tile;
const scene = tile.cached.scene;

let hit = null;
if ( activeTiles.has( tile ) ) {

// save the hit if it's closer
intersectTileScene( scene, raycaster, _hitArray );
if ( _hitArray.length > 0 ) {

if ( _hitArray.length > 1 ) {

_hitArray.sort( distanceSort );

}

hit = _hitArray[ 0 ];

}

} else {

hit = raycastTraverseFirstHit( tile, group, activeTiles, raycaster );

}

if ( hit ) {

const hitDistanceSquared = hit.distance * hit.distance;
if ( hitDistanceSquared < bestDistanceSquared ) {

bestDistanceSquared = hitDistanceSquared;
bestHit = hit;

}
_hitArray.length = 0;

}

}

}

return bestHit;

}

export function raycastTraverse( tile, group, activeTiles, raycaster, intersects ) {

const cached = tile.cached;
const groupMatrixWorld = group.matrixWorld;

_mat.copy( groupMatrixWorld );

// Early out if we don't hit this tile sphere
const sphere = cached.sphere;
if ( sphere ) {

_sphere.copy( sphere );
_sphere.applyMatrix4( _mat );
if ( ! raycaster.ray.intersectsSphere( _sphere ) ) {

return;

}

}

// Early out if we don't this this tile box
const boundingBox = cached.box;
const obbMat = cached.boxTransform;
if ( boundingBox ) {

_mat.multiply( obbMat ).invert();
_ray.copy( raycaster.ray ).applyMatrix4( _mat );
if ( ! _ray.intersectsBox( boundingBox ) ) {

return;

}

}

// TODO: check region

const scene = cached.scene;
if ( activeTiles.has( tile ) ) {

intersectTileScene( scene, raycaster, intersects );
return;

}

const children = tile.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {

raycastTraverse( children[ i ], group, activeTiles, raycaster, intersects );

}

}
1 change: 1 addition & 0 deletions packages/scene-composer/src/utils/objectThreeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function cloneMaterials(obj: THREE.Object3D) {
}
}

// Only run once on model load
export function acceleratedRaycasting(obj: THREE.Object3D) {
if (obj instanceof THREE.Mesh) {
const mesh = obj;
Expand Down

0 comments on commit 84d2ce4

Please sign in to comment.