3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
374 lines (248 loc) • 8.94 kB
JavaScript
/** @import { Scene } from '@babylonjs/core/scene' */
import { TilesRendererBase, LoaderUtils } from '3d-tiles-renderer/core';
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { Matrix, Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Frustum } from '@babylonjs/core/Maths/math.frustum';
import { Observable } from '@babylonjs/core/Misc/observable';
import { Plane } from '@babylonjs/core/Maths/math.plane';
import { B3DMLoader } from '../loaders/B3DMLoader.js';
import { GLTFLoader } from '../loaders/GLTFLoader.js';
import { TileBoundingVolume } from '../math/TileBoundingVolume.js';
// Scratch variables to avoid allocations
const _worldToTiles = /* @__PURE__ */ Matrix.Identity();
const _cameraPositionInTiles = /* @__PURE__ */ new Vector3();
const _frustumPlanes = /* @__PURE__ */ new Array( 6 ).fill( null ).map( () => new Plane( 0, 0, 0, 0 ) );
/**
* Babylon.js implementation of the 3D Tiles renderer. Manages tile loading, caching, traversal,
* and scene management using the Babylon.js scene graph and camera APIs. Dispatches all events
* defined by TilesRendererBase via Babylon.js Observables.
* @extends TilesRendererBase
* @warn Left-handed coordinate systems are not currently supported.
*/
export class TilesRenderer extends TilesRendererBase {
/**
* @param {string} url - URL of the root tileset JSON.
* @param {Scene} scene - The Babylon.js scene to render tiles into.
*/
constructor( url, scene ) {
super( url );
/**
* The Babylon.js scene tiles are rendered into.
* @type {Scene}
*/
this.scene = scene;
/**
* Root node that all loaded tile scenes are parented to.
* @type {TransformNode}
*/
this.group = new TransformNode( 'tiles-root', scene );
/**
* Whether to enable collision checking on loaded tile meshes.
* @type {boolean}
*/
this.checkCollisions = false;
this._upRotationMatrix = Matrix.Identity();
// Babylon.js Observables for events
this._observables = new Map();
}
addEventListener( type, listener ) {
if ( ! this._observables.has( type ) ) {
this._observables.set( type, new Observable() );
}
this._observables.get( type ).add( listener );
}
removeEventListener( type, listener ) {
if ( ! this._observables.has( type ) ) {
return;
}
const observable = this._observables.get( type );
observable.removeCallback( listener );
}
dispatchEvent( event ) {
if ( ! this._observables.has( event.type ) ) {
return;
}
const observable = this._observables.get( event.type );
observable.notifyObservers( event );
}
loadRootTileset( ...args ) {
return super.loadRootTileset( ...args )
.then( root => {
// cache the gltf tileset rotation matrix
const { asset } = root;
const upAxis = asset && asset.gltfUpAxis || 'y';
switch ( upAxis.toLowerCase() ) {
case 'x':
Matrix.RotationYToRef( - Math.PI / 2, this._upRotationMatrix );
break;
case 'y':
Matrix.RotationXToRef( Math.PI / 2, this._upRotationMatrix );
break;
}
return root;
} );
}
preprocessNode( tile, tilesetDir, parentTile = null ) {
super.preprocessNode( tile, tilesetDir, parentTile );
// Build the transform matrix for this tile
const transform = Matrix.Identity();
if ( tile.transform ) {
// 3d tiles uses column major
Matrix.FromValuesToRef( ...tile.transform, transform );
}
if ( parentTile ) {
// parentTransform * transform
transform.multiplyToRef( parentTile.engineData.transform, transform );
}
const transformInverse = Matrix.Identity();
transform.invertToRef( transformInverse );
const boundingVolume = new TileBoundingVolume();
if ( 'sphere' in tile.boundingVolume ) {
boundingVolume.setSphereData( ...tile.boundingVolume.sphere, transform );
}
if ( 'box' in tile.boundingVolume ) {
boundingVolume.setObbData( tile.boundingVolume.box, transform );
}
tile.engineData.transform = transform;
tile.engineData.transformInverse = transformInverse;
tile.engineData.boundingVolume = boundingVolume;
tile.engineData.active = false;
tile.engineData.scene = null;
tile.engineData.container = null;
}
async parseTile( buffer, tile, extension, url, abortSignal ) {
const engineData = tile.engineData;
const rootScene = this.scene;
const workingPath = LoaderUtils.getWorkingPath( url );
const fetchOptions = this.fetchOptions;
const tileTransform = engineData.transform;
const upRotationMatrix = this._upRotationMatrix;
let result = null;
const fileType = ( LoaderUtils.readMagicBytes( buffer ) || extension ).toLowerCase();
switch ( fileType ) {
case 'b3dm': {
const loader = new B3DMLoader( rootScene );
loader.workingPath = workingPath;
loader.fetchOptions = fetchOptions;
loader.adjustmentTransform.copyFrom( upRotationMatrix );
result = await loader.parse( buffer, url );
break;
}
case 'gltf':
case 'glb': {
const loader = new GLTFLoader( rootScene );
loader.workingPath = workingPath;
loader.fetchOptions = fetchOptions;
loader.adjustmentTransform.copyFrom( upRotationMatrix );
result = await loader.parse( buffer, url, extension );
break;
}
default:
throw new Error( `BabylonTilesRenderer: Content type "${ fileType }" not supported.` );
}
const scene = result.scene;
scene.setEnabled( false );
// apply the tile's cached transform to the loaded scene
scene
.computeWorldMatrix( true )
.multiply( tileTransform )
.decompose( scene.scaling, scene.rotationQuaternion, scene.position );
// exit early if a new request has already started
if ( abortSignal.aborted ) {
result.container.dispose();
return;
}
if ( this.checkCollisions ) {
for ( const mesh of scene.getChildMeshes() ) {
mesh.checkCollisions = true;
}
}
engineData.scene = scene;
engineData.container = result.container;
engineData.metadata = result.metadata || null;
}
disposeTile( tile ) {
super.disposeTile( tile );
const engineData = tile.engineData;
if ( engineData.container ) {
engineData.container.dispose();
engineData.container = null;
engineData.scene = null;
engineData.metadata = null;
}
}
setTileVisible( tile, visible ) {
const engineData = tile.engineData;
const scene = engineData.scene;
if ( ! scene ) {
return;
}
if ( visible ) {
scene.parent = this.group;
scene.setEnabled( true );
} else {
scene.parent = null;
scene.setEnabled( false );
}
super.setTileVisible( tile, visible );
}
calculateBytesUsed( tile ) {
// TODO: return the estimated amount of bytes used by the renderer
return 1;
}
calculateTileViewError( tile, target ) {
// TODO: cache frustum planes etc to improve performance
const { scene } = this;
const engineData = tile.engineData;
const boundingVolume = engineData.boundingVolume;
const camera = scene.activeCamera;
// get the render resolution
const engine = scene.getEngine();
const hardwareScaling = engine.getHardwareScalingLevel();
const width = engine.getRenderWidth() * hardwareScaling;
const height = engine.getRenderHeight() * hardwareScaling;
// get projection camera info
const projection = camera.getProjectionMatrix();
const projectionElements = projection.m;
const isOrthographic = projectionElements[ 15 ] === 1;
// calculate SSE denominator or pixel size
let sseDenominator;
let pixelSize;
if ( isOrthographic ) {
const w = 2 / projectionElements[ 0 ];
const h = 2 / projectionElements[ 5 ];
pixelSize = Math.max( h / height, w / width );
} else {
sseDenominator = ( 2 / projectionElements[ 5 ] ) / height;
}
// calculate the frustum planes and distances in local tile coordinates
this.group.getWorldMatrix().invertToRef( _worldToTiles );
Vector3.TransformCoordinatesToRef( camera.globalPosition, _worldToTiles, _cameraPositionInTiles );
// get frustums in local space: note tht it seems there's no way to transform to ref in Babylon
Frustum.GetPlanesToRef( camera.getTransformationMatrix( true ), _frustumPlanes );
const frustumPlanes = _frustumPlanes.map( plane => {
return plane.transform( _worldToTiles );
} );
const distance = boundingVolume.distanceToPoint( _cameraPositionInTiles );
let error;
if ( isOrthographic ) {
error = tile.geometricError / pixelSize;
} else {
// Avoid dividing by 0
error = distance === 0 ? Infinity : tile.geometricError / ( distance * sseDenominator );
}
// Check frustum intersection
const inView = boundingVolume.intersectsFrustum( frustumPlanes );
target.inView = inView;
target.error = error;
target.distanceFromCamera = distance;
}
/**
* Disposes the renderer, releasing all loaded tile content and the root transform node.
* @returns {void}
*/
dispose() {
super.dispose();
this.group.dispose();
}
}