diff --git a/example/babylonjs/googleMapsAerial.html b/example/babylonjs/googleMapsAerial.html
new file mode 100644
index 000000000..846993ecb
--- /dev/null
+++ b/example/babylonjs/googleMapsAerial.html
@@ -0,0 +1,31 @@
+
+
+
+
+ Google Maps Aerial - Babylon.js 3D Tiles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/babylonjs/googleMapsAerial.js b/example/babylonjs/googleMapsAerial.js
new file mode 100644
index 000000000..3360d9eb4
--- /dev/null
+++ b/example/babylonjs/googleMapsAerial.js
@@ -0,0 +1,86 @@
+import * as BABYLON from 'babylonjs';
+import { TilesRenderer } from '3d-tiles-renderer/babylonjs';
+import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins';
+import GUI from 'lil-gui';
+
+const GOOGLE_TILES_ASSET_ID = 2275207;
+
+// gui
+const params = {
+ enabled: true,
+ visibleTiles: 0,
+ errorTarget: 20,
+};
+
+const gui = new GUI();
+gui.add( params, 'enabled' );
+gui.add( params, 'visibleTiles' ).name( 'Visible Tiles' ).listen().disable();
+gui.add( params, 'errorTarget', 1, 100 );
+
+// engine
+const canvas = document.getElementById( 'renderCanvas' );
+const engine = new BABYLON.Engine( canvas, true );
+engine.setHardwareScalingLevel( 1 / window.devicePixelRatio );
+
+// scene
+const scene = new BABYLON.Scene( engine );
+scene.useRightHandedSystem = true;
+
+// camera
+const camera = new BABYLON.ArcRotateCamera(
+ 'camera',
+ - Math.PI / 2,
+ Math.PI / 3,
+ 100000,
+ new BABYLON.Vector3( 0, 0, 0 ),
+ scene,
+);
+camera.attachControl( canvas, true );
+camera.minZ = 1;
+camera.maxZ = 1e7;
+camera.wheelPrecision = 0.25;
+camera.setPosition( new BABYLON.Vector3( 500, 300, - 500 ) );
+
+// tiles
+const tiles = new TilesRenderer( null, scene );
+tiles.registerPlugin( new CesiumIonAuthPlugin( {
+ apiToken: import.meta.env.VITE_ION_KEY,
+ assetId: GOOGLE_TILES_ASSET_ID,
+ autoRefreshToken: true,
+} ) );
+tiles.errorTarget = params.errorTarget;
+
+// position so Tokyo Tower is visible
+tiles.group.rotation.set( - 0.6223599766516501, 8.326672684688674e-17, - 0.8682210177215869 );
+tiles.group.position.set( 0, - 6370877.772522855 - 150, 20246.934953993885 );
+
+// Babylon render loop
+scene.onBeforeRenderObservable.add( () => {
+
+ if ( params.enabled ) {
+
+ tiles.errorTarget = params.errorTarget;
+ tiles.update();
+ params.visibleTiles = tiles.visibleTiles.size;
+
+ }
+
+ // update attributions
+ const attributions = tiles.getAttributions();
+ const creditsEl = document.getElementById( 'credits' );
+ creditsEl.innerText = attributions[ 0 ]?.value || '';
+
+} );
+
+engine.runRenderLoop( () => {
+
+ scene.render();
+
+} );
+
+// Handle window resize
+window.addEventListener( 'resize', () => {
+
+ engine.resize();
+
+} );
diff --git a/example/babylonjs/index.html b/example/babylonjs/index.html
new file mode 100644
index 000000000..3436ce947
--- /dev/null
+++ b/example/babylonjs/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Babylon.js 3D Tiles Demo
+
+
+
+
+
+
+
+ Dingo Gap site reconstructed from multiple sols of Curiosity Rover and Mars Reconnaissance Orbiter images.
+
+ See more information
here.
+
+
+
+
+
+
+
+
diff --git a/example/babylonjs/index.js b/example/babylonjs/index.js
new file mode 100644
index 000000000..60d7da018
--- /dev/null
+++ b/example/babylonjs/index.js
@@ -0,0 +1,68 @@
+import * as BABYLON from 'babylonjs';
+import { TilesRenderer } from '3d-tiles-renderer/babylonjs';
+import GUI from 'lil-gui';
+
+const TILESET_URL = 'https://raw.githubusercontent.com/NASA-AMMOS/3DTilesSampleData/master/msl-dingo-gap/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize_tileset.json';
+
+// gui
+const params = {
+ enabled: true,
+ visibleTiles: 0,
+};
+
+const gui = new GUI();
+gui.add( params, 'enabled' );
+gui.add( params, 'visibleTiles' ).listen().disable();
+
+// init engine
+const canvas = document.getElementById( 'renderCanvas' );
+const engine = new BABYLON.Engine( canvas, true );
+engine.setHardwareScalingLevel( 1 / window.devicePixelRatio );
+
+// TODO: Babylon uses left handed coordinate system but our data is in a right handed one.
+// The coordinate system flag may need to be accounted for when parsing the data
+const scene = new BABYLON.Scene( engine );
+scene.useRightHandedSystem = true;
+
+// Camera controls
+const camera = new BABYLON.ArcRotateCamera(
+ 'camera',
+ - Math.PI / 2,
+ Math.PI / 2.5,
+ 50,
+ new BABYLON.Vector3( 0, 0, 0 ),
+ scene,
+);
+camera.attachControl( canvas, true );
+camera.minZ = 0.1;
+camera.maxZ = 1000;
+
+// instantiate tiles renderer and orient the group so it's Z+ down
+const tiles = new TilesRenderer( TILESET_URL, scene );
+tiles.group.rotation.x = Math.PI / 2;
+
+// render
+scene.onBeforeRenderObservable.add( () => {
+
+ if ( params.enabled ) {
+
+ tiles.update();
+
+ }
+
+ params.visibleTiles = tiles.visibleTiles.size;
+
+} );
+
+engine.runRenderLoop( () => {
+
+ scene.render();
+
+} );
+
+// resize
+window.addEventListener( 'resize', () => {
+
+ engine.resize();
+
+} );
diff --git a/example/styles.css b/example/styles.css
index 8bd2f969e..986a4beb3 100644
--- a/example/styles.css
+++ b/example/styles.css
@@ -3,13 +3,18 @@
padding: 0;
}
-html {
+html, body {
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
user-select: none;
+ width: 100%;
+ height: 100%;
+
}
canvas {
+ width: 100%;
+ height: 100%;
image-rendering: pixelated;
outline: none;
}
diff --git a/package-lock.json b/package-lock.json
index 9b93eabe4..4dc2ecbf2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,8 @@
"@types/three": "^0.170.0",
"@vitejs/plugin-react": "^4.3.2",
"@vitest/eslint-plugin": "^1.5.1",
+ "babylonjs": "^7.0.0",
+ "babylonjs-loaders": "^7.0.0",
"cesium": "^1.132.0",
"concurrently": "^6.2.1",
"eslint": "^9.0.0",
@@ -29,6 +31,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"globals": "^16.5.0",
"leva": "^0.10.0",
+ "lil-gui": "^0.21.0",
"postprocessing": "^6.36.4",
"three": "^0.170.0",
"typescript": "^5.6.0",
@@ -38,6 +41,8 @@
},
"peerDependencies": {
"@react-three/fiber": "^8.17.9 || ^9.0.0",
+ "babylonjs": "^7.0.0",
+ "babylonjs-loaders": "^7.0.0",
"react": "^18.3.1 || ^19.0.0",
"react-dom": "^18.3.1 || ^19.0.0",
"three": ">=0.167.0"
@@ -46,11 +51,20 @@
"@react-three/fiber": {
"optional": true
},
+ "babylonjs": {
+ "optional": true
+ },
+ "babylonjs-loaders": {
+ "optional": true
+ },
"react": {
"optional": true
},
"react-dom": {
"optional": true
+ },
+ "three": {
+ "optional": true
}
}
},
@@ -2973,6 +2987,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/babylonjs": {
+ "version": "7.54.3",
+ "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-7.54.3.tgz",
+ "integrity": "sha512-DFkTQhOavmr9sgnXnzHlxknUH6qhHqnDnWhjGnD87w9oqTP6Z/gOlC5anpReDgdi0CPFmXaKqhqQ1cA9GXJlug==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/babylonjs-gltf2interface": {
+ "version": "7.54.3",
+ "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.54.3.tgz",
+ "integrity": "sha512-ZAWYFyE+SOczfWT19O4e3YRkCZ5i57SiD2eK2kqc+Tow/t9X1S45xgSFNuHZff++dd5BlVIEQDSnFV+McFLSnQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/babylonjs-loaders": {
+ "version": "7.54.3",
+ "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-7.54.3.tgz",
+ "integrity": "sha512-2irNiXHTHKpo7Od+f8GtNjuJpPB6VBH4cdkdQ105RyVkRqls2BF5i2dBHksJfd1jE2GV8NE99SUcmpbk7Dhiaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babylonjs": "^7.54.3",
+ "babylonjs-gltf2interface": "^7.54.3"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -5823,6 +5863,13 @@
"immediate": "~3.0.5"
}
},
+ "node_modules/lil-gui": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
+ "integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
diff --git a/package.json b/package.json
index 06e978232..087500d73 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,10 @@
"types": "./src/r3f/index.d.ts",
"import": "./build/index.r3f.js"
},
+ "./babylonjs": {
+ "types": "./src/babylonjs/index.d.ts",
+ "import": "./build/index.babylonjs.js"
+ },
"./core/plugins": {
"types": "./src/core/plugins/index.d.ts",
"import": "./build/index.core-plugins.js"
@@ -87,6 +91,8 @@
"@types/three": "^0.170.0",
"@vitejs/plugin-react": "^4.3.2",
"@vitest/eslint-plugin": "^1.5.1",
+ "babylonjs": "^7.0.0",
+ "babylonjs-loaders": "^7.0.0",
"cesium": "^1.132.0",
"concurrently": "^6.2.1",
"eslint": "^9.0.0",
@@ -95,6 +101,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"globals": "^16.5.0",
"leva": "^0.10.0",
+ "lil-gui": "^0.21.0",
"postprocessing": "^6.36.4",
"three": "^0.170.0",
"typescript": "^5.6.0",
@@ -104,6 +111,8 @@
},
"peerDependencies": {
"@react-three/fiber": "^8.17.9 || ^9.0.0",
+ "babylonjs": "^7.0.0",
+ "babylonjs-loaders": "^7.0.0",
"react": "^18.3.1 || ^19.0.0",
"react-dom": "^18.3.1 || ^19.0.0",
"three": ">=0.167.0"
@@ -112,11 +121,20 @@
"@react-three/fiber": {
"optional": true
},
+ "babylonjs": {
+ "optional": true
+ },
+ "babylonjs-loaders": {
+ "optional": true
+ },
"react": {
"optional": true
},
"react-dom": {
"optional": true
+ },
+ "three": {
+ "optional": true
}
}
}
diff --git a/src/babylonjs/renderer/README.md b/src/babylonjs/renderer/README.md
new file mode 100644
index 000000000..5a1612f1c
--- /dev/null
+++ b/src/babylonjs/renderer/README.md
@@ -0,0 +1,42 @@
+# 3D Tiles Renderer for Babylon JS
+
+Implementation of the TilesRendererBase class for Babylon js.
+
+[Dingo Gap Mars dataset](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/babylonjs/mars.html)
+
+[Google Photorealistic Tiles](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/babylonjs/googleMapsAerial.html)
+
+# Use
+
+```js
+import * as BABYLON from 'babylonjs';
+import { TilesRenderer } from '3d-tiles-renderer/babylonjs';
+
+// create engine
+const canvas = document.getElementById( 'renderCanvas' );
+const engine = new BABYLON.Engine( canvas, true );
+
+// right handed coordinate system is required
+const scene = new BABYLON.Scene( engine );
+scene.useRightHandedSystem = true;
+
+// create the babylon tile renderer
+const tiles = new BabylonTilesRenderer( TILESET_URL, scene );
+
+// ... initialize the camera
+
+// update the tiles
+scene.onBeforeRenderObservable.add( () => {
+
+ tiles.update();
+
+} );
+
+// render the scene
+engine.runRenderLoop( () => {
+
+ scene.render();
+
+} );
+
+```
diff --git a/src/babylonjs/renderer/index.d.ts b/src/babylonjs/renderer/index.d.ts
new file mode 100644
index 000000000..5d681473b
--- /dev/null
+++ b/src/babylonjs/renderer/index.d.ts
@@ -0,0 +1,9 @@
+import { TilesRendererBase } from '3d-tiles-renderer/core';
+import { Scene, TransformNode } from 'babylonjs';
+
+export class TilesRenderer extends TilesRendererBase {
+
+ group: TransformNode;
+ constructor( url: string, scene: Scene );
+
+}
diff --git a/src/babylonjs/renderer/index.js b/src/babylonjs/renderer/index.js
new file mode 100644
index 000000000..24b08291c
--- /dev/null
+++ b/src/babylonjs/renderer/index.js
@@ -0,0 +1 @@
+export * from './tiles/TilesRenderer.js';
diff --git a/src/babylonjs/renderer/loaders/B3DMLoader.js b/src/babylonjs/renderer/loaders/B3DMLoader.js
new file mode 100644
index 000000000..e98b3c6af
--- /dev/null
+++ b/src/babylonjs/renderer/loaders/B3DMLoader.js
@@ -0,0 +1,42 @@
+import { Matrix } from 'babylonjs';
+import { B3DMLoaderBase } from '3d-tiles-renderer/core';
+import { GLTFLoader } from './GLTFLoader.js';
+
+export class B3DMLoader extends B3DMLoaderBase {
+
+ constructor( scene ) {
+
+ super();
+ this.scene = scene;
+ this.adjustmentTransform = Matrix.Identity();
+
+ }
+
+ async parse( buffer, uri ) {
+
+ const b3dm = super.parse( buffer );
+
+ const { scene, workingPath, fetchOptions, adjustmentTransform } = this;
+
+ // init gltf loader
+ const gltfLoader = new GLTFLoader( scene );
+ gltfLoader.workingPath = workingPath;
+ gltfLoader.fetchOptions = fetchOptions;
+ if ( adjustmentTransform ) {
+
+ gltfLoader.adjustmentTransform = adjustmentTransform;
+
+ }
+
+ // parse the file
+ const result = await gltfLoader.parse( b3dm.glbBytes, uri );
+ const gltfScene = result.scene;
+ return {
+ ...b3dm,
+ scene: gltfScene,
+ container: result.container,
+ };
+
+ }
+
+}
diff --git a/src/babylonjs/renderer/loaders/GLTFLoader.js b/src/babylonjs/renderer/loaders/GLTFLoader.js
new file mode 100644
index 000000000..ad57d6da9
--- /dev/null
+++ b/src/babylonjs/renderer/loaders/GLTFLoader.js
@@ -0,0 +1,58 @@
+import { LoaderBase } from '3d-tiles-renderer/core';
+import { Matrix, Quaternion, SceneLoader } from 'babylonjs';
+import 'babylonjs-loaders';
+
+const _worldMatrix = /* @__PURE__ */ Matrix.Identity();
+export class GLTFLoader extends LoaderBase {
+
+ constructor( scene ) {
+
+ super();
+ this.scene = scene;
+ this.adjustmentTransform = Matrix.Identity();
+
+ }
+
+ async parse( buffer, uri ) {
+
+ const { scene, workingPath, adjustmentTransform } = this;
+
+ // ensure working path ends in a slash for proper resource resolution
+ let rootUrl = workingPath;
+ if ( rootUrl.length && ! /[\\/]$/.test( rootUrl ) ) {
+
+ rootUrl += '/';
+
+ }
+
+ // Use unique filename to prevent texture caching issues
+ // TODO: What is the correct method for loading gltf files in babylon?
+ const container = await SceneLoader.LoadAssetContainerAsync(
+ rootUrl,
+ new File( [ buffer ], uri ),
+ scene,
+ null,
+ '.glb',
+ );
+
+ container.addAllToScene();
+
+ // retrieve the primary scene
+ const root = container.meshes[ 0 ];
+
+ // ensure rotationQuaternion is initialized so we can decompose the matrix
+ root.rotationQuaternion = Quaternion.Identity();
+
+ // adjust the transform the model by the necessary rotation correction
+ const worldMatrix = root.computeWorldMatrix( true );
+ adjustmentTransform.multiplyToRef( worldMatrix, _worldMatrix );
+ _worldMatrix.decompose( root.scaling, root.rotationQuaternion, root.position );
+
+ return {
+ scene: root,
+ container,
+ };
+
+ }
+
+}
diff --git a/src/babylonjs/renderer/math/OBB.js b/src/babylonjs/renderer/math/OBB.js
new file mode 100644
index 000000000..cec778f7c
--- /dev/null
+++ b/src/babylonjs/renderer/math/OBB.js
@@ -0,0 +1,79 @@
+import { Vector3, Matrix, BoundingBox } from 'babylonjs';
+
+const _vec = /* @__PURE__ */ new Vector3();
+export class OBB {
+
+ constructor() {
+
+ this.min = new Vector3( - 1, - 1, - 1 );
+ this.max = new Vector3( 1, 1, 1 );
+ this.transform = Matrix.Identity();
+ this.inverseTransform = Matrix.Identity();
+ this.points = new Array( 8 ).fill( null ).map( () => new Vector3() );
+
+ }
+
+ update() {
+
+ const { min, max, points, transform } = this;
+ transform.invertToRef( this.inverseTransform );
+
+ // update corner points
+ let index = 0;
+ for ( let x = 0; x <= 1; x ++ ) {
+
+ for ( let y = 0; y <= 1; y ++ ) {
+
+ for ( let z = 0; z <= 1; z ++ ) {
+
+ points[ index ].set(
+ x === 0 ? min.x : max.x,
+ y === 0 ? min.y : max.y,
+ z === 0 ? min.z : max.z,
+ );
+ Vector3.TransformCoordinatesToRef(
+ points[ index ],
+ transform,
+ points[ index ],
+ );
+ index ++;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ clampPoint( point, result ) {
+
+ const { min, max, transform, inverseTransform } = this;
+
+ Vector3.TransformCoordinatesToRef( point, inverseTransform, result );
+ result.x = Math.max( min.x, Math.min( max.x, result.x ) );
+ result.y = Math.max( min.y, Math.min( max.y, result.y ) );
+ result.z = Math.max( min.z, Math.min( max.z, result.z ) );
+
+ // transform back to world space
+ Vector3.TransformCoordinatesToRef( result, transform, result );
+
+ return result;
+
+ }
+
+ distanceToPoint( point ) {
+
+ this.clampPoint( point, _vec );
+ return Vector3.Distance( _vec, point );
+
+ }
+
+ intersectsFrustum( frustumPlanes ) {
+
+ // TODO: implement a more robust OBB / Frustum check. This one includes false positives.
+ return BoundingBox.IsInFrustum( this.points, frustumPlanes );
+
+ }
+
+}
diff --git a/src/babylonjs/renderer/math/TileBoundingVolume.js b/src/babylonjs/renderer/math/TileBoundingVolume.js
new file mode 100644
index 000000000..d8b9fcae8
--- /dev/null
+++ b/src/babylonjs/renderer/math/TileBoundingVolume.js
@@ -0,0 +1,136 @@
+import { Vector3, Matrix, BoundingSphere } from 'babylonjs';
+import { OBB } from './OBB.js';
+
+const _vecX = /* @__PURE__ */ new Vector3();
+const _vecY = /* @__PURE__ */ new Vector3();
+const _vecZ = /* @__PURE__ */ new Vector3();
+const _scale = /* @__PURE__ */ new Vector3();
+const _empty = /* @__PURE__ */ new Vector3();
+
+export class TileBoundingVolume {
+
+ constructor() {
+
+ this.sphere = null;
+ this.obb = null;
+
+ }
+
+ setSphereData( x, y, z, radius, transform ) {
+
+ const sphere = new BoundingSphere( _empty, _empty );
+
+ const center = sphere.centerWorld.set( x, y, z );
+ Vector3.TransformCoordinatesToRef( center, transform, center );
+
+ transform.decompose( _scale, null, null );
+ sphere.radiusWorld = radius * Math.max( Math.abs( _scale.x ), Math.abs( _scale.y ), Math.abs( _scale.z ) );
+
+ this.sphere = sphere;
+
+ }
+
+ setObbData( data, transform ) {
+
+ const obb = new OBB();
+
+ // 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 ) {
+
+ Vector3.CrossToRef( _vecY, _vecZ, _vecX );
+
+ }
+
+ if ( scaleY === 0 ) {
+
+ Vector3.CrossToRef( _vecX, _vecZ, _vecY );
+
+ }
+
+ if ( scaleZ === 0 ) {
+
+ Vector3.CrossToRef( _vecX, _vecY, _vecZ );
+
+ }
+
+ // create the oriented frame that the box exists in
+ // Note that Babylon seems to take data in column major ordering rather than row-major like three.js
+ // (despite the docs seeming to imply that it's row major) so we transpose afterward
+ obb.transform = Matrix
+ .FromValues(
+ _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,
+ )
+ .transpose()
+ .multiply( transform );
+
+ // scale the box by the extents
+ obb.min.set( - scaleX, - scaleY, - scaleZ );
+ obb.max.set( scaleX, scaleY, scaleZ );
+ obb.update();
+ this.obb = obb;
+
+ }
+
+ distanceToPoint( point ) {
+
+ const { sphere, obb } = this;
+
+ let sphereDistance = - Infinity;
+ let obbDistance = - Infinity;
+
+ if ( sphere ) {
+
+ sphereDistance = Vector3.Distance( point, sphere.centerWorld ) - sphere.radiusWorld;
+ sphereDistance = Math.max( sphereDistance, 0 );
+
+ }
+
+ if ( obb ) {
+
+ obbDistance = obb.distanceToPoint( point );
+
+ }
+
+ // return the further distance of the two volumes
+ return sphereDistance > obbDistance ? sphereDistance : obbDistance;
+
+ }
+
+ intersectsFrustum( frustumPlanes ) {
+
+ const { sphere, obb } = this;
+
+ if ( sphere && ! sphere.isInFrustum( frustumPlanes ) ) {
+
+ return false;
+
+ }
+
+ if ( obb && ! obb.intersectsFrustum( frustumPlanes ) ) {
+
+ return false;
+
+ }
+
+ // if we don't have a sphere or obb then just say we did intersect
+ return Boolean( sphere || obb );
+
+ }
+
+}
diff --git a/src/babylonjs/renderer/tiles/TilesRenderer.js b/src/babylonjs/renderer/tiles/TilesRenderer.js
new file mode 100644
index 000000000..3841e3f0a
--- /dev/null
+++ b/src/babylonjs/renderer/tiles/TilesRenderer.js
@@ -0,0 +1,296 @@
+import { TilesRendererBase, LoaderUtils } from '3d-tiles-renderer/core';
+import { Matrix, Vector3, Plane, TransformNode, Frustum } from 'babylonjs';
+import { B3DMLoader } from '../loaders/B3DMLoader.js';
+import { GLTFLoader } from '../loaders/GLTFLoader.js';
+import { TileBoundingVolume } from '../math/TileBoundingVolume.js';
+
+// Scratch variables to avoid allocations
+const _worldToTiles = /* @__PURE__ */ Matrix.Identity();
+const _cameraPositionInTiles = /* @__PURE__ */ new Vector3();
+const _frustumPlanes = /* @__PURE__ */ new Array( 6 ).fill( null ).map( () => new Plane( 0, 0, 0, 0 ) );
+
+// TODO: implementation does not support left handed coordinate system
+export class TilesRenderer extends TilesRendererBase {
+
+ constructor( url, scene ) {
+
+ super( url );
+
+ this.scene = scene;
+ this.group = new TransformNode( 'tiles-root', scene );
+ this._upRotationMatrix = Matrix.Identity();
+
+ }
+
+ // TODO: implement these with Babylon constructs
+ addEventListener() {}
+
+ removeEventListener() {}
+
+ dispatchEvent() {}
+
+ loadRootTileset( ...args ) {
+
+ return super.loadRootTileset( ...args )
+ .then( root => {
+
+ // cache the gltf tileset rotation matrix
+ const { asset } = root;
+ const upAxis = asset && asset.gltfUpAxis || 'y';
+ switch ( upAxis.toLowerCase() ) {
+
+ case 'x':
+ Matrix.RotationYToRef( - Math.PI / 2, this._upRotationMatrix );
+ break;
+
+ case 'y':
+ Matrix.RotationXToRef( Math.PI / 2, this._upRotationMatrix );
+ break;
+
+ }
+
+ return root;
+
+ } );
+
+ }
+
+ preprocessNode( tile, tilesetDir, parentTile = null ) {
+
+ super.preprocessNode( tile, tilesetDir, parentTile );
+
+ // Build the transform matrix for this tile
+ const transform = Matrix.Identity();
+ if ( tile.transform ) {
+
+ // 3d tiles uses column major
+ Matrix.FromValuesToRef( ...tile.transform, transform );
+
+ }
+
+ if ( parentTile ) {
+
+ // parentTransform * transform
+ transform.multiplyToRef( parentTile.cached.transform, transform );
+
+ }
+
+ const transformInverse = Matrix.Identity();
+ transform.invertToRef( transformInverse );
+ const boundingVolume = new TileBoundingVolume();
+ if ( 'sphere' in tile.boundingVolume ) {
+
+ boundingVolume.setSphereData( ...tile.boundingVolume.sphere, transform );
+
+ }
+
+ if ( 'box' in tile.boundingVolume ) {
+
+ boundingVolume.setObbData( tile.boundingVolume.box, transform );
+
+ }
+
+ tile.cached = {
+ transform,
+ transformInverse,
+ boundingVolume,
+ active: false,
+ group: null,
+ container: null,
+ };
+
+ }
+
+ async parseTile( buffer, tile, extension, uri, abortSignal ) {
+
+ const cached = tile.cached;
+ const scene = this.scene;
+ const workingPath = LoaderUtils.getWorkingPath( uri );
+ const fetchOptions = this.fetchOptions;
+
+ const tileTransform = cached.transform;
+ const upRotationMatrix = this._upRotationMatrix;
+
+ let result = null;
+ const fileType = ( LoaderUtils.readMagicBytes( buffer ) || extension ).toLowerCase();
+
+ switch ( fileType ) {
+
+ case 'b3dm': {
+
+ const loader = new B3DMLoader( scene );
+ loader.workingPath = workingPath;
+ loader.fetchOptions = fetchOptions;
+ loader.adjustmentTransform.copyFrom( upRotationMatrix );
+
+ result = await loader.parse( buffer, uri );
+ break;
+
+ }
+
+ case 'gltf':
+ case 'glb': {
+
+ const loader = new GLTFLoader( scene );
+ loader.workingPath = workingPath;
+ loader.fetchOptions = fetchOptions;
+ loader.adjustmentTransform.copyFrom( upRotationMatrix );
+
+ result = await loader.parse( buffer, uri );
+ break;
+
+ }
+
+ default:
+ throw new Error( `BabylonTilesRenderer: Content type "${ fileType }" not supported.` );
+
+ }
+
+ const group = result.scene;
+ group.setEnabled( false );
+
+ // apply the tile's cached transform to the loaded scene
+ group
+ .computeWorldMatrix( true )
+ .multiply( tileTransform )
+ .decompose( group.scaling, group.rotationQuaternion, group.position );
+
+ // exit early if a new request has already started
+ if ( abortSignal.aborted ) {
+
+ result.container.dispose();
+ return;
+
+ }
+
+ cached.group = group;
+ cached.container = result.container;
+
+ }
+
+ disposeTile( tile ) {
+
+ super.disposeTile( tile );
+
+ const cached = tile.cached;
+ if ( cached.container ) {
+
+ cached.container.dispose();
+ cached.container = null;
+ cached.group = null;
+
+ }
+
+ }
+
+ setTileVisible( tile, visible ) {
+
+ const cached = tile.cached;
+ const group = cached.group;
+ if ( ! group ) {
+
+ return;
+
+ }
+
+ if ( visible ) {
+
+ group.parent = this.group;
+ group.setEnabled( true );
+
+ } else {
+
+ group.parent = null;
+ group.setEnabled( false );
+
+ }
+
+ super.setTileVisible( tile, visible );
+
+ }
+
+ calculateBytesUsed( tile ) {
+
+ // TODO: return the estimated amount of bytes used by the renderer
+ return 1;
+
+ }
+
+ calculateTileViewError( tile, target ) {
+
+ // TODO: cache frustum planes etc to improve performance
+ const { scene } = this;
+
+ const cached = tile.cached;
+ const boundingVolume = cached.boundingVolume;
+ const camera = scene.activeCamera;
+
+ // get the render resolution
+ const engine = scene.getEngine();
+ const hardwareScaling = engine.getHardwareScalingLevel();
+ const width = engine.getRenderWidth() * hardwareScaling;
+ const height = engine.getRenderHeight() * hardwareScaling;
+
+ // get projection camera info
+ const projection = camera.getProjectionMatrix();
+ const projectionElements = projection.m;
+ const isOrthographic = projectionElements[ 15 ] === 1;
+
+ // calculate SSE denominator or pixel size
+ let sseDenominator;
+ let pixelSize;
+ if ( isOrthographic ) {
+
+ const w = 2 / projectionElements[ 0 ];
+ const h = 2 / projectionElements[ 5 ];
+ pixelSize = Math.max( h / height, w / width );
+
+ } else {
+
+ sseDenominator = ( 2 / projectionElements[ 5 ] ) / height;
+
+ }
+
+ // calculate the frustum planes and distances in local tile coordinates
+ this.group.getWorldMatrix().invertToRef( _worldToTiles );
+ Vector3.TransformCoordinatesToRef( camera.globalPosition, _worldToTiles, _cameraPositionInTiles );
+
+ // get frustums in local space: note tht it seems there's no way to transform to ref in Babylon
+ Frustum.GetPlanesToRef( camera.getTransformationMatrix( true ), _frustumPlanes );
+ const frustumPlanes = _frustumPlanes.map( plane => {
+
+ return plane.transform( _worldToTiles );
+
+ } );
+
+ const distance = boundingVolume.distanceToPoint( _cameraPositionInTiles );
+
+ let error;
+ if ( isOrthographic ) {
+
+ error = tile.geometricError / pixelSize;
+
+ } else {
+
+ // Avoid dividing by 0
+ error = distance === 0 ? Infinity : tile.geometricError / ( distance * sseDenominator );
+
+ }
+
+ // Check frustum intersection
+ const inView = boundingVolume.intersectsFrustum( frustumPlanes );
+
+ target.inView = inView;
+ target.error = error;
+ target.distanceFromCamera = distance;
+
+ }
+
+ dispose() {
+
+ super.dispose();
+ this.group.dispose();
+
+ }
+
+}
diff --git a/vite.config.js b/vite.config.js
index 6f50f62b0..db66d69fb 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -3,6 +3,7 @@ import fs from 'fs';
import react from '@vitejs/plugin-react';
import path from 'path';
+// alias order matters so longer paths are listed first
export const packageAliases = {
'3d-tiles-renderer/core/plugins': path.resolve( './src/core/plugins/index.js' ),
'3d-tiles-renderer/three/plugins': path.resolve( './src/three/plugins/index.js' ),
@@ -10,6 +11,7 @@ export const packageAliases = {
'3d-tiles-renderer/r3f': path.resolve( './src/r3f/index.jsx' ),
'3d-tiles-renderer/core': path.resolve( './src/core/renderer/index.js' ),
'3d-tiles-renderer/three': path.resolve( './src/three/renderer/index.js' ),
+ '3d-tiles-renderer/babylonjs': path.resolve( './src/babylonjs/renderer/index.js' ),
'3d-tiles-renderer/plugins': path.resolve( './src/plugins.js' ),
'3d-tiles-renderer': path.resolve( './src/index.js' ),
@@ -19,10 +21,8 @@ export default ( { mode } ) => {
process.env = { ...process.env, ...loadEnv( mode, process.cwd() ) };
- // alias order matters so longer paths are listed first
const useBuild = mode === 'use-build';
-
return {
root: './example/',
envDir: '.',
@@ -37,6 +37,7 @@ export default ( { mode } ) => {
input: [
...fs.readdirSync( './example/three/' ).map( name => 'three/' + name ),
...fs.readdirSync( './example/r3f/' ).map( name => 'r3f/' + name ),
+ ...fs.readdirSync( './example/babylonjs/' ).map( name => 'babylonjs/' + name ),
]
.filter( p => /\.html$/.test( p ) )
.map( p => `./example/${ p }` ),
diff --git a/vite.lib-config.js b/vite.lib-config.js
index 68fd12d69..9edc05696 100644
--- a/vite.lib-config.js
+++ b/vite.lib-config.js
@@ -9,9 +9,12 @@ export default ( { mode } ) => {
const entry = {
'index': './src/index.js',
'index.plugins': './src/plugins.js',
+
'index.core': './src/core/renderer/index.js',
'index.three': './src/three/renderer/index.js',
+ 'index.babylonjs': './src/babylonjs/renderer/index.js',
'index.r3f': './src/r3f/index.jsx',
+
'index.core-plugins': './src/core/plugins/index.js',
'index.three-plugins': './src/three/plugins/index.js'
};
diff --git a/vitest.config.js b/vitest.config.js
index 1572f0e04..09ce89172 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -1,4 +1,5 @@
import { defineConfig } from 'vitest/config';
+import { packageAliases } from './vite.config.js';
export default defineConfig( {
test: {
@@ -11,14 +12,6 @@ export default defineConfig( {
],
},
resolve: {
- alias: {
- '3d-tiles-renderer/r3f': '/src/r3f/index.jsx',
- '3d-tiles-renderer/core': '/src/core/renderer/index.js',
- '3d-tiles-renderer/three': '/src/three/renderer/index.js',
- '3d-tiles-renderer/core/plugins': '/src/core/plugins/index.js',
- '3d-tiles-renderer/three/plugins': '/src/three/plugins/index.js',
- '3d-tiles-renderer/plugins': '/src/plugins.js',
- '3d-tiles-renderer': '/src/index.js',
- },
+ alias: packageAliases,
},
} );