TilesRenderer.js 12 KB

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