Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3D Tiles fixes and perf improvements #2143

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Core/3DTiles/C3DTBatchTable.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
198 changes: 123 additions & 75 deletions src/Core/3DTiles/C3DTBoundingVolume.js
Original file line number Diff line number Diff line change
@@ -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');
jailln marked this conversation as resolved.
Show resolved Hide resolved
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();
mgermerie marked this conversation as resolved.
Show resolved Hide resolved
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);
jailln marked this conversation as resolved.
Show resolved Hide resolved
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();
mgermerie marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a poor pattern to store in a single variable different type of object (volume) then to use another variable (here initialVolumeType) to know how to manipulate volume.. (btw you could use instanceof instead of initialVolumeType)

you could create a wrapper abstract class called Volume implementing a method isVisible(camera, tileMatrixWorld) then specifying SphereVolume and BoxVolume (class extending Volume) the specific behavior.

you could also put two attributes in C3DTBoundingVolume instead of volume one called box and the other one sphere which is the solution I rather, since a C3DTBoundingVolume could have the both and it avoids to create an abstract class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last pattern was the previously used pattern. However, if we choose this one I would keep the initialVolumeType to be able to know if the sphere comes from a bounding sphere or from a bounding region, in case it may be useful for itowns' users in some way. The second pattern is also interesting, I actually thought about it when implementing this. I don't have time to implement it right now but I'll come back to it soon.

* 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) {
Expand All @@ -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) {
mgermerie marked this conversation as resolved.
Show resolved Hide resolved
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
21 changes: 11 additions & 10 deletions src/Core/3DTiles/C3DTileset.js
Original file line number Diff line number Diff line change
@@ -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
jailln marked this conversation as resolved.
Show resolved Hide resolved
// 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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Geographic/Coordinates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>|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.
Expand Down
2 changes: 1 addition & 1 deletion src/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading