ThreeTilesRenderer.js 11 KB

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