From 1de40ffa7e4724bf18c20dee44a3b4d1f032ee06 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Wed, 4 Mar 2026 14:38:58 -0800 Subject: [PATCH 1/5] Add spark support to GLTFExtensionsPlugin. Upgrade three.js dependency to 0.182 --- src/three/plugins/GLTFExtensionsPlugin.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/three/plugins/GLTFExtensionsPlugin.js b/src/three/plugins/GLTFExtensionsPlugin.js index 8813e259b..96625f770 100644 --- a/src/three/plugins/GLTFExtensionsPlugin.js +++ b/src/three/plugins/GLTFExtensionsPlugin.js @@ -2,6 +2,7 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFStructuralMetadataExtension } from './gltf/GLTFStructuralMetadataExtension.js'; import { GLTFMeshFeaturesExtension } from './gltf/GLTFMeshFeaturesExtension.js'; import { GLTFCesiumRTCExtension } from './gltf/GLTFCesiumRTCExtension.js'; +import { registerSparkLoader } from '@ludicon/spark.js/three-gltf'; export class GLTFExtensionsPlugin { @@ -17,6 +18,8 @@ export class GLTFExtensionsPlugin { ktxLoader: null, meshoptDecoder: null, autoDispose: true, + spark: null, + sparkOptions: null, ...options, }; @@ -29,6 +32,8 @@ export class GLTFExtensionsPlugin { this.dracoLoader = options.dracoLoader; this.ktxLoader = options.ktxLoader; this.meshoptDecoder = options.meshoptDecoder; + this.spark = options.spark; + this.sparkOptions = options.sparkOptions; this._gltfRegex = /\.(gltf|glb)$/g; this._dracoRegex = /\.drc$/g; this._loader = null; @@ -57,6 +62,12 @@ export class GLTFExtensionsPlugin { } + if ( this.spark ) { + + registerSparkLoader( loader, this.spark, this.sparkOptions ); + + } + if ( this.rtc ) { loader.register( () => new GLTFCesiumRTCExtension() ); From 601cd1930a49869e8886ba149f94839782580331 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Wed, 4 Mar 2026 14:39:50 -0800 Subject: [PATCH 2/5] Actually add spark.js dependency. --- package-lock.json | 33 ++++++++++++++++++++++++++++----- package.json | 5 +++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2e5e2223..ff241cd8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@babylonjs/core": "^8.47.2", "@babylonjs/loaders": "^8.47.2", "@eslint/js": "^9.0.0", + "@ludicon/spark.js": "^0.1.0", "@react-three/drei": "^10.0.0", "@react-three/fiber": "^9.0.0", "@types/node": "^24.3.0", @@ -33,7 +34,7 @@ "leva": "^0.10.0", "lil-gui": "^0.21.0", "postprocessing": "^6.36.4", - "three": "^0.170.0", + "three": "^0.182.0", "typescript": "^5.6.0", "typescript-eslint": "^8.48.1", "vite": "^6.2.2", @@ -45,7 +46,7 @@ "@react-three/fiber": "^8.17.9 || ^9.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", - "three": ">=0.167.0" + "three": ">=0.182.0" }, "peerDependenciesMeta": { "@babylonjs/core": { @@ -1550,6 +1551,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@ludicon/spark.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ludicon/spark.js/-/spark.js-0.1.0.tgz", + "integrity": "sha512-d/y+VRwWr2rzqFhv4yKYRU13tEymg9agSgI15hbGSIk0ToRZd8CNFhtcxJKZeWqNXfIW0WlCgNwclWIWmODD+w==", + "dev": true, + "license": "See LICENSE", + "peerDependencies": { + "three": ">=0.182.0" + }, + "peerDependenciesMeta": { + "three": { + "optional": true + } + } + }, "node_modules/@mediapipe/tasks-vision": { "version": "0.10.17", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", @@ -7363,6 +7379,13 @@ "three": "*" } }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stats.js": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", @@ -7570,9 +7593,9 @@ } }, "node_modules/three": { - "version": "0.170.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", - "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", + "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 5fcea2643..220b1b81a 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.0.0", + "@ludicon/spark.js": "^0.1.0", "@react-three/drei": "^10.0.0", "@react-three/fiber": "^9.0.0", "@types/node": "^24.3.0", @@ -105,7 +106,7 @@ "leva": "^0.10.0", "lil-gui": "^0.21.0", "postprocessing": "^6.36.4", - "three": "^0.170.0", + "three": "^0.182.0", "typescript": "^5.6.0", "typescript-eslint": "^8.48.1", "vite": "^6.2.2", @@ -117,7 +118,7 @@ "@babylonjs/loaders": ">=8.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", - "three": ">=0.167.0" + "three": ">=0.182.0" }, "peerDependenciesMeta": { "@react-three/fiber": { From 08c227f87409ce00bb7168285b79e3a3e0a7c267 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Thu, 5 Mar 2026 00:41:30 -0800 Subject: [PATCH 3/5] Add Spark support to googleMaps example. --- example/three/googleMapsAerial.js | 62 ++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/example/three/googleMapsAerial.js b/example/three/googleMapsAerial.js index 7603abc3a..79bd65747 100644 --- a/example/three/googleMapsAerial.js +++ b/example/three/googleMapsAerial.js @@ -1,5 +1,7 @@ import { GeoUtils, WGS84_ELLIPSOID, TilesRenderer } from '3d-tiles-renderer'; -import { TilesFadePlugin, TileCompressionPlugin, GLTFExtensionsPlugin, CesiumIonAuthPlugin, ReorientationPlugin } from '3d-tiles-renderer/plugins'; +import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins'; +import { TilesFadePlugin, TileCompressionPlugin, GLTFExtensionsPlugin, ReorientationPlugin } from '3d-tiles-renderer/plugins'; +import { estimateBytesUsed } from '../../src/three/renderer/utils/MemoryUtils.js'; import { Scene, WebGLRenderer, @@ -9,14 +11,26 @@ import { } from 'three'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { SparkGL } from '@ludicon/spark.js'; +import Stats from 'three/examples/jsm/libs/stats.module.js'; +import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; let camera, controls, scene, renderer, tiles; +let spark; +let stats; + +const params = { + enableSpark: true, + preferLowQuality: true, + generateMipmaps: false, + errorTarget: 16.0, + textureVRAM: 0, +}; const raycaster = new Raycaster(); raycaster.firstHitOnly = true; -init(); -animate(); +init().then( () => animate() ); function reinstantiateTiles() { @@ -35,7 +49,9 @@ function reinstantiateTiles() { tiles.registerPlugin( new GLTFExtensionsPlugin( { // Note the DRACO compression files need to be supplied via an explicit source. // We use unpkg here but in practice should be provided by the application. - dracoLoader: new DRACOLoader().setDecoderPath( 'https://unpkg.com/three@0.153.0/examples/jsm/libs/draco/gltf/' ) + dracoLoader: new DRACOLoader().setDecoderPath( 'https://unpkg.com/three@0.153.0/examples/jsm/libs/draco/gltf/' ), + spark: params.enableSpark ? spark : undefined, + sparkOptions: { preferLowQuality: params.preferLowQuality, generateMipmaps: params.generateMipmaps }, } ) ); tiles.registerPlugin( new ReorientationPlugin( { lat: 35.6586 * MathUtils.DEG2RAD, lon: 139.7454 * MathUtils.DEG2RAD } ) ); @@ -55,7 +71,7 @@ function reinstantiateTiles() { } -function init() { +async function init() { scene = new Scene(); @@ -79,8 +95,35 @@ function init() { controls.autoRotateSpeed = 0.5; controls.enablePan = false; + // initialize Spark + const gl = renderer.getContext(); + spark = await SparkGL.create( gl, { + preload: [ 'rgb' ], + cacheTempResources: true, + verbose: true, + } ); + reinstantiateTiles(); + // Initialize stats + stats = new Stats(); + stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom + document.body.appendChild( stats.dom ); + + // Create GUI + const gui = new GUI(); + gui.add( params, 'enableSpark' ).name( 'Enable Spark' ).onChange( () => { + reinstantiateTiles(); + } ); + gui.add( params, 'preferLowQuality' ).name( 'BC1 / ETC2' ).onChange( () => { + reinstantiateTiles(); + } ); + gui.add( params, 'generateMipmaps' ).name( 'Generate Mipmaps' ).onChange( () => { + reinstantiateTiles(); + } ); + gui.add( params, 'errorTarget', 4, 16, 0.1 ).name( 'Error Target' ); + gui.add( params, 'textureVRAM' ).name( 'Texture VRAM (MB)' ).listen().disable(); + onWindowResize(); window.addEventListener( 'resize', onWindowResize, false ); window.addEventListener( 'hashchange', initFromHash ); @@ -120,11 +163,14 @@ function animate() { requestAnimationFrame( animate ); + if ( stats ) stats.begin(); + if ( ! tiles ) return; controls.update(); // update options + tiles.errorTarget = params.errorTarget; tiles.setResolutionFromRenderer( camera, renderer ); tiles.setCamera( camera ); @@ -134,6 +180,8 @@ function animate() { render(); + if ( stats ) stats.end(); + } function render() { @@ -154,4 +202,8 @@ function render() { } + // Update GPU memory info + const approximateVRAM = estimateBytesUsed( scene ); + params.textureVRAM = ( approximateVRAM / ( 1024 * 1024 ) ).toFixed( 1 ); + } From b96fd1416e0f99f337b669cd797dc6d05df863b4 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Thu, 5 Mar 2026 00:42:01 -0800 Subject: [PATCH 4/5] Track memory footprint of spark textures. --- src/three/renderer/utils/MemoryUtils.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/three/renderer/utils/MemoryUtils.js b/src/three/renderer/utils/MemoryUtils.js index dffb42a02..e123e2e10 100644 --- a/src/three/renderer/utils/MemoryUtils.js +++ b/src/three/renderer/utils/MemoryUtils.js @@ -1,5 +1,5 @@ import { estimateBytesUsed as _estimateBytesUsed } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; -import { TextureUtils } from 'three'; +import { TextureUtils, ExternalTexture } from 'three'; export function getTextureByteLength( tex ) { @@ -9,6 +9,13 @@ export function getTextureByteLength( tex ) { } + // IC: The code below assumes tex was created from an ImageBitmap + if ( tex instanceof ExternalTexture && tex.userData?.byteLength ) { + + return tex.userData.byteLength; + + } + const { format, type, image } = tex; const { width, height } = image; From 73f9481f67f0ca2f217b05b38225ee1a88e097f7 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Mon, 16 Mar 2026 18:00:55 -0700 Subject: [PATCH 5/5] Undo changes to GLTFExtensionPlugin. Update googleMapsAerial example to enable spark through the plugins argument. Require spark 0.1.3 --- example/three/googleMapsAerial.js | 4 ++-- package.json | 2 +- src/three/plugins/GLTFExtensionsPlugin.js | 11 ----------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/example/three/googleMapsAerial.js b/example/three/googleMapsAerial.js index 79bd65747..14a511a65 100644 --- a/example/three/googleMapsAerial.js +++ b/example/three/googleMapsAerial.js @@ -14,6 +14,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { SparkGL } from '@ludicon/spark.js'; import Stats from 'three/examples/jsm/libs/stats.module.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; +import { createSparkPlugins } from '@ludicon/spark.js/three-gltf'; let camera, controls, scene, renderer, tiles; let spark; @@ -50,8 +51,7 @@ function reinstantiateTiles() { // Note the DRACO compression files need to be supplied via an explicit source. // We use unpkg here but in practice should be provided by the application. dracoLoader: new DRACOLoader().setDecoderPath( 'https://unpkg.com/three@0.153.0/examples/jsm/libs/draco/gltf/' ), - spark: params.enableSpark ? spark : undefined, - sparkOptions: { preferLowQuality: params.preferLowQuality, generateMipmaps: params.generateMipmaps }, + plugins: params.enableSpark ? createSparkPlugins( spark, { preferLowQuality: params.preferLowQuality, generateMipmaps: params.generateMipmaps } ) : [] } ) ); tiles.registerPlugin( new ReorientationPlugin( { lat: 35.6586 * MathUtils.DEG2RAD, lon: 139.7454 * MathUtils.DEG2RAD } ) ); diff --git a/package.json b/package.json index 220b1b81a..7b341e0c7 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.0.0", - "@ludicon/spark.js": "^0.1.0", + "@ludicon/spark.js": "^0.1.3", "@react-three/drei": "^10.0.0", "@react-three/fiber": "^9.0.0", "@types/node": "^24.3.0", diff --git a/src/three/plugins/GLTFExtensionsPlugin.js b/src/three/plugins/GLTFExtensionsPlugin.js index 96625f770..8813e259b 100644 --- a/src/three/plugins/GLTFExtensionsPlugin.js +++ b/src/three/plugins/GLTFExtensionsPlugin.js @@ -2,7 +2,6 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFStructuralMetadataExtension } from './gltf/GLTFStructuralMetadataExtension.js'; import { GLTFMeshFeaturesExtension } from './gltf/GLTFMeshFeaturesExtension.js'; import { GLTFCesiumRTCExtension } from './gltf/GLTFCesiumRTCExtension.js'; -import { registerSparkLoader } from '@ludicon/spark.js/three-gltf'; export class GLTFExtensionsPlugin { @@ -18,8 +17,6 @@ export class GLTFExtensionsPlugin { ktxLoader: null, meshoptDecoder: null, autoDispose: true, - spark: null, - sparkOptions: null, ...options, }; @@ -32,8 +29,6 @@ export class GLTFExtensionsPlugin { this.dracoLoader = options.dracoLoader; this.ktxLoader = options.ktxLoader; this.meshoptDecoder = options.meshoptDecoder; - this.spark = options.spark; - this.sparkOptions = options.sparkOptions; this._gltfRegex = /\.(gltf|glb)$/g; this._dracoRegex = /\.drc$/g; this._loader = null; @@ -62,12 +57,6 @@ export class GLTFExtensionsPlugin { } - if ( this.spark ) { - - registerSparkLoader( loader, this.spark, this.sparkOptions ); - - } - if ( this.rtc ) { loader.register( () => new GLTFCesiumRTCExtension() );