UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

954 lines (584 loc) 18.4 kB
import { Box3Helper, Group, MeshStandardMaterial, PointsMaterial, Sphere, Color, MeshBasicMaterial } from 'three'; import { SphereHelper } from './objects/SphereHelper.js'; import { EllipsoidRegionLineHelper } 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 ColorModes = Object.freeze( { NONE, SCREEN_ERROR, GEOMETRIC_ERROR, DISTANCE, DEPTH, RELATIVE_DEPTH, IS_LEAF, RANDOM_COLOR, RANDOM_NODE_COLOR, CUSTOM_COLOR, LOAD_ORDER, } ); export class DebugTilesPlugin { static get ColorModes() { return ColorModes; } 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 enabled() { return this._enabled; } set enabled( v ) { if ( v !== this._enabled && this.tiles !== null ) { if ( v ) { this.init( this.tiles ); } else { this.dispose(); } } this._enabled = v; } 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.__visible ); } ); } else { // Initialize ref count for existing tiles this.tiles.traverse( tile => { if ( tile.__visible ) { this._onTileVisibilityChange( tile, true ); } } ); } } } constructor( options ) { options = { displayParentBounds: false, displayBoxBounds: false, displaySphereBounds: false, displayRegionBounds: false, colorMode: 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._unlit = 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.maxDebugDepth = options.maxDebugDepth; this.maxDebugDistance = options.maxDebugDistance; this.maxDebugError = options.maxDebugError; this.customColorCallback = options.customColorCallback; this.unlit = options.unlit; 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; // 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._onUpdateAfter(); }; this._onTileVisibilityChangeCB = ( { scene, tile, visible } ) => { this._onTileVisibilityChange( tile, visible ); }; tiles.addEventListener( 'load-tile-set', 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.cached.scene ) { this._onLoadModel( tile.cached.scene, tile ); } } ); tiles.visibleTiles.forEach( tile => { this._onTileVisibilityChange( tile, true ); } ); } getTileInformationFromActiveObject( object ) { // Find which tile this scene is associated with. This is slow and // intended for debug purposes only. let targetTile = null; const activeTiles = this.tiles.activeTiles; activeTiles.forEach( tile => { if ( targetTile ) { return true; } const scene = tile.cached.scene; if ( scene ) { scene.traverse( c => { if ( c === object ) { targetTile = tile; } } ); } } ); if ( targetTile ) { return { distanceToCamera: targetTile.__distanceFromCamera, geometricError: targetTile.geometricError, screenSpaceError: targetTile.__error, depth: targetTile.__depth, isLeaf: targetTile.__isLeaf }; } else { return null; } } _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; } _onUpdateAfter() { const { tiles, colorMode } = 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 ) { sortedTiles = Array.from( visibleTiles ).sort( ( a, b ) => { return a[ LOAD_TIME ] - b[ LOAD_TIME ]; } ); } // update plugins visibleTiles.forEach( tile => { const scene = tile.cached.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 ) { if ( colorMode !== RANDOM_COLOR ) { delete c.material[ HAS_RANDOM_COLOR ]; } if ( colorMode !== RANDOM_NODE_COLOR ) { delete c.material[ HAS_RANDOM_NODE_COLOR ]; } // Set the color on the basic material switch ( colorMode ) { case DEPTH: { const val = tile.__depth / maxDepth; this.getDebugColor( val, c.material.color ); break; } case RELATIVE_DEPTH: { const val = tile.__depthFromRenderedParent / maxDepth; this.getDebugColor( val, c.material.color ); break; } case SCREEN_ERROR: { const val = tile.__error / errorTarget; if ( val > 1.0 ) { c.material.color.setRGB( 1.0, 0.0, 0.0 ); } else { this.getDebugColor( val, c.material.color ); } break; } case GEOMETRIC_ERROR: { const val = Math.min( tile.geometricError / maxError, 1 ); this.getDebugColor( val, c.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.__distanceFromCamera / maxDistance, 1 ); this.getDebugColor( val, c.material.color ); break; } case IS_LEAF: { if ( ! tile.children || tile.children.length === 0 ) { this.getDebugColor( 1.0, c.material.color ); } else { this.getDebugColor( 0.0, c.material.color ); } break; } case RANDOM_NODE_COLOR: { if ( ! c.material[ HAS_RANDOM_NODE_COLOR ] ) { c.material.color.setHSL( h, s, l ); c.material[ HAS_RANDOM_NODE_COLOR ] = true; } break; } case RANDOM_COLOR: { if ( ! c.material[ HAS_RANDOM_COLOR ] ) { c.material.color.setHSL( h, s, l ); c.material[ HAS_RANDOM_COLOR ] = true; } break; } case CUSTOM_COLOR: { if ( this.customColorCallback ) { this.customColorCallback( tile, c ); } else { console.warn( 'DebugTilesRenderer: customColorCallback not defined' ); } break; } case LOAD_ORDER: { const value = sortedTiles.indexOf( tile ); this.getDebugColor( value / ( sortedTiles.length - 1 ), c.material.color ); break; } } } } ); } ); } _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 cached = tile.cached; const { sphere, obb, region } = cached.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; const boxHelper = new Box3Helper( obb.box, getIndexedRandomColor( tile.__depth ) ); boxHelper.raycast = emptyRaycast; boxHelperGroup.add( boxHelper ); cached.boxHelperGroup = boxHelperGroup; 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.__depth ) ); sphereHelper.raycast = emptyRaycast; cached.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.__depth ) ); regionHelper.raycast = emptyRaycast; // 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 ); cached.regionHelper = regionHelper; if ( tiles.visibleTiles.has( tile ) && this.displayRegionBounds ) { this.regionGroup.add( regionHelper ); regionHelper.updateMatrixWorld( true ); } } } _updateHelperMaterial( tile, material ) { if ( tile.__visible || ! this.displayParentBounds ) { material.opacity = 1; } else { material.opacity = 0.2; } const transparent = material.transparent; material.transparent = material.opacity < 1; if ( material.transparent !== transparent ) { material.needsUpdate = true; } } _updateBoundHelper( tile, visible ) { const cached = tile.cached; if ( ! cached ) { return; } const sphereGroup = this.sphereGroup; const boxGroup = this.boxGroup; const regionGroup = this.regionGroup; if ( visible && ( cached.boxHelperGroup == null && cached.sphereHelper == null && cached.regionHelper == null ) ) { this._createBoundHelper( tile ); } const boxHelperGroup = cached.boxHelperGroup; const sphereHelper = cached.sphereHelper; const regionHelper = cached.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._updateHelperMaterial( tile, boxHelperGroup.children[ 0 ].material ); } if ( sphereHelper ) { sphereGroup.add( sphereHelper ); sphereHelper.updateMatrixWorld( true ); this._updateHelperMaterial( tile, sphereHelper.material ); } if ( regionHelper ) { regionGroup.add( regionHelper ); regionHelper.updateMatrixWorld( true ); this._updateHelperMaterial( tile, regionHelper.material ); } } } _updateMaterial( scene ) { // update the materials for debug rendering const { colorMode, unlit } = 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(); } else { c.material = new MeshStandardMaterial(); 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 cached = tile.cached; if ( cached.boxHelperGroup ) { cached.boxHelperGroup.children[ 0 ].geometry.dispose(); delete cached.boxHelperGroup; } if ( cached.sphereHelper ) { cached.sphereHelper.geometry.dispose(); delete cached.sphereHelper; } if ( cached.regionHelper ) { cached.regionHelper.geometry.dispose(); delete cached.regionHelper; } } 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 ); tiles.removeEventListener( 'tile-visibility-change', this._onTileVisibilityChangeCB ); // reset all materials this.colorMode = NONE; this.unlit = false; tiles.forEachLoadedModel( scene => { this._updateMaterial( scene ); } ); // dispose of all helper objects tiles.traverse( tile => { this._onDisposeModel( tile ); } ); this.boxGroup?.removeFromParent(); this.sphereGroup?.removeFromParent(); this.regionGroup?.removeFromParent(); } }