diff --git a/src/Core/3DTiles/C3DTBatchTable.js b/src/Core/3DTiles/C3DTBatchTable.js index 2157846670..7574e7e532 100644 --- a/src/Core/3DTiles/C3DTBatchTable.js +++ b/src/Core/3DTiles/C3DTBatchTable.js @@ -1,6 +1,6 @@ import utf8Decoder from 'Utils/Utf8Decoder'; import binaryPropertyAccessor from './utils/BinaryPropertyAccessor'; -import C3DTilesTypes from './C3DTilesTypes'; +import { C3DTilesTypes } from './C3DTilesEnums'; /** @classdesc * A 3D Tiles diff --git a/src/Core/3DTiles/C3DTBoundingVolume.js b/src/Core/3DTiles/C3DTBoundingVolume.js index 4150600167..59e92cc43b 100644 --- a/src/Core/3DTiles/C3DTBoundingVolume.js +++ b/src/Core/3DTiles/C3DTBoundingVolume.js @@ -1,44 +1,122 @@ import * as THREE from 'three'; -import Extent from '../Geographic/Extent'; -import OBB from '../../Renderer/OBB'; -import C3DTilesTypes from './C3DTilesTypes'; - -const matrix = new THREE.Matrix4(); -const center = new THREE.Vector3(); -const size = new THREE.Vector3(); -const extent = new Extent('EPSG:4326', 0, 0, 0, 0); -const sphereCenter = new THREE.Vector3(); +import Ellipsoid from 'Core/Math/Ellipsoid'; +import Coordinates from '../Geographic/Coordinates'; +import { C3DTilesTypes, C3DTilesBoundingVolumeTypes } from './C3DTilesEnums'; + +const ellipsoid = new Ellipsoid(); + +// bounding box scratch variables +const boxSize = new THREE.Vector3(); +const boxCenter = new THREE.Vector3(); + +// Bounding region scratch variables +const southEastUpCarto = new Coordinates('EPSG:4326'); +const southEastUpVec3 = new THREE.Vector3(); +const northWestBottomCarto = new Coordinates('EPSG:4326'); +const northWestBottomVec3 = new THREE.Vector3(); +const radiusScratch = new THREE.Vector3(); + +// Culling scratch value const worldCoordinateCenter = new THREE.Vector3(); +/** + * Bounding region is converted to a bounding sphere to simplify and speed computation and culling. This function + * computes a sphere enclosing the bounding region. + * @param {Object} region - the parsed json from the tile representing the region + * @param {THREE.Matrix4} tileMatrixInverse - the inverse transformation matrix of the tile to transform the produced + * sphere from a global to a reference local to the tile + * @return {THREE.Sphere} a sphere enclosing the given region + */ +function initFromRegion(region, tileMatrixInverse) { + const east = region[2]; + const west = region[0]; + const south = region[1]; + const north = region[3]; + const minHeight = region[4]; + const maxHeight = region[5]; + + const eastDeg = THREE.MathUtils.radToDeg(east); + const westDeg = THREE.MathUtils.radToDeg(west); + const southDeg = THREE.MathUtils.radToDeg(south); + const northDeg = THREE.MathUtils.radToDeg(north); + + northWestBottomCarto.setFromValues(westDeg, northDeg, minHeight); + ellipsoid.cartographicToCartesian(northWestBottomCarto, northWestBottomVec3); + + southEastUpCarto.setFromValues(eastDeg, southDeg, maxHeight); + ellipsoid.cartographicToCartesian(southEastUpCarto, southEastUpVec3); + + const regionCenter = new THREE.Vector3(); + regionCenter.lerpVectors(northWestBottomVec3, southEastUpVec3, 0.5); + const radius = radiusScratch.subVectors(northWestBottomVec3, southEastUpVec3).length() / 2; + + const sphere = new THREE.Sphere(regionCenter, radius); + sphere.applyMatrix4(tileMatrixInverse); + + return sphere; +} + +/** + * Create a bounding box from a json describing a box in a 3D Tiles tile. + * @param {Object} box - the parsed json from the tile representing the box + * @return {THREE.Box3} the bounding box of the tile + */ +function initFromBox(box) { + // box[0], box[1], box[2] = center of the box + // box[3], box[4], box[5] = x axis direction and half-length + // box[6], box[7], box[8] = y axis direction and half-length + // box[9], box[10], box[11] = z axis direction and half-length + boxCenter.set(box[0], box[1], box[2]); + boxSize.set(box[3], box[7], box[11]).multiplyScalar(2); + const box3 = new THREE.Box3(); + box3.setFromCenterAndSize(boxCenter, boxSize); + return box3; +} + +/** + * Creats a bounding sphere from a json describing a sphere in a 3D Tiles tile. + * @param {Object} sphere - the parsed json from the tile representing the sphere + * @returns {THREE.Sphere} the bounding sphere of the tile + */ +function initFromSphere(sphere) { + const sphereCenter = new THREE.Vector3(); + sphereCenter.set(sphere[0], sphere[1], sphere[2]); + return new THREE.Sphere(sphereCenter, sphere[3]); +} + /** * @classdesc 3D Tiles * [bounding volume](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/specification/schema/boundingVolume.schema.json) + * Used to represent bounding volumes and viewer request volumes. The input bounding volume (from the dataset) can be a + * box, a sphere or a region. Regions are transformed to spheres internally for simplification of parsing and to speed + * up computations such as culling. * @property {C3DTilesTypes} type - Used by 3D Tiles extensions * (e.g. {@link C3DTBatchTableHierarchyExtension}) to know in which context * (i.e. for which 3D Tiles class) the parsing of the extension should be done. - * @property {THREE.Box3} box - Bounding box, defined only if the Bounding Volume - * is a box. - * @property {OBB} region - Bounding region, defined only if the Bounding - * Volume is a region. - * @property {THREE.Sphere} sphere - Bounding sphere, defined only if the - * Bounding Volume is a sphere. + * @property {String} initialVolumeType - the initial volume type to be able to dissociate spheres + * and regions if needed since both are converted to spheres (one of {@link C3DTilesBoundingVolumeTypes}) + * @property {THREE.Box3|THREE.Sphere} volume - The 3D bounding volume created. Can be a THREE.Box3 for bounding volumes + * of types box or a THREE.Sphere for bounding volumes of type sphere or region. * @property {object} extensions - 3D Tiles extensions of the bounding volume * stored in the following format: * {extensioName1: extensionObject1, extensioName2: extensionObject2, ...} */ class C3DTBoundingVolume { - constructor(json, inverseTileTransform, registeredExtensions) { + constructor(json, tileMatrixInverse, registeredExtensions) { this.type = C3DTilesTypes.boundingVolume; - // Init bounding volume if (json.region) { - this.initBoundingRegion(json.region, inverseTileTransform); + this.initialVolumeType = C3DTilesBoundingVolumeTypes.region; + this.volume = initFromRegion(json.region, tileMatrixInverse); } else if (json.box) { - this.initBoundingBox(json.box); + this.initialVolumeType = C3DTilesBoundingVolumeTypes.box; + this.volume = initFromBox(json.box); } else if (json.sphere) { - this.initBoundingSphere(json.sphere); + this.initialVolumeType = C3DTilesBoundingVolumeTypes.sphere; + this.volume = initFromSphere(json.sphere); } else { - throw new Error('3D Tiles nodes must have a bounding volume'); + throw new Error(`Unknown bounding volume type: ${json}. 3D Tiles nodes must have a bounding volume of type + region, box or sphere.`); } if (json.extensions) { @@ -47,73 +125,43 @@ class C3DTBoundingVolume { } } - initBoundingRegion(region, inverseTileTransform) { - extent.set(THREE.MathUtils.radToDeg(region[0]), - THREE.MathUtils.radToDeg(region[2]), - THREE.MathUtils.radToDeg(region[1]), - THREE.MathUtils.radToDeg(region[3])); - const regionBox = new OBB(); - regionBox.setFromExtent(extent); - regionBox.updateZ({ min: region[4], max: region[5] }); - // at this point box.matrix = box.epsg4978_from_local, so - // we transform it in parent_from_local by using parent's - // epsg4978_from_local which from our point of view is - // epsg4978_from_parent. box.matrix = (epsg4978_from_parent ^ -1) * - // epsg4978_from_local = parent_from_epsg4978 * epsg4978_from_local = - // parent_from_local - regionBox.matrix.premultiply(inverseTileTransform); - // update position, rotation and scale - regionBox.matrix.decompose(regionBox.position, regionBox.quaternion, regionBox.scale); - this.region = regionBox; - } - - initBoundingBox(box) { - // box[0], box[1], box[2] = center of the box - // box[3], box[4], box[5] = x axis direction and half-length - // box[6], box[7], box[8] = y axis direction and half-length - // box[9], box[10], box[11] = z axis direction and half-length - center.set(box[0], box[1], box[2]); - size.set(box[3], box[7], box[11]).multiplyScalar(2); - this.box = new THREE.Box3(); - this.box.setFromCenterAndSize(center, size); - } - - initBoundingSphere(sphere) { - sphereCenter.set(sphere[0], sphere[1], sphere[2]); - this.sphere = new THREE.Sphere(sphereCenter, sphere[3]); - } - + /** + * Performs camera frustum culling on bounding volumes. + * @param {Camera} camera - the camera to perform culling for + * @param {THREE.Matrix4} tileMatrixWorld - the world matrix of the tile + * @returns {boolean} true if the tile should be culled out (bounding volume not in camera frustum), false otherwise. + */ boundingVolumeCulling(camera, tileMatrixWorld) { - if (this.region && - !camera.isBox3Visible(this.region.box3D, - matrix.multiplyMatrices(tileMatrixWorld, this.region.matrix))) { - return true; - } - if (this.box && !camera.isBox3Visible(this.box, - tileMatrixWorld)) { - return true; + if (this.initialVolumeType === C3DTilesBoundingVolumeTypes.box) { + return !camera.isBox3Visible(this.volume, tileMatrixWorld); + } else if (this.initialVolumeType === C3DTilesBoundingVolumeTypes.sphere || + this.initialVolumeType === C3DTilesBoundingVolumeTypes.region) { + return !camera.isSphereVisible(this.volume, tileMatrixWorld); + } else { + throw new Error('Unknown bounding volume type.'); } - return this.sphere && - !camera.isSphereVisible(this.sphere, tileMatrixWorld); } + /** + * Checks if the camera is inside the [viewer request volumes](@link https://github.com/CesiumGS/3d-tiles/tree/main/specification#viewer-request-volume). + * @param {Camera} camera - the camera to perform culling for + * @param {THREE.Matrix4} tileMatrixWorld - the world matrix of the tile + * @returns {boolean} true if the camera is outside the viewer request volume, false otherwise. + */ viewerRequestVolumeCulling(camera, tileMatrixWorld) { - if (this.region) { + if (this.initialVolumeType === C3DTilesBoundingVolumeTypes.region) { console.warn('Region viewerRequestVolume not yet supported'); return true; } - if (this.box) { + if (this.initialVolumeType === C3DTilesBoundingVolumeTypes.box) { console.warn('Bounding box viewerRequestVolume not yet supported'); return true; } - if (this.sphere) { - worldCoordinateCenter.copy(this.sphere.center); + if (this.initialVolumeType === C3DTilesBoundingVolumeTypes.sphere) { + worldCoordinateCenter.copy(this.volume.center); worldCoordinateCenter.applyMatrix4(tileMatrixWorld); // To check the distance between the center sphere and the camera - if (!(camera.camera3D.position.distanceTo(worldCoordinateCenter) <= - this.sphere.radius)) { - return true; - } + return !(camera.camera3D.position.distanceTo(worldCoordinateCenter) <= this.volume.radius); } return false; } diff --git a/src/Core/3DTiles/C3DTilesTypes.js b/src/Core/3DTiles/C3DTilesEnums.js similarity index 75% rename from src/Core/3DTiles/C3DTilesTypes.js rename to src/Core/3DTiles/C3DTilesEnums.js index 5f0953dae8..06a55aafdc 100644 --- a/src/Core/3DTiles/C3DTilesTypes.js +++ b/src/Core/3DTiles/C3DTilesEnums.js @@ -8,10 +8,14 @@ * @property {String} batchtable - value: 'batchtable' * @property {String} boundingVolume - value: 'bounding volume' */ -const C3DTilesTypes = { +export const C3DTilesTypes = { tileset: 'tileset', batchtable: 'batchtable', boundingVolume: 'boundingVolume', }; -export default C3DTilesTypes; +export const C3DTilesBoundingVolumeTypes = { + region: 'region', + box: 'box', + sphere: 'sphere', +}; diff --git a/src/Core/3DTiles/C3DTileset.js b/src/Core/3DTiles/C3DTileset.js index b377d91b27..e21c5d8bde 100644 --- a/src/Core/3DTiles/C3DTileset.js +++ b/src/Core/3DTiles/C3DTileset.js @@ -1,8 +1,12 @@ import * as THREE from 'three'; import C3DTBoundingVolume from './C3DTBoundingVolume'; -import C3DTilesTypes from './C3DTilesTypes'; +import { C3DTilesTypes } from './C3DTilesEnums'; -const inverseTileTransform = new THREE.Matrix4(); +// Inverse transform of a tile, computed from the tile transform and used when parsing the bounding volume of a tile +// if the bounding volume is a region (https://github.com/CesiumGS/3d-tiles/tree/main/specification#region) which is +// in global coordinates and other bounding volumes are not. To harmonize, we transform back the bounding volume region +// to a reference local to the tile. +const tileMatrixInverse = new THREE.Matrix4(); /** @classdesc * A 3D Tiles @@ -74,23 +78,20 @@ class C3DTileset { } } - // inverseTileTransform is only used for volume.region + // tileMatrixInverse is only used for volume.region if ((tile.viewerRequestVolume && tile.viewerRequestVolume.region) || (tile.boundingVolume && tile.boundingVolume.region)) { if (tile._worldFromLocalTransform) { - inverseTileTransform.copy(tile._worldFromLocalTransform).invert(); + tileMatrixInverse.copy(tile._worldFromLocalTransform).invert(); } else { - inverseTileTransform.identity(); + tileMatrixInverse.identity(); } } tile.viewerRequestVolume = tile.viewerRequestVolume ? - new C3DTBoundingVolume(tile.viewerRequestVolume, - inverseTileTransform, - registeredExtensions) : undefined; + new C3DTBoundingVolume(tile.viewerRequestVolume, tileMatrixInverse, registeredExtensions) : null; tile.boundingVolume = tile.boundingVolume ? - new C3DTBoundingVolume(tile.boundingVolume, - inverseTileTransform, registeredExtensions) : undefined; + new C3DTBoundingVolume(tile.boundingVolume, tileMatrixInverse, registeredExtensions) : null; this.tiles.push(tile); tile.tileId = this.tiles.length - 1; diff --git a/src/Core/Geographic/Coordinates.js b/src/Core/Geographic/Coordinates.js index aea1e0cd58..425bfa6e45 100644 --- a/src/Core/Geographic/Coordinates.js +++ b/src/Core/Geographic/Coordinates.js @@ -65,7 +65,7 @@ class Coordinates { * You can find most projections and their proj4 code at [epsg.io]{@link https://epsg.io/} * @param {number|Array|Coordinates|THREE.Vector3} [v0=0] - * x or longitude value, or a more complex one: it can be an array of three - * numbers, being x/lon, x/lat, z/alt, or it can be `THREE.Vector3`. It can + * numbers, being x/lon, y/lat, z/alt, or it can be `THREE.Vector3`. It can * also simply be a Coordinates. * @param {number} [v1=0] - y or latitude value. * @param {number} [v2=0] - z or altitude value. diff --git a/src/Main.js b/src/Main.js index 3e849a88d5..ac7d40a6e4 100644 --- a/src/Main.js +++ b/src/Main.js @@ -101,6 +101,6 @@ export { default as C3DTileset } from './Core/3DTiles/C3DTileset'; export { default as C3DTBoundingVolume } from './Core/3DTiles/C3DTBoundingVolume'; export { default as C3DTBatchTable } from './Core/3DTiles/C3DTBatchTable'; export { default as C3DTExtensions } from './Core/3DTiles/C3DTExtensions'; -export { default as C3DTilesTypes } from './Core/3DTiles/C3DTilesTypes'; +export { C3DTilesTypes, C3DTilesBoundingVolumeTypes } from './Core/3DTiles/C3DTilesEnums'; export { default as C3DTBatchTableHierarchyExtension } from './Core/3DTiles/C3DTBatchTableHierarchyExtension'; export { process3dTilesNode, $3dTilesCulling, $3dTilesSubdivisionControl } from 'Process/3dTilesProcessing'; diff --git a/src/Process/3dTilesProcessing.js b/src/Process/3dTilesProcessing.js index f8054362ef..44b2352db0 100644 --- a/src/Process/3dTilesProcessing.js +++ b/src/Process/3dTilesProcessing.js @@ -1,6 +1,6 @@ import * as THREE from 'three'; -import Extent from 'Core/Geographic/Extent'; import ObjectRemovalHelper from 'Process/ObjectRemovalHelper'; +import { C3DTilesBoundingVolumeTypes } from 'Core/3DTiles/C3DTilesEnums'; import { C3DTILES_LAYER_EVENTS } from '../Layer/C3DTilesLayer'; /** @module 3dTilesProcessing @@ -39,23 +39,6 @@ function subdivideNode(context, layer, node, cullingTest) { } } -const tmpBox3 = new THREE.Box3(); -const tmpSphere = new THREE.Sphere(); -function boundingVolumeToExtent(crs, volume, transform) { - if (volume.region) { - const box = tmpBox3.copy(volume.region.box3D) - .applyMatrix4(volume.region.matrixWorld); - return Extent.fromBox3(crs, box); - } else if (volume.box) { - const box = tmpBox3.copy(volume.box).applyMatrix4(transform); - return Extent.fromBox3(crs, box); - } else { - const sphere = tmpSphere.copy(volume.sphere).applyMatrix4(transform); - const box = sphere.getBoundingBox(tmpBox3); - return Extent.fromBox3(crs, box); - } -} - const tmpMatrix = new THREE.Matrix4(); function _subdivideNodeAdditive(context, layer, node, cullingTest) { for (const child of layer.tileset.tiles[node.tileId].children) { @@ -80,12 +63,6 @@ function _subdivideNodeAdditive(context, layer, node, cullingTest) { child.promise = requestNewTile(context.view, context.scheduler, layer, child, node, true).then((tile) => { node.add(tile); tile.updateMatrixWorld(); - - // The extent is calculated but it's never used in 3D tiles process - const extent = boundingVolumeToExtent(layer.extent.crs, tile.boundingVolume, tile.matrixWorld); - tile.traverse((obj) => { - obj.extent = extent; - }); layer.onTileContentLoaded(tile); context.view.notifyChange(child); @@ -110,6 +87,7 @@ function _subdivideNodeSubstractive(context, layer, node) { childrenTiles[i].loaded = true; node.add(tile); tile.updateMatrixWorld(); + // TODO: remove because cannot happen? if (node.additiveRefinement) { context.view.notifyChange(node); } @@ -142,12 +120,8 @@ export function $3dTilesCulling(layer, camera, node, tileMatrixWorld) { } // For bounding volume - if (node.boundingVolume && - node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld)) { - return true; - } - - return false; + return !!(node.boundingVolume && + node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld)); } // Cleanup all 3dtiles|three.js starting from a given node n. @@ -232,18 +206,13 @@ const boundingVolumeBox = new THREE.Box3(); const boundingVolumeSphere = new THREE.Sphere(); export function computeNodeSSE(camera, node) { node.distance = 0; - if (node.boundingVolume.region) { - boundingVolumeBox.copy(node.boundingVolume.region.box3D); - boundingVolumeBox.applyMatrix4(node.boundingVolume.region.matrixWorld); - node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position); - } else if (node.boundingVolume.box) { - // boundingVolume.box is affected by matrixWorld - boundingVolumeBox.copy(node.boundingVolume.box); + if (node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.box) { + boundingVolumeBox.copy(node.boundingVolume.volume); boundingVolumeBox.applyMatrix4(node.matrixWorld); node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position); - } else if (node.boundingVolume.sphere) { - // boundingVolume.sphere is affected by matrixWorld - boundingVolumeSphere.copy(node.boundingVolume.sphere); + } else if (node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.sphere || + node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.region) { + boundingVolumeSphere.copy(node.boundingVolume.volume); boundingVolumeSphere.applyMatrix4(node.matrixWorld); // TODO: see https://github.com/iTowns/itowns/issues/800 node.distance = Math.max(0.0, @@ -265,9 +234,6 @@ export function init3dTilesLayer(view, scheduler, layer, rootTile) { tile.updateMatrixWorld(); layer.tileset.tiles[tile.tileId].loaded = true; layer.root = tile; - // The extent is calculated but it's never used in 3D tiles process - layer.extent = boundingVolumeToExtent(layer.crs || view.referenceCrs, - tile.boundingVolume, tile.matrixWorld); layer.onTileContentLoaded(tile); }); } @@ -306,6 +272,7 @@ export function process3dTilesNode(cullingTest = $3dTilesCulling, subdivisionTes return undefined; } + // do proper culling const isVisible = cullingTest ? (!cullingTest(layer, context.camera, node, node.matrixWorld)) : true; node.visible = isVisible; diff --git a/src/Provider/3dTilesProvider.js b/src/Provider/3dTilesProvider.js index d66a4214b9..a44b87360d 100644 --- a/src/Provider/3dTilesProvider.js +++ b/src/Provider/3dTilesProvider.js @@ -73,9 +73,6 @@ export function configureTile(tile, layer, metadata, parent) { } tile.viewerRequestVolume = metadata.viewerRequestVolume; tile.boundingVolume = metadata.boundingVolume; - if (tile.boundingVolume.region) { - tile.add(tile.boundingVolume.region); - } tile.updateMatrixWorld(); } diff --git a/test/unit/3dtiles.js b/test/unit/3dtiles.js index 61fb2f33c9..c5ac00d76a 100644 --- a/test/unit/3dtiles.js +++ b/test/unit/3dtiles.js @@ -1,6 +1,6 @@ import proj4 from 'proj4'; import assert from 'assert'; -import { Matrix4, Object3D } from 'three'; +import { Matrix4, Object3D, Sphere } from 'three'; import Camera from 'Renderer/Camera'; import Coordinates from 'Core/Geographic/Coordinates'; import { computeNodeSSE } from 'Process/3dTilesProcessing'; @@ -13,9 +13,9 @@ function tilesetWithRegion(transformMatrix) { root: { boundingVolume: { region: [ - -0.1, -0.1, - 0.1, 0.1, - 0, 0], + -0.01, -0.01, + 0.01, 0.01, + -10, 10], }, }, }; @@ -57,9 +57,9 @@ function tilesetWithSphere(transformMatrix) { return tileset; } -describe('Distance computation using boundingVolume.region', function () { +describe('Distance computation for boundingVolume region', function () { const camera = new Camera('EPSG:4978', 100, 100); - camera.camera3D.position.copy(new Coordinates('EPSG:4326', 0, 0, 10000).as('EPSG:4978').toVector3()); + camera.camera3D.position.copy(new Coordinates('EPSG:4326', 0, 0, 100000).as('EPSG:4978').toVector3()); camera.camera3D.updateMatrixWorld(true); it('should compute distance correctly', function () { @@ -70,7 +70,8 @@ describe('Distance computation using boundingVolume.region', function () { computeNodeSSE(camera, tile); - assert.ok(compareWithEpsilon(tile.distance, camera.position().as('EPSG:4326').altitude, 10e-5)); + const expectedDist = Math.max(0.0, tile.boundingVolume.volume.distanceToPoint(camera.camera3D.position)); + assert.ok(compareWithEpsilon(tile.distance, expectedDist, 10e-5)); }); it('should not be affected by transform', function () { @@ -83,11 +84,15 @@ describe('Distance computation using boundingVolume.region', function () { computeNodeSSE(camera, tile); - assert.ok(compareWithEpsilon(tile.distance, camera.position().as('EPSG:4326').altitude, 10e-5)); + const boundingVolumeSphere = new Sphere(); + boundingVolumeSphere.copy(tile.boundingVolume.volume); + boundingVolumeSphere.applyMatrix4(tile.matrixWorld); + const expectedDist = Math.max(0.0, boundingVolumeSphere.distanceToPoint(camera.camera3D.position)); + assert.ok(compareWithEpsilon(tile.distance, expectedDist, 10e-5)); }); }); -describe('Distance computation using boundingVolume.box', function () { +describe('Distance computation for boundingVolume box', function () { proj4.defs('EPSG:3946', '+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); @@ -125,7 +130,7 @@ describe('Distance computation using boundingVolume.box', function () { }); }); -describe('Distance computation using boundingVolume.sphere', function () { +describe('Distance computation for boundingVolume sphere', function () { proj4.defs('EPSG:3946', '+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); diff --git a/test/unit/3dtileslayerprocessbatchtable.js b/test/unit/3dtileslayerprocessbatchtable.js index 0cc83f40de..4a90ee87fd 100644 --- a/test/unit/3dtileslayerprocessbatchtable.js +++ b/test/unit/3dtileslayerprocessbatchtable.js @@ -3,7 +3,7 @@ import C3DTilesLayer from 'Layer/C3DTilesLayer'; import C3DTBatchTableHierarchyExtension from 'Core/3DTiles/C3DTBatchTableHierarchyExtension'; import C3DTilesSource from 'Source/C3DTilesSource'; import C3DTExtensions from 'Core/3DTiles/C3DTExtensions'; -import C3DTilesTypes from 'Core/3DTiles/C3DTilesTypes'; +import { C3DTilesTypes } from 'Core/3DTiles/C3DTilesEnums'; import View from 'Core/View'; import GlobeView from 'Core/Prefab/GlobeView'; import { HttpsProxyAgent } from 'https-proxy-agent'; diff --git a/utils/debug/3dTilesDebug.js b/utils/debug/3dTilesDebug.js index 8747ceb748..7f01c31f49 100644 --- a/utils/debug/3dTilesDebug.js +++ b/utils/debug/3dTilesDebug.js @@ -1,109 +1,75 @@ import * as THREE from 'three'; import View from 'Core/View'; import GeometryLayer from 'Layer/GeometryLayer'; +import { C3DTilesBoundingVolumeTypes } from 'Core/3DTiles/C3DTilesEnums'; import { PNTS_SHAPE, PNTS_SIZE_MODE } from 'Renderer/PointsMaterial'; import GeometryDebug from './GeometryDebug'; -import OBBHelper from './OBBHelper'; const bboxMesh = new THREE.Mesh(); export default function create3dTilesDebugUI(datDebugTool, view, _3dTileslayer) { const gui = GeometryDebug.createGeometryDebugUI(datDebugTool, view, _3dTileslayer); - const regionBoundingBoxParent = new THREE.Group(); - view.scene.add(regionBoundingBoxParent); - // add wireframe GeometryDebug.addWireFrameCheckbox(gui, view, _3dTileslayer); // Bounding box control - const obb_layer_id = `${_3dTileslayer.id}_obb_debug`; + const boundingVolumeID = `${_3dTileslayer.id}_bounding_volume_debug`; function debugIdUpdate(context, layer, node) { - const metadata = node.userData.metadata; + // Tile (https://github.com/CesiumGS/3d-tiles/blob/main/specification/schema/tile.schema.json) containing + // metadata for the tile + const tile = node.userData.metadata; - let helper = node.userData.obb; + // Get helper from the node if it has already been computed + let helper = node.userData.boundingVolumeHelper; - if (!layer.visible) { - if (helper) { - helper.visible = false; - if (typeof helper.setMaterialVisibility === 'function') { - helper.setMaterialVisibility(false); - } - } + // Hide bounding volumes if 3D Tiles layer is hidden + if (helper) { + helper.visible = !!(layer.visible && node.visible); return; } - if (node.visible && metadata.boundingVolume) { - if (!helper) { - // 3dtiles with region - if (metadata.boundingVolume.region) { - helper = new OBBHelper(metadata.boundingVolume.region, `id:${node.id}`); - regionBoundingBoxParent.add(helper); - helper.updateMatrixWorld(true); - // 3dtiles with box - } else if (metadata.boundingVolume.box) { - bboxMesh.geometry.boundingBox = metadata.boundingVolume.box; - helper = new THREE.BoxHelper(bboxMesh); - helper.material.linewidth = 2; - // 3dtiles with Sphere - } else if (metadata.boundingVolume.sphere) { - const geometry = new THREE.SphereGeometry(metadata.boundingVolume.sphere.radius, 32, 32); - const material = new THREE.MeshBasicMaterial({ wireframe: true }); - helper = new THREE.Mesh(geometry, material); - helper.position.copy(metadata.boundingVolume.sphere.center); - } - - if (helper) { - helper.layer = layer; - // add the ability to hide all the debug obj for one layer at once - const l3js = layer.threejsLayer; - helper.layers.set(l3js); - if (helper.children.length) { - helper.children[0].layers.set(l3js); - } - node.userData.obb = helper; - helper.updateMatrixWorld(); - } - - if (helper && !metadata.boundingVolume.region) { - // compensate B3dm orientation correction + if (layer.visible && node.visible && tile.boundingVolume) { + if (tile.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.box) { + bboxMesh.geometry.boundingBox = tile.boundingVolume.volume; + helper = new THREE.BoxHelper(bboxMesh); + helper.material.linewidth = 2; + // compensate GLTF orientation correction based on gltfUpAxis only for b3dm tiles + if (tile.content?.uri && tile.content?.uri.endsWith('b3dm')) { const gltfUpAxis = _3dTileslayer.tileset.asset.gltfUpAxis; if (gltfUpAxis === undefined || gltfUpAxis === 'Y') { helper.rotation.x = -Math.PI * 0.5; } else if (gltfUpAxis === 'X') { helper.rotation.z = -Math.PI * 0.5; } - - // Add helper to parent to apply the correct transformation - node.parent.add(helper); helper.updateMatrix(); - helper.updateMatrixWorld(true); } + } else if (tile.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.sphere || + tile.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.region) { + const geometry = new THREE.SphereGeometry(tile.boundingVolume.volume.radius, 32, 32); + const material = new THREE.MeshBasicMaterial({ wireframe: true, color: Math.random() * 0xffffff }); + helper = new THREE.Mesh(geometry, material); + } else { + console.warn(`[3D Tiles Debug]: Unknown bounding volume: ${tile.boundingVolume}`); + return; } - if (helper) { - helper.visible = true; - if (typeof helper.setMaterialVisibility === 'function') { - helper.setMaterialVisibility(true); - } - } - } else if (helper) { - helper.visible = false; - if (typeof helper.setMaterialVisibility === 'function') { - helper.setMaterialVisibility(false); - } + node.userData.boundingVolumeHelper = helper; + + node.parent.add(helper); + helper.updateMatrixWorld(true); } } - const obbLayer = new GeometryLayer(obb_layer_id, new THREE.Object3D(), { + const boundingVolumeLayer = new GeometryLayer(boundingVolumeID, new THREE.Object3D(), { update: debugIdUpdate, visible: false, cacheLifeTime: Infinity, source: false, }); - View.prototype.addLayer.call(view, obbLayer, _3dTileslayer).then((l) => { + View.prototype.addLayer.call(view, boundingVolumeLayer, _3dTileslayer).then((l) => { gui.add(l, 'visible').name('Bounding boxes').onChange(() => { view.notifyChange(view.camera3D); }); @@ -113,6 +79,10 @@ export default function create3dTilesDebugUI(datDebugTool, view, _3dTileslayer) gui.add(_3dTileslayer, 'sseThreshold', 0, 100).name('sseThreshold').onChange(() => { view.notifyChange(view.camera3D); }); + gui.add({ frozen: _3dTileslayer.frozen }, 'frozen').onChange(((value) => { + _3dTileslayer.frozen = value; + view.notifyChange(_3dTileslayer); + })); gui.add(_3dTileslayer, 'pntsShape', PNTS_SHAPE).name('Points Shape').onChange(() => { view.notifyChange(view.camera.camera3D); });