Skip to content

Commit ba06fec

Browse files
authored
TilesRenderer: Add concept of "virtual" tiles, support additive tile splitting (#1498)
* Add "isVirtual" flag * Adjust handling of virtual children in QuantizedMeshPlugin * ImageOverlayPlugin: use isVirtual * Update imageoverlayplugin * Update ImageOverlayPlugin * Cleanup * Add virtual support to "optimizedTraverseFunctions" * Add types * Use internal virtual properties * Use any pre-seeded fields * Fixed * Fix "processed" check * Small updates * fix race condition for quantized mesh splitting * Update comment * Update ImageOverlayPlugin
1 parent aa61065 commit ba06fec

6 files changed

Lines changed: 121 additions & 70 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ export interface TileInternalData {
1111
basePath: string;
1212
depth: number;
1313
depthFromRenderedParent: number;
14+
/**
15+
* Whether this tile was synthetically based on loaded parent tile state.
16+
*/
17+
isVirtual: boolean;
18+
/**
19+
* The number of virtual children appended to this tile's children array by plugins.
20+
*/
21+
virtualChildCount: number;
1422
}
1523

1624
/**

src/core/renderer/tiles/TilesRendererBase.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,11 @@ export class TilesRendererBase {
812812
basePath: tilesetDir,
813813
depth: - 1,
814814
depthFromRenderedParent: - 1,
815+
isVirtual: false,
816+
virtualChildCount: 0,
817+
818+
// preserve any pre-seeded fields
819+
...tile.internal,
815820
};
816821

817822
if ( tile.content?.uri ) {
@@ -983,7 +988,7 @@ export class TilesRendererBase {
983988
ensureChildrenArePreprocessed( tile, forceImmediate = this.stats.tilesProcessed < this.maxTilesProcessed ) {
984989

985990
const children = tile.children;
986-
if ( children.length === 0 || children[ 0 ].internal ) {
991+
if ( children.length === 0 || children[ 0 ].traversal ) {
987992

988993
return;
989994

@@ -995,7 +1000,6 @@ export class TilesRendererBase {
9951000

9961001
this.preprocessNode( children[ i ], tile.internal.basePath, tile );
9971002

998-
9991003
}
10001004

10011005
};

src/core/renderer/tiles/optimizedTraverseFunctions.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ function isUsedThisFrame( tile, frameCount ) {
2121

2222
function isProcessed( tile ) {
2323

24+
// TODO: here "traversal" is used to determine whether a tile has been initialized. We should
25+
// move these to tiles utils
2426
return Boolean( tile.traversal );
2527

2628
}
@@ -29,7 +31,7 @@ function isProcessed( tile ) {
2931
function areChildrenProcessed( tile ) {
3032

3133
// all children are processed at once
32-
const childrenReady = tile.children.length === 0 || Boolean( tile.children[ 0 ].internal );
34+
const childrenReady = tile.children.length === 0 || isProcessed( tile.children[ 0 ] );
3335
const contentReady = ! tile.internal.hasUnrenderableContent || isDownloadFinished( tile.internal.loadingState );
3436
return childrenReady && contentReady;
3537

@@ -437,6 +439,14 @@ function toggleTiles( tile, renderer ) {
437439

438440
}
439441

442+
// keep tiles with virtual children retained in the LRU cache so the content is
443+
// available to regenerate virtual children if the overlay configuration changes.
444+
if ( tile.internal.virtualChildCount > 0 && tile.internal.hasContent ) {
445+
446+
renderer.markTileUsed( tile );
447+
448+
}
449+
440450
// if the tile is loaded and in frustum we can mark it as visible
441451
tile.traversal.visible = tile.internal.hasRenderableContent && tile.traversal.active && tile.traversal.inFrustum && tile.internal.loadingState === LOADED;
442452
renderer.stats.used ++;

src/core/renderer/tiles/traverseFunctions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function isProcessed( tile ) {
3434
function areChildrenProcessed( tile ) {
3535

3636
// all children are processed at once
37-
return tile.children.length === 0 || Boolean( tile.children[ 0 ].internal );
37+
return tile.children.length === 0 || isProcessed( tile.children[ 0 ] );
3838

3939
}
4040

src/three/plugins/QuantizedMeshPlugin.js

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ export class QuantizedMeshPlugin {
314314
// data is ready for clipping. It's possible that this child data gets to the parse stage
315315
// first, otherwise, while the parent is still downloading.
316316
// Ideally we would be able to guarantee parents are loaded first but this is an odd case.
317+
// Assign the scene value preemptively to ensure it's available for splitting.
318+
tile.engineData.scene = result;
317319
this.expandChildren( tile );
318320

319321
return result;
@@ -406,8 +408,11 @@ export class QuantizedMeshPlugin {
406408

407409
} else {
408410

409-
tile.children.push( child );
411+
// mark the child as "virtual" since it relies on the parent geometry
410412
child.content = { uri: `tile.quantized_tile_split?bottom=${ cy === 0 }&left=${ cx === 0 }` };
413+
child.internal = { isVirtual: true };
414+
tile.internal.virtualChildCount ++;
415+
tile.children.push( child );
411416

412417
}
413418

@@ -417,7 +422,8 @@ export class QuantizedMeshPlugin {
417422

418423
if ( ! hasChildren ) {
419424

420-
tile.children.length = 0;
425+
tile.children.length -= tile.internal.virtualChildCount;
426+
tile.internal.virtualChildCount = 0;
421427

422428
}
423429

@@ -436,27 +442,31 @@ export class QuantizedMeshPlugin {
436442

437443
disposeTile( tile ) {
438444

445+
const { tiles, layer } = this;
446+
439447
// dispose of the available array since we will get it again if this tile is loaded
440-
if ( getTileHasMetadata( tile, this.layer ) ) {
448+
if ( getTileHasMetadata( tile, layer ) ) {
441449

442450
tile[ TILE_AVAILABLE ] = null;
443451

444452
}
445453

446-
// Note: we remove all children always because child tiles can rely on splitting parent tiles
447-
// and we can find ourselves in a situation where a child tile is ready first but the parent tile
448-
// hasn't loaded, causing a stall / race condition in the parsing queue. To avoid this dependency
449-
// we just remove all children and generate them again one the parent is loaded.
450-
// Only get rid of the children if this plugin was responsible for them.
454+
// Remove virtual children when the parent is disposed since they depend on the parent's
455+
// loaded scene for clipping and cannot be rendered or re-generated without it. They will
456+
// be re-created once the parent is loaded again.
451457
if ( TILE_AVAILABLE in tile ) {
452458

453-
tile.children.forEach( child => {
459+
const { virtualChildCount } = tile.internal;
460+
const len = tile.children.length;
461+
const start = len - virtualChildCount;
462+
for ( let i = start; i < len; i ++ ) {
454463

455-
// TODO: there should be a reliable way for removing children like this.
456-
this.tiles.processNodeQueue.remove( child );
464+
tiles.processNodeQueue.remove( tile.children[ i ] );
457465

458-
} );
459-
tile.children.length = 0;
466+
}
467+
468+
tile.children.length -= virtualChildCount;
469+
tile.internal.virtualChildCount = 0;
460470

461471
}
462472

src/three/plugins/images/ImageOverlayPlugin.js

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const _normal = /* @__PURE__ */ new Vector3();
2222
const _box = /* @__PURE__ */ new Box3();
2323
const SPLIT_TILE_DATA = Symbol( 'SPLIT_TILE_DATA' );
2424
const SPLIT_HASH = Symbol( 'SPLIT_HASH' );
25+
const ORIGINAL_REFINE = Symbol( 'ORIGINAL_REFINE' );
2526

2627
// Plugin for overlaying tiled image data on top of 3d tiles geometry.
2728
export class ImageOverlayPlugin {
@@ -221,12 +222,46 @@ export class ImageOverlayPlugin {
221222

222223
}
223224

225+
_removeVirtualChildren( tile ) {
226+
227+
if ( ! ( ORIGINAL_REFINE in tile ) ) {
228+
229+
return;
230+
231+
}
232+
233+
// remove the virtual children associated with the given tile
234+
const { tiles } = this;
235+
const { virtualChildCount } = tile.internal;
236+
const len = tile.children.length;
237+
const start = len - virtualChildCount;
238+
for ( let i = start; i < len; i ++ ) {
239+
240+
const child = tile.children[ i ];
241+
tiles.processNodeQueue.remove( child );
242+
tiles.lruCache.remove( child );
243+
child.parent = null;
244+
245+
}
246+
247+
tile.children.length -= virtualChildCount;
248+
tile.internal.virtualChildCount = 0;
249+
tile.refine = tile[ ORIGINAL_REFINE ];
250+
delete tile[ ORIGINAL_REFINE ];
251+
delete tile[ SPLIT_HASH ];
252+
253+
}
254+
224255
disposeTile( tile ) {
225256

226257
const { overlayInfo, tileControllers, processQueue, pendingTiles, processedTiles } = this;
227258

228259
processedTiles.delete( tile );
229260

261+
// remove any virtual children since they depend on this tile being loaded for regeneration.
262+
// they will be recreated with fresh split configuration when the tile is reloaded.
263+
this._removeVirtualChildren( tile );
264+
230265
// Cancel any ongoing tasks. If a tile is cancelled while downloading
231266
// this will not have been created, yet.
232267
if ( tileControllers.has( tile ) ) {
@@ -354,8 +389,6 @@ export class ImageOverlayPlugin {
354389
this._updateLayers( tile );
355390
this.disposeTile( tile );
356391

357-
delete tile[ SPLIT_HASH ];
358-
359392
} );
360393

361394
tiles.removeEventListener( 'update-after', this._onUpdateAfter );
@@ -403,49 +436,37 @@ export class ImageOverlayPlugin {
403436

404437
}
405438

406-
// collect the tiles split into virtual tiles
439+
// collect the tiles split into virtual tiles, sorted deepest-first so nested virtual tiles
440+
// are cleaned up before their parents when iterating
407441
const { tiles } = this;
408-
const parents = new Set();
442+
const splitTiles = [];
409443
this.processedTiles.forEach( tile => {
410444

411445
if ( SPLIT_HASH in tile ) {
412446

413-
parents.add( tile );
447+
splitTiles.push( tile );
414448

415449
}
416450

417451
} );
418452

419-
// dispose of the virtual children if this tile would not be split or the spilt could change
420-
// under the current overlays used.
421-
parents.forEach( parent => {
422-
423-
if ( parent.parent === null ) {
424-
425-
return;
453+
// ensure we clean depth first
454+
splitTiles.sort( ( a, b ) => b.internal.depth - a.internal.depth );
426455

427-
}
456+
// dispose of the virtual children if this tile would not be split or the split could change
457+
// under the current overlays used.
458+
splitTiles.forEach( tile => {
428459

429-
const clone = parent.engineData.scene.clone();
460+
const clone = tile.engineData.scene.clone();
430461
clone.updateMatrixWorld();
431462

432-
if ( fullDispose || parent[ SPLIT_HASH ] !== this._getSplitVectors( clone, parent ).hash ) {
433-
434-
// TODO: if are parent tile is forcibly remove then we should make sure that all the children are, too?
435-
const children = collectChildren( parent );
436-
children.sort( ( a, b ) => ( b.internal.depth || 0 ) - ( a.internal.depth || 0 ) );
463+
if ( fullDispose || tile[ SPLIT_HASH ] !== this._getSplitVectors( clone, tile ).hash ) {
437464

438465
// note that we need to remove children from the processing queue in this case
439-
// because we are forcibly evicting them from the cache.
440-
children.forEach( child => {
441-
442-
tiles.processNodeQueue.remove( child );
443-
tiles.lruCache.remove( child );
444-
child.parent = null;
445-
446-
} );
447-
448-
parent.children.length = 0;
466+
// because we are forcibly evicting them from the cache. Since parents is sorted
467+
// deepest-first, nested virtual tiles are already cleaned up before we reach
468+
// their parent here.
469+
this._removeVirtualChildren( tile );
449470

450471
}
451472

@@ -462,18 +483,6 @@ export class ImageOverlayPlugin {
462483

463484
}
464485

465-
function collectChildren( root, target = [] ) {
466-
467-
root.children.forEach( child => {
468-
469-
target.push( child );
470-
collectChildren( child, target );
471-
472-
} );
473-
return target;
474-
475-
}
476-
477486
}
478487

479488
_getSplitVectors( scene, tile, centerTarget = _center ) {
@@ -567,7 +576,16 @@ export class ImageOverlayPlugin {
567576

568577
async expandVirtualChildren( scene, tile ) {
569578

570-
if ( tile.children.length !== 0 || this.enableTileSplitting === false ) {
579+
const { refine } = tile;
580+
581+
// Only split tiles that would benefit from it:
582+
// - REPLACE tiles with no children are leaf tiles where splitting improves overlay UV projection quality. REPLACE tiles
583+
// that already have children are already refined by their children so splitting is unnecessary.
584+
// - ADD tiles always need splitting since their content is rendered alongside children at all levels.
585+
// Also skip any tiles that already have virtual children to avoid interfering with other plugins.
586+
const shouldSplit = ( refine === 'REPLACE' && tile.children.length === 0 ) || refine === 'ADD';
587+
const alreadySplit = tile.internal.virtualChildCount !== 0;
588+
if ( this.enableTileSplitting === false || ! shouldSplit || alreadySplit ) {
571589

572590
return;
573591

@@ -577,17 +595,16 @@ export class ImageOverlayPlugin {
577595
const clone = scene.clone();
578596
clone.updateMatrixWorld();
579597

580-
// get the directions to split on
598+
// get the directions to split on & if there are no directions to split on then exit early
581599
const { directions, hash } = this._getSplitVectors( clone, tile, _center );
582-
tile[ SPLIT_HASH ] = hash;
583-
584-
// if there are no directions to split on then exit early
585600
if ( directions.length === 0 ) {
586601

587602
return;
588603

589604
}
590605

606+
tile[ SPLIT_HASH ] = hash;
607+
591608
// set up the splitter to ignore overlay uvs
592609
const clipper = new GeometryClipper();
593610
clipper.attributeList = key => ! /^layer_uv_\d+/.test( key );
@@ -604,7 +621,7 @@ export class ImageOverlayPlugin {
604621

605622
// run the clipping operations by performing every permutation of sides
606623
// defined by the split directions
607-
const children = [];
624+
const splitChildren = [];
608625
clipper.forEachSplitPermutation( () => {
609626

610627
// clip the object itself
@@ -720,7 +737,8 @@ export class ImageOverlayPlugin {
720737

721738
}
722739

723-
children.push( {
740+
splitChildren.push( {
741+
internal: { isVirtual: true },
724742
refine: 'REPLACE',
725743
geometricError: tile.geometricError * 0.5,
726744
boundingVolume: boundingVolume,
@@ -731,12 +749,13 @@ export class ImageOverlayPlugin {
731749

732750
} );
733751

734-
// force the tile "refine" mode to be set to "REPLACE" if we're splitting tiles
735-
// TODO: If a tile is of type "ADD" refine and it has children then it will not be split
736-
// as expected since only geometry tiles with no children are split. Instead we'd want
737-
// to split this tiles geometry in addition to adding the child tiles.
752+
// force the tile "refine" mode to be set to "REPLACE" so that the virtual children
753+
// replace this tile's geometry display. Save the original mode so it can be restored
754+
// if virtual children are later removed.
755+
tile[ ORIGINAL_REFINE ] = tile.refine;
738756
tile.refine = 'REPLACE';
739-
tile.children.push( ...children );
757+
tile.children.push( ...splitChildren );
758+
tile.internal.virtualChildCount += splitChildren.length;
740759

741760
}
742761

0 commit comments

Comments
 (0)