TilesRenderer.js 15 KB

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