ThreeTilesRenderer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import { TilesRenderer } from '../base/TilesRenderer.js';
  2. import { ThreeB3DMLoader } from './ThreeB3DMLoader.js';
  3. import { TilesGroup } from './TilesGroup.js';
  4. import {
  5. Matrix4,
  6. Box3,
  7. Sphere,
  8. Vector3,
  9. Group,
  10. Vector2,
  11. Math as MathUtils,
  12. Box3Helper,
  13. Quaternion,
  14. Frustum,
  15. Ray,
  16. Mesh
  17. } from 'three';
  18. const DEG2RAD = MathUtils.DEG2RAD;
  19. const tempMat = new Matrix4();
  20. const tempQuaternion = new Quaternion();
  21. const tempVector = new Vector3();
  22. const resVector = new Vector2();
  23. const vecX = new Vector3();
  24. const vecY = new Vector3();
  25. const vecZ = new Vector3();
  26. const ray = new Ray();
  27. const _sphere = new Sphere();
  28. function emptyRaycast () {};
  29. class ThreeTilesRenderer extends TilesRenderer {
  30. get displayBounds() {
  31. return this._displayBounds;
  32. }
  33. set displayBounds( val ) {
  34. if ( val !== this.displayBounds ) {
  35. this._displayBounds = val;
  36. this.traverse( t => {
  37. const scene = t.cached.scene;
  38. const boxHelper = t.cached.boxHelper;
  39. if ( scene ) {
  40. if ( val ) {
  41. scene.add( boxHelper );
  42. boxHelper.updateMatrixWorld( true );
  43. } else {
  44. scene.remove( boxHelper );
  45. }
  46. }
  47. } );
  48. }
  49. }
  50. constructor( url, cameras, renderer ) {
  51. super( url );
  52. this.group = new TilesGroup( this );
  53. this.cameras = Array.isArray( cameras ) ? cameras : [ cameras ];
  54. this.frustums = [];
  55. this.renderer = renderer;
  56. this.activeSet = new Set();
  57. this.visibleSet = new Set();
  58. }
  59. /* Public API */
  60. getBounds( box ) {
  61. if ( ! this.root ) {
  62. return false;
  63. }
  64. const cached = this.root.cached;
  65. const boundingBox = cached.box;
  66. const obbMat = cached.boxTransform;
  67. const transformMat = cached.transform;
  68. box.copy( boundingBox );
  69. tempMat.multiplyMatrices( transformMat, obbMat );
  70. box.applyMatrix4( tempMat );
  71. return true;
  72. }
  73. raycast( raycaster, intersects ) {
  74. const activeSet = this.activeSet;
  75. const group = this.group;
  76. this.traverse( tile => {
  77. const cached = tile.cached;
  78. const groupMatrixWorld = group.matrixWorld;
  79. const transformMat = cached.transform;
  80. tempMat.copy( groupMatrixWorld );
  81. tempMat.multiply( transformMat );
  82. const sphere = cached.sphere;
  83. if ( sphere ) {
  84. _sphere.copy( sphere );
  85. _sphere.applyMatrix4( tempMat );
  86. if ( ! raycaster.ray.intersectsSphere( _sphere ) ) {
  87. return true;
  88. }
  89. }
  90. const boundingBox = cached.box;
  91. const obbMat = cached.boxTransform;
  92. if ( boundingBox ) {
  93. tempMat.multiply( obbMat );
  94. tempMat.getInverse( tempMat );
  95. ray.copy( raycaster.ray ).applyMatrix4( tempMat );
  96. if ( ! ray.intersectsBox( boundingBox ) ) {
  97. return true;
  98. }
  99. }
  100. // TODO: check region
  101. const scene = cached.scene;
  102. if ( activeSet.has( scene ) ) {
  103. raycaster.intersectObject( scene, true, intersects );
  104. scene.traverse( c => {
  105. if ( c !== cached.boxHelper ) {
  106. Object.getPrototypeOf( c ).raycast.call( c, raycaster, intersects );
  107. }
  108. } );
  109. return true;
  110. }
  111. } );
  112. }
  113. /* Overriden */
  114. update() {
  115. const group = this.group;
  116. const renderer = this.renderer;
  117. const cameras = this.cameras;
  118. const frustums = this.frustums;
  119. // automatically scale the array of frustums to match the cameras
  120. while ( frustums.length > cameras.length ) {
  121. frustums.pop();
  122. }
  123. while ( frustums.length < cameras.length ) {
  124. frustums.push( new Frustum() );
  125. }
  126. // store the camera frustums in the 3d tiles root frame
  127. for ( let i = 0, l = frustums.length; i < l; i ++ ) {
  128. const camera = cameras[ i ];
  129. const frustum = frustums[ i ];
  130. tempMat.copy( group.matrixWorld );
  131. tempMat.premultiply( camera.matrixWorldInverse );
  132. tempMat.premultiply( camera.projectionMatrix );
  133. frustum.setFromMatrix( tempMat );
  134. }
  135. // store the resolution of the render
  136. renderer.getSize( resVector );
  137. resVector.multiplyScalar( renderer.getPixelRatio() );
  138. super.update();
  139. }
  140. preprocessNode( tile, parentTile, tileSetDir ) {
  141. super.preprocessNode( tile, parentTile, tileSetDir );
  142. const transform = new Matrix4();
  143. if ( tile.transform ) {
  144. const transformArr = tile.transform;
  145. for ( let i = 0; i < 16; i ++ ) {
  146. transform.elements[ i ] = transformArr[ i ];
  147. }
  148. } else {
  149. transform.identity();
  150. }
  151. if ( parentTile ) {
  152. transform.multiply( parentTile.cached.transform );
  153. }
  154. let box = null;
  155. let boxTransform = null;
  156. if ( 'box' in tile.boundingVolume ) {
  157. const data = tile.boundingVolume.box;
  158. box = new Box3();
  159. boxTransform = new Matrix4();
  160. // get the extents of the bounds in each axis
  161. vecX.set( data[ 3 ], data[ 4 ], data[ 5 ] );
  162. vecY.set( data[ 6 ], data[ 7 ], data[ 8 ] );
  163. vecZ.set( data[ 9 ], data[ 10 ], data[ 11 ] );
  164. const scaleX = vecX.length();
  165. const scaleY = vecY.length();
  166. const scaleZ = vecZ.length();
  167. vecX.normalize();
  168. vecY.normalize();
  169. vecZ.normalize();
  170. // create the oriented frame that the box exists in
  171. boxTransform.set(
  172. vecX.x, vecY.x, vecZ.x, data[ 0 ],
  173. vecX.y, vecY.y, vecZ.y, data[ 1 ],
  174. vecX.z, vecY.z, vecZ.z, data[ 2 ],
  175. 0, 0, 0, 1
  176. );
  177. // scale the box by the extents
  178. box.min.set( - scaleX, - scaleY, - scaleZ );
  179. box.max.set( scaleX, scaleY, scaleZ );
  180. }
  181. let sphere = null;
  182. if ( 'sphere' in tile.boundingVolume ) {
  183. const data = tile.boundingVolume.sphere;
  184. sphere = new Sphere();
  185. sphere.center.set( data[ 0 ], data[ 1 ], data[ 2 ] );
  186. sphere.radius = data[ 3 ];
  187. } else if ( 'box' in tile.boundingVolume ) {
  188. sphere = new Sphere();
  189. box.getBoundingSphere( sphere );
  190. boxTransform.decompose( sphere.center, tempQuaternion, tempVector );
  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. sphere,
  203. region,
  204. boxHelper: null,
  205. scene: null,
  206. };
  207. }
  208. parseTile( buffer, tile ) {
  209. tile._loadIndex = tile._loadIndex || 0;
  210. tile._loadIndex ++;
  211. // TODO: 90 degree rotation must be applied to GLTF file to resolve "up"
  212. const loadIndex = tile._loadIndex;
  213. return new ThreeB3DMLoader().parse( buffer ).then( res => {
  214. if ( tile._loadIndex !== loadIndex ) {
  215. return;
  216. }
  217. const cached = tile.cached;
  218. const cachedBox = cached.box;
  219. const cachedBoxMat = cached.boxTransform;
  220. // add a helper group to represent the obb rotation matrix
  221. const boxHelper = new Box3Helper( cachedBox );
  222. const boxHelperGroup = new Group();
  223. cachedBoxMat.decompose( boxHelperGroup.position, boxHelperGroup.quaternion, boxHelperGroup.scale );
  224. boxHelperGroup.add( boxHelper );
  225. boxHelperGroup.updateMatrixWorld( true );
  226. const scene = res.scene;
  227. cached.transform.decompose( scene.position, scene.quaternion, scene.scale );
  228. scene.traverse( c => c.frustumCulled = false );
  229. cached.boxHelper = boxHelperGroup;
  230. cached.scene = res.scene;
  231. if ( this.displayBounds ) {
  232. cached.scene.add( cached.boxHelper );
  233. }
  234. // We handle raycasting in a custom way so remove it from here
  235. boxHelper.raycast = emptyRaycast;
  236. scene.traverse( c => {
  237. c.raycast = emptyRaycast;
  238. } );
  239. } );
  240. }
  241. disposeTile( tile ) {
  242. const cached = tile.cached;
  243. if ( cached.scene ) {
  244. const scene = cached.scene;
  245. if ( scene.parent ) {
  246. scene.parent.remove( scene );
  247. }
  248. scene.traverse( c => {
  249. if ( c.geometry ) {
  250. c.geometry.dispose();
  251. }
  252. if ( c.material ) {
  253. c.material.dispose();
  254. // TODO: dispose textures
  255. }
  256. } );
  257. cached.scene = null;
  258. cached.boxHelper = null;
  259. }
  260. tile._loadIndex ++;
  261. }
  262. setTileVisible( tile, visible ) {
  263. const scene = tile.cached.scene;
  264. const visibleSet = this.visibleSet;
  265. const group = this.group;
  266. if ( visible ) {
  267. if ( scene && ! scene.parent ) {
  268. group.add( scene );
  269. visibleSet.add( scene );
  270. scene.updateMatrixWorld( true );
  271. }
  272. } else {
  273. group.remove( scene );
  274. visibleSet.delete( scene );
  275. }
  276. }
  277. setTileActive( tile, active ) {
  278. const cached = tile.cached;
  279. const activeSet = this.activeSet;
  280. if ( active !== cached.active ) {
  281. cached.active = active;
  282. if ( active ) {
  283. activeSet.add( cached.scene );
  284. } else {
  285. activeSet.delete( cached.scene );
  286. }
  287. }
  288. }
  289. calculateError( tile ) {
  290. const cached = tile.cached;
  291. const cameras = this.cameras;
  292. // TODO: Use the content bounding volume here?
  293. const boundingVolume = tile.boundingVolume;
  294. const transformMat = cached.transform;
  295. if ( 'box' in boundingVolume ) {
  296. const group = this.group;
  297. const boundingBox = cached.box;
  298. const obbMat = cached.boxTransform;
  299. // TODO: these can likely be cached? Or the world transform mat can be used
  300. // transformMat can be rolled into oobMat
  301. tempMat.copy( transformMat );
  302. tempMat.multiply( obbMat );
  303. tempMat.getInverse( tempMat );
  304. let minError = Infinity;
  305. for ( let i = 0, l = cameras.length; i < l; i ++ ) {
  306. // transform camera position into local frame of the tile bounding box
  307. const cam = cameras[ i ];
  308. tempVector.copy( cam.position );
  309. tempVector.applyMatrix4( tempMat );
  310. let error;
  311. if ( cam.isOrthographic ) {
  312. const w = cam.right - cam.left;
  313. const h = cam.top - cam.bottom;
  314. const pixelSize = Math.Max( h, w ) / Math.Max( resVector.width, resVector.height );
  315. error = tile.geometricError / pixelSize;
  316. } else {
  317. const distance = boundingBox.distanceToPoint( tempVector );
  318. // assume the scales on all axes are uniform.
  319. let scale;
  320. // account for tile scale.
  321. tempVector.setFromMatrixScale( tempMat );
  322. scale = tempVector.x;
  323. // account for parent group scale. Divide because this matrix has not been inverted like the previous one.
  324. tempVector.setFromMatrixScale( group.matrixWorld );
  325. scale /= tempVector.x;
  326. if ( Math.abs( Math.max( scale.x - scale.y, scale.x - scale.z ) ) > 1e-6 ) {
  327. console.warn( 'ThreeTilesRenderer : Non uniform scale used for tile which may cause issues when claculating screen space error.' );
  328. }
  329. const scaledDistance = distance * scale;
  330. const sseDenominator = 2 * Math.tan( 0.5 * cam.fov * DEG2RAD );
  331. error = ( tile.geometricError * resVector.height ) / ( scaledDistance * sseDenominator );
  332. }
  333. minError = Math.min( minError, error );
  334. }
  335. return minError;
  336. } else if ( 'sphere' in boundingVolume ) {
  337. // const sphere = cached.sphere;
  338. console.warn( 'ThreeTilesRenderer : Sphere bounds not supported.' );
  339. } else if ( 'region' in boundingVolume ) {
  340. // unsupported
  341. console.warn( 'ThreeTilesRenderer : Region bounds not supported.' );
  342. }
  343. return Infinity;
  344. }
  345. tileInView( tile ) {
  346. // TODO: we should use the more precise bounding volumes here if possible
  347. // cache the root-space planes
  348. const sphere = tile.cached.sphere;
  349. if ( sphere ) {
  350. _sphere.copy( sphere );
  351. _sphere.applyMatrix4( tile.cached.transform );
  352. const frustums = this.frustums;
  353. for ( let i = 0, l = frustums.length; i < l; i ++ ) {
  354. const frustum = frustums[ i ];
  355. if ( frustum.intersectsSphere( _sphere ) ) {
  356. return true;
  357. }
  358. }
  359. return false;
  360. }
  361. return true;
  362. }
  363. }
  364. export { ThreeTilesRenderer };