diff --git a/example/babylonjs/googleMapsAerial.js b/example/babylonjs/googleMapsAerial.js index d5830c80c..a40c95af8 100644 --- a/example/babylonjs/googleMapsAerial.js +++ b/example/babylonjs/googleMapsAerial.js @@ -1,4 +1,4 @@ -import * as BABYLON from 'babylonjs'; +import { Scene, Engine, Vector3, ArcRotateCamera } from '@babylonjs'; import { TilesRenderer } from '3d-tiles-renderer/babylonjs'; import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins'; import GUI from 'lil-gui'; @@ -19,27 +19,27 @@ gui.add( params, 'errorTarget', 1, 100 ); // engine const canvas = document.getElementById( 'renderCanvas' ); -const engine = new BABYLON.Engine( canvas, true ); +const engine = new Engine( canvas, true ); engine.setHardwareScalingLevel( 1 / window.devicePixelRatio ); // scene -const scene = new BABYLON.Scene( engine ); +const scene = new Scene( engine ); scene.useRightHandedSystem = true; // camera -const camera = new BABYLON.ArcRotateCamera( +const camera = new ArcRotateCamera( 'camera', - Math.PI / 2, Math.PI / 3, 100000, - new BABYLON.Vector3( 0, 0, 0 ), + new 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 ) ); +camera.setPosition( new Vector3( 500, 300, - 500 ) ); // tiles const tiles = new TilesRenderer( null, scene ); diff --git a/example/babylonjs/index.js b/example/babylonjs/index.js index 60d7da018..fbe334f3c 100644 --- a/example/babylonjs/index.js +++ b/example/babylonjs/index.js @@ -1,4 +1,4 @@ -import * as BABYLON from 'babylonjs'; +import { Engine, Scene, ArcRotateCamera, Vector3 } from '@babylonjs/core' import { TilesRenderer } from '3d-tiles-renderer/babylonjs'; import GUI from 'lil-gui'; @@ -16,21 +16,21 @@ gui.add( params, 'visibleTiles' ).listen().disable(); // init engine const canvas = document.getElementById( 'renderCanvas' ); -const engine = new BABYLON.Engine( canvas, true ); +const engine = new 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 ); +const scene = new Scene( engine ); scene.useRightHandedSystem = true; // Camera controls -const camera = new BABYLON.ArcRotateCamera( +const camera = new ArcRotateCamera( 'camera', - Math.PI / 2, Math.PI / 2.5, 50, - new BABYLON.Vector3( 0, 0, 0 ), + new Vector3( 0, 0, 0 ), scene, ); camera.attachControl( canvas, true ); @@ -40,6 +40,9 @@ 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; +tiles.addEventListener('load-tileset', ( tileset ) => { + console.log('tileset loaded!'); +}); // render scene.onBeforeRenderObservable.add( () => { diff --git a/package-lock.json b/package-lock.json index 4dc2ecbf2..9095048d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@babel/preset-modules": "^0.1.6", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", + "@babylonjs/core": "^8.39.3", + "@babylonjs/loaders": "^8.39.3", "@eslint/js": "^9.0.0", "@react-three/drei": "^10.0.0", "@react-three/fiber": "^9.0.0", @@ -21,8 +23,6 @@ "@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", @@ -717,6 +717,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babylonjs/core": { + "version": "8.40.1", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-8.40.1.tgz", + "integrity": "sha512-J//Wodlu1m1elh3cKZUh4HillxVc4QxgWyJxUABFYtr0kfSzk2vwJ5f9F3BvEq3X+XU8dEEK7i9buU1aV6w11A==", + "dev": true + }, + "node_modules/@babylonjs/loaders": { + "version": "8.40.1", + "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-8.40.1.tgz", + "integrity": "sha512-kmmgsgCSm5v8MCZ4z96Iv9AnWWO4fOaPpRwO5ftpIK5cnVdX5eA1qzHVhF+BkRGJUOGv2RSWWS51Ucw2gFW/Yg==", + "dev": true, + "peerDependencies": { + "@babylonjs/core": "^8.0.0", + "babylonjs-gltf2interface": "^8.0.0" + } + }, "node_modules/@cesium/engine": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-19.0.0.tgz", @@ -2991,28 +3007,37 @@ "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" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "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==", + "version": "8.40.1", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.40.1.tgz", + "integrity": "sha512-d++rCxLZrVDtaPDD4qK7sczs/J3XBWp86mALaYZiZysnbw6hDlcf2bqoQ4pp6NErXSI8llUiJEs++INo+XBTVQ==", "dev": true, - "license": "Apache-2.0" + "peer": true }, "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", + "optional": true, + "peer": true, "dependencies": { "babylonjs": "^7.54.3", "babylonjs-gltf2interface": "^7.54.3" } }, + "node_modules/babylonjs-loaders/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==", + "optional": true, + "peer": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index 087500d73..deb2c3cd7 100644 --- a/package.json +++ b/package.json @@ -91,8 +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", + "@babylonjs/core": "^8.39.3", + "@babylonjs/loaders": "^8.39.3", "cesium": "^1.132.0", "concurrently": "^6.2.1", "eslint": "^9.0.0", diff --git a/src/babylonjs/renderer/README.md b/src/babylonjs/renderer/README.md index 5a1612f1c..87608b0ef 100644 --- a/src/babylonjs/renderer/README.md +++ b/src/babylonjs/renderer/README.md @@ -9,19 +9,19 @@ Implementation of the TilesRendererBase class for Babylon js. # Use ```js -import * as BABYLON from 'babylonjs'; +import { Engine, Scene } from '@babylonjs/core'; import { TilesRenderer } from '3d-tiles-renderer/babylonjs'; // create engine const canvas = document.getElementById( 'renderCanvas' ); -const engine = new BABYLON.Engine( canvas, true ); +const engine = new Engine( canvas, true ); // right handed coordinate system is required -const scene = new BABYLON.Scene( engine ); +const scene = new Scene( engine ); scene.useRightHandedSystem = true; // create the babylon tile renderer -const tiles = new BabylonTilesRenderer( TILESET_URL, scene ); +const tiles = new TilesRenderer( TILESET_URL, scene ); // ... initialize the camera diff --git a/src/babylonjs/renderer/loaders/GLTFLoader.js b/src/babylonjs/renderer/loaders/GLTFLoader.js index ad57d6da9..b5ebeea47 100644 --- a/src/babylonjs/renderer/loaders/GLTFLoader.js +++ b/src/babylonjs/renderer/loaders/GLTFLoader.js @@ -1,8 +1,11 @@ import { LoaderBase } from '3d-tiles-renderer/core'; -import { Matrix, Quaternion, SceneLoader } from 'babylonjs'; -import 'babylonjs-loaders'; +import { Matrix, Quaternion, ImportMeshAsync } from '@babylonjs/core'; +import "@babylonjs/loaders/glTF/2.0"; +// GLB magic bytes: "glTF" in ASCII (0x67, 0x6C, 0x54, 0x46) +const GLB_MAGIC = 0x46546C67; const _worldMatrix = /* @__PURE__ */ Matrix.Identity(); + export class GLTFLoader extends LoaderBase { constructor( scene ) { @@ -13,6 +16,57 @@ export class GLTFLoader extends LoaderBase { } + /** + * Detect if buffer contains GLB binary data by checking magic bytes + * @param {ArrayBuffer|Uint8Array} buffer - The file buffer + * @returns {boolean} True if GLB format + */ + isGLB( buffer ) { + + // Handle both ArrayBuffer and typed arrays (e.g., Uint8Array) + const arrayBuffer = buffer instanceof ArrayBuffer ? buffer : buffer.buffer; + const byteOffset = buffer instanceof ArrayBuffer ? 0 : buffer.byteOffset; + const byteLength = buffer.byteLength; + + if ( byteLength < 4 ) { + + return false; + + } + + const view = new DataView( arrayBuffer, byteOffset, byteLength ); + const magic = view.getUint32( 0, true ); // little-endian + return magic === GLB_MAGIC; + + } + + /** + * Detect file extension from URI or buffer content + * @param {ArrayBuffer} buffer - The file buffer + * @param {string} uri - The file URI + * @returns {string} '.glb' or '.gltf' + */ + detectExtension( buffer, uri ) { + + // First check magic bytes in buffer (most reliable) + if ( this.isGLB( buffer ) ) { + + return '.glb'; + + } + + // Fallback to URI extension + const lowerUri = uri.toLowerCase(); + if ( lowerUri.endsWith( '.glb' ) ) { + + return '.glb'; + + } + + // Default to gltf + return '.gltf'; + } + async parse( buffer, uri ) { const { scene, workingPath, adjustmentTransform } = this; @@ -25,18 +79,15 @@ export class GLTFLoader extends LoaderBase { } + // Detect format from buffer magic bytes or URI extension + const pluginExtension = this.detectExtension( buffer, uri ); // 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, + const container = await ImportMeshAsync( new File( [ buffer ], uri ), scene, - null, - '.glb', + { pluginExtension } ); - container.addAllToScene(); - // retrieve the primary scene const root = container.meshes[ 0 ]; diff --git a/src/babylonjs/renderer/tiles/TilesRenderer.js b/src/babylonjs/renderer/tiles/TilesRenderer.js index 3841e3f0a..77040e848 100644 --- a/src/babylonjs/renderer/tiles/TilesRenderer.js +++ b/src/babylonjs/renderer/tiles/TilesRenderer.js @@ -1,5 +1,5 @@ import { TilesRendererBase, LoaderUtils } from '3d-tiles-renderer/core'; -import { Matrix, Vector3, Plane, TransformNode, Frustum } from 'babylonjs'; +import { TransformNode, Matrix, Vector3, Frustum, Observable, Plane } from '@babylonjs/core'; import { B3DMLoader } from '../loaders/B3DMLoader.js'; import { GLTFLoader } from '../loaders/GLTFLoader.js'; import { TileBoundingVolume } from '../math/TileBoundingVolume.js'; @@ -19,15 +19,62 @@ export class TilesRenderer extends TilesRendererBase { this.scene = scene; this.group = new TransformNode( 'tiles-root', scene ); this._upRotationMatrix = Matrix.Identity(); + + // Babylon.js Observables for events + this._observables = new Map(); } - // TODO: implement these with Babylon constructs - addEventListener() {} + /** + * Get or create an observable for the given event type + * @param {string} type - Event type name + * @returns {Observable} The observable for this event type + */ + getObservable( type ) { - removeEventListener() {} + if ( ! this._observables.has( type ) ) { - dispatchEvent() {} + this._observables.set( type, new Observable() ); + + } + + return this._observables.get( type ); + + } + + // Event handling - adapter for TilesRendererBase using Babylon Observables + addEventListener( type, listener ) { + + const observable = this.getObservable( type ); + observable.add( listener ); + + } + + removeEventListener( type, listener ) { + + if ( ! this._observables.has( type ) ) { + + return; + + } + + const observable = this._observables.get( type ); + observable.removeCallback( listener ); + + } + + dispatchEvent( event ) { + + if ( ! this._observables.has( event.type ) ) { + + return; + + } + + const observable = this._observables.get( event.type ); + observable.notifyObservers( event ); + + } loadRootTileset( ...args ) {