ModelVisualizer.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import AssociativeArray from "../Core/AssociativeArray.js";
  2. import BoundingSphere from "../Core/BoundingSphere.js";
  3. import Cartesian2 from "../Core/Cartesian2.js";
  4. import Cartesian3 from "../Core/Cartesian3.js";
  5. import Check from "../Core/Check.js";
  6. import Color from "../Core/Color.js";
  7. import defined from "../Core/defined.js";
  8. import destroyObject from "../Core/destroyObject.js";
  9. import DeveloperError from "../Core/DeveloperError.js";
  10. import Matrix4 from "../Core/Matrix4.js";
  11. import Resource from "../Core/Resource.js";
  12. import ColorBlendMode from "../Scene/ColorBlendMode.js";
  13. import HeightReference from "../Scene/HeightReference.js";
  14. import Model from "../Scene/Model/Model.js";
  15. import ModelAnimationLoop from "../Scene/ModelAnimationLoop.js";
  16. import ShadowMode from "../Scene/ShadowMode.js";
  17. import BoundingSphereState from "./BoundingSphereState.js";
  18. import Property from "./Property.js";
  19. import sampleTerrainMostDetailed from "../Core/sampleTerrainMostDetailed.js";
  20. import Cartographic from "../Core/Cartographic.js";
  21. const defaultScale = 1.0;
  22. const defaultMinimumPixelSize = 0.0;
  23. const defaultIncrementallyLoadTextures = true;
  24. const defaultClampAnimations = true;
  25. const defaultShadows = ShadowMode.ENABLED;
  26. const defaultHeightReference = HeightReference.NONE;
  27. const defaultSilhouetteColor = Color.RED;
  28. const defaultSilhouetteSize = 0.0;
  29. const defaultColor = Color.WHITE;
  30. const defaultColorBlendMode = ColorBlendMode.HIGHLIGHT;
  31. const defaultColorBlendAmount = 0.5;
  32. const defaultImageBasedLightingFactor = new Cartesian2(1.0, 1.0);
  33. const modelMatrixScratch = new Matrix4();
  34. const nodeMatrixScratch = new Matrix4();
  35. const scratchColor = new Color();
  36. const scratchArray = new Array(4);
  37. const scratchCartesian = new Cartesian3();
  38. /**
  39. * A {@link Visualizer} which maps {@link Entity#model} to a {@link Model}.
  40. * @alias ModelVisualizer
  41. * @constructor
  42. *
  43. * @param {Scene} scene The scene the primitives will be rendered in.
  44. * @param {EntityCollection} entityCollection The entityCollection to visualize.
  45. */
  46. function ModelVisualizer(scene, entityCollection) {
  47. //>>includeStart('debug', pragmas.debug);
  48. Check.typeOf.object("scene", scene);
  49. Check.typeOf.object("entityCollection", entityCollection);
  50. //>>includeEnd('debug');
  51. entityCollection.collectionChanged.addEventListener(
  52. ModelVisualizer.prototype._onCollectionChanged,
  53. this
  54. );
  55. this._scene = scene;
  56. this._primitives = scene.primitives;
  57. this._entityCollection = entityCollection;
  58. this._modelHash = {};
  59. this._entitiesToVisualize = new AssociativeArray();
  60. this._onCollectionChanged(entityCollection, entityCollection.values, [], []);
  61. }
  62. async function createModelPrimitive(
  63. visualizer,
  64. entity,
  65. resource,
  66. incrementallyLoadTextures
  67. ) {
  68. const primitives = visualizer._primitives;
  69. const modelHash = visualizer._modelHash;
  70. try {
  71. const model = await Model.fromGltfAsync({
  72. url: resource,
  73. incrementallyLoadTextures: incrementallyLoadTextures,
  74. scene: visualizer._scene,
  75. });
  76. if (visualizer.isDestroyed() || !defined(modelHash[entity.id])) {
  77. return;
  78. }
  79. model.id = entity;
  80. primitives.add(model);
  81. modelHash[entity.id].modelPrimitive = model;
  82. model.errorEvent.addEventListener((error) => {
  83. if (!defined(modelHash[entity.id])) {
  84. return;
  85. }
  86. console.log(error);
  87. // Texture failures when incrementallyLoadTextures
  88. // will not affect the ability to compute the bounding sphere
  89. if (error.name !== "TextureError" && model.incrementallyLoadTextures) {
  90. modelHash[entity.id].loadFailed = true;
  91. }
  92. });
  93. } catch (error) {
  94. if (visualizer.isDestroyed() || !defined(modelHash[entity.id])) {
  95. return;
  96. }
  97. console.log(error);
  98. modelHash[entity.id].loadFailed = true;
  99. }
  100. }
  101. /**
  102. * Updates models created this visualizer to match their
  103. * Entity counterpart at the given time.
  104. *
  105. * @param {JulianDate} time The time to update to.
  106. * @returns {boolean} This function always returns true.
  107. */
  108. ModelVisualizer.prototype.update = function (time) {
  109. //>>includeStart('debug', pragmas.debug);
  110. if (!defined(time)) {
  111. throw new DeveloperError("time is required.");
  112. }
  113. //>>includeEnd('debug');
  114. const entities = this._entitiesToVisualize.values;
  115. const modelHash = this._modelHash;
  116. const primitives = this._primitives;
  117. for (let i = 0, len = entities.length; i < len; i++) {
  118. const entity = entities[i];
  119. const modelGraphics = entity._model;
  120. let resource;
  121. let modelData = modelHash[entity.id];
  122. let show =
  123. entity.isShowing &&
  124. entity.isAvailable(time) &&
  125. Property.getValueOrDefault(modelGraphics._show, time, true);
  126. let modelMatrix;
  127. if (show) {
  128. modelMatrix = entity.computeModelMatrix(time, modelMatrixScratch);
  129. resource = Resource.createIfNeeded(
  130. Property.getValueOrUndefined(modelGraphics._uri, time)
  131. );
  132. show = defined(modelMatrix) && defined(resource);
  133. }
  134. if (!show) {
  135. if (defined(modelData) && modelData.modelPrimitive) {
  136. modelData.modelPrimitive.show = false;
  137. }
  138. continue;
  139. }
  140. if (!defined(modelData) || resource.url !== modelData.url) {
  141. if (defined(modelData?.modelPrimitive)) {
  142. primitives.removeAndDestroy(modelData.modelPrimitive);
  143. delete modelHash[entity.id];
  144. }
  145. modelData = {
  146. modelPrimitive: undefined,
  147. url: resource.url,
  148. animationsRunning: false,
  149. nodeTransformationsScratch: {},
  150. articulationsScratch: {},
  151. loadFailed: false,
  152. modelUpdated: false,
  153. awaitingSampleTerrain: false,
  154. clampedBoundingSphere: undefined,
  155. sampleTerrainFailed: false,
  156. };
  157. modelHash[entity.id] = modelData;
  158. const incrementallyLoadTextures = Property.getValueOrDefault(
  159. modelGraphics._incrementallyLoadTextures,
  160. time,
  161. defaultIncrementallyLoadTextures
  162. );
  163. createModelPrimitive(this, entity, resource, incrementallyLoadTextures);
  164. }
  165. const model = modelData.modelPrimitive;
  166. if (!defined(model)) {
  167. continue;
  168. }
  169. model.show = true;
  170. model.scale = Property.getValueOrDefault(
  171. modelGraphics._scale,
  172. time,
  173. defaultScale
  174. );
  175. model.minimumPixelSize = Property.getValueOrDefault(
  176. modelGraphics._minimumPixelSize,
  177. time,
  178. defaultMinimumPixelSize
  179. );
  180. model.maximumScale = Property.getValueOrUndefined(
  181. modelGraphics._maximumScale,
  182. time
  183. );
  184. model.modelMatrix = Matrix4.clone(modelMatrix, model.modelMatrix);
  185. model.shadows = Property.getValueOrDefault(
  186. modelGraphics._shadows,
  187. time,
  188. defaultShadows
  189. );
  190. model.heightReference = Property.getValueOrDefault(
  191. modelGraphics._heightReference,
  192. time,
  193. defaultHeightReference
  194. );
  195. model.distanceDisplayCondition = Property.getValueOrUndefined(
  196. modelGraphics._distanceDisplayCondition,
  197. time
  198. );
  199. model.silhouetteColor = Property.getValueOrDefault(
  200. modelGraphics._silhouetteColor,
  201. time,
  202. defaultSilhouetteColor,
  203. scratchColor
  204. );
  205. model.silhouetteSize = Property.getValueOrDefault(
  206. modelGraphics._silhouetteSize,
  207. time,
  208. defaultSilhouetteSize
  209. );
  210. model.color = Property.getValueOrDefault(
  211. modelGraphics._color,
  212. time,
  213. defaultColor,
  214. scratchColor
  215. );
  216. model.colorBlendMode = Property.getValueOrDefault(
  217. modelGraphics._colorBlendMode,
  218. time,
  219. defaultColorBlendMode
  220. );
  221. model.colorBlendAmount = Property.getValueOrDefault(
  222. modelGraphics._colorBlendAmount,
  223. time,
  224. defaultColorBlendAmount
  225. );
  226. model.clippingPlanes = Property.getValueOrUndefined(
  227. modelGraphics._clippingPlanes,
  228. time
  229. );
  230. model.clampAnimations = Property.getValueOrDefault(
  231. modelGraphics._clampAnimations,
  232. time,
  233. defaultClampAnimations
  234. );
  235. model.imageBasedLighting.imageBasedLightingFactor = Property.getValueOrDefault(
  236. modelGraphics._imageBasedLightingFactor,
  237. time,
  238. defaultImageBasedLightingFactor
  239. );
  240. let lightColor = Property.getValueOrUndefined(
  241. modelGraphics._lightColor,
  242. time
  243. );
  244. // Convert from Color to Cartesian3
  245. if (defined(lightColor)) {
  246. Color.pack(lightColor, scratchArray, 0);
  247. lightColor = Cartesian3.unpack(scratchArray, 0, scratchCartesian);
  248. }
  249. model.lightColor = lightColor;
  250. model.customShader = Property.getValueOrUndefined(
  251. modelGraphics._customShader,
  252. time
  253. );
  254. // It's possible for getBoundingSphere to run before
  255. // model becomes ready and these properties are updated
  256. modelHash[entity.id].modelUpdated = true;
  257. if (model.ready) {
  258. const runAnimations = Property.getValueOrDefault(
  259. modelGraphics._runAnimations,
  260. time,
  261. true
  262. );
  263. if (modelData.animationsRunning !== runAnimations) {
  264. if (runAnimations) {
  265. model.activeAnimations.addAll({
  266. loop: ModelAnimationLoop.REPEAT,
  267. });
  268. } else {
  269. model.activeAnimations.removeAll();
  270. }
  271. modelData.animationsRunning = runAnimations;
  272. }
  273. // Apply node transformations
  274. const nodeTransformations = Property.getValueOrUndefined(
  275. modelGraphics._nodeTransformations,
  276. time,
  277. modelData.nodeTransformationsScratch
  278. );
  279. if (defined(nodeTransformations)) {
  280. const nodeNames = Object.keys(nodeTransformations);
  281. for (
  282. let nodeIndex = 0, nodeLength = nodeNames.length;
  283. nodeIndex < nodeLength;
  284. ++nodeIndex
  285. ) {
  286. const nodeName = nodeNames[nodeIndex];
  287. const nodeTransformation = nodeTransformations[nodeName];
  288. if (!defined(nodeTransformation)) {
  289. continue;
  290. }
  291. const modelNode = model.getNode(nodeName);
  292. if (!defined(modelNode)) {
  293. continue;
  294. }
  295. const transformationMatrix = Matrix4.fromTranslationRotationScale(
  296. nodeTransformation,
  297. nodeMatrixScratch
  298. );
  299. modelNode.matrix = Matrix4.multiply(
  300. modelNode.originalMatrix,
  301. transformationMatrix,
  302. transformationMatrix
  303. );
  304. }
  305. }
  306. // Apply articulations
  307. let anyArticulationUpdated = false;
  308. const articulations = Property.getValueOrUndefined(
  309. modelGraphics._articulations,
  310. time,
  311. modelData.articulationsScratch
  312. );
  313. if (defined(articulations)) {
  314. const articulationStageKeys = Object.keys(articulations);
  315. for (
  316. let s = 0, numKeys = articulationStageKeys.length;
  317. s < numKeys;
  318. ++s
  319. ) {
  320. const key = articulationStageKeys[s];
  321. const articulationStageValue = articulations[key];
  322. if (!defined(articulationStageValue)) {
  323. continue;
  324. }
  325. anyArticulationUpdated = true;
  326. model.setArticulationStage(key, articulationStageValue);
  327. }
  328. }
  329. if (anyArticulationUpdated) {
  330. model.applyArticulations();
  331. }
  332. }
  333. }
  334. return true;
  335. };
  336. /**
  337. * Returns true if this object was destroyed; otherwise, false.
  338. *
  339. * @returns {boolean} True if this object was destroyed; otherwise, false.
  340. */
  341. ModelVisualizer.prototype.isDestroyed = function () {
  342. return false;
  343. };
  344. /**
  345. * Removes and destroys all primitives created by this instance.
  346. */
  347. ModelVisualizer.prototype.destroy = function () {
  348. this._entityCollection.collectionChanged.removeEventListener(
  349. ModelVisualizer.prototype._onCollectionChanged,
  350. this
  351. );
  352. const entities = this._entitiesToVisualize.values;
  353. const modelHash = this._modelHash;
  354. const primitives = this._primitives;
  355. for (let i = entities.length - 1; i > -1; i--) {
  356. removeModel(this, entities[i], modelHash, primitives);
  357. }
  358. return destroyObject(this);
  359. };
  360. // Used for testing.
  361. ModelVisualizer._sampleTerrainMostDetailed = sampleTerrainMostDetailed;
  362. const scratchPosition = new Cartesian3();
  363. const scratchCartographic = new Cartographic();
  364. /**
  365. * Computes a bounding sphere which encloses the visualization produced for the specified entity.
  366. * The bounding sphere is in the fixed frame of the scene's globe.
  367. *
  368. * @param {Entity} entity The entity whose bounding sphere to compute.
  369. * @param {BoundingSphere} result The bounding sphere onto which to store the result.
  370. * @returns {BoundingSphereState} BoundingSphereState.DONE if the result contains the bounding sphere,
  371. * BoundingSphereState.PENDING if the result is still being computed, or
  372. * BoundingSphereState.FAILED if the entity has no visualization in the current scene.
  373. * @private
  374. */
  375. ModelVisualizer.prototype.getBoundingSphere = function (entity, result) {
  376. //>>includeStart('debug', pragmas.debug);
  377. if (!defined(entity)) {
  378. throw new DeveloperError("entity is required.");
  379. }
  380. if (!defined(result)) {
  381. throw new DeveloperError("result is required.");
  382. }
  383. //>>includeEnd('debug');
  384. const modelData = this._modelHash[entity.id];
  385. if (!defined(modelData)) {
  386. return BoundingSphereState.FAILED;
  387. }
  388. if (modelData.loadFailed) {
  389. return BoundingSphereState.FAILED;
  390. }
  391. const model = modelData.modelPrimitive;
  392. if (!defined(model) || !model.show) {
  393. return BoundingSphereState.PENDING;
  394. }
  395. if (!model.ready || !modelData.modelUpdated) {
  396. return BoundingSphereState.PENDING;
  397. }
  398. const scene = this._scene;
  399. const globe = scene.globe;
  400. // cannot access a terrain provider if there is no globe; formally set to undefined
  401. const terrainProvider = defined(globe) ? globe.terrainProvider : undefined;
  402. const hasHeightReference = model.heightReference !== HeightReference.NONE;
  403. if (defined(globe) && hasHeightReference) {
  404. const ellipsoid = globe.ellipsoid;
  405. // We cannot query the availability of the terrain provider till its ready, so the
  406. // bounding sphere state will remain pending till the terrain provider is ready.
  407. // ready is deprecated. This is here for backwards compatibility
  408. if (!terrainProvider._ready) {
  409. return BoundingSphereState.PENDING;
  410. }
  411. const modelMatrix = model.modelMatrix;
  412. scratchPosition.x = modelMatrix[12];
  413. scratchPosition.y = modelMatrix[13];
  414. scratchPosition.z = modelMatrix[14];
  415. const cartoPosition = ellipsoid.cartesianToCartographic(scratchPosition);
  416. // For a terrain provider that does not have availability, like the EllipsoidTerrainProvider,
  417. // we can directly assign the bounding sphere's center from model matrix's translation.
  418. if (!defined(terrainProvider.availability)) {
  419. // Regardless of what the original model's position is set to, for CLAMP_TO_GROUND, we reset it to 0
  420. // when computing the position to zoom/fly to.
  421. if (model.heightReference === HeightReference.CLAMP_TO_GROUND) {
  422. cartoPosition.height = 0;
  423. }
  424. const scratchPosition = ellipsoid.cartographicToCartesian(cartoPosition);
  425. BoundingSphere.clone(model.boundingSphere, result);
  426. result.center = scratchPosition;
  427. return BoundingSphereState.DONE;
  428. }
  429. // Otherwise, in the case of terrain providers with availability,
  430. // since the model's bounding sphere may be clamped to a lower LOD tile if
  431. // the camera is initially far away, we use sampleTerrainMostDetailed to estimate
  432. // where the bounding sphere should be and set that as the target bounding sphere
  433. // for the camera.
  434. let clampedBoundingSphere = this._modelHash[entity.id]
  435. .clampedBoundingSphere;
  436. // Check if the sample terrain function has failed.
  437. const sampleTerrainFailed = this._modelHash[entity.id].sampleTerrainFailed;
  438. if (sampleTerrainFailed) {
  439. this._modelHash[entity.id].sampleTerrainFailed = false;
  440. return BoundingSphereState.FAILED;
  441. }
  442. if (!defined(clampedBoundingSphere)) {
  443. clampedBoundingSphere = new BoundingSphere();
  444. // Since this function is called per-frame, we set a flag when sampleTerrainMostDetailed
  445. // is called and check for it to avoid calling it again.
  446. const awaitingSampleTerrain = this._modelHash[entity.id]
  447. .awaitingSampleTerrain;
  448. if (!awaitingSampleTerrain) {
  449. Cartographic.clone(cartoPosition, scratchCartographic);
  450. this._modelHash[entity.id].awaitingSampleTerrain = true;
  451. ModelVisualizer._sampleTerrainMostDetailed(terrainProvider, [
  452. scratchCartographic,
  453. ])
  454. .then((result) => {
  455. if (this.isDestroyed()) {
  456. return;
  457. }
  458. this._modelHash[entity.id].awaitingSampleTerrain = false;
  459. const updatedCartographic = result[0];
  460. if (model.heightReference === HeightReference.RELATIVE_TO_GROUND) {
  461. updatedCartographic.height += cartoPosition.height;
  462. }
  463. ellipsoid.cartographicToCartesian(
  464. updatedCartographic,
  465. scratchPosition
  466. );
  467. // Update the bounding sphere with the updated position.
  468. BoundingSphere.clone(model.boundingSphere, clampedBoundingSphere);
  469. clampedBoundingSphere.center = scratchPosition;
  470. this._modelHash[
  471. entity.id
  472. ].clampedBoundingSphere = BoundingSphere.clone(
  473. clampedBoundingSphere
  474. );
  475. })
  476. .catch((e) => {
  477. if (this.isDestroyed()) {
  478. return;
  479. }
  480. this._modelHash[entity.id].sampleTerrainFailed = true;
  481. this._modelHash[entity.id].awaitingSampleTerrain = false;
  482. });
  483. }
  484. // We will return the state as pending until the clamped bounding sphere is defined,
  485. // which happens when the sampleTerrainMostDetailed promise returns.
  486. return BoundingSphereState.PENDING;
  487. }
  488. BoundingSphere.clone(clampedBoundingSphere, result);
  489. // Reset the clamped bounding sphere.
  490. this._modelHash[entity.id].clampedBoundingSphere = undefined;
  491. return BoundingSphereState.DONE;
  492. }
  493. BoundingSphere.clone(model.boundingSphere, result);
  494. return BoundingSphereState.DONE;
  495. };
  496. /**
  497. * @private
  498. */
  499. ModelVisualizer.prototype._onCollectionChanged = function (
  500. entityCollection,
  501. added,
  502. removed,
  503. changed
  504. ) {
  505. let i;
  506. let entity;
  507. const entities = this._entitiesToVisualize;
  508. const modelHash = this._modelHash;
  509. const primitives = this._primitives;
  510. for (i = added.length - 1; i > -1; i--) {
  511. entity = added[i];
  512. if (defined(entity._model) && defined(entity._position)) {
  513. entities.set(entity.id, entity);
  514. }
  515. }
  516. for (i = changed.length - 1; i > -1; i--) {
  517. entity = changed[i];
  518. if (defined(entity._model) && defined(entity._position)) {
  519. clearNodeTransformationsArticulationsScratch(entity, modelHash);
  520. entities.set(entity.id, entity);
  521. } else {
  522. removeModel(this, entity, modelHash, primitives);
  523. entities.remove(entity.id);
  524. }
  525. }
  526. for (i = removed.length - 1; i > -1; i--) {
  527. entity = removed[i];
  528. removeModel(this, entity, modelHash, primitives);
  529. entities.remove(entity.id);
  530. }
  531. };
  532. function removeModel(visualizer, entity, modelHash, primitives) {
  533. const modelData = modelHash[entity.id];
  534. if (defined(modelData)) {
  535. primitives.removeAndDestroy(modelData.modelPrimitive);
  536. delete modelHash[entity.id];
  537. }
  538. }
  539. function clearNodeTransformationsArticulationsScratch(entity, modelHash) {
  540. const modelData = modelHash[entity.id];
  541. if (defined(modelData)) {
  542. modelData.nodeTransformationsScratch = {};
  543. modelData.articulationsScratch = {};
  544. }
  545. }
  546. export default ModelVisualizer;