TilesRenderer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. import { TilesRendererBase } from '../base/TilesRendererBase.js';
  2. import { B3DMLoader } from './B3DMLoader.js';
  3. import { PNTSLoader } from './PNTSLoader.js';
  4. import { I3DMLoader } from './I3DMLoader.js';
  5. import { CMPTLoader } from './CMPTLoader.js';
  6. import { TilesGroup } from './TilesGroup.js';
  7. import {
  8. Matrix4,
  9. Box3,
  10. Sphere,
  11. Vector3,
  12. Vector2,
  13. Math as MathUtils,
  14. Frustum,
  15. LoadingManager
  16. } from 'three';
  17. import { raycastTraverse, raycastTraverseFirstHit } from './raycastTraverse.js';
  18. const INITIAL_FRUSTUM_CULLED = Symbol( 'INITIAL_FRUSTUM_CULLED' );
  19. const DEG2RAD = MathUtils.DEG2RAD;
  20. const tempMat = new Matrix4();
  21. const tempMat2 = new Matrix4();
  22. const tempVector = new Vector3();
  23. const vecX = new Vector3();
  24. const vecY = new Vector3();
  25. const vecZ = new Vector3();
  26. const X_AXIS = new Vector3( 1, 0, 0 );
  27. const Y_AXIS = new Vector3( 0, 1, 0 );
  28. function updateFrustumCulled( object, toInitialValue ) {
  29. object.traverse( c => {
  30. c.frustumCulled = c[ INITIAL_FRUSTUM_CULLED ] && toInitialValue;
  31. } );
  32. }
  33. export class TilesRenderer extends TilesRendererBase {
  34. get autoDisableRendererCulling() {
  35. return this._autoDisableRendererCulling;
  36. }
  37. set autoDisableRendererCulling( value ) {
  38. if ( this._autoDisableRendererCulling !== value ) {
  39. super._autoDisableRendererCulling = value;
  40. this.traverse( tile => {
  41. if ( tile.scene ) {
  42. updateFrustumCulled( tile.scene, value );
  43. }
  44. } );
  45. }
  46. }
  47. constructor( ...args ) {
  48. super( ...args );
  49. this.group = new TilesGroup( this );
  50. this.cameras = [];
  51. this.cameraMap = new Map();
  52. this.cameraInfo = [];
  53. this.activeTiles = new Set();
  54. this.visibleTiles = new Set();
  55. this._autoDisableRendererCulling = true;
  56. this.optimizeRaycast = true;
  57. this.onLoadTileSet = null;
  58. this.onLoadModel = null;
  59. this.onDisposeModel = null;
  60. const manager = new LoadingManager();
  61. manager.setURLModifier( url => {
  62. if ( this.preprocessURL ) {
  63. return this.preprocessURL( url );
  64. } else {
  65. return url;
  66. }
  67. } );
  68. this.manager = manager;
  69. // Setting up the override raycasting function to be used by
  70. // 3D objects created by this renderer
  71. const tilesRenderer = this;
  72. this._overridenRaycast = function ( raycaster, intersects ) {
  73. if ( ! tilesRenderer.optimizeRaycast ) {
  74. Object.getPrototypeOf( this ).raycast.call( this, raycaster, intersects );
  75. }
  76. };
  77. }
  78. /* Public API */
  79. getBounds( box ) {
  80. if ( ! this.root ) {
  81. return false;
  82. }
  83. const cached = this.root.cached;
  84. const boundingBox = cached.box;
  85. const obbMat = cached.boxTransform;
  86. if ( boundingBox ) {
  87. box.copy( boundingBox );
  88. box.applyMatrix4( obbMat );
  89. return true;
  90. } else {
  91. return false;
  92. }
  93. }
  94. getOrientedBounds( box, matrix ) {
  95. if ( ! this.root ) {
  96. return false;
  97. }
  98. const cached = this.root.cached;
  99. const boundingBox = cached.box;
  100. const obbMat = cached.boxTransform;
  101. if ( box ) {
  102. box.copy( boundingBox );
  103. matrix.copy( obbMat );
  104. return true;
  105. } else {
  106. return false;
  107. }
  108. }
  109. forEachLoadedModel( callback ) {
  110. this.traverse( tile => {
  111. const scene = tile.cached.scene;
  112. if ( scene ) {
  113. callback( scene, tile );
  114. }
  115. } );
  116. }
  117. raycast( raycaster, intersects ) {
  118. if ( ! this.root ) {
  119. return;
  120. }
  121. if ( raycaster.firstHitOnly ) {
  122. const hit = raycastTraverseFirstHit( this.root, this.group, this.activeTiles, raycaster );
  123. if ( hit ) {
  124. intersects.push( hit );
  125. }
  126. } else {
  127. raycastTraverse( this.root, this.group, this.activeTiles, raycaster, intersects );
  128. }
  129. }
  130. hasCamera( camera ) {
  131. return this.cameraMap.has( camera );
  132. }
  133. setCamera( camera ) {
  134. const cameras = this.cameras;
  135. const cameraMap = this.cameraMap;
  136. if ( ! cameraMap.has( camera ) ) {
  137. cameraMap.set( camera, new Vector2() );
  138. cameras.push( camera );
  139. return true;
  140. }
  141. return false;
  142. }
  143. setResolution( camera, xOrVec, y ) {
  144. const cameraMap = this.cameraMap;
  145. if ( ! cameraMap.has( camera ) ) {
  146. return false;
  147. }
  148. if ( xOrVec instanceof Vector2 ) {
  149. cameraMap.get( camera ).copy( xOrVec );
  150. } else {
  151. cameraMap.get( camera ).set( xOrVec, y );
  152. }
  153. return true;
  154. }
  155. setResolutionFromRenderer( camera, renderer ) {
  156. const cameraMap = this.cameraMap;
  157. if ( ! cameraMap.has( camera ) ) {
  158. return false;
  159. }
  160. const resolution = cameraMap.get( camera );
  161. renderer.getSize( resolution );
  162. resolution.multiplyScalar( renderer.getPixelRatio() );
  163. return true;
  164. }
  165. deleteCamera( camera ) {
  166. const cameras = this.cameras;
  167. const cameraMap = this.cameraMap;
  168. if ( cameraMap.has( camera ) ) {
  169. const index = cameras.indexOf( camera );
  170. cameras.splice( index, 1 );
  171. cameraMap.delete( camera );
  172. return true;
  173. }
  174. return false;
  175. }
  176. /* Overriden */
  177. fetchTileSet( url, ...rest ) {
  178. const pr = super.fetchTileSet( url, ...rest );
  179. pr.then( json => {
  180. if ( this.onLoadTileSet ) {
  181. // Push this onto the end of the event stack to ensure this runs
  182. // after the base renderer has placed the provided json where it
  183. // needs to be placed and is ready for an update.
  184. Promise.resolve().then( () => {
  185. this.onLoadTileSet( json, url );
  186. } );
  187. }
  188. } );
  189. return pr;
  190. }
  191. update() {
  192. const group = this.group;
  193. const cameras = this.cameras;
  194. const cameraMap = this.cameraMap;
  195. const cameraInfo = this.cameraInfo;
  196. if ( cameras.length === 0 ) {
  197. console.warn( 'TilesRenderer: no cameras defined. Cannot update 3d tiles.' );
  198. return;
  199. }
  200. // automatically scale the array of cameraInfo to match the cameras
  201. while ( cameraInfo.length > cameras.length ) {
  202. cameraInfo.pop();
  203. }
  204. while ( cameraInfo.length < cameras.length ) {
  205. cameraInfo.push( {
  206. frustum: new Frustum(),
  207. sseDenominator: - 1,
  208. position: new Vector3(),
  209. invScale: - 1,
  210. pixelSize: 0,
  211. } );
  212. }
  213. // extract scale of group container
  214. tempMat2.copy( group.matrixWorld ).invert();
  215. let invScale;
  216. tempVector.setFromMatrixScale( tempMat2 );
  217. invScale = tempVector.x;
  218. if ( Math.abs( Math.max( tempVector.x - tempVector.y, tempVector.x - tempVector.z ) ) > 1e-6 ) {
  219. console.warn( 'ThreeTilesRenderer : Non uniform scale used for tile which may cause issues when calculating screen space error.' );
  220. }
  221. // store the camera cameraInfo in the 3d tiles root frame
  222. for ( let i = 0, l = cameraInfo.length; i < l; i ++ ) {
  223. const camera = cameras[ i ];
  224. const info = cameraInfo[ i ];
  225. const frustum = info.frustum;
  226. const position = info.position;
  227. const resolution = cameraMap.get( camera );
  228. if ( resolution.width === 0 || resolution.height === 0 ) {
  229. console.warn( 'TilesRenderer: resolution for camera error calculation is not set.' );
  230. }
  231. if ( camera.isPerspectiveCamera ) {
  232. info.sseDenominator = 2 * Math.tan( 0.5 * camera.fov * DEG2RAD ) / resolution.height;
  233. }
  234. if ( camera.isOrthographicCamera ) {
  235. const w = camera.right - camera.left;
  236. const h = camera.top - camera.bottom;
  237. info.pixelSize = Math.max( h / resolution.height, w / resolution.width );
  238. }
  239. info.invScale = invScale;
  240. // get frustum in group root frame
  241. tempMat.copy( group.matrixWorld );
  242. tempMat.premultiply( camera.matrixWorldInverse );
  243. tempMat.premultiply( camera.projectionMatrix );
  244. frustum.setFromProjectionMatrix( tempMat );
  245. // get transform position in group root frame
  246. position.set( 0, 0, 0 );
  247. position.applyMatrix4( camera.matrixWorld );
  248. position.applyMatrix4( tempMat2 );
  249. }
  250. super.update();
  251. }
  252. preprocessNode( tile, parentTile, tileSetDir ) {
  253. super.preprocessNode( tile, parentTile, tileSetDir );
  254. const transform = new Matrix4();
  255. if ( tile.transform ) {
  256. const transformArr = tile.transform;
  257. for ( let i = 0; i < 16; i ++ ) {
  258. transform.elements[ i ] = transformArr[ i ];
  259. }
  260. } else {
  261. transform.identity();
  262. }
  263. if ( parentTile ) {
  264. transform.premultiply( parentTile.cached.transform );
  265. }
  266. let box = null;
  267. let boxTransform = null;
  268. let boxTransformInverse = null;
  269. if ( 'box' in tile.boundingVolume ) {
  270. const data = tile.boundingVolume.box;
  271. box = new Box3();
  272. boxTransform = new Matrix4();
  273. boxTransformInverse = new Matrix4();
  274. // get the extents of the bounds in each axis
  275. vecX.set( data[ 3 ], data[ 4 ], data[ 5 ] );
  276. vecY.set( data[ 6 ], data[ 7 ], data[ 8 ] );
  277. vecZ.set( data[ 9 ], data[ 10 ], data[ 11 ] );
  278. const scaleX = vecX.length();
  279. const scaleY = vecY.length();
  280. const scaleZ = vecZ.length();
  281. vecX.normalize();
  282. vecY.normalize();
  283. vecZ.normalize();
  284. // create the oriented frame that the box exists in
  285. boxTransform.set(
  286. vecX.x, vecY.x, vecZ.x, data[ 0 ],
  287. vecX.y, vecY.y, vecZ.y, data[ 1 ],
  288. vecX.z, vecY.z, vecZ.z, data[ 2 ],
  289. 0, 0, 0, 1
  290. );
  291. boxTransform.premultiply( transform );
  292. boxTransformInverse.copy( boxTransform ).invert();
  293. // scale the box by the extents
  294. box.min.set( - scaleX, - scaleY, - scaleZ );
  295. box.max.set( scaleX, scaleY, scaleZ );
  296. }
  297. let sphere = null;
  298. if ( 'sphere' in tile.boundingVolume ) {
  299. const data = tile.boundingVolume.sphere;
  300. sphere = new Sphere();
  301. sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] );
  302. sphere.radius = data[ 3 ];
  303. sphere.applyMatrix4( transform );
  304. } else if ( 'box' in tile.boundingVolume ) {
  305. const data = tile.boundingVolume.box;
  306. sphere = new Sphere();
  307. box.getBoundingSphere( sphere );
  308. sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] );
  309. sphere.applyMatrix4( transform );
  310. }
  311. let region = null;
  312. if ( 'region' in tile.boundingVolume ) {
  313. console.warn( 'ThreeTilesRenderer: region bounding volume not supported.' );
  314. }
  315. tile.cached = {
  316. loadIndex: 0,
  317. transform,
  318. active: false,
  319. inFrustum: [],
  320. box,
  321. boxTransform,
  322. boxTransformInverse,
  323. sphere,
  324. region,
  325. scene: null,
  326. geometry: null,
  327. material: null,
  328. distance: Infinity
  329. };
  330. }
  331. parseTile( buffer, tile, extension ) {
  332. tile._loadIndex = tile._loadIndex || 0;
  333. tile._loadIndex ++;
  334. const uri = tile.content.uri;
  335. const uriSplits = uri.split( /[\\\/]/g );
  336. uriSplits.pop();
  337. const workingPath = uriSplits.join( '/' );
  338. const fetchOptions = this.fetchOptions;
  339. const manager = this.manager;
  340. const loadIndex = tile._loadIndex;
  341. let promise = null;
  342. switch ( extension ) {
  343. case 'b3dm':
  344. const loader = new B3DMLoader( manager );
  345. loader.workingPath = workingPath;
  346. loader.fetchOptions = fetchOptions;
  347. promise = loader
  348. .parse( buffer )
  349. .then( res => res.scene );
  350. break;
  351. case 'pnts':
  352. promise = Promise.resolve( new PNTSLoader( manager ).parse( buffer ).scene );
  353. break;
  354. case 'i3dm': {
  355. const loader = new I3DMLoader( manager );
  356. loader.workingPath = workingPath;
  357. loader.fetchOptions = fetchOptions;
  358. promise = loader
  359. .parse( buffer )
  360. .then( res => res.scene );
  361. break;
  362. }
  363. case 'cmpt': {
  364. const loader = new CMPTLoader( manager );
  365. loader.workingPath = workingPath;
  366. loader.fetchOptions = fetchOptions;
  367. promise = loader
  368. .parse( buffer )
  369. .then( res => res.scene );
  370. break;
  371. }
  372. default:
  373. console.warn( `TilesRenderer: Content type "${ extension }" not supported.` );
  374. promise = Promise.resolve( null );
  375. break;
  376. }
  377. return promise.then( scene => {
  378. if ( tile._loadIndex !== loadIndex ) {
  379. return;
  380. }
  381. const upAxis = this.rootTileSet.asset && this.rootTileSet.asset.gltfUpAxis || 'y';
  382. const cached = tile.cached;
  383. const cachedTransform = cached.transform;
  384. switch ( upAxis.toLowerCase() ) {
  385. case 'x':
  386. tempMat.makeRotationAxis( Y_AXIS, - Math.PI / 2 );
  387. break;
  388. case 'y':
  389. tempMat.makeRotationAxis( X_AXIS, Math.PI / 2 );
  390. break;
  391. case 'z':
  392. tempMat.identity();
  393. break;
  394. }
  395. // ensure the matrix is up to date in case the scene has a transform applied
  396. scene.updateMatrix();
  397. // apply the local up-axis correction rotation
  398. // GLTFLoader seems to never set a transformation on the root scene object so
  399. // any transformations applied to it can be assumed to be applied after load
  400. // (such as applying RTC_CENTER) meaning they should happen _after_ the z-up
  401. // rotation fix which is why "multiply" happens here.
  402. if ( extension !== 'pnts' ) {
  403. scene.matrix.multiply( tempMat );
  404. }
  405. scene.matrix.premultiply( cachedTransform );
  406. scene.matrix.decompose( scene.position, scene.quaternion, scene.scale );
  407. scene.traverse( c => {
  408. c[ INITIAL_FRUSTUM_CULLED ] = c.frustumCulled;
  409. } );
  410. updateFrustumCulled( scene, this.autoDisableRendererCulling );
  411. cached.scene = scene;
  412. // We handle raycasting in a custom way so remove it from here
  413. scene.traverse( c => {
  414. c.raycast = this._overridenRaycast;
  415. } );
  416. const materials = [];
  417. const geometry = [];
  418. const textures = [];
  419. scene.traverse( c => {
  420. if ( c.geometry ) {
  421. geometry.push( c.geometry );
  422. }
  423. if ( c.material ) {
  424. const material = c.material;
  425. materials.push( c.material );
  426. for ( const key in material ) {
  427. const value = material[ key ];
  428. if ( value && value.isTexture ) {
  429. textures.push( value );
  430. }
  431. }
  432. }
  433. } );
  434. cached.materials = materials;
  435. cached.geometry = geometry;
  436. cached.textures = textures;
  437. if ( this.onLoadModel ) {
  438. this.onLoadModel( scene, tile );
  439. }
  440. } );
  441. }
  442. disposeTile( tile ) {
  443. // This could get called before the tile has finished downloading
  444. const cached = tile.cached;
  445. if ( cached.scene ) {
  446. const materials = cached.materials;
  447. const geometry = cached.geometry;
  448. const textures = cached.textures;
  449. for ( let i = 0, l = geometry.length; i < l; i ++ ) {
  450. geometry[ i ].dispose();
  451. }
  452. for ( let i = 0, l = materials.length; i < l; i ++ ) {
  453. materials[ i ].dispose();
  454. }
  455. for ( let i = 0, l = textures.length; i < l; i ++ ) {
  456. const texture = textures[ i ];
  457. texture.dispose();
  458. }
  459. if ( this.onDisposeModel ) {
  460. this.onDisposeModel( cached.scene, tile );
  461. }
  462. cached.scene = null;
  463. cached.materials = null;
  464. cached.textures = null;
  465. cached.geometry = null;
  466. }
  467. tile._loadIndex ++;
  468. }
  469. setTileVisible( tile, visible ) {
  470. const scene = tile.cached.scene;
  471. const visibleTiles = this.visibleTiles;
  472. const group = this.group;
  473. if ( visible ) {
  474. group.add( scene );
  475. visibleTiles.add( tile );
  476. scene.updateMatrixWorld( true );
  477. } else {
  478. group.remove( scene );
  479. visibleTiles.delete( tile );
  480. }
  481. }
  482. setTileActive( tile, active ) {
  483. const activeTiles = this.activeTiles;
  484. if ( active ) {
  485. activeTiles.add( tile );
  486. } else {
  487. activeTiles.delete( tile );
  488. }
  489. }
  490. calculateError( tile ) {
  491. const cached = tile.cached;
  492. const inFrustum = cached.inFrustum;
  493. const cameras = this.cameras;
  494. const cameraInfo = this.cameraInfo;
  495. // TODO: Use the content bounding volume here?
  496. const boundingVolume = tile.boundingVolume;
  497. if ( 'box' in boundingVolume ) {
  498. const boundingBox = cached.box;
  499. const boxTransformInverse = cached.boxTransformInverse;
  500. let maxError = - Infinity;
  501. let minDistance = Infinity;
  502. for ( let i = 0, l = cameras.length; i < l; i ++ ) {
  503. if ( ! inFrustum[ i ] ) {
  504. continue;
  505. }
  506. // transform camera position into local frame of the tile bounding box
  507. const camera = cameras[ i ];
  508. const info = cameraInfo[ i ];
  509. const invScale = info.invScale;
  510. tempVector.copy( info.position );
  511. tempVector.applyMatrix4( boxTransformInverse );
  512. let error;
  513. if ( camera.isOrthographicCamera ) {
  514. const pixelSize = info.pixelSize;
  515. error = tile.geometricError / ( pixelSize * invScale );
  516. // TODO: distance not updated while in orthographic views
  517. } else {
  518. const distance = boundingBox.distanceToPoint( tempVector );
  519. const scaledDistance = distance * invScale;
  520. const sseDenominator = info.sseDenominator;
  521. error = Math.max( 0.01, tile.geometricError ) / ( scaledDistance * sseDenominator );
  522. minDistance = Math.min( minDistance, scaledDistance );
  523. }
  524. maxError = Math.max( maxError, error );
  525. }
  526. tile.cached.distance = minDistance;
  527. tile.__distanceFromCamera = minDistance;
  528. return maxError;
  529. } else if ( 'sphere' in boundingVolume ) {
  530. // const sphere = cached.sphere;
  531. console.warn( 'ThreeTilesRenderer : Sphere bounds not supported.' );
  532. } else if ( 'region' in boundingVolume ) {
  533. // unsupported
  534. console.warn( 'ThreeTilesRenderer : Region bounds not supported.' );
  535. }
  536. return Infinity;
  537. }
  538. tileInView( tile ) {
  539. // TODO: we should use the more precise bounding volumes here if possible
  540. // cache the root-space planes
  541. // Use separating axis theorem for frustum and obb
  542. const cached = tile.cached;
  543. const sphere = cached.sphere;
  544. const inFrustum = cached.inFrustum;
  545. if ( sphere ) {
  546. const cameraInfo = this.cameraInfo;
  547. let inView = false;
  548. for ( let i = 0, l = cameraInfo.length; i < l; i ++ ) {
  549. // Track which camera frustums this tile is in so we can use it
  550. // to ignore the error calculations for cameras that can't see it
  551. const frustum = cameraInfo[ i ].frustum;
  552. if ( frustum.intersectsSphere( sphere ) ) {
  553. inView = true;
  554. inFrustum[ i ] = true;
  555. } else {
  556. inFrustum[ i ] = false;
  557. }
  558. }
  559. return inView;
  560. }
  561. return true;
  562. }
  563. }