3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
495 lines (309 loc) • 12.1 kB
JavaScript
import { Matrix4, Vector3, Quaternion } from 'three';
import { FadeManager } from './FadeManager.js';
import { FadeMaterialManager } from './FadeMaterialManager.js';
import { FadeBatchedMesh } from './FadeBatchedMesh.js';
const HAS_POPPED_IN = Symbol( 'HAS_POPPED_IN' );
function tileWasInFrustumLastFrame( tile ) {
// Check if an ancestor was outside the frustum last frame, indicating that this one
// would have been, too.
let current = tile;
while ( current ) {
if ( current.traversal.wasSetActive ) {
return current.traversal.wasInFrustum;
}
current = current.parent;
}
return false;
}
const _fromPos = /* @__PURE__ */ new Vector3();
const _toPos = /* @__PURE__ */ new Vector3();
const _fromQuat = /* @__PURE__ */ new Quaternion();
const _toQuat = /* @__PURE__ */ new Quaternion();
const _scale = /* @__PURE__ */ new Vector3();
function onUpdateAfter() {
const fadeManager = this._fadeManager;
const fadeMaterialManager = this._fadeMaterialManager;
const fadingBefore = this._fadingBefore;
const prevCameraTransforms = this._prevCameraTransforms;
const { tiles, maximumFadeOutTiles, batchedMesh } = this;
const { cameras } = tiles;
// update fade step
fadeManager.update();
// fire an event
const fadingAfter = fadeManager.fadeCount;
if ( fadingBefore !== 0 && fadingAfter !== 0 ) {
tiles.dispatchEvent( { type: 'fade-change' } );
tiles.dispatchEvent( { type: 'needs-render' } );
}
if ( maximumFadeOutTiles < this._fadingOutCount ) {
// determine whether all the rendering cameras are moving
// quickly so we can adjust how tiles fade accordingly
let isMovingFast = true;
cameras.forEach( camera => {
if ( ! prevCameraTransforms.has( camera ) ) {
return;
}
const currMatrix = camera.matrixWorld;
const prevMatrix = prevCameraTransforms.get( camera );
currMatrix.decompose( _toPos, _toQuat, _scale );
prevMatrix.decompose( _fromPos, _fromQuat, _scale );
const angleTo = _toQuat.angleTo( _fromQuat );
const positionTo = _toPos.distanceTo( _fromPos );
// if rotation is moving > 0.25 radians per frame or position is moving > 0.1 units
// then we are considering the camera to be moving too fast to notice a faster / abrupt fade
isMovingFast = isMovingFast && ( angleTo > 0.25 || positionTo > 0.1 );
} );
if ( isMovingFast ) {
fadeManager.completeAllFades();
}
}
// track the camera movement so we can use it for next frame
cameras.forEach( camera => {
prevCameraTransforms.get( camera ).copy( camera.matrixWorld );
} );
// update the fade state for each tile
fadeManager.forEachObject( ( tile, { fadeIn, fadeOut } ) => {
// prevent faded tiles from being unloaded
const scene = tile.engineData.scene;
tiles.markTileUsed( tile );
if ( scene ) {
fadeMaterialManager.setFade( scene, fadeIn, fadeOut );
}
// fade the tiles and toggle the visibility appropriately
this.forEachBatchIds( tile, ( id, batchedMesh, plugin ) => {
batchedMesh.setFadeAt( id, fadeIn, fadeOut );
batchedMesh.setVisibleAt( id, true );
plugin.batchedMesh.setVisibleAt( id, false );
} );
} );
// update the batched mesh fields
if ( batchedMesh ) {
const material = tiles.getPluginByName( 'BATCHED_TILES_PLUGIN' ).batchedMesh.material;
batchedMesh.material.map = material.map;
}
}
/**
* Plugin that overrides material shaders to fade tile geometry in and out as tile LODs
* change, preventing pop-in. Dispatches `fade-change`, `fade-start`, and `fade-end`
* events on the `TilesRenderer` during animation — use these when doing on-demand
* rendering. Works alongside `BatchedTilesPlugin` when present.
* @param {Object} [options]
* @param {number} [options.fadeDuration=250] Time in milliseconds for a tile to fully fade in or out.
* @param {number} [options.maximumFadeOutTiles=50] Maximum simultaneous fade-out tiles. If exceeded, tiles pop instead of fading.
* @param {boolean} [options.fadeRootTiles=false] Whether root-level tiles fade in on their first appearance.
*/
export class TilesFadePlugin {
get fadeDuration() {
return this._fadeManager.duration;
}
set fadeDuration( value ) {
this._fadeManager.duration = Number( value );
}
get fadingTiles() {
return this._fadeManager.fadeCount;
}
constructor( options ) {
options = {
maximumFadeOutTiles: 50,
fadeRootTiles: false,
fadeDuration: 250,
...options,
};
this.name = 'FADE_TILES_PLUGIN';
this.priority = - 2;
this.tiles = null;
this.batchedMesh = null;
this._quickFadeTiles = new Set();
this._fadeManager = new FadeManager();
this._fadeMaterialManager = new FadeMaterialManager();
this._prevCameraTransforms = null;
this._fadingOutCount = 0;
this.maximumFadeOutTiles = options.maximumFadeOutTiles;
this.fadeRootTiles = options.fadeRootTiles;
this.fadeDuration = options.fadeDuration;
}
init( tiles ) {
// event callback initialization
this._onLoadModel = ( { scene } )=> {
// initialize all the scene materials to fade
this._fadeMaterialManager.prepareScene( scene );
};
this._onDisposeModel = ( { tile, scene } ) => {
if ( this.tiles.visibleTiles.has( tile ) ) {
// mark the parent as needing to fade in quickly to accommodate the children disappearing.
// this can happen when a tile is forcefully disposed or removed from the lru cache while visible.
this._quickFadeTiles.add( tile.parent );
}
// delete the fade info from the managers on disposal of model
this._fadeManager.deleteObject( tile );
this._fadeMaterialManager.deleteScene( scene );
};
this._onAddCamera = ( { camera } ) => {
// track the camera transform
this._prevCameraTransforms.set( camera, new Matrix4() );
};
this._onDeleteCamera = ( { camera } )=> {
// remove the camera transform
this._prevCameraTransforms.delete( camera );
};
this._onTileVisibilityChange = ( { tile } ) => {
// reset batched mesh fade state when tile visibility changes
this.forEachBatchIds( tile, ( id, batchedMesh, plugin ) => {
batchedMesh.setFadeAt( id, 0, 0 );
batchedMesh.setVisibleAt( id, false );
plugin.batchedMesh.setVisibleAt( id, false );
} );
};
this._onUpdateBefore = () => {
// store the fade count before the update so we can check whether fading started or stopped
this._fadingBefore = this._fadeManager.fadeCount;
};
this._onUpdateAfter = () => {
onUpdateAfter.call( this );
};
tiles.addEventListener( 'load-model', this._onLoadModel );
tiles.addEventListener( 'dispose-model', this._onDisposeModel );
tiles.addEventListener( 'add-camera', this._onAddCamera );
tiles.addEventListener( 'delete-camera', this._onDeleteCamera );
tiles.addEventListener( 'update-before', this._onUpdateBefore );
tiles.addEventListener( 'update-after', this._onUpdateAfter );
tiles.addEventListener( 'tile-visibility-change', this._onTileVisibilityChange );
// initialize fade manager
const fadeManager = this._fadeManager;
fadeManager.onFadeSetStart = () => {
tiles.dispatchEvent( { type: 'fade-start' } );
tiles.dispatchEvent( { type: 'needs-render' } );
};
fadeManager.onFadeSetComplete = () => {
tiles.dispatchEvent( { type: 'fade-end' } );
tiles.dispatchEvent( { type: 'needs-render' } );
};
fadeManager.onFadeComplete = ( tile, visible ) => {
// mark the fade as finished and reset the fade parameters
this._fadeMaterialManager.setFade( tile.engineData.scene, 0, 0 );
this.forEachBatchIds( tile, ( id, batchedMesh, plugin ) => {
batchedMesh.setFadeAt( id, 0, 0 );
batchedMesh.setVisibleAt( id, false );
plugin.batchedMesh.setVisibleAt( id, visible );
} );
if ( ! visible ) {
// now that the tile is hidden we can run the built-in setTileVisible function for the tile
tiles.invokeOnePlugin( plugin => plugin !== this && plugin.setTileVisible && plugin.setTileVisible( tile, false ) );
this._fadingOutCount --;
}
};
// initialize the state based on what's already present
const prevCameraTransforms = new Map();
tiles.cameras.forEach( camera => {
prevCameraTransforms.set( camera, new Matrix4() );
} );
tiles.forEachLoadedModel( ( scene, tile ) => {
this._onLoadModel( { scene } );
} );
this.tiles = tiles;
this._fadeManager = fadeManager;
this._prevCameraTransforms = prevCameraTransforms;
}
// initializes the batched mesh if it needs to be, dispose if it it's no longer needed
initBatchedMesh() {
const otherBatchedMesh = this.tiles.getPluginByName( 'BATCHED_TILES_PLUGIN' )?.batchedMesh;
if ( otherBatchedMesh ) {
if ( this.batchedMesh === null ) {
this._onBatchedMeshDispose = () => {
this.batchedMesh.dispose();
this.batchedMesh.removeFromParent();
this.batchedMesh = null;
otherBatchedMesh.removeEventListener( 'dispose', this._onBatchedMeshDispose );
};
const material = otherBatchedMesh.material.clone();
material.onBeforeCompile = otherBatchedMesh.material.onBeforeCompile;
this.batchedMesh = new FadeBatchedMesh( otherBatchedMesh, material );
this.tiles.group.add( this.batchedMesh );
}
} else {
if ( this.batchedMesh !== null ) {
this._onBatchedMeshDispose();
this._onBatchedMeshDispose = null;
}
}
}
// callback for fading to prevent tiles from being removed until the fade effect has completed
setTileVisible( tile, visible ) {
const fadeManager = this._fadeManager;
const wasFading = fadeManager.isFading( tile );
// Tiles that were off-screen last frame pop in/out without fading
if ( ! tileWasInFrustumLastFrame( tile ) ) {
if ( wasFading ) {
fadeManager.completeFade( tile );
}
return false;
}
// track the fade state
if ( fadeManager.isFadingOut( tile ) ) {
this._fadingOutCount --;
}
// trigger any necessary fades
if ( ! visible ) {
this._fadingOutCount ++;
fadeManager.fadeOut( tile );
} else {
// if this is a root renderable tile and this is the first time rendering in
// then pop it in
const isRootRenderableTile = tile.internal.depthFromRenderedParent === 1;
if ( isRootRenderableTile ) {
if ( tile[ HAS_POPPED_IN ] || this.fadeRootTiles ) {
this._fadeManager.fadeIn( tile );
}
tile[ HAS_POPPED_IN ] = true;
} else {
this._fadeManager.fadeIn( tile );
}
}
// if a tile needs to be jumped in then complete the fade here
if ( this._quickFadeTiles.has( tile ) ) {
this._fadeManager.completeFade( tile );
this._quickFadeTiles.delete( tile );
}
// if a tile was already fading then it's already marked as visible and in the scene
if ( wasFading ) {
return true;
}
// cancel the visibility change trigger because we're fading and will call this after
// fade completes.
const isFading = this._fadeManager.isFading( tile );
if ( ! visible && isFading ) {
return true;
}
return false;
}
dispose() {
const tiles = this.tiles;
this._fadeManager.completeAllFades();
if ( this.batchedMesh !== null ) {
this._onBatchedMeshDispose();
}
tiles.removeEventListener( 'load-model', this._onLoadModel );
tiles.removeEventListener( 'dispose-model', this._onDisposeModel );
tiles.removeEventListener( 'add-camera', this._onAddCamera );
tiles.removeEventListener( 'delete-camera', this._onDeleteCamera );
tiles.removeEventListener( 'update-before', this._onUpdateBefore );
tiles.removeEventListener( 'update-after', this._onUpdateAfter );
tiles.removeEventListener( 'tile-visibility-change', this._onTileVisibilityChange );
tiles.forEachLoadedModel( ( scene, tile ) => {
this._fadeManager.deleteObject( tile );
} );
}
// helper for iterating over the batch ids for a given tile
forEachBatchIds( tile, cb ) {
this.initBatchedMesh();
if ( this.batchedMesh ) {
const batchedPlugin = this.tiles.getPluginByName( 'BATCHED_TILES_PLUGIN' );
const instanceIds = batchedPlugin.getTileBatchIds( tile );
if ( instanceIds ) {
instanceIds.forEach( id => {
cb( id, this.batchedMesh, batchedPlugin );
} );
}
}
}
}