Repository: iTowns/itowns (JavaScript)

Issue:
Unstable 3d tiles rendering in itowns
When I zoom or pan some tiles just not render or whole tileset destructs until the view angle or zoom will not be changed.

## Your Environment
- itowns version used: 2.38.1
- b3dm tiles
- link to json: hided

## Context
Here is videos to see the behavior:
1. https://youtu.be/ei3bzoLoTrs
2. https://youtu.be/EzqhBOsuaBs
3. https://youtu.be/XywzT3f29ps

I've tried:
- to set the sseThreshold = 0.3 and 100
- tileset.geometricError = 1 and 100 and other values
But these options don't solve the issue.

## Steps to Reproduce (for bugs)
```
const tiles3dLayer = new itowns.C3DTilesLayer(datasetTilesSet3dObject.id, {
    name: datasetTilesSet3dObject.name,
    source: new itowns.C3DTilesSource({
      url: datasetTilesSet3dObject.url,
    }),
    sseThreshold: 1,
}, this.itownsViewer)
itowns.View.prototype.addLayer.call(this.itownsViewer, tiles3dLayer)
```

## Expected Behavior
I expect that all tiles in camera focus will be rendered no matter the zoom or view angle.
This issue and the [previous](https://github.com/iTowns/itowns/issues/1853) are critical for us and if there is no solution for them we'll have to avoid the itowns in our projects.
3D Tiles debug tools: bounding spheres are not displayed
## Your Environment
* Version used: 2.38.2
* Browser Name and version: Firefox and Chrome
* Operating System and version (desktop or mobile): Linux

## Context

3D Tiles debug tools "bounding box" checkbox does not work for 3D Tiles tilesets with bounding spheres. There is no error in the console. It is limiting for debugging such tilesets.

## Steps to Reproduce (for bugs)
1. Create an example with a 3D Tiles tileset with bounding spheres (e.g. https://storage.yandexcloud.net/gis-3d-models/data/tiles3d/Valaam_Aerial_Survey/Scene/Cesium.json from #1859) and with a 3D Tiles debug UI (e.g. `debug.create3dTilesDebugUI(menuGlobe.gui, view, tileset, d);`)
2. Zoom to the tileset. Camera position for the above-mentioned tileset:
```
        var placement = {
            coord: new itowns.Coordinates('EPSG:4326', 30.950135, 61.395949, 0),
            range: 3000,
            tilt: 22,
        }
```
4. Check the "Bounding box" checkbox

## Expected Behavior
Bounding spheres should be displayed (the code exists but I don't know if it ever worked ?).

## Actual Behavior
Nothing is displayed.




Discussion: Thanks to report this issue!
I try with your data and there's issue with itowns
> Thanks to report this issue! I try with your data and there's issue with itowns

Thank you very much for your involvment. We really like your product and hope the issue can be solved.
@gchoqueux 
is there any news about this issue?
Hi, 
Could you share your tileset or a subset of your tileset with me? I'll try to take a look at it.
@jailln 
Do you have any news about the issue?
Could you share your tileset (or a subset of it) with me so I can give it a try? I do not reproduce the issue with the tilesets I have.
> Could you share your tileset (or a subset of it) with me so I can give it a try? I do not reproduce the issue with the tilesets I have.

Dear @jailln, I've already posted the link in the discussion section here: https://github.com/iTowns/itowns/discussions/1880#discussioncomment-3346892

Here is the link:
https://storage.yandexcloud.net/gis-3d-models/data/tiles3d/Valaam_Aerial_Survey/Scene/Cesium.json
@gchoqueux @jailln @mgermerie 
Do you have any news about this issue? Now itowns looks like it does not meet minimum industry 3D-tiles standards. All datasets that we have couldn't be used with it and there is no really effective suggestion to render the 3D tiles that itowns can work with. It looks like the dead end. 
Hi @alexLuky,
Sorry I've missed your previous message with the link. I tested it and you just need to remove the `sseThreshold` parameter from your 3D Tiles layer config to make it work:

```
const tiles3dLayer = new itowns.C3DTilesLayer(datasetTilesSet3dObject.id, {
    name: datasetTilesSet3dObject.name,
    source: new itowns.C3DTilesSource({
      url: datasetTilesSet3dObject.url,
    }),
}, this.itownsViewer)
itowns.View.prototype.addLayer.call(this.itownsViewer, tiles3dLayer)
```

Cheers.
@jailln 
Well... it;s not solution at all... I don't know why you have closed the issue. When you remove the sseThreshold then the LOD of the models becomes awfall. Take a look at the screen. Is this ok? What should I do in this case? Open a new issue or codepen example to prove? It looks like just a come-off.
![image](https://user-images.githubusercontent.com/43720642/186907783-13647f79-18d8-439e-ac52-6882d5a5eded.png)

Sorry if I closed it too quickly, but it solved the initial problem you described: `When I zoom or pan some tiles just not render or whole tileset destructs until the view angle or zoom will not be changed.`

I will look into the level of details problem and let you know. Note that I'm on leave next week so it might happen the week after.
dear @jailln. 
Thank you very much for your envolvement. 
But if you put this link to the Cesium then you'll get very good rendering result with all LODs in the propper place. But at the same time if I use this tileset for itowns I have huge rendering problems, such as bad details. This problem was described in this [issue](https://github.com/iTowns/itowns/issues/1853).

I've solved the light issue by refactor the MeshBasicMaterial to MeshStandardMaterial after this the light effects on gltf in b3dms and it looks much better but there is still the SSE kind of issues... and now we are in the dead-end because we can't use itowns with Cesium-standard 3d tiles based on sphere realization. I want to emphasize that this tileset works pretty good on Cesium.js but not in itowns. So we even tried to refactor the tileset.json from shpere to region. It becomes a little bit better more stable, but stil deconstructive. 

Compromise solution for us could be generation our own b3dms tree from obj/gltf models but all libs that we found just make the one big b3dm without tilesets hierarchy,

Any way I think itowns should be patched to understand the cesium standard because all modern enterprise software generates the 3D tiles in this standard. In Cesium it works fine, in itowns - big issues.
I think I found out why the LODs (ie the child tiles) don't all get displayed:
* Only the root tile has a refine property in your tileset (which is compliant with the spec)
* In itowns, we only look for the refine property in the tile and in its parent (in the `3dTilesProvider.js`). Instead, we should search for a refine property recursively in the parent tiles until the root tile. 

I will test this lead when I get back in one week.

Regarding the discussion about the 3D Tiles support in itowns, it is a bit less well supported than in Cesium because the Cesium team are the one making the 3D Tiles standard, BUT we are working on improving this support (and your usecase helps us to do it) (see also #1867 and #1834, and there is more to come). Hopefully we can achieve a better support of 3D Tiles and benefit from the threejs features and performance + of all the other features of itowns that are not in Cesium (e.g. MVT support).
This is great news! If you will solve this and other common issues than itowns will become much more perspective framework than Cesium is.
Hello @alexLuky,
I've looked into it and it turned out that my first lead was not the right one. Sadly I wasn't able to fix the problem but here is what I've found out along the way:

* Everything is fine until level 18
* The data disappears from the scene when the level 17 should be replaced by level 18 because somehow level 18 is not displayed. It seems that they are culled out [here](https://github.com/iTowns/itowns/blob/master/src/Process/3dTilesProcessing.js#L331) but I haven't found out why yet and their bounding sphere seems ok. Maybe it comes from [Camera.isSphereVisible()](
* Then, the other levels after level 18 don't get displayed
* Setting `isVisible` to true makes the level 18 to get loaded and other child levels get loaded too but I'm not sure that they are displayed.
* Level 17 objects have childrens that have no content (no uri and no mesh associated), which seems odd.

Do you now if there a structure change in the tiles at level 18 that could introduce this bug ?

I don't have time to continue looking into it this week but I will try to come back at it later on. 
> 
Greetings @jailln.
When I investigated the problem, I had a version that the reason lies in the insufficient accuracy of the calculation SSE value for the camera becuase of the possible overlapping sphere boundaries, It calculates [here](https://github.com/iTowns/itowns/blob/master/src/Process/3dTilesProcessing.js#L270) in the code. So perhaps we have to improve the SSE calculation for margin spheres.

I suggest you to put the dataset to the Cesium and inspect the behavior there. And then search for their implementation of SSE calculation for this case.

Hi @alexLuky 
I also thought it came from the SSE computation at first and I actually checked the SSE computation against Cesium's one. The difference is that iTowns makes an approximation described [here](https://github.com/iTowns/itowns/blob/master/src/Renderer/Camera.js#L29). Maybe we should try to replace this SSE computation to see what happens but I would rather tend to think it comes from [this culling](https://github.com/iTowns/itowns/blob/master/src/Process/3dTilesProcessing.js#L331). Anyhow, the SSE computation should be more precise or the user should at least be able to choose between a precise mode and an approximated mode (faster).

I will take a look when I find some time to do it... Don't hesitate to try it also if you have time :)
@jailln 
is there any hopes?
Haven't had time to look at it yet sadly... 
@jailln
how do you do?
@gchoqueux can you take a look at it please ? :)
The complete 3d-tiles support is planned in the short term road map. We are currently studying possibilities in terms of libraries to enhance support (as mentioned in https://github.com/iTowns/itowns/discussions/1917).

The complete 3d-tiles support is planned in the short term road map. We are currently studying possibilities in terms of libraries to enhance support (as mentioned in https://github.com/iTowns/itowns/discussions/1917).



Fix (patch):
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<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.
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/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);
     });