TilesRendererBase.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import path from 'path';
  2. import { urlJoin } from '../utilities/urlJoin.js';
  3. import { LRUCache } from '../utilities/LRUCache.js';
  4. import { PriorityQueue } from '../utilities/PriorityQueue.js';
  5. import { determineFrustumSet, toggleTiles, skipTraversal, markUsedSetLeaves, traverseSet } from './traverseFunctions.js';
  6. import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';
  7. // Function for sorting the evicted LRU items. We should evict the shallowest depth first.
  8. const priorityCallback = tile => 1 / ( tile.__depthFromRenderedParent + 1 );
  9. export class TilesRendererBase {
  10. get rootTileSet() {
  11. const tileSet = this.tileSets[ this.rootURL ];
  12. if ( ! tileSet || tileSet instanceof Promise ) {
  13. return null;
  14. } else {
  15. return tileSet;
  16. }
  17. }
  18. get root() {
  19. const tileSet = this.rootTileSet;
  20. return tileSet ? tileSet.root : null;
  21. }
  22. constructor( url ) {
  23. // state
  24. this.tileSets = {};
  25. this.rootURL = url;
  26. this.fetchOptions = {};
  27. const lruCache = new LRUCache();
  28. lruCache.unloadPriorityCallback = priorityCallback;
  29. const downloadQueue = new PriorityQueue();
  30. downloadQueue.maxJobs = 4;
  31. downloadQueue.priorityCallback = priorityCallback;
  32. const parseQueue = new PriorityQueue();
  33. parseQueue.maxJobs = 1;
  34. parseQueue.priorityCallback = priorityCallback;
  35. this.lruCache = lruCache;
  36. this.downloadQueue = downloadQueue;
  37. this.parseQueue = parseQueue;
  38. this.stats = {
  39. parsing: 0,
  40. downloading: 0,
  41. failed: 0,
  42. inFrustum: 0,
  43. used: 0,
  44. active: 0,
  45. visible: 0,
  46. };
  47. this.frameCount = 0;
  48. // options
  49. this.errorTarget = 6.0;
  50. this.errorThreshold = Infinity;
  51. this.loadSiblings = true;
  52. this.displayActiveTiles = false;
  53. this.maxDepth = Infinity;
  54. this.stopAtEmptyTiles = true;
  55. }
  56. traverse( beforecb, aftercb ) {
  57. const tileSets = this.tileSets;
  58. const rootTileSet = tileSets[ this.rootURL ];
  59. if ( ! rootTileSet || ! rootTileSet.root ) return;
  60. traverseSet( rootTileSet.root, beforecb, aftercb );
  61. }
  62. // Public API
  63. update() {
  64. const stats = this.stats;
  65. const lruCache = this.lruCache;
  66. const tileSets = this.tileSets;
  67. const rootTileSet = tileSets[ this.rootURL ];
  68. if ( ! ( this.rootURL in tileSets ) ) {
  69. this.loadTileSet( this.rootURL );
  70. return;
  71. } else if ( ! rootTileSet || ! rootTileSet.root ) {
  72. return;
  73. }
  74. const root = rootTileSet.root;
  75. stats.inFrustum = 0,
  76. stats.used = 0,
  77. stats.active = 0,
  78. stats.visible = 0,
  79. this.frameCount ++;
  80. determineFrustumSet( root, this );
  81. markUsedSetLeaves( root, this );
  82. skipTraversal( root, this );
  83. toggleTiles( root, this );
  84. lruCache.scheduleUnload();
  85. }
  86. // Overrideable
  87. parseTile( buffer, tile, extension ) {
  88. return null;
  89. }
  90. disposeTile( tile ) {
  91. }
  92. preprocessNode( tile, parentTile, tileSetDir ) {
  93. if ( tile.content ) {
  94. // Fix old file formats
  95. if ( ! ( 'uri' in tile.content ) && 'url' in tile.content ) {
  96. tile.content.uri = tile.content.url;
  97. delete tile.content.url;
  98. }
  99. if ( tile.content.uri ) {
  100. tile.content.uri = urlJoin( tileSetDir, tile.content.uri );
  101. }
  102. // NOTE: fix for some cases where tilesets provide the bounding volume
  103. // but volumes are not present.
  104. if (
  105. tile.content.boundingVolume &&
  106. ! (
  107. 'box' in tile.content.boundingVolume ||
  108. 'sphere' in tile.content.boundingVolume ||
  109. 'region' in tile.content.boundingVolume
  110. )
  111. ) {
  112. delete tile.content.boundingVolume;
  113. }
  114. }
  115. tile.parent = parentTile;
  116. tile.children = tile.children || [];
  117. const uri = tile.content && tile.content.uri;
  118. if ( uri ) {
  119. // "content" should only indicate loadable meshes, not external tile sets
  120. const isExternalTileSet = /\.json$/i.test( tile.content.uri );
  121. tile.__externalTileSet = isExternalTileSet;
  122. tile.__contentEmpty = isExternalTileSet;
  123. } else {
  124. tile.__externalTileSet = false;
  125. tile.__contentEmpty = true;
  126. }
  127. tile.__error = 0.0;
  128. tile.__inFrustum = false;
  129. tile.__isLeaf = false;
  130. tile.__usedLastFrame = false;
  131. tile.__used = false;
  132. tile.__wasSetVisible = false;
  133. tile.__visible = false;
  134. tile.__childrenWereVisible = false;
  135. tile.__allChildrenLoaded = false;
  136. tile.__wasSetActive = false;
  137. tile.__active = false;
  138. tile.__loadingState = UNLOADED;
  139. tile.__loadIndex = 0;
  140. tile.__loadAbort = null;
  141. tile.__depthFromRenderedParent = - 1;
  142. if ( parentTile === null ) {
  143. tile.__depth = 0;
  144. tile.refine = tile.refine || 'REPLACE';
  145. } else {
  146. tile.__depth = parentTile.__depth + 1;
  147. tile.refine = tile.refine || parentTile.refine;
  148. }
  149. }
  150. setTileActive( tile, state ) {
  151. }
  152. setTileVisible( tile, state ) {
  153. }
  154. calculateError( tile ) {
  155. return 0;
  156. }
  157. tileInView( tile ) {
  158. return true;
  159. }
  160. // Private Functions
  161. fetchTileSet( url, fetchOptions, parent = null ) {
  162. return fetch( url, fetchOptions )
  163. .then( res => {
  164. if ( res.ok ) {
  165. return res.json();
  166. } else {
  167. throw new Error( `TilesRenderer: Failed to load tileset "${ url }" with status ${ res.status } : ${ res.statusText }` );
  168. }
  169. } )
  170. .then( json => {
  171. const version = json.asset.version;
  172. console.assert(
  173. version === '1.0' || version === '0.0',
  174. 'asset.version is expected to be a string of "1.0" or "0.0"'
  175. );
  176. const basePath = path.dirname( url );
  177. traverseSet(
  178. json.root,
  179. ( node, parent ) => this.preprocessNode( node, parent, basePath ),
  180. null,
  181. parent,
  182. parent ? parent.__depth : 0,
  183. );
  184. return json;
  185. } );
  186. }
  187. loadTileSet( url ) {
  188. const tileSets = this.tileSets;
  189. if ( ! ( url in tileSets ) ) {
  190. const pr = this
  191. .fetchTileSet( url, this.fetchOptions )
  192. .then( json => {
  193. tileSets[ url ] = json;
  194. } );
  195. pr.catch( err => {
  196. console.error( err );
  197. tileSets[ url ] = err;
  198. } );
  199. tileSets[ url ] = pr;
  200. return pr;
  201. } else if ( tileSets[ url ] instanceof Error ) {
  202. return Promise.reject( tileSets[ url ] );
  203. } else {
  204. return Promise.resolve( tileSets[ url ] );
  205. }
  206. }
  207. requestTileContents( tile ) {
  208. // If the tile is already being loaded then don't
  209. // start it again.
  210. if ( tile.__loadingState !== UNLOADED ) {
  211. return;
  212. }
  213. const stats = this.stats;
  214. const lruCache = this.lruCache;
  215. const downloadQueue = this.downloadQueue;
  216. const parseQueue = this.parseQueue;
  217. const isExternalTileSet = tile.__externalTileSet;
  218. lruCache.add( tile, t => {
  219. // Stop the load if it's started
  220. if ( t.__loadingState === LOADING ) {
  221. t.__loadAbort.abort();
  222. t.__loadAbort = null;
  223. } else if ( isExternalTileSet ) {
  224. t.children.length = 0;
  225. } else {
  226. this.disposeTile( t );
  227. }
  228. // Decrement stats
  229. if ( t.__loadingState === LOADING ) {
  230. stats.downloading --;
  231. } else if ( t.__loadingState === PARSING ) {
  232. stats.parsing --;
  233. }
  234. t.__loadingState = UNLOADED;
  235. t.__loadIndex ++;
  236. parseQueue.remove( t );
  237. downloadQueue.remove( t );
  238. } );
  239. // Track a new load index so we avoid the condition where this load is stopped and
  240. // another begins soon after so we don't parse twice.
  241. tile.__loadIndex ++;
  242. const loadIndex = tile.__loadIndex;
  243. const controller = new AbortController();
  244. const signal = controller.signal;
  245. stats.downloading ++;
  246. tile.__loadAbort = controller;
  247. tile.__loadingState = LOADING;
  248. const errorCallback = e => {
  249. // if it has been unloaded then the tile has been disposed
  250. if ( tile.__loadIndex !== loadIndex ) {
  251. return;
  252. }
  253. if ( e.name !== 'AbortError' ) {
  254. parseQueue.remove( tile );
  255. downloadQueue.remove( tile );
  256. if ( tile.__loadingState === PARSING ) {
  257. stats.parsing --;
  258. } else if ( tile.__loadingState === LOADING ) {
  259. stats.downloading --;
  260. }
  261. stats.failed ++;
  262. console.error( 'TilesRenderer : Failed to load tile.' );
  263. console.error( e );
  264. tile.__loadingState = FAILED;
  265. } else {
  266. lruCache.remove( tile );
  267. }
  268. };
  269. if ( isExternalTileSet ) {
  270. downloadQueue.add( tile, tile => {
  271. if ( tile.__loadIndex !== loadIndex ) {
  272. return Promise.resolve();
  273. }
  274. return this.fetchTileSet( tile.content.uri, Object.assign( { signal }, this.fetchOptions ), tile );
  275. } )
  276. .then( json => {
  277. if ( tile.__loadIndex !== loadIndex ) {
  278. return;
  279. }
  280. stats.downloading --;
  281. tile.__loadAbort = null;
  282. tile.__loadingState = LOADED;
  283. tile.children.push( json.root );
  284. } )
  285. .catch( errorCallback );
  286. } else {
  287. downloadQueue.add( tile, tile => {
  288. if ( tile.__loadIndex !== loadIndex ) {
  289. return Promise.resolve();
  290. }
  291. return fetch( tile.content.uri, Object.assign( { signal }, this.fetchOptions ) );
  292. } )
  293. .then( res => {
  294. if ( tile.__loadIndex !== loadIndex ) {
  295. return;
  296. }
  297. if ( res.ok ) {
  298. return res.arrayBuffer();
  299. } else {
  300. throw new Error( `Failed to load model with error code ${res.status}` );
  301. }
  302. } )
  303. .then( buffer => {
  304. // if it has been unloaded then the tile has been disposed
  305. if ( tile.__loadIndex !== loadIndex ) {
  306. return;
  307. }
  308. stats.downloading --;
  309. stats.parsing ++;
  310. tile.__loadAbort = null;
  311. tile.__loadingState = PARSING;
  312. return parseQueue.add( tile, tile => {
  313. // if it has been unloaded then the tile has been disposed
  314. if ( tile.__loadIndex !== loadIndex ) {
  315. return Promise.resolve();
  316. }
  317. const uri = tile.content.uri;
  318. const extension = uri.split( /\./g ).pop();
  319. return this.parseTile( buffer, tile, extension );
  320. } );
  321. } )
  322. .then( () => {
  323. // if it has been unloaded then the tile has been disposed
  324. if ( tile.__loadIndex !== loadIndex ) {
  325. return;
  326. }
  327. stats.parsing --;
  328. tile.__loadingState = LOADED;
  329. if ( tile.__wasSetVisible ) {
  330. this.setTileVisible( tile, true );
  331. }
  332. if ( tile.__wasSetActive ) {
  333. this.setTileActive( tile, true );
  334. }
  335. } )
  336. .catch( errorCallback );
  337. }
  338. }
  339. dispose() {
  340. const lruCache = this.lruCache;
  341. this.traverse( tile => {
  342. lruCache.remove( tile );
  343. } );
  344. }
  345. }