Skip to content

Commit 4d85051

Browse files
authored
Tiles Traversal: Only load necessary tiles (#1421)
* Add flag for optimized loading * Add conditions for loading the tiles at the error target * Remove conditions * Update traversal * comments * handle kicking on zoom in * Update examples for testing * Remove unused lines * Updates, comments * Fix zoom out case * Adjust used behavior * Small fix * Small fixes * Basic fixes * Small fixes * Small improvements * Get it working * Fix raycasting * Readd optimization * Prevent intermediate tiles from loading * Fix trickle in * Rename traverse functions * Add old traversal approach * Add toggle for traversal strategy * Fix up example * Cleanup * Improve tiles to load * Load tiles in frustum first, add loadSiblings option * Fix low lod tiles loading too early, adjust load priority * Update LRUCache * Add additive refinement support * Fix case where children were not processed * Fix active flag * remove unneeded field * Small fix * Small cleanup * default loadSiblings to "true" * Update demos * Add optimized settings option * Add more queue information * Add types * README update * Improve demo * Update README * Fix controls issues * Simplification, variable rename * Re-add fade plugin * README update * update demo * update traversal
1 parent ed708a7 commit 4d85051

8 files changed

Lines changed: 654 additions & 19 deletions

File tree

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,31 @@ displayActiveTiles = false : Boolean
393393
394394
Active tiles not currently visible in a camera frustum are removed from the scene as an optimization. Setting `displayActiveTiles` to true will keep them in the scene to be rendered from an outside camera view not accounted for by the tiles renderer.
395395
396+
### .optimizedLoadStrategy
397+
398+
```js
399+
optimizedLoadStrategy = false : Boolean
400+
```
401+
402+
Enables an **experimental** optimized tile loading strategy that loads only the tiles needed for the current view, reducing memory usage and improving initial load times. Tiles are loaded independently based on screen space error without requiring all parent tiles to load first. Prevents visual gaps and flashing during camera movement.
403+
404+
Based in part on [Cesium Native tile selection](https://cesium.com/learn/cesium-native/ref-doc/selection-algorithm-details.html).
405+
406+
Default is `false` which uses the previous approach of loading all parent and sibling tiles for guaranteed smooth transitions.
407+
408+
> [!WARN]
409+
> Setting is currently incompatible with plugins that split tiles and on-the-fly generate and dispose of child tiles including the ImageOverlaysPlugin enableTileSplitting setting, QuantizedMeshPlugin, & ImageFormatPlugins (XYZ, TMS, etc). Any tile sets that share caches or queues must also use the same setting.
410+
411+
### .loadSiblings
412+
413+
```js
414+
loadSiblings = true : Boolean
415+
```
416+
417+
**Experimental** setting that, when true, causes sibling tiles to together to prevent gaps during camera movement. When false, only visible tiles are loaded, minimizing memory but potentially causing brief gaps during rapid movement.
418+
419+
Only applies when `optimizedLoadStrategy` is enabled.
420+
396421
### .autoDisableRendererCulling
397422
398423
```js

example/styles.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ canvas {
4040
display: flex;
4141
padding: 5px;
4242
box-sizing: border-box;
43+
pointer-events: none;
4344
}
4445

4546
#credits {

example/three/googleMapsExample.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ const params = {
3434

3535
orthographic: false,
3636

37+
optimizedLoadStrategy: false,
38+
loadSiblings: true,
39+
3740
enableCacheDisplay: false,
3841
enableRendererStats: false,
3942
useBatchedMesh: Boolean( new URLSearchParams( window.location.hash.replace( /^#/, '' ) ).get( 'batched' ) ),
43+
useFadePlugin: true,
4044
displayTopoLines: false,
4145
errorTarget: 20,
4246

@@ -58,18 +62,25 @@ function reinstantiateTiles() {
5862
}
5963

6064
tiles = new TilesRenderer();
65+
tiles.lruCache.minSize = 0;
6166
tiles.registerPlugin( new CesiumIonAuthPlugin( { apiToken: import.meta.env.VITE_ION_KEY, assetId: '2275207', autoRefreshToken: true } ) );
6267
tiles.registerPlugin( new TileCompressionPlugin() );
6368
tiles.registerPlugin( new UpdateOnChangePlugin() );
6469
tiles.registerPlugin( new UnloadTilesPlugin() );
65-
tiles.registerPlugin( new TilesFadePlugin() );
6670
tiles.registerPlugin( new TopoLinesPlugin( { projection: 'ellipsoid' } ) );
6771
tiles.registerPlugin( new GLTFExtensionsPlugin( {
6872
// Note the DRACO compression files need to be supplied via an explicit source.
6973
// We use unpkg here but in practice should be provided by the application.
7074
dracoLoader: new DRACOLoader().setDecoderPath( 'https://unpkg.com/three@0.153.0/examples/jsm/libs/draco/gltf/' )
7175
} ) );
76+
tiles.optimizedLoadStrategy = params.optimizedLoadStrategy;
77+
tiles.loadSiblings = params.loadSiblings;
78+
79+
if ( params.useFadePlugin ) {
7280

81+
tiles.registerPlugin( new TilesFadePlugin() );
82+
83+
}
7384

7485
if ( params.useBatchedMesh ) {
7586

@@ -155,7 +166,17 @@ function init() {
155166
} );
156167

157168
const mapsOptions = gui.addFolder( 'Google Photorealistic Tiles' );
169+
if ( new URLSearchParams( window.location.search ).has( 'showOptimizedSettings' ) ) {
170+
171+
params.optimizedLoadStrategy = true;
172+
tiles.optimizedLoadStrategy = true;
173+
mapsOptions.add( params, 'optimizedLoadStrategy' ).listen();
174+
mapsOptions.add( params, 'loadSiblings' ).listen();
175+
176+
}
177+
158178
mapsOptions.add( params, 'useBatchedMesh' ).listen();
179+
mapsOptions.add( params, 'useFadePlugin' ).listen();
159180
mapsOptions.add( params, 'reload' );
160181

161182
const exampleOptions = gui.addFolder( 'Example Options' );
@@ -168,6 +189,7 @@ function init() {
168189

169190
} );
170191

192+
// add stats
171193
statsContainer = document.createElement( 'div' );
172194
document.getElementById( 'info' ).appendChild( statsContainer );
173195

@@ -317,6 +339,9 @@ function animate() {
317339

318340
if ( ! tiles ) return;
319341

342+
// ensure transforms are up to date for controls update
343+
scene.updateMatrixWorld();
344+
320345
controls.enabled = ! transition.animating;
321346
controls.update();
322347
transition.update();
@@ -351,7 +376,7 @@ function updateHtml() {
351376

352377
const lruCache = tiles.lruCache;
353378
const cacheFullness = lruCache.cachedBytes / lruCache.maxBytesSize;
354-
str += `Queued: ${ tiles.stats.queued } Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.visibleTiles.size }<br/>`;
379+
str += `Queued: ${ tiles.stats.queued } Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Loaded: ${ tiles.stats.loaded }<br/>Visible: ${ tiles.visibleTiles.size } Active: ${ tiles.activeTiles.size }<br/>`;
355380
str += `Cache: ${ ( 100 * cacheFullness ).toFixed( 2 ) }% ~${ ( lruCache.cachedBytes / 1000 / 1000 ).toFixed( 2 ) }mb<br/>`;
356381

357382
}
@@ -374,7 +399,7 @@ function updateHtml() {
374399

375400
} );
376401

377-
fadePlugin.batchedMesh?._instanceInfo.forEach( info => {
402+
fadePlugin?.batchedMesh?._instanceInfo.forEach( info => {
378403

379404
if ( info.visible && info.active ) tot ++;
380405

example/three/quantMeshOverlays.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ function updateHtml() {
226226

227227
const lruCache = tiles.lruCache;
228228
const cacheFullness = lruCache.cachedBytes / lruCache.maxBytesSize;
229-
str += `Queued: ${ tiles.stats.queued } Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.visibleTiles.size }<br/>`;
229+
str += `Queued: ${ tiles.stats.queued } Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Loaded: ${ tiles.stats.loaded }<br/>Visible: ${ tiles.visibleTiles.size } Active: ${ tiles.activeTiles.size }<br/>`;
230230
str += `Cache: ${ ( 100 * cacheFullness ).toFixed( 2 ) }% ~${ ( lruCache.cachedBytes / 1000 / 1000 ).toFixed( 2 ) }mb<br/>`;
231231

232232
}

src/core/renderer/tiles/TilesRendererBase.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export class TilesRendererBase {
1212
errorThreshold : number;
1313
displayActiveTiles : boolean;
1414
maxDepth : number;
15+
loadSiblings : number;
16+
optimizedLoadStrategy : number;
1517

1618
loadProgress: number;
1719

src/core/renderer/tiles/TilesRendererBase.js

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getUrlExtension } from '../utilities/urlExtension.js';
22
import { LRUCache } from '../utilities/LRUCache.js';
33
import { PriorityQueue } from '../utilities/PriorityQueue.js';
4-
import { markUsedTiles, toggleTiles, markVisibleTiles, markUsedSetLeaves } from './traverseFunctions.js';
4+
import { runTraversal as optimizedRunTraversal } from './optimizedTraverseFunctions.js';
5+
import { runTraversal } from './traverseFunctions.js';
56
import { UNLOADED, QUEUED, LOADING, PARSING, LOADED, FAILED } from '../constants.js';
67
import { throttle } from '../utilities/throttle.js';
78
import { traverseSet } from '../utilities/TraversalUtils.js';
@@ -10,7 +11,7 @@ const PLUGIN_REGISTERED = Symbol( 'PLUGIN_REGISTERED' );
1011

1112
// priority queue sort function that takes two tiles to compare. Returning 1 means
1213
// "tile a" is loaded first.
13-
const priorityCallback = ( a, b ) => {
14+
const defaultPriorityCallback = ( a, b ) => {
1415

1516
const aPriority = a.priority || 0;
1617
const bPriority = b.priority || 0;
@@ -46,6 +47,43 @@ const priorityCallback = ( a, b ) => {
4647

4748
};
4849

50+
// Optimized priority callback - prioritizes distance over error for better user experience
51+
const optimizedPriorityCallback = ( a, b ) => {
52+
53+
const aPriority = a.priority || 0;
54+
const bPriority = b.priority || 0;
55+
56+
if ( aPriority !== bPriority ) {
57+
58+
// lower priority value sorts first
59+
return aPriority > bPriority ? 1 : - 1;
60+
61+
} else if ( a.__used !== b.__used ) {
62+
63+
// load tiles that have been used
64+
return a.__used ? 1 : - 1;
65+
66+
} else if ( a.__inFrustum !== b.__inFrustum ) {
67+
68+
// load tiles that have are in the frustum
69+
return a.__inFrustum ? 1 : - 1;
70+
71+
} else if ( a.__hasUnrenderableContent !== b.__hasUnrenderableContent ) {
72+
73+
// load internal tile sets first
74+
return a.__hasUnrenderableContent ? 1 : - 1;
75+
76+
} else if ( a.__distanceFromCamera !== b.__distanceFromCamera ) {
77+
78+
// load closer tiles first
79+
return a.__distanceFromCamera > b.__distanceFromCamera ? - 1 : 1;
80+
81+
}
82+
83+
return 0;
84+
85+
};
86+
4987
// lru cache unload callback that takes two tiles to compare. Returning 1 means "tile a"
5088
// is unloaded first.
5189
const lruPriorityCallback = ( a, b ) => {
@@ -144,11 +182,11 @@ export class TilesRendererBase {
144182

145183
const downloadQueue = new PriorityQueue();
146184
downloadQueue.maxJobs = 25;
147-
downloadQueue.priorityCallback = priorityCallback;
185+
downloadQueue.priorityCallback = defaultPriorityCallback;
148186

149187
const parseQueue = new PriorityQueue();
150188
parseQueue.maxJobs = 5;
151-
parseQueue.priorityCallback = priorityCallback;
189+
parseQueue.priorityCallback = defaultPriorityCallback;
152190

153191
const processNodeQueue = new PriorityQueue();
154192
processNodeQueue.maxJobs = 25;
@@ -169,6 +207,7 @@ export class TilesRendererBase {
169207
queued: 0,
170208
downloading: 0,
171209
parsing: 0,
210+
loaded: 0,
172211
failed: 0,
173212

174213
inFrustum: 0,
@@ -190,6 +229,8 @@ export class TilesRendererBase {
190229
this._errorThreshold = Infinity;
191230
this.displayActiveTiles = false;
192231
this.maxDepth = Infinity;
232+
this.optimizedLoadStrategy = false;
233+
this.loadSiblings = true;
193234

194235
}
195236

@@ -341,7 +382,7 @@ export class TilesRendererBase {
341382

342383
update() {
343384

344-
const { lruCache, usedSet, stats, root, downloadQueue, parseQueue, processNodeQueue } = this;
385+
const { lruCache, usedSet, stats, root, downloadQueue, parseQueue, processNodeQueue, optimizedLoadStrategy } = this;
345386
if ( this.rootLoadingState === UNLOADED ) {
346387

347388
this.rootLoadingState = LOADING;
@@ -403,10 +444,21 @@ export class TilesRendererBase {
403444
usedSet.forEach( tile => lruCache.markUnused( tile ) );
404445
usedSet.clear();
405446

406-
markUsedTiles( root, this );
407-
markUsedSetLeaves( root, this );
408-
markVisibleTiles( root, this );
409-
toggleTiles( root, this );
447+
// assign the correct callbacks
448+
const priorityCallback = optimizedLoadStrategy ? optimizedPriorityCallback : defaultPriorityCallback;
449+
downloadQueue.priorityCallback = priorityCallback;
450+
parseQueue.priorityCallback = priorityCallback;
451+
452+
// run traversal
453+
if ( optimizedLoadStrategy ) {
454+
455+
optimizedRunTraversal( root, this );
456+
457+
} else {
458+
459+
runTraversal( root, this );
460+
461+
}
410462

411463
// remove any tiles that are loading but no longer used
412464
this.removeUnusedPendingTiles();
@@ -504,6 +556,7 @@ export class TilesRendererBase {
504556
downloading: 0,
505557
failed: 0,
506558
inFrustum: 0,
559+
traversed: 0,
507560
used: 0,
508561
active: 0,
509562
visible: 0,
@@ -965,6 +1018,10 @@ export class TilesRendererBase {
9651018

9661019
stats.parsing --;
9671020

1021+
} else if ( t.__loadingState === LOADED ) {
1022+
1023+
stats.loaded --;
1024+
9681025
}
9691026

9701027
t.__loadingState = UNLOADED;
@@ -1088,6 +1145,7 @@ export class TilesRendererBase {
10881145
}
10891146

10901147
stats.parsing --;
1148+
stats.loaded ++;
10911149
tile.__loadingState = LOADED;
10921150
loadingTiles.delete( tile );
10931151
lruCache.setLoaded( tile, true );
@@ -1151,13 +1209,17 @@ export class TilesRendererBase {
11511209

11521210
stats.queued --;
11531211

1212+
} else if ( tile.__loadingState === LOADING ) {
1213+
1214+
stats.downloading --;
1215+
11541216
} else if ( tile.__loadingState === PARSING ) {
11551217

11561218
stats.parsing --;
11571219

1158-
} else if ( tile.__loadingState === LOADING ) {
1220+
} else if ( tile.__loadingState === LOADED ) {
11591221

1160-
stats.downloading --;
1222+
stats.loaded --;
11611223

11621224
}
11631225

0 commit comments

Comments
 (0)