From a00ab2eea30473a40af6fb547a391aa130c3590d Mon Sep 17 00:00:00 2001 From: Dave Buchhofer Date: Thu, 30 Dec 2021 11:31:36 -0500 Subject: [PATCH 1/3] [type] - add missing optional transform to base tile --- src/base/TileBase.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/base/TileBase.d.ts b/src/base/TileBase.d.ts index ccff0af4d..f71c6e421 100644 --- a/src/base/TileBase.d.ts +++ b/src/base/TileBase.d.ts @@ -59,4 +59,6 @@ export interface TileBase { refine?: 'REPLACE' | 'ADD'; + transform?: number[]; + } From 1e8e53b8413ac5e7cacada452afa0a9c3ab21a1a Mon Sep 17 00:00:00 2001 From: Dave Buchhofer Date: Tue, 28 Dec 2021 12:46:00 -0500 Subject: [PATCH 2/3] [util] - expose isTileDownloadFinished as utility function allows a way to get overall progress via traverse --- src/base/traverseFunctions.d.ts | 3 +++ src/base/traverseFunctions.js | 10 +++++----- src/index.d.ts | 1 + src/index.js | 3 +++ 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/base/traverseFunctions.d.ts diff --git a/src/base/traverseFunctions.d.ts b/src/base/traverseFunctions.d.ts new file mode 100644 index 000000000..6b79addfd --- /dev/null +++ b/src/base/traverseFunctions.d.ts @@ -0,0 +1,3 @@ +import { Tile } from './Tile'; + +export function isTileDownloadFinished( tile: Tile ): boolean; diff --git a/src/base/traverseFunctions.js b/src/base/traverseFunctions.js index 64bdc5997..9269eb61c 100644 --- a/src/base/traverseFunctions.js +++ b/src/base/traverseFunctions.js @@ -1,8 +1,8 @@ import { LOADED, FAILED } from './constants.js'; -function isDownloadFinished( value ) { +export function isTileDownloadFinished( tile ) { - return value === LOADED || value === FAILED; + return tile.__loadingState === LOADED || tile.__loadingState === FAILED; } @@ -59,7 +59,7 @@ function recursivelyLoadTiles( tile, depthFromRenderedParent, renderer ) { const doTraverse = tile.__contentEmpty && ( ! tile.__externalTileSet || - isDownloadFinished( tile.__loadingState ) + isTileDownloadFinished( tile ) ); if ( doTraverse ) { @@ -238,7 +238,7 @@ export function markUsedSetLeaves( tile, renderer ) { const childLoaded = c.__allChildrenLoaded || - ( ! c.__contentEmpty && isDownloadFinished( c.__loadingState ) ) || + ( ! c.__contentEmpty && isTileDownloadFinished( c ) ) || ( c.__externalTileSet && c.__loadingState === FAILED ); allChildrenLoaded = allChildrenLoaded && childLoaded; @@ -300,7 +300,7 @@ export function skipTraversal( tile, renderer ) { const includeTile = meetsSSE || tile.refine === 'ADD'; const hasModel = ! tile.__contentEmpty; const hasContent = hasModel || tile.__externalTileSet; - const loadedContent = isDownloadFinished( tile.__loadingState ) && hasContent; + const loadedContent = isTileDownloadFinished( tile ) && hasContent; const childrenWereVisible = tile.__childrenWereVisible; const children = tile.children; let allChildrenHaveContent = tile.__allChildrenLoaded; diff --git a/src/index.d.ts b/src/index.d.ts index b9fe5db06..7acecaf85 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -28,6 +28,7 @@ export { I3DMLoaderBase } from './base/I3DMLoaderBase'; export { PNTSLoaderBase } from './base/PNTSLoaderBase'; export { CMPTLoaderBase } from './base/CMPTLoaderBase'; export { LoaderBase } from './base/LoaderBase'; +export { isTileDownloadFinished } from './base/traverseFunctions'; export { LRUCache } from './utilities/LRUCache'; export { PriorityQueue } from './utilities/PriorityQueue'; diff --git a/src/index.js b/src/index.js index f7963cc45..c5cf0d52f 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ import { B3DMLoaderBase } from './base/B3DMLoaderBase.js'; import { I3DMLoaderBase } from './base/I3DMLoaderBase.js'; import { PNTSLoaderBase } from './base/PNTSLoaderBase.js'; import { CMPTLoaderBase } from './base/CMPTLoaderBase.js'; +import { isTileDownloadFinished } from './base/traverseFunctions.js'; import { LRUCache } from './utilities/LRUCache.js'; import { PriorityQueue } from './utilities/PriorityQueue.js'; @@ -47,6 +48,8 @@ export { LRUCache, PriorityQueue, + isTileDownloadFinished, + NONE, SCREEN_ERROR, GEOMETRIC_ERROR, From 542afbd5f679659938e856289028269c3c223051 Mon Sep 17 00:00:00 2001 From: Dave Buchhofer Date: Thu, 30 Dec 2021 11:40:34 -0500 Subject: [PATCH 3/3] [util] - separate / share-able 3d-tiles -> three conversion utils Maybe? So far I have needed to duplicate the majority of this functionality in order to work with my custom extension? Is it too much to export the utility functions from the package as a way to keep internal/cached properties from being relied upon by external package users / exposed via types? --- src/index.d.ts | 2 + src/index.js | 2 + src/three/ThreeTileUtils.d.ts | 19 ++++ src/three/ThreeTileUtils.js | 173 ++++++++++++++++++++++++++++++++++ src/three/TilesRenderer.js | 118 ++--------------------- test/ThreeTileUtils.test.js | 78 +++++++++++++++ 6 files changed, 281 insertions(+), 111 deletions(-) create mode 100644 src/three/ThreeTileUtils.d.ts create mode 100644 src/three/ThreeTileUtils.js create mode 100644 test/ThreeTileUtils.test.js diff --git a/src/index.d.ts b/src/index.d.ts index 7acecaf85..85c15dc12 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -30,5 +30,7 @@ export { CMPTLoaderBase } from './base/CMPTLoaderBase'; export { LoaderBase } from './base/LoaderBase'; export { isTileDownloadFinished } from './base/traverseFunctions'; +export * as ThreeTileUtils from './three/ThreeTileUtils'; + export { LRUCache } from './utilities/LRUCache'; export { PriorityQueue } from './utilities/PriorityQueue'; diff --git a/src/index.js b/src/index.js index c5cf0d52f..2c9f6b530 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ import { PNTSLoader } from './three/PNTSLoader.js'; import { I3DMLoader } from './three/I3DMLoader.js'; import { CMPTLoader } from './three/CMPTLoader.js'; import { GLTFExtensionLoader } from './three/GLTFExtensionLoader.js'; +import * as ThreeTileUtils from './three/ThreeTileUtils.js'; import { TilesRendererBase } from './base/TilesRendererBase.js'; import { LoaderBase } from './base/LoaderBase.js'; @@ -49,6 +50,7 @@ export { PriorityQueue, isTileDownloadFinished, + ThreeTileUtils, NONE, SCREEN_ERROR, diff --git a/src/three/ThreeTileUtils.d.ts b/src/three/ThreeTileUtils.d.ts new file mode 100644 index 000000000..8f724413d --- /dev/null +++ b/src/three/ThreeTileUtils.d.ts @@ -0,0 +1,19 @@ +import type { Box3, Matrix4, Sphere } from 'three'; +import type { TileBase } from '../base/TileBase'; + +// Convert optional 3d-tiles transform object into a THREE.Matrix4 +export function convertTileTransform( transform: TileBase['transform'], parentMatrix: Matrix4 ): Matrix4; + +type BoundingVolumeDescriptor = { + box: Box3|null, + boxTransform: Matrix4|null, + boxTransformInverse: Matrix4|null, + sphere: Sphere, + region: null, +} + +// Convert 3d-tiles boundingVolume optional definitions into concrete THREE box/sphere/matrix description +// - ex: used in 'tile.cached' +export function convertTileBoundingVolume( boundingVolume: TileBase['boundingVolume'], transform: Matrix4 ): BoundingVolumeDescriptor; + +export function convertBox3ToBoundingVolume( localBox: Box3, boxTransform: Matrix4 ): number[]; \ No newline at end of file diff --git a/src/three/ThreeTileUtils.js b/src/three/ThreeTileUtils.js new file mode 100644 index 000000000..3c4f6ccd2 --- /dev/null +++ b/src/three/ThreeTileUtils.js @@ -0,0 +1,173 @@ +import { Box3, Matrix4, Vector3, Sphere } from 'three'; + +const vecX = new Vector3(); +const vecY = new Vector3(); +const vecZ = new Vector3(); + +// Convert optional 3d-tiles transform object into a THREE.Matrix4 +export function convertTileTransform( transform, parentMatrix ) { + + const result = new Matrix4(); + if ( transform ) { + + const transformArr = transform; + for ( let i = 0; i < 16; i ++ ) { + + result.elements[ i ] = transformArr[ i ]; + + } + + } else { + + result.identity(); + + } + + if ( parentMatrix ) { + + result.premultiply( parentMatrix ); + + } + + return result; + +} + +// Convert 3d-tiles boundingVolume optional definitions into concrete THREE box/sphere/matrix description +// - ex: used in 'tile.cached' +export function convertTileBoundingVolume( boundingVolume, transform ) { + + let box = null; + let boxTransform = null; + let boxTransformInverse = null; + if ( 'box' in boundingVolume ) { + + const data = boundingVolume.box; + box = new Box3(); + boxTransform = new Matrix4(); + boxTransformInverse = new Matrix4(); + + // get the extents of the bounds in each axis + vecX.set( data[ 3 ], data[ 4 ], data[ 5 ] ); + vecY.set( data[ 6 ], data[ 7 ], data[ 8 ] ); + vecZ.set( data[ 9 ], data[ 10 ], data[ 11 ] ); + + const scaleX = vecX.length(); + const scaleY = vecY.length(); + const scaleZ = vecZ.length(); + + vecX.normalize(); + vecY.normalize(); + vecZ.normalize(); + + // handle the case where the box has a dimension of 0 in one axis + if ( scaleX === 0 ) { + + vecX.crossVectors( vecY, vecZ ); + + } + + if ( scaleY === 0 ) { + + vecY.crossVectors( vecX, vecZ ); + + } + + if ( scaleZ === 0 ) { + + vecZ.crossVectors( vecX, vecY ); + + } + + // create the oriented frame that the box exists in + boxTransform.set( + vecX.x, vecY.x, vecZ.x, data[ 0 ], + vecX.y, vecY.y, vecZ.y, data[ 1 ], + vecX.z, vecY.z, vecZ.z, data[ 2 ], + 0, 0, 0, 1 + ); + boxTransform.premultiply( transform ); + boxTransformInverse.copy( boxTransform ).invert(); + + // scale the box by the extents + box.min.set( - scaleX, - scaleY, - scaleZ ); + box.max.set( scaleX, scaleY, scaleZ ); + + } + + let sphere = null; + if ( 'sphere' in boundingVolume ) { + + const data = boundingVolume.sphere; + sphere = new Sphere(); + sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] ); + sphere.radius = data[ 3 ]; + sphere.applyMatrix4( transform ); + + } else if ( 'box' in boundingVolume ) { + + const data = boundingVolume.box; + sphere = new Sphere(); + box.getBoundingSphere( sphere ); + sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] ); + sphere.applyMatrix4( transform ); + + } + + let region = null; + if ( 'region' in boundingVolume ) { + + console.warn( 'ThreeTilesRenderer: region bounding volume not supported.' ); + + } + + return { + + box, + boxTransform, + boxTransformInverse, + sphere, + region, + + }; + +} + +// Convert a three box3 + transform into a 3d-tiles boundingVolume.box array +export function convertBox3ToBoundingVolume( box, boxTransform ) { + + const worldBox = new Box3().copy( box ).applyMatrix4( boxTransform ); + const min = [ worldBox.min.x, worldBox.min.y, worldBox.min.z ]; + const max = [ worldBox.max.x, worldBox.max.y, worldBox.max.z ]; + + const center = [ + ( max[ 0 ] - min[ 0 ] ) / 2 + min[ 0 ], + ( max[ 1 ] - min[ 1 ] ) / 2 + min[ 1 ], + ( max[ 2 ] - min[ 2 ] ) / 2 + min[ 2 ], + ]; + + const halfX = ( max[ 0 ] - min[ 0 ] ) / 2.0; + const halfY = ( max[ 1 ] - min[ 1 ] ) / 2.0; + const halfZ = ( max[ 2 ] - min[ 2 ] ) / 2.0; + + // oriented bounding box + // a right-handed 3-axis (x, y, z) Cartesian coordinate system where the z-axis is up. + const boundingVolumeBox = [ + // The first three elements define the x, y, and z values for the center of the box. + // center + center[ 0 ], center[ 1 ], center[ 2 ], + + // The next three elements (with indices 3, 4, and 5) define the x-axis direction and half-length. + halfX, 0.0, 0.0, + + // The next three elements (indices 6, 7, and 8) define the y-axis direction and half-length. + // TODO: verify handedness swap and whether we really need to flip the Y axis direction, or if something + // else is wrong internally? + 0.0, - halfY, 0.0, + + // The last three elements (indices 9, 10, and 11) define the z-axis direction and half-length. + 0.0, 0.0, halfZ, + ]; + return boundingVolumeBox; + +} diff --git a/src/three/TilesRenderer.js b/src/three/TilesRenderer.js index e2c2bda85..d2bb31fab 100644 --- a/src/three/TilesRenderer.js +++ b/src/three/TilesRenderer.js @@ -7,22 +7,21 @@ import { GLTFExtensionLoader } from './GLTFExtensionLoader.js'; import { TilesGroup } from './TilesGroup.js'; import { Matrix4, - Box3, - Sphere, Vector3, Vector2, Frustum, LoadingManager } from 'three'; import { raycastTraverse, raycastTraverseFirstHit } from './raycastTraverse.js'; +import { + convertTileBoundingVolume, + convertTileTransform, +} from './ThreeTileUtils.js'; const INITIAL_FRUSTUM_CULLED = Symbol( 'INITIAL_FRUSTUM_CULLED' ); const tempMat = new Matrix4(); const tempMat2 = new Matrix4(); const tempVector = new Vector3(); -const vecX = new Vector3(); -const vecY = new Vector3(); -const vecZ = new Vector3(); const X_AXIS = new Vector3( 1, 0, 0 ); const Y_AXIS = new Vector3( 0, 1, 0 ); @@ -435,113 +434,10 @@ export class TilesRenderer extends TilesRendererBase { super.preprocessNode( tile, parentTile, tileSetDir ); - const transform = new Matrix4(); - if ( tile.transform ) { - - const transformArr = tile.transform; - for ( let i = 0; i < 16; i ++ ) { - - transform.elements[ i ] = transformArr[ i ]; - - } - - } else { - - transform.identity(); - - } - - if ( parentTile ) { - - transform.premultiply( parentTile.cached.transform ); - - } - + const parentTransform = parentTile && parentTile.cached ? parentTile.cached.transform : undefined; + const transform = convertTileTransform( tile.transform, parentTransform ); const transformInverse = new Matrix4().copy( transform ).invert(); - - let box = null; - let boxTransform = null; - let boxTransformInverse = null; - if ( 'box' in tile.boundingVolume ) { - - const data = tile.boundingVolume.box; - box = new Box3(); - boxTransform = new Matrix4(); - boxTransformInverse = new Matrix4(); - - // get the extents of the bounds in each axis - vecX.set( data[ 3 ], data[ 4 ], data[ 5 ] ); - vecY.set( data[ 6 ], data[ 7 ], data[ 8 ] ); - vecZ.set( data[ 9 ], data[ 10 ], data[ 11 ] ); - - const scaleX = vecX.length(); - const scaleY = vecY.length(); - const scaleZ = vecZ.length(); - - vecX.normalize(); - vecY.normalize(); - vecZ.normalize(); - - // handle the case where the box has a dimension of 0 in one axis - if ( scaleX === 0 ) { - - vecX.crossVectors( vecY, vecZ ); - - } - - if ( scaleY === 0 ) { - - vecY.crossVectors( vecX, vecZ ); - - } - - if ( scaleZ === 0 ) { - - vecZ.crossVectors( vecX, vecY ); - - } - - // create the oriented frame that the box exists in - boxTransform.set( - vecX.x, vecY.x, vecZ.x, data[ 0 ], - vecX.y, vecY.y, vecZ.y, data[ 1 ], - vecX.z, vecY.z, vecZ.z, data[ 2 ], - 0, 0, 0, 1 - ); - boxTransform.premultiply( transform ); - boxTransformInverse.copy( boxTransform ).invert(); - - // scale the box by the extents - box.min.set( - scaleX, - scaleY, - scaleZ ); - box.max.set( scaleX, scaleY, scaleZ ); - - } - - let sphere = null; - if ( 'sphere' in tile.boundingVolume ) { - - const data = tile.boundingVolume.sphere; - sphere = new Sphere(); - sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] ); - sphere.radius = data[ 3 ]; - sphere.applyMatrix4( transform ); - - } else if ( 'box' in tile.boundingVolume ) { - - const data = tile.boundingVolume.box; - sphere = new Sphere(); - box.getBoundingSphere( sphere ); - sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] ); - sphere.applyMatrix4( transform ); - - } - - let region = null; - if ( 'region' in tile.boundingVolume ) { - - console.warn( 'ThreeTilesRenderer: region bounding volume not supported.' ); - - } + const { box, boxTransform, boxTransformInverse, sphere, region } = convertTileBoundingVolume( tile.boundingVolume, transform ); tile.cached = { diff --git a/test/ThreeTileUtils.test.js b/test/ThreeTileUtils.test.js new file mode 100644 index 000000000..1aef6987a --- /dev/null +++ b/test/ThreeTileUtils.test.js @@ -0,0 +1,78 @@ +import { Matrix4, Quaternion, Vector3 } from 'three'; +import * as utils from '../src/three/ThreeTileUtils.js'; + +/** Tests to verify portions of the tile spec <-> threejs conversions */ + +const identity = new Matrix4().identity(); +const defaultTransform = { + "transform": [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ] +}; +const testBounds = { + "boundingVolume": { + "box": [ + 66, - 13, 6, + 3, 0, 0, + 0, - 7, 0, + 0, 0, 2, + ] + }, +}; + +function decompose( transform ) { + + const position = new Vector3(); + const rotation = new Quaternion(); + const scale = new Vector3(); + transform.decompose( position, rotation, scale ); + return { position, rotation, scale }; + +} + +describe( 'ThreeTileUtils', () => { + + describe( 'transform', () => { + + it( 'default tile.transform is identity', () => { + + const transform = utils.convertTileTransform( defaultTransform.transform, identity ); + expect( transform.elements ).toEqual( identity.elements ); + + } ); + + } ); + + describe( 'boundingVolume', () => { + + it( 'boundingVolume.box to Box3', () => { + + const { box, boxTransform } = utils.convertTileBoundingVolume( testBounds.boundingVolume, identity ); + expect( box.min ).toEqual( new Vector3( - 3, - 7, - 2 ) ); + expect( box.max ).toEqual( new Vector3( 3, 7, 2 ) ); + + const transform = decompose( boxTransform ); + // - 0 is a thing three does internally apparently. ~= 0. + expect( transform.rotation ).toEqual( new Quaternion( 0, 0, 1, - 0 ) ); + expect( transform.position ).toEqual( new Vector3( 66, - 13, 6 ) ); + expect( transform.scale ).toEqual( new Vector3( - 1, 1, 1 ) ); + + } ); + + it( 'boundingVolume.box to Box3 to boundingVolume.box', () => { + + // from 3d-tiles tile data to threejs box objects + const { box, boxTransform } = utils.convertTileBoundingVolume( testBounds.boundingVolume, identity ); + // from threejs objects back to 3d-tiles volume box + const boundingVolumeBox = utils.convertBox3ToBoundingVolume( box, boxTransform ); + // should be roughly equal + expect( boundingVolumeBox ).toEqual( testBounds.boundingVolume.box ); + + } ); + + } ); + +} );