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 + + + + + +
+
+ Tokyo Tower example using Googles Photorealistic 3D Tiles & Cesium Ion +
+ Google Cloud or Cesium Ion API token required +
+
+ + + + + + + + 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, }, } );