Skip to content

Commit

Permalink
DebugTilesPlugin: optimize performance and make it toggleable (#885)
Browse files Browse the repository at this point in the history
* perf(traverseSet): use a non-recursive tree traversal

For deep hierarchy of nested tilesets, recursive traversal has a severe
performance cost. Use a stack-based, depth first traversal instead.

* perf(DebugTilesPlugin): use cheap tree traversal in _initExtremes()

We don't need the tiles to be preprocessed in this function, which has
a severe performance cost.

* feat(DebugTilesPlugin): allow toggling the plugin (#647)

* example: add toggle to enable/disable DebugTilesPlugin
  • Loading branch information
sguimmara authored Dec 22, 2024
1 parent e9dd91f commit ad3b888
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 33 deletions.
3 changes: 3 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const params = {
resolutionScale: 1.0,

up: hashUrl ? '+Z' : '+Y',
enableDebug: true,
displayBoxBounds: false,
displaySphereBounds: false,
displayRegionBounds: false,
Expand Down Expand Up @@ -271,6 +272,7 @@ function init() {
tileOptions.open();

const debug = gui.addFolder( 'Debug Options' );
debug.add( params, 'enableDebug' );
debug.add( params, 'displayBoxBounds' );
debug.add( params, 'displaySphereBounds' );
debug.add( params, 'displayRegionBounds' );
Expand Down Expand Up @@ -485,6 +487,7 @@ function animate() {

// update plugin
const plugin = tiles.getPluginByName( 'DEBUG_TILES_PLUGIN' );
plugin.enabled = params.enableDebug;
plugin.displayBoxBounds = params.displayBoxBounds;
plugin.displaySphereBounds = params.displaySphereBounds;
plugin.displayRegionBounds = params.displayRegionBounds;
Expand Down
51 changes: 38 additions & 13 deletions src/base/traverseFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,32 +134,57 @@ function canTraverse( tile, renderer ) {

}

// Helper function for recursively traversing a tile set. If `beforeCb` returns `true` then the
// Helper function for traversing a tile set. If `beforeCb` returns `true` then the
// traversal will end early.
export function traverseSet( tile, beforeCb = null, afterCb = null, parent = null, depth = 0 ) {
export function traverseSet( tile, beforeCb = null, afterCb = null ) {

if ( beforeCb && beforeCb( tile, parent, depth ) ) {
const stack = [];

if ( afterCb ) {
// A stack-based, depth-first traversal, storing
// triplets (tile, parent, depth) in the stack array.

afterCb( tile, parent, depth );
stack.push( tile );
stack.push( null );
stack.push( 0 );

while ( stack.length > 0 ) {

const depth = stack.pop();
const parent = stack.pop();
const tile = stack.pop();

if ( beforeCb && beforeCb( tile, parent, depth ) ) {

if ( afterCb ) {

afterCb( tile, parent, depth );

}

return;

}

return;
const children = tile.children;

}
// Children might be undefined if the tile has not been preprocessed yet
if ( children ) {

const children = tile.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
for ( let i = children.length - 1; i >= 0; i -- ) {

traverseSet( children[ i ], beforeCb, afterCb, tile, depth + 1 );
stack.push( children[ i ] );
stack.push( tile );
stack.push( depth + 1 );

}
}

if ( afterCb ) {
}

afterCb( tile, parent, depth );
if ( afterCb ) {

afterCb( tile, parent, depth );

}

}

Expand Down
2 changes: 2 additions & 0 deletions src/plugins/three/DebugTilesPlugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const RANDOM_NODE_COLOR: ColorMode;
export const CUSTOM_COLOR: ColorMode;
export class DebugTilesPlugin {

enabled: boolean;

displayBoxBounds : boolean;
displaySphereBounds : boolean;
displayRegionBounds : boolean;
Expand Down
84 changes: 64 additions & 20 deletions src/plugins/three/DebugTilesPlugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box3Helper, Group, MeshStandardMaterial, PointsMaterial, Sphere, Color } from 'three';
import { SphereHelper } from './objects/SphereHelper.js';
import { EllipsoidRegionLineHelper } from './objects/EllipsoidRegionHelper.js';
import { traverseSet } from '../../base/traverseFunctions.js';

const ORIGINAL_MATERIAL = Symbol( 'ORIGINAL_MATERIAL' );
const HAS_RANDOM_COLOR = Symbol( 'HAS_RANDOM_COLOR' );
Expand Down Expand Up @@ -59,6 +60,8 @@ export class DebugTilesPlugin {
this.name = 'DEBUG_TILES_PLUGIN';
this.tiles = null;

this._enabled = true;

this.extremeDebugDepth = - 1;
this.extremeDebugError = - 1;
this.boxGroup = null;
Expand All @@ -83,6 +86,36 @@ export class DebugTilesPlugin {

}

get enabled() {

return this._enabled;

}

set enabled( v ) {

if ( v !== this._enabled ) {

this._enabled = v;

if ( this._enabled ) {

if ( this.tiles ) {

this.init( this.tiles );

}

} else {

this.dispose();

}

}

}

// initialize the groups for displaying helpers, register events, and initialize existing tiles
init( tiles ) {

Expand Down Expand Up @@ -142,6 +175,8 @@ export class DebugTilesPlugin {
tiles.addEventListener( 'update-after', this._onUpdateAfterCB );
tiles.addEventListener( 'tile-visibility-change', this._onTileVisibilityChangeCB );

this._initExtremes();

// initialize an already-loaded tiles
tiles.traverse( tile => {

Expand Down Expand Up @@ -214,17 +249,21 @@ export class DebugTilesPlugin {

_initExtremes() {

// initialize the extreme values of the hierarchy
let maxDepth = - 1;
this.tiles.traverse( tile => {
if ( ! ( this.tiles && this.tiles.root ) ) {

maxDepth = Math.max( maxDepth, tile.__depth );
return;

} );
}

// initialize the extreme values of the hierarchy
let maxDepth = - 1;
let maxError = - 1;
this.tiles.traverse( tile => {

// Note that we are not using this.tiles.traverse()
// as we don't want to pay the cost of preprocessing tiles.
traverseSet( this.tiles.root, null, ( tile, _, depth ) => {

maxDepth = Math.max( maxDepth, depth );
maxError = Math.max( maxError, tile.geometricError );

} );
Expand Down Expand Up @@ -652,25 +691,30 @@ export class DebugTilesPlugin {
dispose() {

const tiles = this.tiles;
tiles.removeEventListener( 'load-tile-set', this._onLoadTileSetCB );
tiles.removeEventListener( 'load-model', this._onLoadModelCB );
tiles.removeEventListener( 'dispose-model', this._onDisposeModelCB );
tiles.removeEventListener( 'update-after', this._onUpdateAfterCB );

// reset all materials
this.colorMode = NONE;
this._onUpdateAfter();
if ( tiles ) {

// dispose of all helper objects
tiles.traverse( tile => {
tiles.removeEventListener( 'load-tile-set', this._onLoadTileSetCB );
tiles.removeEventListener( 'load-model', this._onLoadModelCB );
tiles.removeEventListener( 'dispose-model', this._onDisposeModelCB );
tiles.removeEventListener( 'update-after', this._onUpdateAfterCB );

this._onDisposeModel( tile );
// reset all materials
this.colorMode = NONE;
this._onUpdateAfter();

} );
// dispose of all helper objects
tiles.traverse( tile => {

this._onDisposeModel( tile );

} );

}

this.boxGroup.removeFromParent();
this.sphereGroup.removeFromParent();
this.regionGroup.removeFromParent();
this.boxGroup?.removeFromParent();
this.sphereGroup?.removeFromParent();
this.regionGroup?.removeFromParent();

}

Expand Down
101 changes: 101 additions & 0 deletions test/traverseFunctions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { traverseSet } from '../src/base/traverseFunctions.js';

describe( 'traverseSet', () => {

function makeTile( name, ...children ) {

return { name, children };

}

it( 'should visit all tiles in depth-first order', () => {

/**
* root
* / | \
* a b c
* / | \ /
* d e f g
* / | \
* h i j
*/


const h = makeTile( 'h' );
const i = makeTile( 'i' );
const j = makeTile( 'j' );

const d = makeTile( 'd' );
const e = makeTile( 'e' );
const f = makeTile( 'f' );

const b = makeTile( 'b' );
const a = makeTile( 'a', d, e, f );

const g = makeTile( 'g', h, i, j );

const c = makeTile( 'c', g );

const root = makeTile( 'root', a, b, c );


const visited = [];

traverseSet( root, null, ( tile, parent, depth ) => visited.push( `${tile.name}-${depth}-${parent?.name ?? 'none'}` ) );

expect( visited ).toHaveLength( 11 );
expect( visited ).toEqual( [ 'root-0-none', 'a-1-root', 'd-2-a', 'e-2-a', 'f-2-a', 'b-1-root', 'c-1-root', 'g-2-c', 'h-3-g', 'i-3-g', 'j-3-g' ] );

} );

it( 'should exit traversal if beforeCb returns true', () => {

/**
* root
* / | \
* a b c
* / | \ /
* d e f g
* / | \
* h i j
*/


const h = makeTile( 'h' );
const i = makeTile( 'i' );
const j = makeTile( 'j' );

const d = makeTile( 'd' );
const e = makeTile( 'e' );
const f = makeTile( 'f' );

const b = makeTile( 'b' );
const a = makeTile( 'a', d, e, f );

const g = makeTile( 'g', h, i, j );

const c = makeTile( 'c', g );

const root = makeTile( 'root', a, b, c );


const visited = [];

const beforeCb = ( tile ) => {

if ( tile.name === 'c' ) {

return true;

}

};

traverseSet( root, beforeCb, t => visited.push( t.name ) );

expect( visited ).toHaveLength( 7 );
expect( visited ).toEqual( [ 'root', 'a', 'd', 'e', 'f', 'b', 'c' ] );

} );

} );

0 comments on commit ad3b888

Please sign in to comment.