TilesRenderer.js 14 KB

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