123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- import { TilesRendererBase } from '../base/TilesRendererBase.js';
- import { B3DMLoader } from './B3DMLoader.js';
- import { TilesGroup } from './TilesGroup.js';
- import {
- Matrix4,
- Box3,
- Sphere,
- Vector3,
- Vector2,
- Math as MathUtils,
- Frustum,
- CanvasTexture,
- LoadingManager,
- ImageBitmapLoader,
- } from 'three';
- import { raycastTraverse, raycastTraverseFirstHit } from './raycastTraverse.js';
- const DEG2RAD = MathUtils.DEG2RAD;
- const tempMat = new Matrix4();
- const tempMat2 = new Matrix4();
- const tempVector = new Vector3();
- const vecX = new Vector3();
- const vecY = new Vector3();
- const vecZ = new Vector3();
- const X_AXIS = new Vector3( 1, 0, 0 );
- const Y_AXIS = new Vector3( 0, 1, 0 );
- const useImageBitmap = typeof createImageBitmap !== 'undefined';
- function emptyRaycast() {}
- export class TilesRenderer extends TilesRendererBase {
- get camera() {
- return this.cameras[ 0 ];
- }
- set camera( camera ) {
- const cameras = this.cameras;
- cameras.length = 1;
- cameras[ 0 ] = camera;
- }
- constructor( ...args ) {
- super( ...args );
- this.group = new TilesGroup( this );
- this.cameras = [];
- this.resolution = new Vector2();
- this.cameraInfo = [];
- this.activeTiles = new Set();
- this.visibleTiles = new Set();
- }
- /* Public API */
- getBounds( box ) {
- if ( ! this.root ) {
- return false;
- }
- const cached = this.root.cached;
- const boundingBox = cached.box;
- const obbMat = cached.boxTransform;
- if ( boundingBox ) {
- box.copy( boundingBox );
- box.applyMatrix4( obbMat );
- return true;
- } else {
- return false;
- }
- }
- raycast( raycaster, intersects ) {
- if ( ! this.root ) {
- return;
- }
- if ( raycaster.firstHitOnly ) {
- const hit = raycastTraverseFirstHit( this.root, this.group, this.activeTiles, raycaster );
- if ( hit ) {
- intersects.push( hit );
- }
- } else {
- raycastTraverse( this.root, this.group, this.activeTiles, raycaster, intersects );
- }
- }
- setResolutionFromRenderer( renderer ) {
- const resolution = this.resolution;
- renderer.getSize( resolution );
- resolution.multiplyScalar( renderer.getPixelRatio() );
- }
- /* Overriden */
- update() {
- const group = this.group;
- const cameras = this.cameras;
- const cameraInfo = this.cameraInfo;
- const resolution = this.resolution;
- if ( cameras.length === 0 ) {
- console.warn( 'TilesRenderer: no cameras to use are defined. Cannot update 3d tiles.' );
- return;
- }
- if ( resolution.width === 0 || resolution.height === 0 ) {
- console.warn( 'TilesRenderer: resolution for error calculation is not set. Cannot updated 3d tiles.' );
- return;
- }
- // automatically scale the array of cameraInfo to match the cameras
- while ( cameraInfo.length > cameras.length ) {
- cameraInfo.pop();
- }
- while ( cameraInfo.length < cameras.length ) {
- cameraInfo.push( {
- frustum: new Frustum(),
- sseDenominator: - 1,
- position: new Vector3(),
- invScale: - 1,
- } );
- }
- tempMat2.getInverse( group.matrixWorld );
- let invScale;
- tempVector.setFromMatrixScale( tempMat2 );
- invScale = tempVector.x;
- if ( Math.abs( Math.max( tempVector.x - tempVector.y, tempVector.x - tempVector.z ) ) > 1e-6 ) {
- console.warn( 'ThreeTilesRenderer : Non uniform scale used for tile which may cause issues when calculating screen space error.' );
- }
- // store the camera cameraInfo in the 3d tiles root frame
- for ( let i = 0, l = cameraInfo.length; i < l; i ++ ) {
- const camera = cameras[ i ];
- const info = cameraInfo[ i ];
- const frustum = info.frustum;
- const position = info.position;
- if ( camera.isPerspectiveCamera ) {
- info.sseDenominator = 2 * Math.tan( 0.5 * camera.fov * DEG2RAD ) / resolution.height;
- }
- info.invScale = invScale;
- tempMat.copy( group.matrixWorld );
- tempMat.premultiply( camera.matrixWorldInverse );
- tempMat.premultiply( camera.projectionMatrix );
- frustum.setFromProjectionMatrix( tempMat );
- position.set( 0, 0, 0 );
- position.applyMatrix4( camera.matrixWorld );
- position.applyMatrix4( tempMat2 );
- }
- super.update();
- }
- preprocessNode( tile, parentTile, tileSetDir ) {
- super.preprocessNode( tile, parentTile, tileSetDir );
- const transform = new Matrix4();
- if ( tile.transform ) {
- const transformArr = tile.transform;
- for ( let i = 0; i < 16; i ++ ) {
- transform.elements[ i ] = transformArr[ i ];
- }
- } else {
- transform.identity();
- }
- if ( parentTile ) {
- transform.multiply( parentTile.cached.transform );
- }
- let box = null;
- let boxTransform = null;
- let boxTransformInverse = null;
- if ( 'box' in tile.boundingVolume ) {
- const data = tile.boundingVolume.box;
- box = new Box3();
- boxTransform = new Matrix4();
- boxTransformInverse = new Matrix4();
- // get the extents of the bounds in each axis
- vecX.set( data[ 3 ], data[ 4 ], data[ 5 ] );
- vecY.set( data[ 6 ], data[ 7 ], data[ 8 ] );
- vecZ.set( data[ 9 ], data[ 10 ], data[ 11 ] );
- const scaleX = vecX.length();
- const scaleY = vecY.length();
- const scaleZ = vecZ.length();
- vecX.normalize();
- vecY.normalize();
- vecZ.normalize();
- // create the oriented frame that the box exists in
- boxTransform.set(
- vecX.x, vecY.x, vecZ.x, data[ 0 ],
- vecX.y, vecY.y, vecZ.y, data[ 1 ],
- vecX.z, vecY.z, vecZ.z, data[ 2 ],
- 0, 0, 0, 1
- );
- boxTransform.premultiply( transform );
- boxTransformInverse.getInverse( boxTransform );
- // scale the box by the extents
- box.min.set( - scaleX, - scaleY, - scaleZ );
- box.max.set( scaleX, scaleY, scaleZ );
- }
- let sphere = null;
- if ( 'sphere' in tile.boundingVolume ) {
- const data = tile.boundingVolume.sphere;
- sphere = new Sphere();
- sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] );
- sphere.radius = data[ 3 ];
- sphere.applyMatrix4( transform );
- } else if ( 'box' in tile.boundingVolume ) {
- const data = tile.boundingVolume.box;
- sphere = new Sphere();
- box.getBoundingSphere( sphere );
- sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] );
- sphere.applyMatrix4( transform );
- }
- let region = null;
- if ( 'region' in tile.boundingVolume ) {
- console.warn( 'ThreeTilesRenderer: region bounding volume not supported.' );
- }
- tile.cached = {
- loadIndex: 0,
- transform,
- active: false,
- box,
- boxTransform,
- boxTransformInverse,
- sphere,
- region,
- scene: null,
- geometry: null,
- material: null,
- distance: Infinity
- };
- }
- parseTile( buffer, tile ) {
- tile._loadIndex = tile._loadIndex || 0;
- tile._loadIndex ++;
- const loadIndex = tile._loadIndex;
- const manager = new LoadingManager();
- if ( useImageBitmap ) {
- // TODO: We should verify that `flipY` is false on the resulting texture after load because it can't be modified after
- // the fact. Premultiply alpha default behavior is not well defined, either.
- // TODO: Determine whether or not options are supported before using this so we can force flipY false and premultiply alpha
- // behavior. Fall back to regular texture loading
- manager.addHandler(/(^blob:)|(\.png$)|(\.jpg$)|(\.jpeg$)/g, {
- load( url, onComplete ) {
- const loader = new ImageBitmapLoader();
- loader.load( url, res => {
- onComplete( new CanvasTexture( res ) );
- } );
- }
- });
- }
- return new B3DMLoader( manager ).parse( buffer ).then( res => {
- if ( tile._loadIndex !== loadIndex ) {
- return;
- }
- const upAxis = this.rootTileSet.asset && this.rootTileSet.asset.gltfUpAxis || 'y';
- const cached = tile.cached;
- const cachedTransform = cached.transform;
- const scene = res.scene;
- switch ( upAxis.toLowerCase() ) {
- case 'x':
- scene.matrix.makeRotationAxis( Y_AXIS, - Math.PI / 2 );
- break;
- case 'y':
- scene.matrix.makeRotationAxis( X_AXIS, Math.PI / 2 );
- break;
- case 'z':
- break;
- }
- scene.matrix.premultiply( cachedTransform );
- scene.matrix.decompose( scene.position, scene.quaternion, scene.scale );
- scene.traverse( c => c.frustumCulled = false );
- cached.scene = scene;
- // We handle raycasting in a custom way so remove it from here
- scene.traverse( c => {
- c.raycast = emptyRaycast;
- } );
- const materials = [];
- const geometry = [];
- const textures = [];
- scene.traverse( c => {
- if ( c.geometry ) {
- geometry.push( c.geometry );
- }
- if ( c.material ) {
- const material = c.material;
- materials.push( c.material );
- for ( const key in material ) {
- const value = material[ key ];
- if ( value && value.isTexture ) {
- textures.push( value );
- }
- }
- }
- } );
- cached.materials = materials;
- cached.geometry = geometry;
- cached.textures = textures;
- } );
- }
- disposeTile( tile ) {
- // This could get called before the tile has finished downloading
- const cached = tile.cached;
- if ( cached.scene ) {
- const materials = cached.materials;
- const geometry = cached.geometry;
- const textures = cached.textures;
- for ( let i = 0, l = geometry.length; i < l; i ++ ) {
- geometry[ i ].dispose();
- }
- for ( let i = 0, l = materials.length; i < l; i ++ ) {
- materials[ i ].dispose();
- }
- for ( let i = 0, l = textures.length; i < l; i ++ ) {
- const texture = textures[ i ];
- texture.dispose();
- if ( useImageBitmap && 'close' in texture.image ) {
- texture.image.close();
- }
- }
- cached.scene = null;
- cached.materials = null;
- cached.textures = null;
- cached.geometry = null;
- }
- tile._loadIndex ++;
- }
- setTileVisible( tile, visible ) {
- const scene = tile.cached.scene;
- const visibleTiles = this.visibleTiles;
- const group = this.group;
- if ( visible ) {
- group.add( scene );
- visibleTiles.add( tile );
- scene.updateMatrixWorld( true );
- } else {
- group.remove( scene );
- visibleTiles.delete( tile );
- }
- }
- setTileActive( tile, active ) {
- const activeTiles = this.activeTiles;
- if ( active ) {
- activeTiles.add( tile );
- } else {
- activeTiles.delete( tile );
- }
- }
- calculateError( tile ) {
- if ( tile.geometricError === 0.0 ) {
- return 0.0;
- }
- const cached = tile.cached;
- const cameras = this.cameras;
- const cameraInfo = this.cameraInfo;
- // TODO: Use the content bounding volume here?
- const boundingVolume = tile.boundingVolume;
- const resolution = this.resolution;
- if ( 'box' in boundingVolume ) {
- const boundingBox = cached.box;
- const boxTransformInverse = cached.boxTransformInverse;
- let minError = Infinity;
- for ( let i = 0, l = cameras.length; i < l; i ++ ) {
- // TODO: move this logic (and the distance scale extraction) into the update preprocess step
- // transform camera position into local frame of the tile bounding box
- // TODO: this should be the cameras world position
- const camera = cameras[ i ];
- const info = cameraInfo[ i ];
- const invScale = info.invScale;
- tempVector.copy( info.position );
- tempVector.applyMatrix4( boxTransformInverse );
- let error;
- if ( camera.isOrthographicCamera ) {
- const w = camera.right - camera.left;
- const h = camera.top - camera.bottom;
- const pixelSize = Math.max( h / resolution.height, w / resolution.width );
- error = tile.geometricError / ( pixelSize * invScale );
- } else {
- const distance = boundingBox.distanceToPoint( tempVector );
- const scaledDistance = distance * invScale;
- const sseDenominator = info.sseDenominator;
- error = tile.geometricError / ( scaledDistance * sseDenominator );
- tile.cached.distance = scaledDistance;
- }
- minError = Math.min( minError, error );
- }
- return minError;
- } else if ( 'sphere' in boundingVolume ) {
- // const sphere = cached.sphere;
- console.warn( 'ThreeTilesRenderer : Sphere bounds not supported.' );
- } else if ( 'region' in boundingVolume ) {
- // unsupported
- console.warn( 'ThreeTilesRenderer : Region bounds not supported.' );
- }
- return Infinity;
- }
- tileInView( tile ) {
- // TODO: we should use the more precise bounding volumes here if possible
- // cache the root-space planes
- // Use separating axis theorem for frustum and obb
- const sphere = tile.cached.sphere;
- if ( sphere ) {
- const cameraInfo = this.cameraInfo;
- for ( let i = 0, l = cameraInfo.length; i < l; i ++ ) {
- const frustum = cameraInfo[ i ].frustum;
- if ( frustum.intersectsSphere( sphere ) ) {
- return true;
- }
- }
- return false;
- }
- return true;
- }
- }
|