TilesRenderer.js 15 KB

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