3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
1,183 lines (736 loc) • 24.6 kB
JavaScript
/** @import { Object3D } from 'three' */
import { Box3Helper, Group, MeshStandardMaterial, PointsMaterial, Sphere, Color, MeshBasicMaterial, Mesh, BoxGeometry, SphereGeometry, DoubleSide } from 'three';
import { SphereHelper } from './objects/SphereHelper.js';
import { EllipsoidRegionLineHelper, EllipsoidRegionHelper } from './objects/EllipsoidRegionHelper.js';
import { TraversalUtils } from '3d-tiles-renderer/core';
const ORIGINAL_MATERIAL = Symbol( 'ORIGINAL_MATERIAL' );
const HAS_RANDOM_COLOR = Symbol( 'HAS_RANDOM_COLOR' );
const HAS_RANDOM_NODE_COLOR = Symbol( 'HAS_RANDOM_NODE_COLOR' );
const LOAD_TIME = Symbol( 'LOAD_TIME' );
const PARENT_BOUND_REF_COUNT = Symbol( 'PARENT_BOUND_REF_COUNT' );
const _sphere = /* @__PURE__ */ new Sphere();
const emptyRaycast = () => {};
const colors = {};
// Return a consistent random color for an index
function getIndexedRandomColor( index ) {
if ( ! colors[ index ] ) {
const h = Math.random();
const s = 0.5 + Math.random() * 0.5;
const l = 0.375 + Math.random() * 0.25;
colors[ index ] = new Color().setHSL( h, s, l );
}
return colors[ index ];
}
// color modes
const NONE = 0;
const SCREEN_ERROR = 1;
const GEOMETRIC_ERROR = 2;
const DISTANCE = 3;
const DEPTH = 4;
const RELATIVE_DEPTH = 5;
const IS_LEAF = 6;
const RANDOM_COLOR = 7;
const RANDOM_NODE_COLOR = 8;
const CUSTOM_COLOR = 9;
const LOAD_ORDER = 10;
const INDEXED_COLOR = 11;
const ColorModes = Object.freeze( {
NONE,
SCREEN_ERROR,
GEOMETRIC_ERROR,
DISTANCE,
DEPTH,
RELATIVE_DEPTH,
IS_LEAF,
RANDOM_COLOR,
RANDOM_NODE_COLOR,
CUSTOM_COLOR,
LOAD_ORDER,
INDEXED_COLOR,
} );
/**
* @callback GetDebugColorCallback
* @param {number} val Normalized [0, 1] value.
* @param {Color} target Color to write the result into.
*/
/**
* @callback CustomColorCallback
* @param {Object} tile The tile whose scene is being colored.
* @param {Object3D} child The object within the tile scene being colored. Has a `material` property.
*/
/**
* Plugin that adds visual debugging aids to a `TilesRenderer`: bounding-volume
* helpers (box, sphere, region), tile color modes based on depth/error/distance/load
* order, and an unlit rendering mode. Color modes are available via the static
* `ColorModes` property.
* @param {Object} [options]
* @param {boolean} [options.displayBoxBounds=false] Show OBB bounding-box helpers.
* @param {boolean} [options.displaySphereBounds=false] Show bounding-sphere helpers.
* @param {boolean} [options.displayRegionBounds=false] Show bounding-region helpers.
* @param {boolean} [options.displayParentBounds=false] Also show ancestor bounding volumes for visible tiles.
* @param {number} [options.colorMode=ColorModes.NONE] Initial tile color mode.
* @param {number} [options.boundsColorMode=ColorModes.NONE] Color mode applied to bounding-volume helpers.
* @param {number} [options.maxDebugDepth=-1] Maximum tree depth for depth-based coloring (`-1` = auto).
* @param {number} [options.maxDebugDistance=-1] Maximum distance for distance-based coloring (`-1` = auto).
* @param {number} [options.maxDebugError=-1] Maximum error for error-based coloring (`-1` = auto).
* @param {CustomColorCallback|null} [options.customColorCallback=null] Callback invoked per-object when `colorMode` is `CUSTOM_COLOR`.
* @param {boolean} [options.unlit=false] Replace tile materials with unlit `MeshBasicMaterial`.
* @param {boolean} [options.enabled=true] Whether the plugin is active on init.
*/
export class DebugTilesPlugin {
static get ColorModes() {
return ColorModes;
}
get wireframe() {
return this._wireframe;
}
set wireframe( v ) {
if ( v !== this._wireframe ) {
this._wireframe = v;
this.materialsNeedUpdate = true;
}
}
get unlit() {
return this._unlit;
}
set unlit( v ) {
if ( v !== this._unlit ) {
this._unlit = v;
this.materialsNeedUpdate = true;
}
}
get colorMode() {
return this._colorMode;
}
set colorMode( v ) {
if ( v !== this._colorMode ) {
this._colorMode = v;
this.materialsNeedUpdate = true;
}
}
get boundsColorMode() {
return this._boundsColorMode;
}
set boundsColorMode( v ) {
if ( v !== this._boundsColorMode ) {
this._boundsColorMode = v;
this.materialsNeedUpdate = true;
}
}
get enabled() {
return this._enabled;
}
set enabled( v ) {
if ( v !== this._enabled && this.tiles !== null ) {
this._enabled = v;
if ( v ) {
this.init( this.tiles );
} else {
this.dispose();
}
}
}
get displayParentBounds() {
return this._displayParentBounds;
}
set displayParentBounds( v ) {
if ( this._displayParentBounds !== v ) {
this._displayParentBounds = v;
if ( ! v ) {
// Reset all ref counts
this.tiles.traverse( tile => {
tile[ PARENT_BOUND_REF_COUNT ] = null;
this._onTileVisibilityChange( tile, tile.traversal.visible );
} );
} else {
// Initialize ref count for existing tiles
this.tiles.traverse( tile => {
if ( tile.traversal.visible ) {
this._onTileVisibilityChange( tile, true );
}
} );
}
}
}
constructor( options ) {
options = {
displayParentBounds: false,
displayBoxBounds: false,
displaySphereBounds: false,
displayRegionBounds: false,
colorMode: NONE,
boundsColorMode: NONE,
maxDebugDepth: - 1,
maxDebugDistance: - 1,
maxDebugError: - 1,
customColorCallback: null,
unlit: false,
enabled: true,
...options,
};
this.name = 'DEBUG_TILES_PLUGIN';
this.tiles = null;
this._colorMode = null;
this._boundsColorMode = null;
this._unlit = null;
this._wireframe = null;
this.materialsNeedUpdate = false;
this.extremeDebugDepth = - 1;
this.extremeDebugError = - 1;
this.boxGroup = null;
this.sphereGroup = null;
this.regionGroup = null;
// options
this._enabled = options.enabled;
this._displayParentBounds = options.displayParentBounds;
this.displayBoxBounds = options.displayBoxBounds;
this.displaySphereBounds = options.displaySphereBounds;
this.displayRegionBounds = options.displayRegionBounds;
this.colorMode = options.colorMode;
this.boundsColorMode = options.boundsColorMode;
this.maxDebugDepth = options.maxDebugDepth;
this.maxDebugDistance = options.maxDebugDistance;
this.maxDebugError = options.maxDebugError;
this.customColorCallback = options.customColorCallback;
this.unlit = options.unlit;
this.wireframe = options.wireframe;
/**
* Maps a normalized [0, 1] value to a `Color` for debug visualizations. Defaults to
* a black-to-white gradient. Replace with a custom function to use a different color
* ramp.
* @type {GetDebugColorCallback}
* @default ( value, target ) => target.setRGB( value, value, value )
*/
this.getDebugColor = ( value, target ) => {
target.setRGB( value, value, value );
};
}
// initialize the groups for displaying helpers, register events, and initialize existing tiles
init( tiles ) {
this.tiles = tiles;
if ( ! this.enabled ) {
return;
}
// initialize groups
const tilesGroup = tiles.group;
this.boxGroup = new Group();
this.boxGroup.name = 'DebugTilesRenderer.boxGroup';
tilesGroup.add( this.boxGroup );
this.boxGroup.updateMatrixWorld();
this.sphereGroup = new Group();
this.sphereGroup.name = 'DebugTilesRenderer.sphereGroup';
tilesGroup.add( this.sphereGroup );
this.sphereGroup.updateMatrixWorld();
this.regionGroup = new Group();
this.regionGroup.name = 'DebugTilesRenderer.regionGroup';
tilesGroup.add( this.regionGroup );
this.regionGroup.updateMatrixWorld();
// register events
this._onLoadTilesetCB = () => {
this._initExtremes();
};
this._onLoadModelCB = ( { scene, tile } ) => {
this._onLoadModel( scene, tile );
};
this._onDisposeModelCB = ( { tile } ) => {
this._onDisposeModel( tile );
};
this._onUpdateAfterCB = () => {
this.update();
};
this._onTileVisibilityChangeCB = ( { scene, tile, visible } ) => {
this._onTileVisibilityChange( tile, visible );
};
tiles.addEventListener( 'load-tileset', this._onLoadTilesetCB );
tiles.addEventListener( 'load-model', this._onLoadModelCB );
tiles.addEventListener( 'dispose-model', this._onDisposeModelCB );
tiles.addEventListener( 'update-after', this._onUpdateAfterCB );
tiles.addEventListener( 'tile-visibility-change', this._onTileVisibilityChangeCB );
this._initExtremes();
// initialize an already-loaded tiles
tiles.traverse( tile => {
if ( tile.engineData.scene ) {
this._onLoadModel( tile.engineData.scene, tile );
}
} );
tiles.visibleTiles.forEach( tile => {
this._onTileVisibilityChange( tile, true );
} );
}
getTileFromObject3D( object ) {
// Find which tile this scene is associated with. This is slow and
// intended for debug purposes only.
let result = null;
const activeTiles = this.tiles.activeTiles;
activeTiles.forEach( tile => {
if ( result ) {
return;
}
const scene = tile.engineData.scene;
if ( scene ) {
scene.traverse( c => {
if ( c === object ) {
result = tile;
}
} );
}
} );
return result;
}
setEmptyTileVisible( tile, visible ) {
this._onTileVisibilityChange( tile, visible );
}
_initExtremes() {
if ( ! ( this.tiles && this.tiles.root ) ) {
return;
}
// initialize the extreme values of the hierarchy
let maxDepth = - 1;
let maxError = - 1;
// Note that we are not using this.tiles.traverse()
// as we don't want to pay the cost of preprocessing tiles.
this.tiles.traverse( null, ( tile, _, depth ) => {
maxDepth = Math.max( maxDepth, depth );
maxError = Math.max( maxError, tile.geometricError );
}, false );
this.extremeDebugDepth = maxDepth;
this.extremeDebugError = maxError;
}
/**
* Applies the current plugin field values to all visible tile geometry. Call this
* after modifying properties such as `colorMode`, `displayBoxBounds`, or
* `displayParentBounds` when `TilesRenderer.update` is not being called every frame
* so changes can be reflected.
*/
update() {
const { tiles, colorMode, boundsColorMode } = this;
if ( ! tiles.root ) {
return;
}
if ( this.materialsNeedUpdate ) {
tiles.forEachLoadedModel( scene => {
this._updateMaterial( scene );
} );
this.materialsNeedUpdate = false;
}
// set box or sphere visibility
this.boxGroup.visible = this.displayBoxBounds;
this.sphereGroup.visible = this.displaySphereBounds;
this.regionGroup.visible = this.displayRegionBounds;
// get max values to use for materials
let maxDepth = - 1;
if ( this.maxDebugDepth === - 1 ) {
maxDepth = this.extremeDebugDepth;
} else {
maxDepth = this.maxDebugDepth;
}
let maxError = - 1;
if ( this.maxDebugError === - 1 ) {
maxError = this.extremeDebugError;
} else {
maxError = this.maxDebugError;
}
let maxDistance = - 1;
if ( this.maxDebugDistance === - 1 ) {
tiles.getBoundingSphere( _sphere );
maxDistance = _sphere.radius;
} else {
maxDistance = this.maxDebugDistance;
}
const { errorTarget, visibleTiles } = tiles;
let sortedTiles;
if ( colorMode === LOAD_ORDER || boundsColorMode === LOAD_ORDER ) {
sortedTiles = Array.from( visibleTiles ).sort( ( a, b ) => {
return a[ LOAD_TIME ] - b[ LOAD_TIME ];
} );
}
// Set the color on the material based on the given color mode
const applyColor = ( mode, tile, child, h, s, l ) => {
if ( mode !== RANDOM_COLOR ) {
delete child.material[ HAS_RANDOM_COLOR ];
}
if ( mode !== RANDOM_NODE_COLOR ) {
delete child.material[ HAS_RANDOM_NODE_COLOR ];
}
switch ( mode ) {
case DEPTH: {
const val = tile.internal.depth / maxDepth;
this.getDebugColor( val, child.material.color );
break;
}
case RELATIVE_DEPTH: {
const val = tile.internal.depthFromRenderedParent / maxDepth;
this.getDebugColor( val, child.material.color );
break;
}
case SCREEN_ERROR: {
const val = tile.traversal.error / errorTarget;
if ( val > 1.0 ) {
child.material.color.setRGB( 1.0, 0.0, 0.0 );
} else {
this.getDebugColor( val, child.material.color );
}
break;
}
case GEOMETRIC_ERROR: {
const val = Math.min( tile.geometricError / maxError, 1 );
this.getDebugColor( val, child.material.color );
break;
}
case DISTANCE: {
// We don't update the distance if the geometric error is 0.0 so
// it will always be black.
const val = Math.min( tile.traversal.distanceFromCamera / maxDistance, 1 );
this.getDebugColor( val, child.material.color );
break;
}
case IS_LEAF: {
if ( ! tile.children || tile.children.length === 0 ) {
this.getDebugColor( 1.0, child.material.color );
} else {
this.getDebugColor( 0.0, child.material.color );
}
break;
}
case RANDOM_NODE_COLOR: {
if ( ! child.material[ HAS_RANDOM_NODE_COLOR ] ) {
child.material.color.setHSL( h, s, l );
child.material[ HAS_RANDOM_NODE_COLOR ] = true;
}
break;
}
case RANDOM_COLOR: {
if ( ! child.material[ HAS_RANDOM_COLOR ] ) {
child.material.color.setHSL( h, s, l );
child.material[ HAS_RANDOM_COLOR ] = true;
}
break;
}
case CUSTOM_COLOR: {
if ( this.customColorCallback ) {
this.customColorCallback( tile, child );
} else {
console.warn( 'DebugTilesRenderer: customColorCallback not defined' );
}
break;
}
case LOAD_ORDER: {
const value = sortedTiles.indexOf( tile );
this.getDebugColor( value / ( sortedTiles.length - 1 ), child.material.color );
break;
}
case INDEXED_COLOR: {
child.material.color.copy( getIndexedRandomColor( tile.internal.depth ) );
delete child.material[ HAS_RANDOM_COLOR ];
delete child.material[ HAS_RANDOM_NODE_COLOR ];
break;
}
}
};
// update tile materials
visibleTiles.forEach( tile => {
const scene = tile.engineData.scene;
// create a random color per-tile
let h, s, l;
if ( colorMode === RANDOM_COLOR ) {
h = Math.random();
s = 0.5 + Math.random() * 0.5;
l = 0.375 + Math.random() * 0.25;
}
scene.traverse( c => {
if ( colorMode === RANDOM_NODE_COLOR ) {
h = Math.random();
s = 0.5 + Math.random() * 0.5;
l = 0.375 + Math.random() * 0.25;
}
if ( c.material ) {
applyColor( colorMode, tile, c, h, s, l );
}
} );
} );
// update bounds helper colors
const effectiveBoundsColorMode = boundsColorMode === NONE ? INDEXED_COLOR : boundsColorMode;
const groups = [ this.boxGroup, this.sphereGroup, this.regionGroup ];
for ( const group of groups ) {
for ( const helper of group.children ) {
const tile = helper.userData.tile;
let h, s, l;
if ( effectiveBoundsColorMode === RANDOM_COLOR ) {
h = Math.random();
s = 0.5 + Math.random() * 0.5;
l = 0.375 + Math.random() * 0.25;
}
helper.traverse( c => {
if ( effectiveBoundsColorMode === RANDOM_NODE_COLOR ) {
h = Math.random();
s = 0.5 + Math.random() * 0.5;
l = 0.375 + Math.random() * 0.25;
}
if ( c.material ) {
applyColor( effectiveBoundsColorMode, tile, c, h, s, l );
}
} );
}
}
}
_onTileVisibilityChange( tile, visible ) {
if ( this.displayParentBounds ) {
TraversalUtils.traverseAncestors( tile, current => {
if ( current[ PARENT_BOUND_REF_COUNT ] == null ) {
current[ PARENT_BOUND_REF_COUNT ] = 0;
}
if ( visible ) {
current[ PARENT_BOUND_REF_COUNT ] ++;
} else if ( current[ PARENT_BOUND_REF_COUNT ] > 0 ) {
current[ PARENT_BOUND_REF_COUNT ] --;
}
const tileVisible = ( current === tile && visible ) || ( this.displayParentBounds && current[ PARENT_BOUND_REF_COUNT ] > 0 );
this._updateBoundHelper( current, tileVisible );
} );
} else {
this._updateBoundHelper( tile, visible );
}
}
_createBoundHelper( tile ) {
const tiles = this.tiles;
const engineData = tile.engineData;
const { sphere, obb, region } = engineData.boundingVolume;
if ( obb ) {
// Create debug bounding box
// In some cases the bounding box may have a scale of 0 in one dimension resulting
// in the NaNs in an extracted rotation so we disable matrix updates instead.
const boxHelperGroup = new Group();
boxHelperGroup.name = 'DebugTilesRenderer.boxHelperGroup';
boxHelperGroup.matrix.copy( obb.transform );
boxHelperGroup.matrixAutoUpdate = false;
boxHelperGroup.userData.tile = tile;
engineData.boxHelperGroup = boxHelperGroup;
const boxHelper = new Box3Helper( obb.box, getIndexedRandomColor( tile.internal.depth ) );
boxHelper.raycast = emptyRaycast;
boxHelperGroup.add( boxHelper );
// Create partially transparent mesh
const fillMesh = new Mesh( new BoxGeometry(), new MeshBasicMaterial( {
color: getIndexedRandomColor( tile.internal.depth ),
transparent: true,
depthWrite: false,
opacity: 0.05,
side: DoubleSide,
} ) );
obb.box.getSize( fillMesh.scale );
fillMesh.raycast = emptyRaycast;
boxHelperGroup.add( fillMesh );
if ( tiles.visibleTiles.has( tile ) && this.displayBoxBounds ) {
this.boxGroup.add( boxHelperGroup );
boxHelperGroup.updateMatrixWorld( true );
}
}
if ( sphere ) {
// Create debug bounding sphere
const sphereHelper = new SphereHelper( sphere, getIndexedRandomColor( tile.internal.depth ) );
sphereHelper.raycast = emptyRaycast;
sphereHelper.userData.tile = tile;
// Create partially transparent mesh
const sphereFillMesh = new Mesh( new SphereGeometry( 1 ), new MeshBasicMaterial( {
color: getIndexedRandomColor( tile.internal.depth ),
transparent: true,
depthWrite: false,
opacity: 0.05,
side: DoubleSide,
} ) );
sphereFillMesh.raycast = emptyRaycast;
sphereHelper.add( sphereFillMesh );
engineData.sphereHelper = sphereHelper;
if ( tiles.visibleTiles.has( tile ) && this.displaySphereBounds ) {
this.sphereGroup.add( sphereHelper );
sphereHelper.updateMatrixWorld( true );
}
}
if ( region ) {
// Create debug bounding region
const regionHelper = new EllipsoidRegionLineHelper( region, getIndexedRandomColor( tile.internal.depth ) );
regionHelper.raycast = emptyRaycast;
regionHelper.userData.tile = tile;
// create partially transparent mesh
const regionFillMesh = new EllipsoidRegionHelper( region, getIndexedRandomColor( tile.internal.depth ) );
regionFillMesh.material.transparent = true;
regionFillMesh.material.depthWrite = false;
regionFillMesh.material.opacity = 0.05;
regionFillMesh.material.side = DoubleSide;
regionFillMesh.raycast = emptyRaycast;
regionHelper.add( regionFillMesh );
// recenter the geometry to avoid rendering artifacts
const sphere = new Sphere();
region.getBoundingSphere( sphere );
regionHelper.position.copy( sphere.center );
sphere.center.multiplyScalar( - 1 );
regionHelper.geometry.translate( ...sphere.center );
regionFillMesh.geometry.translate( ...sphere.center );
engineData.regionHelper = regionHelper;
if ( tiles.visibleTiles.has( tile ) && this.displayRegionBounds ) {
this.regionGroup.add( regionHelper );
regionHelper.updateMatrixWorld( true );
}
}
}
_updateHelperMaterials( tile, group ) {
group.traverse( c => {
const { material } = c;
if ( ! material ) {
return;
}
if ( tile.traversal.visible || ! this.displayParentBounds ) {
material.opacity = c.isMesh ? 0.05 : 1;
} else {
material.opacity = c.isMesh ? 0.01 : 0.2;
}
const transparent = material.transparent;
material.transparent = material.opacity < 1;
if ( material.transparent !== transparent ) {
material.needsUpdate = true;
}
} );
}
_updateBoundHelper( tile, visible ) {
const engineData = tile.engineData;
if ( ! engineData ) {
return;
}
const sphereGroup = this.sphereGroup;
const boxGroup = this.boxGroup;
const regionGroup = this.regionGroup;
if ( visible && ( engineData.boxHelperGroup == null && engineData.sphereHelper == null && engineData.regionHelper == null ) ) {
this._createBoundHelper( tile );
}
const boxHelperGroup = engineData.boxHelperGroup;
const sphereHelper = engineData.sphereHelper;
const regionHelper = engineData.regionHelper;
if ( ! visible ) {
if ( boxHelperGroup ) {
boxGroup.remove( boxHelperGroup );
}
if ( sphereHelper ) {
sphereGroup.remove( sphereHelper );
}
if ( regionHelper ) {
regionGroup.remove( regionHelper );
}
} else {
// TODO: consider updating the volumes based on the bounding regions here in case they've been changed
if ( boxHelperGroup ) {
boxGroup.add( boxHelperGroup );
boxHelperGroup.updateMatrixWorld( true );
this._updateHelperMaterials( tile, boxHelperGroup );
}
if ( sphereHelper ) {
sphereGroup.add( sphereHelper );
sphereHelper.updateMatrixWorld( true );
this._updateHelperMaterials( tile, sphereHelper );
}
if ( regionHelper ) {
regionGroup.add( regionHelper );
regionHelper.updateMatrixWorld( true );
this._updateHelperMaterials( tile, regionHelper );
}
}
}
_updateMaterial( scene ) {
// update the materials for debug rendering
const { colorMode, unlit, wireframe } = this;
scene.traverse( c => {
if ( ! c.material ) {
return;
}
const currMaterial = c.material;
const originalMaterial = c[ ORIGINAL_MATERIAL ];
// dispose the previous material
if ( currMaterial !== originalMaterial ) {
currMaterial.dispose();
}
// assign the new material
if ( colorMode !== NONE || unlit ) {
if ( c.isPoints ) {
const pointsMaterial = new PointsMaterial();
pointsMaterial.size = originalMaterial.size;
pointsMaterial.sizeAttenuation = originalMaterial.sizeAttenuation;
c.material = pointsMaterial;
} else if ( unlit ) {
c.material = new MeshBasicMaterial( { wireframe: wireframe } );
} else {
c.material = new MeshStandardMaterial( { wireframe: wireframe } );
c.material.flatShading = true;
}
// if no debug rendering is happening then assign the material properties
if ( colorMode === NONE ) {
c.material.map = originalMaterial.map;
c.material.color.set( originalMaterial.color );
}
} else {
c.material = originalMaterial;
}
} );
}
_onLoadModel( scene, tile ) {
tile[ LOAD_TIME ] = performance.now();
// Cache the original materials
scene.traverse( c => {
const material = c.material;
if ( material ) {
c[ ORIGINAL_MATERIAL ] = material;
}
} );
// Update the materials to align with the settings
this._updateMaterial( scene );
}
_onDisposeModel( tile ) {
const engineData = tile.engineData;
if ( engineData?.boxHelperGroup ) {
engineData.boxHelperGroup.traverse( c => {
if ( c.geometry ) {
c.geometry.dispose();
c.material.dispose();
}
} );
delete engineData.boxHelperGroup;
}
if ( engineData?.sphereHelper ) {
engineData.sphereHelper.traverse( c => {
if ( c.geometry ) {
c.geometry.dispose();
c.material.dispose();
}
} );
delete engineData.sphereHelper;
}
if ( engineData?.regionHelper ) {
engineData.regionHelper.traverse( c => {
if ( c.geometry ) {
c.geometry.dispose();
c.material.dispose();
}
} );
delete engineData.regionHelper;
}
}
dispose() {
const tiles = this.tiles;
tiles.removeEventListener( 'load-tileset', this._onLoadTilesetCB );
tiles.removeEventListener( 'load-model', this._onLoadModelCB );
tiles.removeEventListener( 'dispose-model', this._onDisposeModelCB );
tiles.removeEventListener( 'update-after', this._onUpdateAfterCB );
tiles.removeEventListener( 'tile-visibility-change', this._onTileVisibilityChangeCB );
// reset all materials
this.colorMode = NONE;
this.boundsColorMode = NONE;
this.unlit = false;
tiles.forEachLoadedModel( scene => {
this._updateMaterial( scene );
} );
// dispose of all helper objects
tiles.traverse( tile => {
this._onDisposeModel( tile );
}, null, false );
this.boxGroup?.removeFromParent();
this.sphereGroup?.removeFromParent();
this.regionGroup?.removeFromParent();
}
}