UNPKG

3d-tiles-renderer

Version:

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

333 lines (206 loc) 6.79 kB
import { DoubleSide, Matrix4, MeshBasicMaterial, Raycaster, Sphere, Vector3 } from 'three'; import { OBB } from '../../three/math/OBB.js'; // Limitations: // - No support for BatchedTilesPlugin when resetting or modifying geometry // - Sharing geometry between models may result in incorrect flattening const _sphere = /* @__PURE__ */ new Sphere(); const _obb = /* @__PURE__ */ new OBB(); const _vec = /* @__PURE__ */ new Vector3(); const _matrix = /* @__PURE__ */ new Matrix4(); const _invMatrix = /* @__PURE__ */ new Matrix4(); const _raycaster = /* @__PURE__ */ new Raycaster(); const _doubleSidedMaterial = /* @__PURE__ */ new MeshBasicMaterial( { side: DoubleSide } ); const RAYCAST_DISTANCE = 1e5; function calculateSphere( object, target ) { if ( object instanceof OBB ) { _obb.copy( object ); } else { // clone the object so we can calculate the root bounding box const clone = object.clone(); clone.position.set( 0, 0, 0 ); clone.quaternion.identity(); clone.scale.setScalar( 1 ); // construct obb _obb.box.setFromObject( clone, true ); _obb.box.getSize( _vec ); _obb.transform.copy( object.matrix ); } // get sphere _obb.box.getBoundingSphere( target ).applyMatrix4( _obb.transform ); return target; } export class TileFlatteningPlugin { constructor() { this.name = 'TILE_FLATTENING_PLUGIN'; this.priority = - 100; this.tiles = null; this.shapes = new Map(); this.positionsMap = new Map(); this.positionsUpdated = new Set(); this.needsUpdate = false; } init( tiles ) { this.tiles = tiles; this.needsUpdate = true; this._updateBeforeCallback = () => { if ( this.needsUpdate ) { this._updateTiles(); this.needsUpdate = false; } }; this._disposeModelCallback = ( { tile } ) => { this.positionsMap.delete( tile ); this.positionsUpdated.delete( tile ); }; tiles.addEventListener( 'update-before', this._updateBeforeCallback ); tiles.addEventListener( 'dispose-model', this._disposeModelCallback ); } // update tile flattening state if it has not been made visible, yet setTileActive( tile, active ) { if ( active && ! this.positionsUpdated.has( tile ) ) { this._updateTile( tile ); } } _updateTile( tile ) { const { positionsUpdated, positionsMap, shapes, tiles } = this; positionsUpdated.add( tile ); const scene = tile.cached.scene; if ( ! positionsMap.has( tile ) ) { // save the geometry positions for resetting after const geomMap = new Map(); positionsMap.set( tile, geomMap ); scene.traverse( c => { if ( c.geometry ) { geomMap.set( c.geometry, c.geometry.attributes.position.array.slice() ); } } ); } else { // reset the geometry state before re-flattening tiles const geomMap = positionsMap.get( tile ); scene.traverse( c => { if ( c.geometry ) { const buffer = geomMap.get( c.geometry ); if ( buffer ) { c.geometry.attributes.position.array.set( buffer ); c.geometry.attributes.position.needsUpdate = true; } } } ); } const ray = _raycaster.ray; shapes.forEach( ( { shape, direction, sphere, threshold, } ) => { // TODO: if we save the sphere of the original mesh we can check the height to limit the tiles checked // TODO: we should use the tile bounding volume sphere if present // calculate the project distance between circles const { boundingVolume } = tile.cached; calculateSphere( boundingVolume.obb || boundingVolume.regionObb, _sphere ); _vec.subVectors( _sphere.center, sphere.center ); _vec.addScaledVector( direction, - direction.dot( _vec ) ); const r2 = ( _sphere.radius + sphere.radius ) ** 2; if ( _vec.lengthSq() > r2 ) { return; } // prepare the shape and ray ray.direction.copy( direction ).multiplyScalar( - 1 ); scene.updateMatrixWorld( true ); // iterate over every geometry scene.traverse( c => { if ( c.geometry ) { const { position } = c.geometry.attributes; position.needsUpdate = true; _matrix.copy( c.matrixWorld ); if ( scene.parent !== null ) { _matrix.premultiply( tiles.group.matrixWorldInverse ); } _invMatrix.copy( _matrix ).invert(); // iterate over every vertex position for ( let i = 0, l = position.count; i < l; i ++ ) { ray.origin .fromBufferAttribute( position, i ) .applyMatrix4( _matrix ) .addScaledVector( direction, RAYCAST_DISTANCE ); _raycaster.far = RAYCAST_DISTANCE; const hit = _raycaster.intersectObject( shape )[ 0 ]; if ( hit && RAYCAST_DISTANCE - hit.distance < threshold ) { hit.point.applyMatrix4( _invMatrix ); position.setXYZ( i, ...hit.point ); } } } } ); } ); this.tiles.dispatchEvent( { type: 'needs-render' } ); } _updateTiles() { this.positionsUpdated.clear(); this.tiles.activeTiles.forEach( tile => this._updateTile( tile ) ); } // API for updating and shapes to flatten the vertices hasShape( mesh ) { return this.shapes.has( mesh ); } addShape( mesh, direction = new Vector3( 0, 0, - 1 ), threshold = Infinity ) { if ( this.hasShape( mesh ) ) { throw new Error( 'TileFlatteningPlugin: Shape is already used.' ); } this.needsUpdate = true; mesh.updateMatrix(); const sphere = calculateSphere( mesh, new Sphere() ); const shape = mesh.clone(); shape.matrixWorld.copy( shape.matrix ); shape.traverse( c => { if ( c.material ) { c.material = _doubleSidedMaterial; } } ); this.shapes.set( mesh, { shape: shape, direction: direction.clone(), sphere: sphere, threshold: threshold, } ); } updateShape( mesh ) { if ( ! this.hasShape( mesh ) ) { throw new Error( 'TileFlatteningPlugin: Shape is not present.' ); } this.needsUpdate = true; mesh.updateMatrix(); const info = this.shapes.get( mesh ); calculateSphere( mesh, info.sphere ); info.shape = mesh.clone(); info.shape.traverse( c => { if ( c.material ) { c.material = _doubleSidedMaterial; } } ); } deleteShape( mesh ) { this.needsUpdate = true; return this.shapes.delete( mesh ); } clearShapes() { if ( this.shapes.size === 0 ) { return; } this.needsUpdate = true; this.shapes.clear(); } // reset the vertex positions and remove the update callback dispose() { this.tiles.removeEventListener( 'before-update', this._updateBeforeCallback ); this.tiles.removeEventListener( 'dispose-model', this._disposeModelCallback ); this.positionsMap.forEach( geomMap => { geomMap.forEach( ( buffer, geometry ) => { const { position } = geometry.attributes; position.array.set( buffer ); position.needsUpdate = true; } ); } ); } }