From f3097718606c392d76754922bad1bab76ca54681 Mon Sep 17 00:00:00 2001
From: Gabby Getz
Date: Wed, 8 Mar 2023 15:58:23 -0500
Subject: [PATCH 01/18] Model readyPromise deprecation and minor loader
refactor
---
Specs/loaderProcess.js | 3 +-
Specs/waitForLoaderProcess.js | 24 +-
.../Source/DataSources/ModelVisualizer.js | 79 +-
packages/engine/Source/Scene/BufferLoader.js | 61 +-
.../Scene/Cesium3DTileContentFactory.js | 2 +
.../Scene/Cesium3DTilesVoxelProvider.js | 7 +-
.../engine/Source/Scene/Cesium3DTileset.js | 6 +-
.../Source/Scene/GltfBufferViewLoader.js | 127 +-
.../engine/Source/Scene/GltfDracoLoader.js | 205 +-
.../engine/Source/Scene/GltfImageLoader.js | 144 +-
.../Source/Scene/GltfIndexBufferLoader.js | 223 +-
.../engine/Source/Scene/GltfJsonLoader.js | 145 +-
packages/engine/Source/Scene/GltfLoader.js | 586 ++--
.../Scene/GltfStructuralMetadataLoader.js | 203 +-
.../engine/Source/Scene/GltfTextureLoader.js | 174 +-
.../Source/Scene/GltfVertexBufferLoader.js | 256 +-
.../engine/Source/Scene/ImplicitSubtree.js | 4 +-
.../Source/Scene/MetadataSchemaLoader.js | 62 +-
packages/engine/Source/Scene/Model/Model.js | 528 +++-
.../Source/Scene/Model/Model3DTileContent.js | 25 +-
.../Scene/Model/ModelAnimationCollection.js | 4 +-
.../engine/Source/Scene/Model/ModelUtility.js | 32 +-
packages/engine/Source/Scene/ResourceCache.js | 152 +-
.../Source/Scene/ResourceCacheStatistics.js | 83 +-
.../engine/Source/Scene/ResourceLoader.js | 22 +-
.../Source/Scene/ResourceLoaderState.js | 16 +-
.../Specs/DataSources/ModelVisualizerSpec.js | 203 +-
.../engine/Specs/Scene/BufferLoaderSpec.js | 100 +-
.../Scene/GlobeSurfaceTileProviderSpec.js | 4 +-
.../Specs/Scene/GltfBufferViewLoaderSpec.js | 143 +-
.../engine/Specs/Scene/GltfDracoLoaderSpec.js | 129 +-
.../engine/Specs/Scene/GltfImageLoaderSpec.js | 271 +-
.../Specs/Scene/GltfIndexBufferLoaderSpec.js | 260 +-
.../engine/Specs/Scene/GltfJsonLoaderSpec.js | 216 +-
packages/engine/Specs/Scene/GltfLoaderSpec.js | 264 +-
.../Scene/GltfStructuralMetadataLoaderSpec.js | 274 +-
.../Specs/Scene/GltfTextureLoaderSpec.js | 216 +-
.../Specs/Scene/GltfVertexBufferLoaderSpec.js | 302 +-
.../Specs/Scene/MetadataSchemaLoaderSpec.js | 95 +-
.../Model/ModelAnimationCollectionSpec.js | 80 +-
.../Scene/Model/ModelMatrixUpdateStageSpec.js | 12 +-
.../Specs/Scene/Model/ModelSceneGraphSpec.js | 102 +-
.../engine/Specs/Scene/Model/ModelSpec.js | 714 +++--
.../Specs/Scene/Model/loadAndZoomToModel.js | 86 -
.../Scene/Model/loadAndZoomToModelAsync.js | 67 +
.../Scene/PostProcessStageLibrarySpec.js | 8 +-
.../Specs/Scene/PostProcessStageSpec.js | 4 +-
.../engine/Specs/Scene/ResourceCacheSpec.js | 2574 +++++++----------
packages/engine/Specs/Scene/ShadowMapSpec.js | 11 +-
49 files changed, 4372 insertions(+), 4936 deletions(-)
delete mode 100644 packages/engine/Specs/Scene/Model/loadAndZoomToModel.js
create mode 100644 packages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js
diff --git a/Specs/loaderProcess.js b/Specs/loaderProcess.js
index d91ca294d4bc..5953cb7dcf7c 100644
--- a/Specs/loaderProcess.js
+++ b/Specs/loaderProcess.js
@@ -4,8 +4,9 @@ function loaderProcess(loader, scene) {
// explicitly. This is only required for loaders that use the job scheduler
// like GltfVertexBufferLoader, GltfIndexBufferLoader, and GltfTextureLoader
scene.jobScheduler.resetBudgets();
- loader.process(scene.frameState);
+ const ready = loader.process(scene.frameState);
scene.jobScheduler.resetBudgets();
+ return ready;
}
export default loaderProcess;
diff --git a/Specs/waitForLoaderProcess.js b/Specs/waitForLoaderProcess.js
index 737fa7c6771a..d46cc9ed3c09 100644
--- a/Specs/waitForLoaderProcess.js
+++ b/Specs/waitForLoaderProcess.js
@@ -2,26 +2,12 @@ import loaderProcess from "./loaderProcess.js";
import pollToPromise from "./pollToPromise.js";
function waitForLoaderProcess(loader, scene) {
- return new Promise(function (resolve, reject) {
- let loaderFinished = false;
+ return pollToPromise(function () {
+ if (loader.isDestroyed()) {
+ return true;
+ }
- pollToPromise(function () {
- loaderProcess(loader, scene);
- return loaderFinished;
- }).catch(function (e) {
- reject(e);
- });
-
- loader.promise
- .then(function (result) {
- resolve(result);
- })
- .catch(function (e) {
- reject(e);
- })
- .finally(function () {
- loaderFinished = true;
- });
+ return loaderProcess(loader, scene);
});
}
diff --git a/packages/engine/Source/DataSources/ModelVisualizer.js b/packages/engine/Source/DataSources/ModelVisualizer.js
index b86c061ffc55..9c2a49b1d291 100644
--- a/packages/engine/Source/DataSources/ModelVisualizer.js
+++ b/packages/engine/Source/DataSources/ModelVisualizer.js
@@ -102,45 +102,72 @@ ModelVisualizer.prototype.update = function (time) {
}
if (!show) {
- if (defined(modelData)) {
+ if (defined(modelData) && modelData.modelPrimitive) {
modelData.modelPrimitive.show = false;
}
continue;
}
- let model = defined(modelData) ? modelData.modelPrimitive : undefined;
- if (!defined(model) || resource.url !== modelData.url) {
- if (defined(model)) {
- primitives.removeAndDestroy(model);
+ if (!defined(modelData) || resource.url !== modelData.url) {
+ if (defined(modelData?.modelPrimitive)) {
+ primitives.removeAndDestroy(modelData.modelPrimitive);
delete modelHash[entity.id];
}
- model = Model.fromGltf({
- url: resource,
- incrementallyLoadTextures: Property.getValueOrDefault(
- modelGraphics._incrementallyLoadTextures,
- time,
- defaultIncrementallyLoadTextures
- ),
- scene: this._scene,
- });
- model.id = entity;
- primitives.add(model);
-
modelData = {
- modelPrimitive: model,
+ modelPrimitive: undefined,
url: resource.url,
animationsRunning: false,
nodeTransformationsScratch: {},
articulationsScratch: {},
- loadFail: false,
+ loadFailed: false,
awaitingSampleTerrain: false,
clampedBoundingSphere: undefined,
sampleTerrainFailed: false,
};
modelHash[entity.id] = modelData;
- checkModelLoad(model, entity, modelHash);
+ (async () => {
+ try {
+ const model = await Model.fromGltfAsync({
+ url: resource,
+ incrementallyLoadTextures: Property.getValueOrDefault(
+ modelGraphics._incrementallyLoadTextures,
+ time,
+ defaultIncrementallyLoadTextures
+ ),
+ scene: this._scene,
+ });
+ model.id = entity;
+ primitives.add(model);
+ modelHash[entity.id].modelPrimitive = model;
+ model.errorEvent.addEventListener((error) => {
+ if (!defined(modelHash[entity.id])) {
+ return;
+ }
+
+ console.error(error);
+
+ // Texture failures when incrementallyLoadTextures
+ // will not affect the ability to compute the bounding sphere
+ if (error.name !== "TextureError") {
+ modelHash[entity.id].loadFailed = true;
+ }
+ });
+ } catch (error) {
+ if (!defined(modelHash[entity.id])) {
+ return;
+ }
+
+ console.error(error);
+ modelHash[entity.id].loadFailed = true;
+ }
+ })();
+ }
+
+ const model = modelData.modelPrimitive;
+ if (!defined(model)) {
+ continue;
}
model.show = true;
@@ -365,7 +392,11 @@ ModelVisualizer.prototype.getBoundingSphere = function (entity, result) {
//>>includeEnd('debug');
const modelData = this._modelHash[entity.id];
- if (!defined(modelData) || modelData.loadFail) {
+ if (!defined(modelData)) {
+ return BoundingSphereState.PENDING;
+ }
+
+ if (modelData.loadFailed) {
return BoundingSphereState.FAILED;
}
@@ -539,10 +570,4 @@ function clearNodeTransformationsArticulationsScratch(entity, modelHash) {
}
}
-function checkModelLoad(model, entity, modelHash) {
- model.readyPromise.catch(function (error) {
- console.error(error);
- modelHash[entity.id].loadFail = true;
- });
-}
export default ModelVisualizer;
diff --git a/packages/engine/Source/Scene/BufferLoader.js b/packages/engine/Source/Scene/BufferLoader.js
index f04cd75ad194..2246f485ac63 100644
--- a/packages/engine/Source/Scene/BufferLoader.js
+++ b/packages/engine/Source/Scene/BufferLoader.js
@@ -50,19 +50,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(BufferLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof BufferLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -96,35 +83,41 @@ Object.defineProperties(BufferLoader.prototype, {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-BufferLoader.prototype.load = function () {
+BufferLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
if (defined(this._typedArray)) {
this._promise = Promise.resolve(this);
- } else {
- this._promise = loadExternalBuffer(this);
+ return this._promise;
}
+
+ this._promise = loadExternalBuffer(this);
return this._promise;
};
-function loadExternalBuffer(bufferLoader) {
+async function loadExternalBuffer(bufferLoader) {
const resource = bufferLoader._resource;
bufferLoader._state = ResourceLoaderState.LOADING;
- return BufferLoader._fetchArrayBuffer(resource)
- .then(function (arrayBuffer) {
- if (bufferLoader.isDestroyed()) {
- return;
- }
- bufferLoader._typedArray = new Uint8Array(arrayBuffer);
- bufferLoader._state = ResourceLoaderState.READY;
- return bufferLoader;
- })
- .catch(function (error) {
- if (bufferLoader.isDestroyed()) {
- return;
- }
- bufferLoader._state = ResourceLoaderState.FAILED;
- const errorMessage = `Failed to load external buffer: ${resource.url}`;
- return Promise.reject(bufferLoader.getError(errorMessage, error));
- });
+ try {
+ const arrayBuffer = await BufferLoader._fetchArrayBuffer(resource);
+ if (bufferLoader.isDestroyed()) {
+ return;
+ }
+
+ bufferLoader._typedArray = new Uint8Array(arrayBuffer);
+ bufferLoader._state = ResourceLoaderState.READY;
+ return bufferLoader;
+ } catch (error) {
+ if (bufferLoader.isDestroyed()) {
+ return;
+ }
+
+ bufferLoader._state = ResourceLoaderState.FAILED;
+ const errorMessage = `Failed to load external buffer: ${resource.url}`;
+ throw bufferLoader.getError(errorMessage, error);
+ }
}
/**
diff --git a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
index 9c8dd3e6230f..23c6b39e8928 100644
--- a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
+++ b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
@@ -92,9 +92,11 @@ const Cesium3DTileContentFactory = {
const dataView = new DataView(arrayBuffer, byteOffset);
const byteLength = dataView.getUint32(8, true);
const glb = new Uint8Array(arrayBuffer, byteOffset, byteLength);
+ // This should be replace with fromGltfAsync when readyPromise is deprecated across 3D Tiles functions
return Model3DTileContent.fromGltf(tileset, tile, resource, glb);
},
gltf: function (tileset, tile, resource, json) {
+ // This should be replace with fromGltfAsync when readyPromise is deprecated across 3D Tiles functions
return Model3DTileContent.fromGltf(tileset, tile, resource, json);
},
geoJson: function (tileset, tile, resource, json) {
diff --git a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
index 90477276db8d..348462c03d36 100644
--- a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
+++ b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
@@ -106,7 +106,8 @@ function Cesium3DTilesVoxelProvider(options) {
.then(function (tileset) {
tilesetJson = tileset;
validate(tilesetJson);
- return getMetadataSchemaLoader(tilesetJson, resource).promise;
+ const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
+ return schemaLoader.load();
})
.then(function (schemaLoader) {
const root = tilesetJson.root;
@@ -306,9 +307,9 @@ function getCylinderShape(cylinder, tileTransform) {
function getMetadataSchemaLoader(tilesetJson, resource) {
const { schemaUri, schema } = tilesetJson;
if (!defined(schemaUri)) {
- return ResourceCache.loadSchema({ schema });
+ return ResourceCache.getSchemaLoader({ schema });
}
- return ResourceCache.loadSchema({
+ return ResourceCache.getSchemaLoader({
resource: resource.getDerivedResource({
url: schemaUri,
}),
diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js
index d3f8209d05c8..977464a25062 100644
--- a/packages/engine/Source/Scene/Cesium3DTileset.js
+++ b/packages/engine/Source/Scene/Cesium3DTileset.js
@@ -2151,11 +2151,11 @@ function processMetadataExtension(tileset, tilesetJson) {
const resource = tileset._resource.getDerivedResource({
url: metadataJson.schemaUri,
});
- schemaLoader = ResourceCache.loadSchema({
+ schemaLoader = ResourceCache.getSchemaLoader({
resource: resource,
});
} else if (defined(metadataJson.schema)) {
- schemaLoader = ResourceCache.loadSchema({
+ schemaLoader = ResourceCache.getSchemaLoader({
schema: metadataJson.schema,
});
} else {
@@ -2164,7 +2164,7 @@ function processMetadataExtension(tileset, tilesetJson) {
tileset._schemaLoader = schemaLoader;
- return schemaLoader.promise.then(function (schemaLoader) {
+ return schemaLoader.load().then(function (schemaLoader) {
tileset._metadataExtension = new Cesium3DTilesetMetadata({
schema: schemaLoader.schema,
metadataJson: metadataJson,
diff --git a/packages/engine/Source/Scene/GltfBufferViewLoader.js b/packages/engine/Source/Scene/GltfBufferViewLoader.js
index e43b2ace1b8b..717ba0b09fab 100644
--- a/packages/engine/Source/Scene/GltfBufferViewLoader.js
+++ b/packages/engine/Source/Scene/GltfBufferViewLoader.js
@@ -87,7 +87,6 @@ function GltfBufferViewLoader(options) {
this._typedArray = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
- this._process = function (loader, frameState) {};
}
if (defined(Object.create)) {
@@ -96,20 +95,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfBufferViewLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfBufferViewLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -145,77 +130,61 @@ Object.defineProperties(GltfBufferViewLoader.prototype, {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfBufferViewLoader.prototype.load = function () {
- const bufferLoader = getBufferLoader(this);
- this._bufferLoader = bufferLoader;
- this._state = ResourceLoaderState.LOADING;
-
- const that = this;
- const bufferViewPromise = new Promise(function (resolve) {
- that._process = function (loader, frameState) {
- if (!loader._hasMeshopt) {
- return;
- }
+GltfBufferViewLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
- if (!defined(loader._typedArray)) {
- return;
- }
+ this._promise = (async () => {
+ this._state = ResourceLoaderState.LOADING;
+ try {
+ const bufferLoader = getBufferLoader(this);
+ this._bufferLoader = bufferLoader;
+ await bufferLoader.load();
- if (loader._state !== ResourceLoaderState.PROCESSING) {
+ if (this.isDestroyed()) {
return;
}
- const count = loader._meshoptCount;
- const byteStride = loader._meshoptByteStride;
- const result = new Uint8Array(count * byteStride);
- MeshoptDecoder.decodeGltfBuffer(
- result,
- count,
- byteStride,
- loader._typedArray,
- loader._meshoptMode,
- loader._meshoptFilter
- );
-
- loader._typedArray = result;
- loader._state = ResourceLoaderState.READY;
- resolve(loader);
- };
- });
-
- this._promise = bufferLoader.promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
const bufferTypedArray = bufferLoader.typedArray;
const bufferViewTypedArray = new Uint8Array(
bufferTypedArray.buffer,
- bufferTypedArray.byteOffset + that._byteOffset,
- that._byteLength
+ bufferTypedArray.byteOffset + this._byteOffset,
+ this._byteLength
);
// Unload the buffer
- that.unload();
-
- that._typedArray = bufferViewTypedArray;
- if (that._hasMeshopt) {
- that._state = ResourceLoaderState.PROCESSING;
- return bufferViewPromise;
+ this.unload();
+
+ this._typedArray = bufferViewTypedArray;
+ if (this._hasMeshopt) {
+ const count = this._meshoptCount;
+ const byteStride = this._meshoptByteStride;
+ const result = new Uint8Array(count * byteStride);
+ MeshoptDecoder.decodeGltfBuffer(
+ result,
+ count,
+ byteStride,
+ this._typedArray,
+ this._meshoptMode,
+ this._meshoptFilter
+ );
+ this._typedArray = result;
}
- that._state = ResourceLoaderState.READY;
- return that;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
+ this._state = ResourceLoaderState.READY;
+ return this;
+ } catch (error) {
+ if (this.isDestroyed()) {
return;
}
- that.unload();
- that._state = ResourceLoaderState.FAILED;
+
+ this.unload();
+ this._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load buffer view";
- return Promise.reject(that.getError(errorMessage, error));
- });
+ throw this.getError(errorMessage, error);
+ }
+ })();
return this._promise;
};
@@ -228,30 +197,16 @@ function getBufferLoader(bufferViewLoader) {
const resource = baseResource.getDerivedResource({
url: buffer.uri,
});
- return resourceCache.loadExternalBuffer({
+ return resourceCache.getExternalBufferLoader({
resource: resource,
});
}
- return resourceCache.loadEmbeddedBuffer({
+ return resourceCache.getEmbeddedBufferLoader({
parentResource: bufferViewLoader._gltfResource,
bufferId: bufferViewLoader._bufferId,
});
}
-/**
- * Processes the resources. For a BufferView that does not have the EXT_meshopt_compression extension, there
- * is no processing that needs to happen, so this function returns immediately.
- *
- * @param {FrameState} frameState The frame state.
- */
-GltfBufferViewLoader.prototype.process = function (frameState) {
- //>>includeStart('debug', pragmas.debug);
- Check.typeOf.object("frameState", frameState);
- //>>includeEnd('debug');
-
- return this._process(this, frameState);
-};
-
/**
* Unloads the resource.
* @private
diff --git a/packages/engine/Source/Scene/GltfDracoLoader.js b/packages/engine/Source/Scene/GltfDracoLoader.js
index 49481b13af00..038e73d5c0d4 100644
--- a/packages/engine/Source/Scene/GltfDracoLoader.js
+++ b/packages/engine/Source/Scene/GltfDracoLoader.js
@@ -54,7 +54,7 @@ function GltfDracoLoader(options) {
this._decodedData = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
- this._process = function (loader, frameState) {};
+ this._dracoError = undefined;
}
if (defined(Object.create)) {
@@ -63,20 +63,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfDracoLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready.
- *
- * @memberof GltfDracoLoader.prototype
- *
- * @type {Promise.}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -112,101 +98,39 @@ Object.defineProperties(GltfDracoLoader.prototype, {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfDracoLoader.prototype.load = function () {
- const resourceCache = this._resourceCache;
- const bufferViewLoader = resourceCache.loadBufferView({
- gltf: this._gltf,
- bufferViewId: this._draco.bufferView,
- gltfResource: this._gltfResource,
- baseResource: this._baseResource,
- });
-
- this._bufferViewLoader = bufferViewLoader;
+GltfDracoLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
this._state = ResourceLoaderState.LOADING;
- const that = this;
- const dracoPromise = new Promise(function (resolve, reject) {
- that._process = function (loader, frameState) {
- if (!defined(loader._bufferViewTypedArray)) {
- // Not ready to decode the Draco buffer
+ const resourceCache = this._resourceCache;
+ this._promise = (async () => {
+ try {
+ const bufferViewLoader = resourceCache.getBufferViewLoader({
+ gltf: this._gltf,
+ bufferViewId: this._draco.bufferView,
+ gltfResource: this._gltfResource,
+ baseResource: this._baseResource,
+ });
+ this._bufferViewLoader = bufferViewLoader;
+ await bufferViewLoader.load();
+
+ if (this.isDestroyed()) {
return;
}
- if (defined(loader._decodePromise)) {
- // Currently decoding
+ this._bufferViewTypedArray = bufferViewLoader.typedArray;
+ this._state = ResourceLoaderState.PROCESSING;
+ return this;
+ } catch (error) {
+ if (this.isDestroyed()) {
return;
}
- const draco = loader._draco;
- const gltf = loader._gltf;
- const bufferViews = gltf.bufferViews;
- const bufferViewId = draco.bufferView;
- const bufferView = bufferViews[bufferViewId];
- const compressedAttributes = draco.attributes;
-
- const decodeOptions = {
- // Need to make a copy of the typed array otherwise the underlying
- // ArrayBuffer may be accessed on both the worker and the main thread. This
- // leads to errors such as "ArrayBuffer at index 0 is already detached".
- // PERFORMANCE_IDEA: Look into SharedArrayBuffer to get around this.
- array: new Uint8Array(loader._bufferViewTypedArray),
- bufferView: bufferView,
- compressedAttributes: compressedAttributes,
- dequantizeInShader: true,
- };
-
- const decodePromise = DracoLoader.decodeBufferView(decodeOptions);
-
- if (!defined(decodePromise)) {
- // Cannot schedule task this frame
- return;
- }
-
- loader._decodePromise = decodePromise
- .then(function (results) {
- if (loader.isDestroyed()) {
- resolve();
- return;
- }
-
- // Unload everything except the decoded data
- loader.unload();
-
- loader._decodedData = {
- indices: results.indexArray,
- vertexAttributes: results.attributeData,
- };
- loader._state = ResourceLoaderState.READY;
- resolve(loader);
- })
- .catch(function (e) {
- if (loader.isDestroyed()) {
- resolve();
- return;
- }
-
- reject(e);
- });
- };
- });
-
- this._promise = bufferViewLoader.promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
- // Now wait for process() to run to finish loading
- that._bufferViewTypedArray = bufferViewLoader.typedArray;
- that._state = ResourceLoaderState.PROCESSING;
-
- return dracoPromise;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
-
- return handleError(that, error);
- });
+ handleError(this, error);
+ }
+ })();
return this._promise;
};
@@ -215,7 +139,7 @@ function handleError(dracoLoader, error) {
dracoLoader.unload();
dracoLoader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load Draco";
- return Promise.reject(dracoLoader.getError(errorMessage, error));
+ throw dracoLoader.getError(errorMessage, error);
}
/**
@@ -229,7 +153,78 @@ GltfDracoLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- return this._process(this, frameState);
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (this._state !== ResourceLoaderState.PROCESSING) {
+ return false;
+ }
+
+ if (defined(this._dracoError)) {
+ handleError(this, this._dracoError);
+ }
+
+ if (!defined(this._bufferViewTypedArray)) {
+ // Not ready to decode the Draco buffer
+ return false;
+ }
+
+ if (defined(this._decodePromise)) {
+ // Currently decoding
+ return false;
+ }
+
+ const draco = this._draco;
+ const gltf = this._gltf;
+ const bufferViews = gltf.bufferViews;
+ const bufferViewId = draco.bufferView;
+ const bufferView = bufferViews[bufferViewId];
+ const compressedAttributes = draco.attributes;
+
+ const decodeOptions = {
+ // Need to make a copy of the typed array otherwise the underlying
+ // ArrayBuffer may be accessed on both the worker and the main thread. This
+ // leads to errors such as "ArrayBuffer at index 0 is already detached".
+ // PERFORMANCE_IDEA: Look into SharedArrayBuffer to get around this.
+ array: new Uint8Array(this._bufferViewTypedArray),
+ bufferView: bufferView,
+ compressedAttributes: compressedAttributes,
+ dequantizeInShader: true,
+ };
+
+ const decodePromise = DracoLoader.decodeBufferView(decodeOptions);
+
+ if (!defined(decodePromise)) {
+ // Cannot schedule task this frame
+ return false;
+ }
+
+ this._decodePromise = (async () => {
+ try {
+ const results = await decodePromise;
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ // Unload everything except the decoded data
+ this.unload();
+
+ this._decodedData = {
+ indices: results.indexArray,
+ vertexAttributes: results.attributeData,
+ };
+ this._state = ResourceLoaderState.READY;
+ return this._baseResource;
+ } catch (error) {
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ // Capture this error so it can be thrown on the next `process` call
+ this._dracoError = error;
+ }
+ })();
};
/**
diff --git a/packages/engine/Source/Scene/GltfImageLoader.js b/packages/engine/Source/Scene/GltfImageLoader.js
index 53e42898325b..5994ed9e94e0 100644
--- a/packages/engine/Source/Scene/GltfImageLoader.js
+++ b/packages/engine/Source/Scene/GltfImageLoader.js
@@ -68,20 +68,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfImageLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfImageLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -132,6 +118,10 @@ Object.defineProperties(GltfImageLoader.prototype, {
* @private
*/
GltfImageLoader.prototype.load = function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
if (defined(this._bufferViewId)) {
this._promise = loadFromBufferView(this);
return this._promise;
@@ -158,78 +148,78 @@ function getImageAndMipLevels(image) {
};
}
-function loadFromBufferView(imageLoader) {
+async function loadFromBufferView(imageLoader) {
+ imageLoader._state = ResourceLoaderState.LOADING;
const resourceCache = imageLoader._resourceCache;
- const bufferViewLoader = resourceCache.loadBufferView({
- gltf: imageLoader._gltf,
- bufferViewId: imageLoader._bufferViewId,
- gltfResource: imageLoader._gltfResource,
- baseResource: imageLoader._baseResource,
- });
+ try {
+ const bufferViewLoader = resourceCache.getBufferViewLoader({
+ gltf: imageLoader._gltf,
+ bufferViewId: imageLoader._bufferViewId,
+ gltfResource: imageLoader._gltfResource,
+ baseResource: imageLoader._baseResource,
+ });
+ imageLoader._bufferViewLoader = bufferViewLoader;
+ await bufferViewLoader.load();
- imageLoader._bufferViewLoader = bufferViewLoader;
- imageLoader._state = ResourceLoaderState.LOADING;
+ if (imageLoader.isDestroyed()) {
+ return;
+ }
- return bufferViewLoader.promise
- .then(function () {
- if (imageLoader.isDestroyed()) {
- return;
- }
-
- const typedArray = bufferViewLoader.typedArray;
- return loadImageFromBufferTypedArray(typedArray).then(function (image) {
- if (imageLoader.isDestroyed()) {
- return;
- }
-
- const imageAndMipLevels = getImageAndMipLevels(image);
-
- // Unload everything except the image
- imageLoader.unload();
-
- imageLoader._image = imageAndMipLevels.image;
- imageLoader._mipLevels = imageAndMipLevels.mipLevels;
- imageLoader._state = ResourceLoaderState.READY;
- return imageLoader;
- });
- })
- .catch(function (error) {
- if (imageLoader.isDestroyed()) {
- return;
- }
- return handleError(imageLoader, error, "Failed to load embedded image");
- });
+ const typedArray = bufferViewLoader.typedArray;
+ const image = await loadImageFromBufferTypedArray(typedArray);
+ if (imageLoader.isDestroyed()) {
+ return;
+ }
+
+ const imageAndMipLevels = getImageAndMipLevels(image);
+
+ // Unload everything except the image
+ imageLoader.unload();
+
+ imageLoader._image = imageAndMipLevels.image;
+ imageLoader._mipLevels = imageAndMipLevels.mipLevels;
+ imageLoader._state = ResourceLoaderState.READY;
+
+ return imageLoader;
+ } catch (error) {
+ if (imageLoader.isDestroyed()) {
+ return;
+ }
+
+ return handleError(imageLoader, error, "Failed to load embedded image");
+ }
}
-function loadFromUri(imageLoader) {
+async function loadFromUri(imageLoader) {
+ imageLoader._state = ResourceLoaderState.LOADING;
const baseResource = imageLoader._baseResource;
const uri = imageLoader._uri;
const resource = baseResource.getDerivedResource({
url: uri,
});
- imageLoader._state = ResourceLoaderState.LOADING;
- return loadImageFromUri(resource)
- .then(function (image) {
- if (imageLoader.isDestroyed()) {
- return;
- }
-
- const imageAndMipLevels = getImageAndMipLevels(image);
-
- // Unload everything except the image
- imageLoader.unload();
-
- imageLoader._image = imageAndMipLevels.image;
- imageLoader._mipLevels = imageAndMipLevels.mipLevels;
- imageLoader._state = ResourceLoaderState.READY;
- return imageLoader;
- })
- .catch(function (error) {
- if (imageLoader.isDestroyed()) {
- return;
- }
- return handleError(imageLoader, error, `Failed to load image: ${uri}`);
- });
+
+ try {
+ const image = await loadImageFromUri(resource);
+ if (imageLoader.isDestroyed()) {
+ return;
+ }
+
+ const imageAndMipLevels = getImageAndMipLevels(image);
+
+ // Unload everything except the image
+ imageLoader.unload();
+
+ imageLoader._image = imageAndMipLevels.image;
+ imageLoader._mipLevels = imageAndMipLevels.mipLevels;
+ imageLoader._state = ResourceLoaderState.READY;
+
+ return imageLoader;
+ } catch (error) {
+ if (imageLoader.isDestroyed()) {
+ return;
+ }
+ return handleError(imageLoader, error, `Failed to load image: ${uri}`);
+ }
}
function handleError(imageLoader, error, errorMessage) {
@@ -269,7 +259,7 @@ function getMimeTypeFromTypedArray(typedArray) {
throw new RuntimeError("Image format is not recognized");
}
-function loadImageFromBufferTypedArray(typedArray) {
+async function loadImageFromBufferTypedArray(typedArray) {
const mimeType = getMimeTypeFromTypedArray(typedArray);
if (mimeType === "image/ktx2") {
// Need to make a copy of the embedded KTX2 buffer otherwise the underlying
diff --git a/packages/engine/Source/Scene/GltfIndexBufferLoader.js b/packages/engine/Source/Scene/GltfIndexBufferLoader.js
index b5006e4cbef2..dc7bdcf9b6d1 100644
--- a/packages/engine/Source/Scene/GltfIndexBufferLoader.js
+++ b/packages/engine/Source/Scene/GltfIndexBufferLoader.js
@@ -79,7 +79,6 @@ function GltfIndexBufferLoader(options) {
this._buffer = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
- this._process = function (loader, frameState) {};
}
if (defined(Object.create)) {
@@ -88,20 +87,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfIndexBufferLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource has not yet started loading.
- *
- * @memberof GltfIndexBufferLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -168,135 +153,72 @@ const scratchIndexBufferJob = new CreateIndexBufferJob();
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfIndexBufferLoader.prototype.load = function () {
- let promise;
+GltfIndexBufferLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
if (defined(this._draco)) {
- promise = loadFromDraco(this);
- } else {
- promise = loadFromBufferView(this);
+ this._promise = loadFromDraco(this);
+ return this._promise;
}
- const that = this;
- const processPromise = new Promise(function (resolve) {
- that._process = function (loader, frameState) {
- if (loader._state === ResourceLoaderState.READY) {
- return;
- }
-
- const typedArray = loader._typedArray;
- const indexDatatype = loader._indexDatatype;
-
- if (defined(loader._dracoLoader)) {
- loader._dracoLoader.process(frameState);
- }
-
- if (defined(loader._bufferViewLoader)) {
- loader._bufferViewLoader.process(frameState);
- }
-
- if (!defined(typedArray)) {
- // Buffer view hasn't been loaded yet
- return;
- }
-
- let buffer;
- if (loader._loadBuffer && loader._asynchronous) {
- const indexBufferJob = scratchIndexBufferJob;
- indexBufferJob.set(typedArray, indexDatatype, frameState.context);
- const jobScheduler = frameState.jobScheduler;
- if (!jobScheduler.execute(indexBufferJob, JobType.BUFFER)) {
- // Job scheduler is full. Try again next frame.
- return;
- }
- buffer = indexBufferJob.buffer;
- } else if (loader._loadBuffer) {
- buffer = createIndexBuffer(
- typedArray,
- indexDatatype,
- frameState.context
- );
- }
-
- // Unload everything except the index buffer and/or typed array.
- loader.unload();
-
- loader._buffer = buffer;
- loader._typedArray = loader._loadTypedArray ? typedArray : undefined;
- loader._state = ResourceLoaderState.READY;
- resolve(loader);
- };
- });
-
- this._promise = promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
-
- return processPromise;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
-
- return handleError(that, error);
- });
-
+ this._promise = loadFromBufferView(this);
return this._promise;
};
-function loadFromDraco(indexBufferLoader) {
+async function loadFromDraco(indexBufferLoader) {
+ indexBufferLoader._state = ResourceLoaderState.LOADING;
const resourceCache = indexBufferLoader._resourceCache;
- const dracoLoader = resourceCache.loadDraco({
- gltf: indexBufferLoader._gltf,
- draco: indexBufferLoader._draco,
- gltfResource: indexBufferLoader._gltfResource,
- baseResource: indexBufferLoader._baseResource,
- });
- indexBufferLoader._dracoLoader = dracoLoader;
- indexBufferLoader._state = ResourceLoaderState.LOADING;
+ try {
+ const dracoLoader = resourceCache.getDracoLoader({
+ gltf: indexBufferLoader._gltf,
+ draco: indexBufferLoader._draco,
+ gltfResource: indexBufferLoader._gltfResource,
+ baseResource: indexBufferLoader._baseResource,
+ });
+ indexBufferLoader._dracoLoader = dracoLoader;
+ await dracoLoader.load();
- return dracoLoader.promise.then(function () {
if (indexBufferLoader.isDestroyed()) {
return;
}
+
// Now wait for process() to run to finish loading
- const typedArray = dracoLoader.decodedData.indices.typedArray;
- indexBufferLoader._typedArray = typedArray;
- // The index datatype may be a smaller datatype after draco decode
- indexBufferLoader._indexDatatype = ComponentDatatype.fromTypedArray(
- typedArray
- );
- indexBufferLoader._state = ResourceLoaderState.PROCESSING;
+ indexBufferLoader._state = ResourceLoaderState.LOADED;
return indexBufferLoader;
- });
+ } catch (error) {
+ if (indexBufferLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(indexBufferLoader, error);
+ }
}
-function loadFromBufferView(indexBufferLoader) {
+async function loadFromBufferView(indexBufferLoader) {
const gltf = indexBufferLoader._gltf;
const accessorId = indexBufferLoader._accessorId;
const accessor = gltf.accessors[accessorId];
const bufferViewId = accessor.bufferView;
- const resourceCache = indexBufferLoader._resourceCache;
- const bufferViewLoader = resourceCache.loadBufferView({
- gltf: gltf,
- bufferViewId: bufferViewId,
- gltfResource: indexBufferLoader._gltfResource,
- baseResource: indexBufferLoader._baseResource,
- });
indexBufferLoader._state = ResourceLoaderState.LOADING;
- indexBufferLoader._bufferViewLoader = bufferViewLoader;
+ const resourceCache = indexBufferLoader._resourceCache;
+ try {
+ const bufferViewLoader = resourceCache.getBufferViewLoader({
+ gltf: gltf,
+ bufferViewId: bufferViewId,
+ gltfResource: indexBufferLoader._gltfResource,
+ baseResource: indexBufferLoader._baseResource,
+ });
+ indexBufferLoader._bufferViewLoader = bufferViewLoader;
- return bufferViewLoader.promise.then(function () {
+ await bufferViewLoader.load();
if (indexBufferLoader.isDestroyed()) {
return;
}
- // Now wait for process() to run to finish loading
const bufferViewTypedArray = bufferViewLoader.typedArray;
indexBufferLoader._typedArray = createIndicesTypedArray(
indexBufferLoader,
@@ -304,7 +226,13 @@ function loadFromBufferView(indexBufferLoader) {
);
indexBufferLoader._state = ResourceLoaderState.PROCESSING;
return indexBufferLoader;
- });
+ } catch (error) {
+ if (indexBufferLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(indexBufferLoader, error);
+ }
}
function createIndicesTypedArray(indexBufferLoader, bufferViewTypedArray) {
@@ -346,8 +274,7 @@ function handleError(indexBufferLoader, error) {
indexBufferLoader.unload();
indexBufferLoader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load index buffer";
- error = indexBufferLoader.getError(errorMessage, error);
- return Promise.reject(error);
+ throw indexBufferLoader.getError(errorMessage, error);
}
function CreateIndexBufferJob() {
@@ -397,7 +324,63 @@ GltfIndexBufferLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- return this._process(this, frameState);
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (
+ this._state !== ResourceLoaderState.LOADED &&
+ this._state !== ResourceLoaderState.PROCESSING
+ ) {
+ return false;
+ }
+
+ const typedArray = this._typedArray;
+ const indexDatatype = this._indexDatatype;
+
+ if (defined(this._dracoLoader)) {
+ try {
+ const ready = this._dracoLoader.process(frameState);
+ if (ready) {
+ const dracoLoader = this._dracoLoader;
+ const typedArray = dracoLoader.decodedData.indices.typedArray;
+ this._typedArray = typedArray;
+ // The index datatype may be a smaller datatype after draco decode
+ this._indexDatatype = ComponentDatatype.fromTypedArray(typedArray);
+ }
+ } catch (error) {
+ handleError(this, error);
+ }
+ }
+
+ if (!defined(typedArray)) {
+ // Buffer view hasn't been loaded yet
+ return false;
+ }
+
+ let buffer;
+ if (this._loadBuffer && this._asynchronous) {
+ const indexBufferJob = scratchIndexBufferJob;
+ indexBufferJob.set(typedArray, indexDatatype, frameState.context);
+ const jobScheduler = frameState.jobScheduler;
+ if (!jobScheduler.execute(indexBufferJob, JobType.BUFFER)) {
+ // Job scheduler is full. Try again next frame.
+ return false;
+ }
+ buffer = indexBufferJob.buffer;
+ } else if (this._loadBuffer) {
+ buffer = createIndexBuffer(typedArray, indexDatatype, frameState.context);
+ }
+
+ // Unload everything except the index buffer and/or typed array.
+ this.unload();
+
+ this._buffer = buffer;
+ this._typedArray = this._loadTypedArray ? typedArray : undefined;
+ this._state = ResourceLoaderState.READY;
+
+ this._resourceCache.statistics.addGeometryLoader(this);
+ return true;
};
/**
diff --git a/packages/engine/Source/Scene/GltfJsonLoader.js b/packages/engine/Source/Scene/GltfJsonLoader.js
index fdf7acb60efd..c61017ba0874 100644
--- a/packages/engine/Source/Scene/GltfJsonLoader.js
+++ b/packages/engine/Source/Scene/GltfJsonLoader.js
@@ -68,20 +68,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfJsonLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfJsonLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -117,56 +103,55 @@ Object.defineProperties(GltfJsonLoader.prototype, {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfJsonLoader.prototype.load = function () {
+GltfJsonLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
this._state = ResourceLoaderState.LOADING;
- let processPromise;
if (defined(this._gltfJson)) {
- processPromise = processGltfJson(this, this._gltfJson);
- } else if (defined(this._typedArray)) {
- processPromise = processGltfTypedArray(this, this._typedArray);
- } else {
- processPromise = loadFromUri(this);
+ this._promise = processGltfJson(this, this._gltfJson);
+ return this._promise;
}
- const that = this;
- this._promise = processPromise
- .then(function (gltf) {
- if (that.isDestroyed()) {
- return;
- }
- that._gltf = gltf;
- that._state = ResourceLoaderState.READY;
- return that;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
- return handleError(that, error);
- });
+ if (defined(this._typedArray)) {
+ this._promise = processGltfTypedArray(this, this._typedArray);
+ return this._promise;
+ }
+ this._promise = loadFromUri(this);
return this._promise;
};
-function loadFromUri(gltfJsonLoader) {
- return gltfJsonLoader._fetchGltf().then(function (arrayBuffer) {
+async function loadFromUri(gltfJsonLoader) {
+ let typedArray;
+ try {
+ const arrayBuffer = await gltfJsonLoader._fetchGltf();
if (gltfJsonLoader.isDestroyed()) {
return;
}
- const typedArray = new Uint8Array(arrayBuffer);
- return processGltfTypedArray(gltfJsonLoader, typedArray);
- });
+
+ typedArray = new Uint8Array(arrayBuffer);
+ } catch (error) {
+ if (gltfJsonLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(gltfJsonLoader, error);
+ }
+
+ return processGltfTypedArray(gltfJsonLoader, typedArray);
}
function handleError(gltfJsonLoader, error) {
gltfJsonLoader.unload();
gltfJsonLoader._state = ResourceLoaderState.FAILED;
const errorMessage = `Failed to load glTF: ${gltfJsonLoader._gltfResource.url}`;
- return Promise.reject(gltfJsonLoader.getError(errorMessage, error));
+ throw gltfJsonLoader.getError(errorMessage, error);
}
-function upgradeVersion(gltfJsonLoader, gltf) {
+async function upgradeVersion(gltfJsonLoader, gltf) {
if (
defined(gltf.asset) &&
gltf.asset.version === "2.0" &&
@@ -188,23 +173,25 @@ function upgradeVersion(gltfJsonLoader, gltf) {
url: buffer.uri,
});
const resourceCache = gltfJsonLoader._resourceCache;
- const bufferLoader = resourceCache.loadExternalBuffer({
+ const bufferLoader = resourceCache.getExternalBufferLoader({
resource: resource,
});
-
gltfJsonLoader._bufferLoaders.push(bufferLoader);
promises.push(
- bufferLoader.promise.then(function (bufferLoader) {
+ bufferLoader.load().then(function () {
+ if (bufferLoader.isDestroyed()) {
+ return;
+ }
+
buffer.extras._pipeline.source = bufferLoader.typedArray;
})
);
}
});
- return Promise.all(promises).then(function () {
- updateVersion(gltf);
- });
+ await Promise.all(promises);
+ updateVersion(gltf);
}
function decodeDataUris(gltf) {
@@ -233,42 +220,54 @@ function loadEmbeddedBuffers(gltfJsonLoader, gltf) {
const source = buffer.extras._pipeline.source;
if (defined(source) && !defined(buffer.uri)) {
const resourceCache = gltfJsonLoader._resourceCache;
- const bufferLoader = resourceCache.loadEmbeddedBuffer({
+ const bufferLoader = resourceCache.getEmbeddedBufferLoader({
parentResource: gltfJsonLoader._gltfResource,
bufferId: bufferId,
typedArray: source,
});
-
gltfJsonLoader._bufferLoaders.push(bufferLoader);
- promises.push(bufferLoader.promise);
+ promises.push(bufferLoader.load());
}
});
return Promise.all(promises);
}
-function processGltfJson(gltfJsonLoader, gltf) {
- addPipelineExtras(gltf);
-
- return decodeDataUris(gltf)
- .then(function () {
- return upgradeVersion(gltfJsonLoader, gltf);
- })
- .then(function () {
- addDefaults(gltf);
- return loadEmbeddedBuffers(gltfJsonLoader, gltf);
- })
- .then(function () {
- removePipelineExtras(gltf);
- return gltf;
- });
+async function processGltfJson(gltfJsonLoader, gltf) {
+ try {
+ addPipelineExtras(gltf);
+
+ await decodeDataUris(gltf);
+ await upgradeVersion(gltfJsonLoader, gltf);
+ addDefaults(gltf);
+ await loadEmbeddedBuffers(gltfJsonLoader, gltf);
+ removePipelineExtras(gltf);
+
+ gltfJsonLoader._gltf = gltf;
+ gltfJsonLoader._state = ResourceLoaderState.READY;
+ return gltfJsonLoader;
+ } catch (error) {
+ if (gltfJsonLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(gltfJsonLoader, error);
+ }
}
-function processGltfTypedArray(gltfJsonLoader, typedArray) {
+async function processGltfTypedArray(gltfJsonLoader, typedArray) {
let gltf;
- if (getMagic(typedArray) === "glTF") {
- gltf = parseGlb(typedArray);
- } else {
- gltf = getJsonFromTypedArray(typedArray);
+ try {
+ if (getMagic(typedArray) === "glTF") {
+ gltf = parseGlb(typedArray);
+ } else {
+ gltf = getJsonFromTypedArray(typedArray);
+ }
+ } catch (error) {
+ if (gltfJsonLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(gltfJsonLoader, error);
}
return processGltfJson(gltfJsonLoader, gltf);
diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js
index 20655d764f42..6b2b5ca7f41e 100644
--- a/packages/engine/Source/Scene/GltfLoader.js
+++ b/packages/engine/Source/Scene/GltfLoader.js
@@ -242,9 +242,8 @@ function GltfLoader(options) {
this._state = GltfLoaderState.NOT_LOADED;
this._textureState = GltfLoaderState.NOT_LOADED;
this._promise = undefined;
- this._texturesLoadedPromise = undefined;
- this._process = function (loader, frameState) {};
- this._processTextures = function (loader, frameState) {};
+ this._processError = undefined;
+ this._textureErrors = [];
// Information about whether to load primitives as typed arrays or buffers,
// and whether post-processing is needed after loading (e.g. for
@@ -255,9 +254,12 @@ function GltfLoader(options) {
this._loaderPromises = [];
this._textureLoaders = [];
this._texturesPromises = [];
+ this._textureCallbacks = {};
this._bufferViewLoaders = [];
this._geometryLoaders = [];
+ this._geometryCallbacks = [];
this._structuralMetadataLoader = undefined;
+ this._loadResourcesPromise = undefined;
// In some cases where geometry post-processing is needed (like generating
// outlines) new attributes are added that may have GPU resources attached.
@@ -275,231 +277,207 @@ if (defined(Object.create)) {
Object.defineProperties(GltfLoader.prototype, {
/**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
+ * The cache key of the resource.
*
* @memberof GltfLoader.prototype
*
- * @type {Promise.|undefined}
+ * @type {String}
* @readonly
* @private
*/
- promise: {
+ cacheKey: {
get: function () {
- return this._promise;
+ return undefined;
},
},
/**
- * The cache key of the resource.
+ * The loaded components.
*
* @memberof GltfLoader.prototype
*
- * @type {String}
+ * @type {ModelComponents.Components}
* @readonly
* @private
*/
- cacheKey: {
+ components: {
get: function () {
- return undefined;
+ return this._components;
},
},
/**
- * The loaded components.
+ * The loaded glTF json.
*
* @memberof GltfLoader.prototype
*
- * @type {ModelComponents.Components}
+ * @type {object}
* @readonly
* @private
*/
- components: {
+ gltfJson: {
get: function () {
- return this._components;
+ if (defined(this._gltfJsonLoader)) {
+ return this._gltfJsonLoader.gltf;
+ }
+ return this._gltfJson;
},
},
-
/**
- * A promise that resolves when all textures are loaded.
- * When incrementallyLoadTextures is true this may resolve after
- * promise resolves.
+ * true if textures are loaded separately from the other glTF resources.
*
* @memberof GltfLoader.prototype
*
- * @type {Promise}
+ * @type {boolean}
* @readonly
* @private
*/
- texturesLoadedPromise: {
+ incrementallyLoadTextures: {
get: function () {
- return this._texturesLoadedPromise;
+ return this._incrementallyLoadTextures;
},
},
});
/**
- * Loads the resource.
- * @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
- * @private
+ * Loads the gltf object
*/
-GltfLoader.prototype.load = function () {
- const gltfJsonLoader = ResourceCache.loadGltfJson({
- gltfResource: this._gltfResource,
- baseResource: this._baseResource,
- typedArray: this._typedArray,
- gltfJson: this._gltfJson,
- });
-
- this._gltfJsonLoader = gltfJsonLoader;
- this._state = GltfLoaderState.LOADING;
- this._textureState = GltfLoaderState.LOADING;
+async function loadGltfJson(loader) {
+ loader._state = GltfLoaderState.LOADING;
+ loader._textureState = GltfLoaderState.LOADING;
+
+ try {
+ const gltfJsonLoader = ResourceCache.getGltfJsonLoader({
+ gltfResource: loader._gltfResource,
+ baseResource: loader._baseResource,
+ typedArray: loader._typedArray,
+ gltfJson: loader._gltfJson,
+ });
+ loader._gltfJsonLoader = gltfJsonLoader;
+ await gltfJsonLoader.load();
- const that = this;
- let textureProcessPromise;
- const processPromise = new Promise(function (resolve, reject) {
- textureProcessPromise = new Promise(function (
- resolveTextures,
- rejectTextures
+ if (
+ loader.isDestroyed() ||
+ loader.isUnloaded() ||
+ gltfJsonLoader.isDestroyed()
) {
- that._process = function (loader, frameState) {
- if (!FeatureDetection.supportsWebP.initialized) {
- FeatureDetection.supportsWebP.initialize();
- return;
- }
-
- if (loader._state === GltfLoaderState.LOADED) {
- loader._state = GltfLoaderState.PROCESSING;
-
- const supportedImageFormats = new SupportedImageFormats({
- webp: FeatureDetection.supportsWebP(),
- basis: frameState.context.supportsBasis,
- });
-
- let gltf;
- if (defined(loader._gltfJsonLoader)) {
- gltf = loader._gltfJsonLoader.gltf;
- } else {
- gltf = loader._gltfJson;
- }
-
- // Parse the glTF which populates the loaders arrays. The promise will
- // resolve once all the loaders are ready (i.e. all external resources
- // have been fetched and all GPU resources have been created). Loaders that
- // create GPU resources need to be processed every frame until they become
- // ready since the JobScheduler is not able to execute all jobs in a single
- // frame. Also note that it's fine to call process before a loader is ready
- // to process; nothing will happen.
- parse(
- loader,
- gltf,
- supportedImageFormats,
- frameState,
- reject,
- rejectTextures
- );
-
- if (defined(loader._gltfJsonLoader) && loader._releaseGltfJson) {
- // Check that the glTF JSON loader is still defined before trying to unload it.
- // It may be undefined if the ready promise rejects immediately (which can happen in unit tests)
- ResourceCache.unload(loader._gltfJsonLoader);
- loader._gltfJsonLoader = undefined;
- }
- }
+ return;
+ }
- if (loader._state === GltfLoaderState.PROCESSING) {
- processLoaders(loader, frameState);
- }
+ const gltf = gltfJsonLoader.gltf;
+ const version = gltf.asset.version;
+ if (version !== "1.0" && version !== "2.0") {
+ const url = loader._gltfResource.url;
+ throw new RuntimeError(
+ `Failed to load ${url}: \nUnsupported glTF version: ${version}`
+ );
+ }
- if (loader._state === GltfLoaderState.POST_PROCESSING) {
- postProcessGeometry(loader, frameState.context);
- loader._state = GltfLoaderState.PROCESSED;
- }
+ const extensionsRequired = gltf.extensionsRequired;
+ if (defined(extensionsRequired)) {
+ ModelUtility.checkSupportedExtensions(extensionsRequired);
- if (loader._state === GltfLoaderState.PROCESSED) {
- // The buffer views can be unloaded once the data is copied.
- unloadBufferViews(loader);
+ // Check for the KHR_mesh_quantization extension here, it will be used later
+ // in loadAttribute().
+ loader._hasKhrMeshQuantization = extensionsRequired.includes(
+ "KHR_mesh_quantization"
+ );
+ }
- // Similarly, if the glTF was loaded from a typed array, release the memory
- loader._typedArray = undefined;
+ loader._state = GltfLoaderState.LOADED;
+ loader._textureState = GltfLoaderState.LOADED;
- loader._state = GltfLoaderState.READY;
- resolve(loader);
- }
- };
+ return loader;
+ } catch (error) {
+ if (loader.isDestroyed()) {
+ return;
+ }
- that._processTextures = function (loader, frameState) {
- if (loader._textureState === GltfLoaderState.LOADED) {
- loader._textureState = GltfLoaderState.PROCESSING;
- }
+ loader._state = GltfLoaderState.FAILED;
+ loader._textureState = GltfLoaderState.FAILED;
+ handleError(loader, error);
+ }
+}
- if (loader._textureState === GltfLoaderState.PROCESSING) {
- let i;
- const textureLoaders = loader._textureLoaders;
- const textureLoadersLength = textureLoaders.length;
- for (i = 0; i < textureLoadersLength; ++i) {
- textureLoaders[i].process(frameState);
- }
- }
+async function loadResources(loader, frameState) {
+ if (!FeatureDetection.supportsWebP.initialized) {
+ await FeatureDetection.supportsWebP.initialize();
+ }
- if (loader._textureState === GltfLoaderState.PROCESSED) {
- loader._textureState = GltfLoaderState.READY;
- resolveTextures(loader);
- }
- };
- });
+ const supportedImageFormats = new SupportedImageFormats({
+ webp: FeatureDetection.supportsWebP(),
+ basis: frameState.context.supportsBasis,
});
- this._promise = gltfJsonLoader.promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
- that._state = GltfLoaderState.LOADED;
- that._textureState = GltfLoaderState.LOADED;
+ // Parse the glTF which populates the loaders arrays. Loading promises will be created, and will
+ // resolve once the loaders are ready (i.e. all external resources
+ // have been fetched and all GPU resources have been created). Loaders that
+ // create GPU resources need to be processed every frame until they become
+ // ready since the JobScheduler is not able to execute all jobs in a single
+ // frame. Any promise failures are collected, and will be handled synchronously in process(). Also note that it's fine to call process before a loader is ready
+ // to process or after it has failed; nothing will happen.
+ const gltf = loader.gltfJson;
+ parse(loader, gltf, supportedImageFormats, frameState);
- return processPromise;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
- that._state = GltfLoaderState.FAILED;
- that._textureState = GltfLoaderState.FAILED;
- return handleError(that, error);
- });
+ // All resource loaders have been created, so we can begin processing
+ loader._state = GltfLoaderState.PROCESSING;
+ loader._textureState = GltfLoaderState.PROCESSING;
- this._texturesLoadedPromise = textureProcessPromise.catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
+ if (defined(loader._gltfJsonLoader) && loader._releaseGltfJson) {
+ // Check that the glTF JSON loader is still defined before trying to unload it.
+ // It can be unloaded if the glTF loader is destroyed.
+ ResourceCache.unload(loader._gltfJsonLoader);
+ loader._gltfJsonLoader = undefined;
+ }
+}
- that._textureState = GltfLoaderState.FAILED;
- return handleError(that, error);
- });
+/**
+ * Loads the resource.
+ * @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
+ * @exception {RuntimeError} Unsupported glTF version
+ * @exception {RuntimeError} Unsupported glTF Extension
+ * @private
+ */
+GltfLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+ this._promise = loadGltfJson(this);
return this._promise;
};
function handleError(gltfLoader, error) {
gltfLoader.unload();
const errorMessage = "Failed to load glTF";
- error = gltfLoader.getError(errorMessage, error);
- return Promise.reject(error);
+ throw gltfLoader.getError(errorMessage, error);
}
function processLoaders(loader, frameState) {
- const bufferViewLoaders = loader._bufferViewLoaders;
- const bufferViewLoadersLength = bufferViewLoaders.length;
- for (let i = 0; i < bufferViewLoadersLength; ++i) {
- bufferViewLoaders[i].process(frameState);
- }
-
+ let i;
+ let ready = true;
const geometryLoaders = loader._geometryLoaders;
const geometryLoadersLength = geometryLoaders.length;
- for (let i = 0; i < geometryLoadersLength; ++i) {
- geometryLoaders[i].process(frameState);
+ for (i = 0; i < geometryLoadersLength; ++i) {
+ const geometryReady = geometryLoaders[i].process(frameState);
+ if (geometryReady && defined(loader._geometryCallbacks[i])) {
+ loader._geometryCallbacks[i]();
+ loader._geometryCallbacks[i] = undefined;
+ }
+ ready = ready && geometryReady;
}
- if (defined(loader._structuralMetadataLoader)) {
- loader._structuralMetadataLoader.process(frameState);
+ const structuralMetadataLoader = loader._structuralMetadataLoader;
+ if (defined(structuralMetadataLoader)) {
+ const metadataReady = structuralMetadataLoader.process(frameState);
+ if (metadataReady) {
+ loader._components.structuralMetadata =
+ structuralMetadataLoader.structuralMetadata;
+ }
+ ready = ready && metadataReady;
+ }
+
+ if (ready) {
+ // Geometry requires further processing
+ loader._state = GltfLoaderState.POST_PROCESSING;
}
}
@@ -549,6 +527,73 @@ function gatherPostProcessBuffers(loader, primitiveLoadPlan) {
}
}
+/**
+ * Process loaders other than textures
+ * @private
+ */
+GltfLoader.prototype._process = function (frameState) {
+ if (this._state === GltfLoaderState.READY) {
+ return true;
+ }
+
+ if (this._state === GltfLoaderState.PROCESSING) {
+ processLoaders(this, frameState);
+ }
+
+ if (this._state === GltfLoaderState.POST_PROCESSING) {
+ postProcessGeometry(this, frameState.context);
+ this._state = GltfLoaderState.PROCESSED;
+ }
+
+ if (this._state === GltfLoaderState.PROCESSED) {
+ // The buffer views can be unloaded once the data is copied.
+ unloadBufferViewLoaders(this);
+
+ // Similarly, if the glTF was loaded from a typed array, release the memory
+ this._typedArray = undefined;
+
+ this._state = GltfLoaderState.READY;
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Process textures other than textures
+ * @private
+ */
+GltfLoader.prototype._processTextures = function (frameState) {
+ if (this._textureState === GltfLoaderState.READY) {
+ return true;
+ }
+
+ if (this._textureState !== GltfLoaderState.PROCESSING) {
+ return false;
+ }
+
+ let i;
+ let ready = true;
+ const textureLoaders = this._textureLoaders;
+ const textureLoadersLength = textureLoaders.length;
+ for (i = 0; i < textureLoadersLength; ++i) {
+ const textureReady = textureLoaders[i].process(frameState);
+ if (textureReady && defined(this._textureCallbacks[i])) {
+ this._textureCallbacks[i]();
+ this._textureCallbacks[i] = undefined;
+ }
+
+ ready = ready && textureReady;
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ this._textureState = GltfLoaderState.READY;
+ return true;
+};
+
/**
* Processes the resource until it becomes ready.
*
@@ -560,12 +605,62 @@ GltfLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- this._process(this, frameState);
+ if (
+ this._state === GltfLoaderState.LOADED &&
+ !defined(this._loadResourcesPromise)
+ ) {
+ this._loadResourcesPromise = loadResources(this, frameState).catch(
+ (error) => {
+ this._processError = error;
+ }
+ );
+ }
+
+ if (defined(this._processError)) {
+ this._state = GltfLoaderState.FAILED;
+ const error = this._processError;
+ this._processError = undefined;
+ handleError(this, error);
+ }
+
+ // Pop the next error of the list in case there are multiple
+ const textureError = this._textureErrors.pop();
+ if (defined(textureError)) {
+ // There shouldn't be the need to completely unload in this case. Just throw the error.
+ const error = this.getError("Failed to load glTF texture", textureError);
+ error.name = "TextureError";
+ throw error;
+ }
+
+ let ready = false;
+ if (this._state === GltfLoaderState.FAILED) {
+ return ready;
+ }
+
+ try {
+ ready = this._process(frameState);
+ } catch (error) {
+ this._state = GltfLoaderState.FAILED;
+ handleError(this, error);
+ }
+
// Since textures can be loaded independently and are handled through a separate promise, they are processed in their own function
- this._processTextures(this, frameState);
+ let texturesReady = false;
+ try {
+ texturesReady = this._processTextures(frameState);
+ } catch (error) {
+ this._textureState = GltfLoaderState.FAILED;
+ handleError(this, error);
+ }
+
+ if (this._incrementallyLoadTextures) {
+ return ready;
+ }
+
+ return ready && texturesReady;
};
-function loadVertexBuffer(
+function getVertexBufferLoader(
loader,
gltf,
accessorId,
@@ -578,7 +673,7 @@ function loadVertexBuffer(
const accessor = gltf.accessors[accessorId];
const bufferViewId = accessor.bufferView;
- const vertexBufferLoader = ResourceCache.loadVertexBuffer({
+ const vertexBufferLoader = ResourceCache.getVertexBufferLoader({
gltf: gltf,
gltfResource: loader._gltfResource,
baseResource: loader._baseResource,
@@ -592,12 +687,10 @@ function loadVertexBuffer(
loadTypedArray: loadTypedArray,
});
- loader._geometryLoaders.push(vertexBufferLoader);
-
return vertexBufferLoader;
}
-function loadIndexBuffer(
+function getIndexBufferLoader(
loader,
gltf,
accessorId,
@@ -606,7 +699,7 @@ function loadIndexBuffer(
loadTypedArray,
frameState
) {
- const indexBufferLoader = ResourceCache.loadIndexBuffer({
+ const indexBufferLoader = ResourceCache.getIndexBufferLoader({
gltf: gltf,
accessorId: accessorId,
gltfResource: loader._gltfResource,
@@ -618,13 +711,11 @@ function loadIndexBuffer(
loadTypedArray: loadTypedArray,
});
- loader._geometryLoaders.push(indexBufferLoader);
-
return indexBufferLoader;
}
-function loadBufferView(loader, gltf, bufferViewId) {
- const bufferViewLoader = ResourceCache.loadBufferView({
+function getBufferViewLoader(loader, gltf, bufferViewId) {
+ const bufferViewLoader = ResourceCache.getBufferViewLoader({
gltf: gltf,
bufferViewId: bufferViewId,
gltfResource: loader._gltfResource,
@@ -727,11 +818,13 @@ function loadAccessor(loader, gltf, accessorId, useQuaternion) {
const bufferViewId = accessor.bufferView;
if (defined(bufferViewId)) {
- const bufferViewLoader = loadBufferView(loader, gltf, bufferViewId);
- const promise = bufferViewLoader.promise.then(function (bufferViewLoader) {
+ const bufferViewLoader = getBufferViewLoader(loader, gltf, bufferViewId);
+ const promise = (async () => {
+ await bufferViewLoader.load();
if (loader.isDestroyed()) {
return;
}
+
const bufferViewTypedArray = bufferViewLoader.typedArray;
const typedArray = getPackedTypedArray(
gltf,
@@ -741,7 +834,8 @@ function loadAccessor(loader, gltf, accessorId, useQuaternion) {
useQuaternion = defaultValue(useQuaternion, false);
loadAccessorValues(accessor, typedArray, values, useQuaternion);
- });
+ })();
+
loader._loaderPromises.push(promise);
return values;
@@ -1062,7 +1156,7 @@ function loadAttribute(
return attribute;
}
- const vertexBufferLoader = loadVertexBuffer(
+ const vertexBufferLoader = getVertexBufferLoader(
loader,
gltf,
accessorId,
@@ -1072,13 +1166,15 @@ function loadAttribute(
loadTypedArray,
frameState
);
- const promise = vertexBufferLoader.promise.then(function (
- vertexBufferLoader
- ) {
- if (loader.isDestroyed()) {
- return;
- }
+ const index = loader._geometryLoaders.length;
+ loader._geometryLoaders.push(vertexBufferLoader);
+ const promise = vertexBufferLoader.load();
+ loader._loaderPromises.push(promise);
+ // This can only execute once vertexBufferLoader.process() has run and returns true
+ // Save this finish callback by the loader index so it can be called
+ // in process().
+ loader._geometryCallbacks[index] = () => {
if (
defined(draco) &&
defined(draco.attributes) &&
@@ -1100,9 +1196,7 @@ function loadAttribute(
loadTypedArray
);
}
- });
-
- loader._loaderPromises.push(promise);
+ };
return attribute;
}
@@ -1276,7 +1370,7 @@ function loadIndices(
const loadBuffer = needsPostProcessing ? false : outputBuffer;
const loadTypedArray = needsPostProcessing ? true : outputTypedArray;
- const indexBufferLoader = loadIndexBuffer(
+ const indexBufferLoader = getIndexBufferLoader(
loader,
gltf,
accessorId,
@@ -1286,18 +1380,18 @@ function loadIndices(
frameState
);
- const promise = indexBufferLoader.promise.then(function (indexBufferLoader) {
- if (loader.isDestroyed()) {
- return;
- }
-
+ const index = loader._geometryLoaders.length;
+ loader._geometryLoaders.push(indexBufferLoader);
+ const promise = indexBufferLoader.load();
+ loader._loaderPromises.push(promise);
+ // This can only execute once indexBufferLoader.process() has run and returns true
+ // Save this finish callback by the loader index so it can be called
+ // in process().
+ loader._geometryCallbacks[index] = () => {
indices.indexDatatype = indexBufferLoader.indexDatatype;
-
indices.buffer = indexBufferLoader.buffer;
indices.typedArray = indexBufferLoader.typedArray;
- });
-
- loader._loaderPromises.push(promise);
+ };
const indicesPlan = new PrimitiveLoadPlan.IndicesLoadPlan(indices);
indicesPlan.loadBuffer = outputBuffer;
@@ -1324,7 +1418,7 @@ function loadTexture(
return undefined;
}
- const textureLoader = ResourceCache.loadTexture({
+ const textureLoader = ResourceCache.getTextureLoader({
gltf: gltf,
textureInfo: textureInfo,
gltfResource: loader._gltfResource,
@@ -1334,23 +1428,37 @@ function loadTexture(
asynchronous: loader._asynchronous,
});
- loader._textureLoaders.push(textureLoader);
-
const textureReader = GltfLoaderUtil.createModelTextureReader({
textureInfo: textureInfo,
});
- const promise = textureLoader.promise.then(function (textureLoader) {
- if (loader.isUnloaded() || loader.isDestroyed()) {
+ const index = loader._textureLoaders.length;
+ loader._textureLoaders.push(textureLoader);
+ const promise = textureLoader.load().catch((error) => {
+ if (loader.isDestroyed()) {
return;
}
+
+ if (!loader._incrementallyLoadTextures) {
+ // If incrementallyLoadTextures is false, throw the error to ensure the loader state
+ // immediately is set to have failed
+ throw error;
+ }
+
+ // Otherwise, save the error so it can be thrown next
+ loader._textureState = GltfLoaderState.FAILED;
+ loader._textureErrors.push(error);
+ });
+ loader._texturesPromises.push(promise);
+ // This can only execute once textureLoader.process() has run and returns true
+ // Save this finish callback by the loader index so it can be called
+ // in process().
+ loader._textureCallbacks[index] = () => {
textureReader.texture = textureLoader.texture;
if (defined(samplerOverride)) {
textureReader.texture.sampler = samplerOverride;
}
- });
-
- loader._texturesPromises.push(promise);
+ };
return textureReader;
}
@@ -2246,7 +2354,7 @@ function loadSkins(loader, gltf, nodes) {
return skins;
}
-function loadStructuralMetadata(
+async function loadStructuralMetadata(
loader,
gltf,
extension,
@@ -2264,11 +2372,8 @@ function loadStructuralMetadata(
frameState: frameState,
asynchronous: loader._asynchronous,
});
- structuralMetadataLoader.load();
-
loader._structuralMetadataLoader = structuralMetadataLoader;
-
- return structuralMetadataLoader;
+ return structuralMetadataLoader.load();
}
function loadAnimationSampler(loader, gltf, gltfSampler) {
@@ -2442,32 +2547,7 @@ function loadScene(gltf, nodes) {
const scratchCenter = new Cartesian3();
-function parse(
- loader,
- gltf,
- supportedImageFormats,
- frameState,
- rejectPromise,
- rejectTexturesPromise
-) {
- const version = gltf.asset.version;
- if (version !== "1.0" && version !== "2.0") {
- const url = loader._gltfResource.url;
- throw new RuntimeError(
- `Failed to load ${url}: \nUnsupported glTF version: ${version}`
- );
- }
- const extensionsRequired = gltf.extensionsRequired;
- if (defined(extensionsRequired)) {
- ModelUtility.checkSupportedExtensions(extensionsRequired);
-
- // Check for the KHR_mesh_quantization extension here, it will be used later
- // in loadAttribute().
- loader._hasKhrMeshQuantization = extensionsRequired.includes(
- "KHR_mesh_quantization"
- );
- }
-
+function parse(loader, gltf, supportedImageFormats, frameState) {
const extensions = defaultValue(gltf.extensions, defaultValue.EMPTY_OBJECT);
const structuralMetadataExtension = extensions.EXT_structural_metadata;
const featureMetadataExtensionLegacy = extensions.EXT_feature_metadata;
@@ -2535,7 +2615,7 @@ function parse(
defined(structuralMetadataExtension) ||
defined(featureMetadataExtensionLegacy)
) {
- const structuralMetadataLoader = loadStructuralMetadata(
+ const promise = loadStructuralMetadata(
loader,
gltf,
structuralMetadataExtension,
@@ -2543,46 +2623,24 @@ function parse(
supportedImageFormats,
frameState
);
- const promise = structuralMetadataLoader.promise.then(function (
- structuralMetadataLoader
- ) {
- if (loader.isDestroyed()) {
- return;
- }
- components.structuralMetadata =
- structuralMetadataLoader.structuralMetadata;
- });
loader._loaderPromises.push(promise);
}
- // Gather promises and reject if any promises fail.
+ // Gather promises and handle any errors
const readyPromises = [];
readyPromises.push.apply(readyPromises, loader._loaderPromises);
+ // When incrementallyLoadTextures is true, the errors are caught and thrown individually
+ // since it doesn't affect the overall loader state
if (!loader._incrementallyLoadTextures) {
readyPromises.push.apply(readyPromises, loader._texturesPromises);
}
- Promise.all(readyPromises)
- .then(function () {
- if (loader.isDestroyed()) {
- return;
- }
- loader._state = GltfLoaderState.POST_PROCESSING;
- })
- .catch(rejectPromise);
-
- // Separate promise will resolve once textures are loaded.
- Promise.all(loader._texturesPromises)
- .then(function () {
- if (loader.isDestroyed()) {
- return;
- }
-
- // post processing only applies for geometry
- loader._textureState = GltfLoaderState.PROCESSED;
- })
- .catch(rejectTexturesPromise);
+ Promise.all(readyPromises).catch((error) => {
+ // This error will be thrown synchronously during the next call
+ // to process()
+ loader._processError = error;
+ });
}
function unloadTextures(loader) {
@@ -2594,7 +2652,7 @@ function unloadTextures(loader) {
loader._textureLoaders.length = 0;
}
-function unloadBufferViews(loader) {
+function unloadBufferViewLoaders(loader) {
const bufferViewLoaders = loader._bufferViewLoaders;
const bufferViewLoadersLength = bufferViewLoaders.length;
for (let i = 0; i < bufferViewLoadersLength; ++i) {
@@ -2644,13 +2702,13 @@ GltfLoader.prototype.isUnloaded = function () {
* @private
*/
GltfLoader.prototype.unload = function () {
- if (defined(this._gltfJsonLoader)) {
+ if (defined(this._gltfJsonLoader) && !this._gltfJsonLoader.isDestroyed()) {
ResourceCache.unload(this._gltfJsonLoader);
}
this._gltfJsonLoader = undefined;
unloadTextures(this);
- unloadBufferViews(this);
+ unloadBufferViewLoaders(this);
unloadGeometry(this);
unloadGeneratedAttributes(this);
unloadStructuralMetadata(this);
diff --git a/packages/engine/Source/Scene/GltfStructuralMetadataLoader.js b/packages/engine/Source/Scene/GltfStructuralMetadataLoader.js
index 032bcdf3f772..c320f05b31dd 100644
--- a/packages/engine/Source/Scene/GltfStructuralMetadataLoader.js
+++ b/packages/engine/Source/Scene/GltfStructuralMetadataLoader.js
@@ -68,7 +68,9 @@ function GltfStructuralMetadataLoader(options) {
this._cacheKey = cacheKey;
this._asynchronous = asynchronous;
this._bufferViewLoaders = [];
+ this._bufferViewIds = [];
this._textureLoaders = [];
+ this._textureIds = [];
this._schemaLoader = undefined;
this._structuralMetadata = undefined;
this._state = ResourceLoaderState.UNLOADED;
@@ -83,20 +85,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfStructuralMetadataLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfStructuralMetadataLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -133,55 +121,39 @@ Object.defineProperties(GltfStructuralMetadataLoader.prototype, {
* @private
*/
GltfStructuralMetadataLoader.prototype.load = function () {
- const bufferViewsPromise = loadBufferViews(this);
- const texturesPromise = loadTextures(this);
- const schemaPromise = loadSchema(this);
+ if (defined(this._promise)) {
+ return this._promise;
+ }
- this._gltf = undefined; // No longer need to hold onto the glTF
this._state = ResourceLoaderState.LOADING;
+ this._promise = (async () => {
+ try {
+ const bufferViewsPromise = loadBufferViews(this);
+ const texturesPromise = loadTextures(this);
+ const schemaPromise = loadSchema(this);
- const that = this;
+ await Promise.all([bufferViewsPromise, texturesPromise, schemaPromise]);
- this._promise = Promise.all([
- bufferViewsPromise,
- texturesPromise,
- schemaPromise,
- ])
- .then(function (results) {
- if (that.isDestroyed()) {
+ if (this.isDestroyed()) {
return;
}
- const bufferViews = results[0];
- const textures = results[1];
- const schema = results[2];
-
- if (defined(that._extension)) {
- that._structuralMetadata = parseStructuralMetadata({
- extension: that._extension,
- schema: schema,
- bufferViews: bufferViews,
- textures: textures,
- });
- } else {
- that._structuralMetadata = parseFeatureMetadataLegacy({
- extension: that._extensionLegacy,
- schema: schema,
- bufferViews: bufferViews,
- textures: textures,
- });
- }
- that._state = ResourceLoaderState.READY;
- return that;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
+
+ this._gltf = undefined; // No longer need to hold onto the glTF
+
+ this._state = ResourceLoaderState.LOADED;
+ return this;
+ } catch (error) {
+ if (this.isDestroyed()) {
return;
}
- that.unload();
- that._state = ResourceLoaderState.FAILED;
+
+ this.unload();
+ this._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load structural metadata";
- return Promise.reject(that.getError(errorMessage, error));
- });
+ throw this.getError(errorMessage, error);
+ }
+ })();
+ return this._promise;
};
function gatherBufferViewIdsFromProperties(properties, bufferViewIdSet) {
@@ -261,7 +233,7 @@ function gatherUsedBufferViewIdsLegacy(extensionLegacy) {
return bufferViewIdSet;
}
-function loadBufferViews(structuralMetadataLoader) {
+async function loadBufferViews(structuralMetadataLoader) {
let bufferViewIds;
if (defined(structuralMetadataLoader._extension)) {
bufferViewIds = gatherUsedBufferViewIds(
@@ -275,40 +247,23 @@ function loadBufferViews(structuralMetadataLoader) {
// Load the buffer views
const bufferViewPromises = [];
- const bufferViewLoaders = {};
for (const bufferViewId in bufferViewIds) {
if (bufferViewIds.hasOwnProperty(bufferViewId)) {
- const bufferViewLoader = ResourceCache.loadBufferView({
+ const bufferViewLoader = ResourceCache.getBufferViewLoader({
gltf: structuralMetadataLoader._gltf,
bufferViewId: parseInt(bufferViewId),
gltfResource: structuralMetadataLoader._gltfResource,
baseResource: structuralMetadataLoader._baseResource,
});
- bufferViewPromises.push(bufferViewLoader.promise);
+
structuralMetadataLoader._bufferViewLoaders.push(bufferViewLoader);
- bufferViewLoaders[bufferViewId] = bufferViewLoader;
- }
- }
+ structuralMetadataLoader._bufferViewIds.push(bufferViewId);
- // Return a promise to a map of buffer view IDs to typed arrays
- return Promise.all(bufferViewPromises).then(function () {
- const bufferViews = {};
- for (const bufferViewId in bufferViewLoaders) {
- if (bufferViewLoaders.hasOwnProperty(bufferViewId)) {
- const bufferViewLoader = bufferViewLoaders[bufferViewId];
- // Copy the typed array and let the underlying ArrayBuffer be freed
- const bufferViewTypedArray = new Uint8Array(
- bufferViewLoader.typedArray
- );
- bufferViews[bufferViewId] = bufferViewTypedArray;
- }
+ bufferViewPromises.push(bufferViewLoader.load());
}
+ }
- // Buffer views can be unloaded after the data has been copied
- unloadBufferViews(structuralMetadataLoader);
-
- return bufferViews;
- });
+ return Promise.all(bufferViewPromises);
}
function gatherUsedTextureIds(structuralMetadataExtension) {
@@ -385,10 +340,9 @@ function loadTextures(structuralMetadataLoader) {
// Load the textures
const texturePromises = [];
- const textureLoaders = {};
for (const textureId in textureIds) {
if (textureIds.hasOwnProperty(textureId)) {
- const textureLoader = ResourceCache.loadTexture({
+ const textureLoader = ResourceCache.getTextureLoader({
gltf: gltf,
textureInfo: textureIds[textureId],
gltfResource: gltfResource,
@@ -397,26 +351,16 @@ function loadTextures(structuralMetadataLoader) {
frameState: frameState,
asynchronous: asynchronous,
});
- texturePromises.push(textureLoader.promise);
structuralMetadataLoader._textureLoaders.push(textureLoader);
- textureLoaders[textureId] = textureLoader;
+ structuralMetadataLoader._textureIds.push(textureId);
+ texturePromises.push(textureLoader.load());
}
}
- // Return a promise to a map of texture IDs to Texture objects
- return Promise.all(texturePromises).then(function () {
- const textures = {};
- for (const textureId in textureLoaders) {
- if (textureLoaders.hasOwnProperty(textureId)) {
- const textureLoader = textureLoaders[textureId];
- textures[textureId] = textureLoader.texture;
- }
- }
- return textures;
- });
+ return Promise.all(texturePromises);
}
-function loadSchema(structuralMetadataLoader) {
+async function loadSchema(structuralMetadataLoader) {
const extension = defaultValue(
structuralMetadataLoader._extension,
structuralMetadataLoader._extensionLegacy
@@ -427,20 +371,20 @@ function loadSchema(structuralMetadataLoader) {
const resource = structuralMetadataLoader._baseResource.getDerivedResource({
url: extension.schemaUri,
});
- schemaLoader = ResourceCache.loadSchema({
+ schemaLoader = ResourceCache.getSchemaLoader({
resource: resource,
});
} else {
- schemaLoader = ResourceCache.loadSchema({
+ schemaLoader = ResourceCache.getSchemaLoader({
schema: extension.schema,
});
}
structuralMetadataLoader._schemaLoader = schemaLoader;
-
- return schemaLoader.promise.then(function (schemaLoader) {
+ await schemaLoader.load();
+ if (!schemaLoader.isDestroyed()) {
return schemaLoader.schema;
- });
+ }
}
/**
@@ -454,17 +398,68 @@ GltfStructuralMetadataLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- if (this._state !== ResourceLoaderState.LOADING) {
- return;
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (this._state !== ResourceLoaderState.LOADED) {
+ return false;
}
const textureLoaders = this._textureLoaders;
const textureLoadersLength = textureLoaders.length;
-
+ let ready = true;
for (let i = 0; i < textureLoadersLength; ++i) {
const textureLoader = textureLoaders[i];
- textureLoader.process(frameState);
+ const textureReady = textureLoader.process(frameState);
+ ready = ready && textureReady;
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ const schema = this._schemaLoader.schema;
+ const bufferViews = {};
+ for (let i = 0; i < this._bufferViewIds.length; ++i) {
+ const bufferViewId = this._bufferViewIds[i];
+ const bufferViewLoader = this._bufferViewLoaders[i];
+ if (!bufferViewLoader.isDestroyed()) {
+ // Copy the typed array and let the underlying ArrayBuffer be freed
+ const bufferViewTypedArray = new Uint8Array(bufferViewLoader.typedArray);
+ bufferViews[bufferViewId] = bufferViewTypedArray;
+ }
}
+
+ const textures = {};
+ for (let i = 0; i < this._textureIds.length; ++i) {
+ const textureId = this._textureIds[i];
+ const textureLoader = textureLoaders[i];
+ if (!textureLoader.isDestroyed()) {
+ textures[textureId] = textureLoader.texture;
+ }
+ }
+ if (defined(this._extension)) {
+ this._structuralMetadata = parseStructuralMetadata({
+ extension: this._extension,
+ schema: schema,
+ bufferViews: bufferViews,
+ textures: textures,
+ });
+ } else {
+ this._structuralMetadata = parseFeatureMetadataLegacy({
+ extension: this._extensionLegacy,
+ schema: schema,
+ bufferViews: bufferViews,
+ textures: textures,
+ });
+ }
+
+ // Buffer views can be unloaded after the data has been copied
+ unloadBufferViews(this);
+
+ this._state = ResourceLoaderState.READY;
+ return true;
};
function unloadBufferViews(structuralMetadataLoader) {
@@ -474,6 +469,7 @@ function unloadBufferViews(structuralMetadataLoader) {
ResourceCache.unload(bufferViewLoaders[i]);
}
structuralMetadataLoader._bufferViewLoaders.length = 0;
+ structuralMetadataLoader._bufferViewIds.length = 0;
}
function unloadTextures(structuralMetadataLoader) {
@@ -483,6 +479,7 @@ function unloadTextures(structuralMetadataLoader) {
ResourceCache.unload(textureLoaders[i]);
}
structuralMetadataLoader._textureLoaders.length = 0;
+ structuralMetadataLoader._textureIds.length = 0;
}
/**
diff --git a/packages/engine/Source/Scene/GltfTextureLoader.js b/packages/engine/Source/Scene/GltfTextureLoader.js
index 4c6139914c95..a71a36448d8f 100644
--- a/packages/engine/Source/Scene/GltfTextureLoader.js
+++ b/packages/engine/Source/Scene/GltfTextureLoader.js
@@ -78,7 +78,6 @@ function GltfTextureLoader(options) {
this._texture = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
- this._process = function (loader, frameState) {};
}
if (defined(Object.create)) {
@@ -87,20 +86,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfTextureLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfTextureLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -138,86 +123,45 @@ const scratchTextureJob = new CreateTextureJob();
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfTextureLoader.prototype.load = function () {
- const resourceCache = this._resourceCache;
- const imageLoader = resourceCache.loadImage({
- gltf: this._gltf,
- imageId: this._imageId,
- gltfResource: this._gltfResource,
- baseResource: this._baseResource,
- });
+GltfTextureLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
- this._imageLoader = imageLoader;
this._state = ResourceLoaderState.LOADING;
- const that = this;
- const processPromise = new Promise(function (resolve) {
- that._process = function (loader, frameState) {
- if (defined(loader._texture)) {
- // Already created texture
- return;
- }
-
- if (!defined(loader._image)) {
- // Not ready to create texture
+ const resourceCache = this._resourceCache;
+ this._promise = (async () => {
+ try {
+ const imageLoader = resourceCache.getImageLoader({
+ gltf: this._gltf,
+ imageId: this._imageId,
+ gltfResource: this._gltfResource,
+ baseResource: this._baseResource,
+ });
+ this._imageLoader = imageLoader;
+ await imageLoader.load();
+
+ if (this.isDestroyed()) {
return;
}
- let texture;
-
- if (loader._asynchronous) {
- const textureJob = scratchTextureJob;
- textureJob.set(
- loader._gltf,
- loader._textureInfo,
- loader._image,
- loader._mipLevels,
- frameState.context
- );
- const jobScheduler = frameState.jobScheduler;
- if (!jobScheduler.execute(textureJob, JobType.TEXTURE)) {
- // Job scheduler is full. Try again next frame.
- return;
- }
- texture = textureJob.texture;
- } else {
- texture = createTexture(
- loader._gltf,
- loader._textureInfo,
- loader._image,
- loader._mipLevels,
- frameState.context
- );
- }
-
- // Unload everything except the texture
- loader.unload();
-
- loader._texture = texture;
- loader._state = ResourceLoaderState.READY;
- resolve(loader);
- };
- });
-
- this._promise = imageLoader.promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
// Now wait for process() to run to finish loading
- that._image = imageLoader.image;
- that._mipLevels = imageLoader.mipLevels;
- that._state = ResourceLoaderState.PROCESSING;
- return processPromise;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
+ this._image = imageLoader.image;
+ this._mipLevels = imageLoader.mipLevels;
+ this._state = ResourceLoaderState.LOADED;
+
+ return this;
+ } catch (error) {
+ if (this.isDestroyed()) {
return;
}
- that.unload();
- that._state = ResourceLoaderState.FAILED;
+
+ this.unload();
+ this._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load texture";
- return Promise.reject(that.getError(errorMessage, error));
- });
+ throw this.getError(errorMessage, error);
+ }
+ })();
return this._promise;
};
@@ -350,6 +294,7 @@ function createTexture(gltf, textureInfo, image, mipLevels, context) {
* Processes the resource until it becomes ready.
*
* @param {FrameState} frameState The frame state.
+ * @returns {boolean} true once all resourced are ready.
* @private
*/
GltfTextureLoader.prototype.process = function (frameState) {
@@ -357,7 +302,62 @@ GltfTextureLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- return this._process(this, frameState);
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (
+ this._state !== ResourceLoaderState.LOADED &&
+ this._state !== ResourceLoaderState.PROCESSING
+ ) {
+ return false;
+ }
+
+ if (defined(this._texture)) {
+ // Already created texture
+ return false;
+ }
+
+ if (!defined(this._image)) {
+ // Not ready to create texture
+ return false;
+ }
+
+ this._state = ResourceLoaderState.PROCESSING;
+
+ let texture;
+ if (this._asynchronous) {
+ const textureJob = scratchTextureJob;
+ textureJob.set(
+ this._gltf,
+ this._textureInfo,
+ this._image,
+ this._mipLevels,
+ frameState.context
+ );
+ const jobScheduler = frameState.jobScheduler;
+ if (!jobScheduler.execute(textureJob, JobType.TEXTURE)) {
+ // Job scheduler is full. Try again next frame.
+ return;
+ }
+ texture = textureJob.texture;
+ } else {
+ texture = createTexture(
+ this._gltf,
+ this._textureInfo,
+ this._image,
+ this._mipLevels,
+ frameState.context
+ );
+ }
+
+ // Unload everything except the texture
+ this.unload();
+
+ this._texture = texture;
+ this._state = ResourceLoaderState.READY;
+ this._resourceCache.statistics.addTextureLoader(this);
+ return true;
};
/**
diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js
index 425d3bad123e..a2769521a398 100644
--- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js
+++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js
@@ -115,7 +115,6 @@ function GltfVertexBufferLoader(options) {
this._buffer = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
- this._process = function (loader, frameState) {};
}
if (defined(Object.create)) {
@@ -124,20 +123,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GltfVertexBufferLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof GltfVertexBufferLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -209,77 +194,17 @@ function hasDracoCompression(draco, semantic) {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-GltfVertexBufferLoader.prototype.load = function () {
- let promise;
+GltfVertexBufferLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
if (hasDracoCompression(this._draco, this._attributeSemantic)) {
- promise = loadFromDraco(this);
- } else {
- promise = loadFromBufferView(this);
+ this._promise = loadFromDraco(this);
+ return this._promise;
}
- const that = this;
- const scratchVertexBufferJob = new CreateVertexBufferJob();
- const processPromise = new Promise(function (resolve) {
- that._process = function (loader, frameState) {
- if (loader._state === ResourceLoaderState.READY) {
- return;
- }
-
- const typedArray = loader._typedArray;
-
- if (defined(loader._dracoLoader)) {
- loader._dracoLoader.process(frameState);
- }
-
- if (defined(loader._bufferViewLoader)) {
- loader._bufferViewLoader.process(frameState);
- }
-
- if (!defined(typedArray)) {
- // Buffer view hasn't been loaded yet
- return;
- }
-
- let buffer;
- if (loader._loadBuffer && loader._asynchronous) {
- const vertexBufferJob = scratchVertexBufferJob;
- vertexBufferJob.set(typedArray, frameState.context);
- const jobScheduler = frameState.jobScheduler;
- if (!jobScheduler.execute(vertexBufferJob, JobType.BUFFER)) {
- // Job scheduler is full. Try again next frame.
- return;
- }
- buffer = vertexBufferJob.buffer;
- } else if (loader._loadBuffer) {
- buffer = createVertexBuffer(typedArray, frameState.context);
- }
-
- // Unload everything except the vertex buffer
- loader.unload();
-
- loader._buffer = buffer;
- loader._typedArray = loader._loadTypedArray ? typedArray : undefined;
- loader._state = ResourceLoaderState.READY;
- resolve(loader);
- };
- });
-
- this._promise = promise
- .then(function () {
- if (that.isDestroyed()) {
- return;
- }
-
- return processPromise;
- })
- .catch(function (error) {
- if (that.isDestroyed()) {
- return;
- }
-
- return handleError(that, error);
- });
+ this._promise = loadFromBufferView(this);
return this._promise;
};
@@ -335,79 +260,98 @@ function getQuantizationInformation(
return quantization;
}
-function loadFromDraco(vertexBufferLoader) {
- const resourceCache = vertexBufferLoader._resourceCache;
- const dracoLoader = resourceCache.loadDraco({
- gltf: vertexBufferLoader._gltf,
- draco: vertexBufferLoader._draco,
- gltfResource: vertexBufferLoader._gltfResource,
- baseResource: vertexBufferLoader._baseResource,
- });
-
- vertexBufferLoader._dracoLoader = dracoLoader;
+async function loadFromDraco(vertexBufferLoader) {
vertexBufferLoader._state = ResourceLoaderState.LOADING;
+ const resourceCache = vertexBufferLoader._resourceCache;
+ try {
+ const dracoLoader = resourceCache.getDracoLoader({
+ gltf: vertexBufferLoader._gltf,
+ draco: vertexBufferLoader._draco,
+ gltfResource: vertexBufferLoader._gltfResource,
+ baseResource: vertexBufferLoader._baseResource,
+ });
+ vertexBufferLoader._dracoLoader = dracoLoader;
+ await dracoLoader.load();
- return dracoLoader.promise.then(function () {
if (vertexBufferLoader.isDestroyed()) {
return;
}
- // Get the typed array and quantization information
- const decodedVertexAttributes = dracoLoader.decodedData.vertexAttributes;
- const attributeSemantic = vertexBufferLoader._attributeSemantic;
- const dracoAttribute = decodedVertexAttributes[attributeSemantic];
- const accessorId = vertexBufferLoader._accessorId;
- const accessor = vertexBufferLoader._gltf.accessors[accessorId];
- const type = accessor.type;
- const typedArray = dracoAttribute.array;
- const dracoQuantization = dracoAttribute.data.quantization;
- if (defined(dracoQuantization)) {
- vertexBufferLoader._quantization = getQuantizationInformation(
- dracoQuantization,
- dracoAttribute.data.componentDatatype,
- dracoAttribute.data.componentsPerAttribute,
- type
- );
- }
// Now wait for process() to run to finish loading
- vertexBufferLoader._typedArray = new Uint8Array(
- typedArray.buffer,
- typedArray.byteOffset,
- typedArray.byteLength
- );
- vertexBufferLoader._state = ResourceLoaderState.PROCESSING;
+ vertexBufferLoader._state = ResourceLoaderState.LOADED;
return vertexBufferLoader;
- });
+ } catch {
+ if (vertexBufferLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(vertexBufferLoader);
+ }
}
-function loadFromBufferView(vertexBufferLoader) {
- const resourceCache = vertexBufferLoader._resourceCache;
- const bufferViewLoader = resourceCache.loadBufferView({
- gltf: vertexBufferLoader._gltf,
- bufferViewId: vertexBufferLoader._bufferViewId,
- gltfResource: vertexBufferLoader._gltfResource,
- baseResource: vertexBufferLoader._baseResource,
- });
+function processDraco(vertexBufferLoader) {
+ vertexBufferLoader._state = ResourceLoaderState.PROCESSING;
+ const dracoLoader = vertexBufferLoader._dracoLoader;
+
+ // Get the typed array and quantization information
+ const decodedVertexAttributes = dracoLoader.decodedData.vertexAttributes;
+ const attributeSemantic = vertexBufferLoader._attributeSemantic;
+ const dracoAttribute = decodedVertexAttributes[attributeSemantic];
+ const accessorId = vertexBufferLoader._accessorId;
+ const accessor = vertexBufferLoader._gltf.accessors[accessorId];
+ const type = accessor.type;
+ const typedArray = dracoAttribute.array;
+ const dracoQuantization = dracoAttribute.data.quantization;
+ if (defined(dracoQuantization)) {
+ vertexBufferLoader._quantization = getQuantizationInformation(
+ dracoQuantization,
+ dracoAttribute.data.componentDatatype,
+ dracoAttribute.data.componentsPerAttribute,
+ type
+ );
+ }
+
+ vertexBufferLoader._typedArray = new Uint8Array(
+ typedArray.buffer,
+ typedArray.byteOffset,
+ typedArray.byteLength
+ );
+}
+
+async function loadFromBufferView(vertexBufferLoader) {
vertexBufferLoader._state = ResourceLoaderState.LOADING;
- vertexBufferLoader._bufferViewLoader = bufferViewLoader;
+ const resourceCache = vertexBufferLoader._resourceCache;
+ try {
+ const bufferViewLoader = resourceCache.getBufferViewLoader({
+ gltf: vertexBufferLoader._gltf,
+ bufferViewId: vertexBufferLoader._bufferViewId,
+ gltfResource: vertexBufferLoader._gltfResource,
+ baseResource: vertexBufferLoader._baseResource,
+ });
+ vertexBufferLoader._bufferViewLoader = bufferViewLoader;
+ await bufferViewLoader.load();
- return bufferViewLoader.promise.then(function () {
if (vertexBufferLoader.isDestroyed()) {
return;
}
- // Now wait for process() to run to finish loading
+
vertexBufferLoader._typedArray = bufferViewLoader.typedArray;
vertexBufferLoader._state = ResourceLoaderState.PROCESSING;
return vertexBufferLoader;
- });
+ } catch (error) {
+ if (vertexBufferLoader.isDestroyed()) {
+ return;
+ }
+
+ handleError(vertexBufferLoader, error);
+ }
}
function handleError(vertexBufferLoader, error) {
vertexBufferLoader.unload();
vertexBufferLoader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load vertex buffer";
- error = vertexBufferLoader.getError(errorMessage, error);
- return Promise.reject(error);
+ throw vertexBufferLoader.getError(errorMessage, error);
}
function CreateVertexBufferJob() {
@@ -435,6 +379,8 @@ function createVertexBuffer(typedArray, context) {
return buffer;
}
+const scratchVertexBufferJob = new CreateVertexBufferJob();
+
/**
* Processes the resource until it becomes ready.
*
@@ -446,7 +392,53 @@ GltfVertexBufferLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- return this._process(this, frameState);
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (
+ this._state !== ResourceLoaderState.LOADED &&
+ this._state !== ResourceLoaderState.PROCESSING
+ ) {
+ return false;
+ }
+
+ if (defined(this._dracoLoader)) {
+ try {
+ const ready = this._dracoLoader.process(frameState);
+ if (!ready) {
+ return false;
+ }
+ } catch (error) {
+ handleError(this, error);
+ }
+
+ processDraco(this);
+ }
+
+ let buffer;
+ const typedArray = this._typedArray;
+ if (this._loadBuffer && this._asynchronous) {
+ const vertexBufferJob = scratchVertexBufferJob;
+ vertexBufferJob.set(typedArray, frameState.context);
+ const jobScheduler = frameState.jobScheduler;
+ if (!jobScheduler.execute(vertexBufferJob, JobType.BUFFER)) {
+ // Job scheduler is full. Try again next frame.
+ return false;
+ }
+ buffer = vertexBufferJob.buffer;
+ } else if (this._loadBuffer) {
+ buffer = createVertexBuffer(typedArray, frameState.context);
+ }
+
+ // Unload everything except the vertex buffer
+ this.unload();
+
+ this._buffer = buffer;
+ this._typedArray = this._loadTypedArray ? typedArray : undefined;
+ this._state = ResourceLoaderState.READY;
+ this._resourceCache.statistics.addGeometryLoader(this);
+ return true;
};
/**
diff --git a/packages/engine/Source/Scene/ImplicitSubtree.js b/packages/engine/Source/Scene/ImplicitSubtree.js
index 9f0ec3cc8bad..3c7f585ba583 100644
--- a/packages/engine/Source/Scene/ImplicitSubtree.js
+++ b/packages/engine/Source/Scene/ImplicitSubtree.js
@@ -714,12 +714,12 @@ function requestExternalBuffer(subtree, bufferHeader) {
url: bufferHeader.uri,
});
- const bufferLoader = ResourceCache.loadExternalBuffer({
+ const bufferLoader = ResourceCache.getExternalBufferLoader({
resource: bufferResource,
});
subtree._bufferLoader = bufferLoader;
- return bufferLoader.promise.then(function (bufferLoader) {
+ return bufferLoader.load().then(function (bufferLoader) {
return bufferLoader.typedArray;
});
}
diff --git a/packages/engine/Source/Scene/MetadataSchemaLoader.js b/packages/engine/Source/Scene/MetadataSchemaLoader.js
index be21b606d479..0aab97eb23af 100644
--- a/packages/engine/Source/Scene/MetadataSchemaLoader.js
+++ b/packages/engine/Source/Scene/MetadataSchemaLoader.js
@@ -52,20 +52,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(MetadataSchemaLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof MetadataSchemaLoader.prototype
- *
- * @type {Promise.|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -101,37 +87,41 @@ Object.defineProperties(MetadataSchemaLoader.prototype, {
* @returns {Promise.} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
-MetadataSchemaLoader.prototype.load = function () {
+MetadataSchemaLoader.prototype.load = async function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
if (defined(this._schema)) {
this._promise = Promise.resolve(this);
- } else {
- this._promise = loadExternalSchema(this);
+ return this._promise;
}
+ this._promise = loadExternalSchema(this);
return this._promise;
};
-function loadExternalSchema(schemaLoader) {
+async function loadExternalSchema(schemaLoader) {
const resource = schemaLoader._resource;
schemaLoader._state = ResourceLoaderState.LOADING;
- return resource
- .fetchJson()
- .then(function (json) {
- if (schemaLoader.isDestroyed()) {
- return;
- }
- schemaLoader._schema = MetadataSchema.fromJson(json);
- schemaLoader._state = ResourceLoaderState.READY;
- return schemaLoader;
- })
- .catch(function (error) {
- if (schemaLoader.isDestroyed()) {
- return;
- }
- schemaLoader._state = ResourceLoaderState.FAILED;
- const errorMessage = `Failed to load schema: ${resource.url}`;
- return Promise.reject(schemaLoader.getError(errorMessage, error));
- });
+ try {
+ const json = await resource.fetchJson();
+ if (schemaLoader.isDestroyed()) {
+ return;
+ }
+
+ schemaLoader._schema = MetadataSchema.fromJson(json);
+ schemaLoader._state = ResourceLoaderState.READY;
+ return schemaLoader;
+ } catch (error) {
+ if (schemaLoader.isDestroyed()) {
+ return;
+ }
+
+ schemaLoader._state = ResourceLoaderState.FAILED;
+ const errorMessage = `Failed to load schema: ${resource.url}`;
+ throw schemaLoader.getError(errorMessage, error);
+ }
}
/**
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index cc1888dc4e07..5500c8faa330 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -6,9 +6,11 @@ import Credit from "../../Core/Credit.js";
import Color from "../../Core/Color.js";
import defined from "../../Core/defined.js";
import defaultValue from "../../Core/defaultValue.js";
+import deprecationWarning from "../../Core/deprecationWarning.js";
import DeveloperError from "../../Core/DeveloperError.js";
import destroyObject from "../../Core/destroyObject.js";
import DistanceDisplayCondition from "../../Core/DistanceDisplayCondition.js";
+import Event from "../../Core/Event.js";
import Matrix3 from "../../Core/Matrix3.js";
import Matrix4 from "../../Core/Matrix4.js";
import Resource from "../../Core/Resource.js";
@@ -39,7 +41,7 @@ import StyleCommandsNeeded from "./StyleCommandsNeeded.js";
/**
*
- * To construct a Model, call {@link Model.fromGltf}. Do not call the constructor directly.
+ * To construct a Model, call {@link Model.fromGltfAsync}. Do not call the constructor directly.
*
* A 3D model based on glTF, the runtime asset format for WebGL, OpenGL ES, and OpenGL.
*
@@ -155,7 +157,7 @@ import StyleCommandsNeeded from "./StyleCommandsNeeded.js";
* @privateParam {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this model. This cannot be set after the model has loaded.
*
- * @see Model.fromGltf
+ * @see Model.fromGltfAsync
*
* @demo {@link https://sandcastle.cesium.com/index.html?src=3D%20Models.html|Cesium Sandcastle Models Demo}
*/
@@ -455,9 +457,65 @@ function Model(options) {
this._skipLevelOfDetail = false;
this._ignoreCommands = defaultValue(options.ignoreCommands, false);
- this._completeLoad = function (model, frameState) {};
this._texturesLoadedPromise = undefined;
- this._readyPromise = initialize(this);
+
+ this._completeLoad = undefined;
+ this._rejectLoad = undefined;
+ this._completeTexturesLoad = undefined;
+ this._rejectTexturesLoad = undefined;
+
+ // This is for backward compatibility. When readyPromise is removed, this block can be too
+ if (!defined(this._loader._promise)) {
+ this._readyPromise = new Promise((resolve, reject) => {
+ this._completeLoad = () => {
+ resolve(this);
+ return false;
+ };
+ this._rejectLoad = (error) => {
+ reject(error);
+ return false;
+ };
+ });
+
+ // Transcoded .pnts models do not have textures
+ if (this._loader instanceof PntsLoader) {
+ this._texturesLoadedPromise = Promise.resolve(this);
+ } else {
+ this._texturesLoadedPromise = new Promise((resolve, reject) => {
+ this._completeTexturesLoad = () => {
+ resolve(this);
+ return false;
+ };
+ this._rejectTexturesLoad = (error) => {
+ reject(error);
+ return false;
+ };
+ });
+ }
+
+ this._loader.load().catch((error) => {
+ // If the model is destroyed before the promise resolves, then
+ // the loader will have been destroyed as well. Return early.
+ if (
+ this.isDestroyed() ||
+ !defined(this._loader) ||
+ this._loader.isDestroyed()
+ ) {
+ return;
+ }
+
+ this._rejectLoad = this._rejectLoad(
+ ModelUtility.getError("model", this._resource, error)
+ );
+ });
+ } else {
+ this._readyPromise = Promise.resolve(this);
+ this._texturesLoadedPromise = Promise.resolve(this);
+ }
+
+ this._errorEvent = new Event();
+ this._readyEvent = new Event();
+ this._texturesReadyEvent = new Event();
this._sceneGraph = undefined;
this._nodesByName = {}; // Stores the nodes by their names in the glTF.
@@ -470,6 +528,15 @@ function Model(options) {
this.pickObject = options.pickObject;
}
+function handleError(model, error) {
+ if (model._errorEvent.numberOfListeners > 0) {
+ model._errorEvent.raiseEvent(error);
+ return;
+ }
+
+ console.error(error);
+}
+
function createModelFeatureTables(model, structuralMetadata) {
const featureTables = model._featureTables;
@@ -561,101 +628,10 @@ function isColorAlphaDirty(currentColor, previousColor) {
);
}
-function initialize(model) {
- const loader = model._loader;
- const resource = model._resource;
-
- loader.load();
-
- const loaderPromise = loader.promise.then(function (loader) {
- // If the model is destroyed before the promise resolves, then
- // the loader will have been destroyed as well. Return early.
- if (!defined(loader)) {
- return;
- }
-
- const components = loader.components;
- if (!defined(components)) {
- if (loader.isUnloaded()) {
- return;
- }
-
- throw new RuntimeError("Failed to load model.");
- }
-
- const structuralMetadata = components.structuralMetadata;
-
- if (
- defined(structuralMetadata) &&
- structuralMetadata.propertyTableCount > 0
- ) {
- createModelFeatureTables(model, structuralMetadata);
- }
-
- const sceneGraph = new ModelSceneGraph({
- model: model,
- modelComponents: components,
- });
-
- model._sceneGraph = sceneGraph;
- model._gltfCredits = sceneGraph.components.asset.credits;
-
- const resourceCredits = model._resource.credits;
- if (defined(resourceCredits)) {
- const length = resourceCredits.length;
- for (let i = 0; i < length; i++) {
- model._resourceCredits.push(resourceCredits[i]);
- }
- }
-
- model._resourcesLoaded = true;
- });
-
- // Transcoded .pnts models do not have textures
- const texturesLoadedPromise = defaultValue(
- loader.texturesLoadedPromise,
- Promise.resolve()
- );
- model._texturesLoadedPromise = texturesLoadedPromise
- .then(function () {
- // If the model was destroyed while loading textures, return.
- if (!defined(model) || model.isDestroyed()) {
- return;
- }
-
- model._texturesLoaded = true;
-
- // Re-run the pipeline so texture memory statistics are re-computed
- if (loader._incrementallyLoadTextures) {
- model.resetDrawCommands();
- }
- })
- .catch(ModelUtility.getFailedLoadFunction(model, "model", resource));
-
- const promise = new Promise(function (resolve, reject) {
- model._completeLoad = function (model, frameState) {
- // Set the model as ready after the first frame render since the user might set up events subscribed to
- // the post render event, and the model may not be ready for those past the first frame.
- frameState.afterRender.push(function () {
- model._ready = true;
- resolve(model);
- return true;
- });
- };
- });
-
- return loaderPromise
- .then(function () {
- return promise;
- })
- .catch(ModelUtility.getFailedLoadFunction(model, "model", resource));
-}
-
Object.defineProperties(Model.prototype, {
/**
* When true, this model is ready to render, i.e., the external binary, image,
- * and shader files were downloaded and the WebGL resources were created. This is set to
- * true right before {@link Model#readyPromise} is resolved.
+ * and shader files were downloaded and the WebGL resources were created.
*
* @memberof Model.prototype
*
@@ -670,6 +646,54 @@ Object.defineProperties(Model.prototype, {
},
},
+ /**
+ * Gets an event that is raised when the model encounters an asynchronous rendering error. By subscribing
+ * to the event, you will be notified of the error and can potentially recover from it. Event listeners
+ * are passed an instance of {@link ModelError}.
+ * @memberof Model.prototype
+ * @type {Event}
+ * @readonly
+ */
+ errorEvent: {
+ get: function () {
+ return this._errorEvent;
+ },
+ },
+
+ /**
+ * Gets an event that is raised when the model is loaded and ready for rendering, i.e. when the external resources
+ * have been downloaded and the WebGL resources are created. Event listeners
+ * are passed an instance of the {@link Model}.
+ *
+ *
+ * If {@link Model.incrementallyLoadTextures} is true, this event will be raised before all textures are loaded and ready for rendering. Subscribe to {@link Model.texturesReadyEvent} to be notified when the textures are ready.
+ *
+ *
+ * @memberof Model.prototype
+ * @type {Event}
+ * @readonly
+ */
+ readyEvent: {
+ get: function () {
+ return this._readyEvent;
+ },
+ },
+
+ /**
+ * Gets an event that, if {@link Model.incrementallyLoadTextures} is true, is raised when the model textures are loaded and ready for rendering, i.e. when the external resources
+ * have been downloaded and the WebGL resources are created. Event listeners
+ * are passed an instance of the {@link Model}.
+ *
+ * @memberof Model.prototype
+ * @type {Event}
+ * @readonly
+ */
+ texturesReadyEvent: {
+ get: function () {
+ return this._texturesReadyEvent;
+ },
+ },
+
/**
* Gets the promise that will be resolved when this model is ready to render, i.e. when the external resources
* have been downloaded and the WebGL resources are created.
@@ -681,9 +705,14 @@ Object.defineProperties(Model.prototype, {
*
* @type {Promise.}
* @readonly
+ * @deprecated
*/
readyPromise: {
get: function () {
+ deprecationWarning(
+ "Model.readyPromise",
+ "Model.readyPromise was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync and Model.readyEvent instead."
+ );
return this._readyPromise;
},
},
@@ -697,11 +726,16 @@ Object.defineProperties(Model.prototype, {
*
* @type {Promise}
* @readonly
+ * @deprecated
*
* @private
*/
texturesLoadedPromise: {
get: function () {
+ deprecationWarning(
+ "Model.texturesLoadedPromise",
+ "Model.texturesLoadedPromise was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync and Model.texturesReadyEvent instead."
+ );
return this._texturesLoadedPromise;
},
},
@@ -1155,7 +1189,7 @@ Object.defineProperties(Model.prototype, {
//>>includeStart('debug', pragmas.debug);
if (!this._ready) {
throw new DeveloperError(
- "The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true."
+ "The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true."
);
}
//>>includeEnd('debug');
@@ -1223,7 +1257,7 @@ Object.defineProperties(Model.prototype, {
) {
oneTimeWarning(
"model-debug-wireframe-ignored",
- "enableDebugWireframe must be set to true in Model.fromGltf, otherwise debugWireframe will be ignored."
+ "enableDebugWireframe must be set to true in Model.fromGltfAsync, otherwise debugWireframe will be ignored."
);
}
},
@@ -1664,7 +1698,7 @@ Object.defineProperties(Model.prototype, {
* @param {String} name The name of the node in the glTF.
* @returns {ModelNode} The node, or undefined if no node with the name exists.
*
- * @exception {DeveloperError} The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true.
+ * @exception {DeveloperError} The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true.
*
* @example
* // Apply non-uniform scale to node "Hand"
@@ -1675,7 +1709,7 @@ Model.prototype.getNode = function (name) {
//>>includeStart('debug', pragmas.debug);
if (!this._ready) {
throw new DeveloperError(
- "The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true."
+ "The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true."
);
}
Check.typeOf.string("name", name);
@@ -1692,7 +1726,7 @@ Model.prototype.getNode = function (name) {
* @param {String} articulationStageKey The name of the articulation, a space, and the name of the stage.
* @param {Number} value The numeric value of this stage of the articulation.
*
- * @exception {DeveloperError} The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true.
+ * @exception {DeveloperError} The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true.
*
* @see Model#applyArticulations
*
@@ -1705,7 +1739,7 @@ Model.prototype.setArticulationStage = function (articulationStageKey, value) {
Check.typeOf.number("value", value);
if (!this._ready) {
throw new DeveloperError(
- "The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true."
+ "The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true."
);
}
//>>includeEnd('debug');
@@ -1718,13 +1752,13 @@ Model.prototype.setArticulationStage = function (articulationStageKey, value) {
* participates in any articulation. Note that this will overwrite any node
* transformations on participating nodes.
*
- * @exception {DeveloperError} The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true.
+ * @exception {DeveloperError} The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true.
*/
Model.prototype.applyArticulations = function () {
//>>includeStart('debug', pragmas.debug);
if (!this._ready) {
throw new DeveloperError(
- "The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true."
+ "The model is not loaded. Use Model.readyEvent or wait for Model.ready to be true."
);
}
//>>includeEnd('debug');
@@ -1764,10 +1798,40 @@ const scratchClippingPlanesMatrix = new Matrix4();
* @exception {RuntimeError} Failed to load external reference.
*/
Model.prototype.update = function (frameState) {
- // Keep processing the model every frame until the main resources
- // (buffer views) and textures (which may be loaded asynchronously)
- // are processed.
- processLoader(this, frameState);
+ let finishedProcessing = false;
+ try {
+ // Keep processing the model every frame until the main resources
+ // (buffer views) and textures (which may be loaded asynchronously)
+ // are processed.
+ finishedProcessing = processLoader(this, frameState);
+ } catch (error) {
+ if (!this._loader.incrementallyLoadTextures) {
+ const runtimeError = ModelUtility.getError(
+ "model",
+ this._resource,
+ error
+ );
+ handleError(this, runtimeError);
+
+ this._rejectLoad = this._rejectLoad && this._rejectLoad(runtimeError);
+ this._rejectTexturesLoad =
+ this._rejectTexturesLoad && this._rejectTexturesLoad(runtimeError);
+ } else if (error.name === "TextureError") {
+ handleError(this, error);
+
+ this._rejectTexturesLoad =
+ this._rejectTexturesLoad && this._rejectTexturesLoad(error);
+ } else {
+ const runtimeError = ModelUtility.getError(
+ "model",
+ this._resource,
+ error
+ );
+ handleError(this, runtimeError);
+
+ this._rejectLoad = this._rejectLoad && this._rejectLoad(runtimeError);
+ }
+ }
// A custom shader may have to load texture uniforms.
updateCustomShader(this, frameState);
@@ -1776,6 +1840,41 @@ Model.prototype.update = function (frameState) {
// for specular maps.
updateImageBasedLighting(this, frameState);
+ if (!this._resourcesLoaded && finishedProcessing) {
+ this._resourcesLoaded = true;
+
+ const components = this._loader.components;
+ if (!defined(components)) {
+ if (this._loader.isUnloaded()) {
+ return;
+ }
+
+ const error = ModelUtility.getError(
+ "model",
+ this._resource,
+ new RuntimeError("Failed to load model.")
+ );
+ handleError(error);
+ this._rejectLoad = this._rejectLoad && this._rejectLoad(error);
+ }
+
+ const structuralMetadata = components.structuralMetadata;
+ if (
+ defined(structuralMetadata) &&
+ structuralMetadata.propertyTableCount > 0
+ ) {
+ createModelFeatureTables(this, structuralMetadata);
+ }
+
+ const sceneGraph = new ModelSceneGraph({
+ model: this,
+ modelComponents: components,
+ });
+
+ this._sceneGraph = sceneGraph;
+ this._gltfCredits = sceneGraph.components.asset.credits;
+ }
+
// Short-circuit if the model resources aren't ready or the scene
// is currently morphing.
if (!this._resourcesLoaded || frameState.mode === SceneMode.MORPHING) {
@@ -1806,14 +1905,37 @@ Model.prototype.update = function (frameState) {
// This check occurs after the bounding sphere has been updated so that
// zooming to the bounding sphere can account for any modifications
// from the clamp-to-ground setting.
- const model = this;
- if (!model._ready) {
- model._completeLoad(model, frameState);
+ if (!this._ready) {
+ // Set the model as ready after the first frame render since the user might set up events subscribed to
+ // the post render event, and the model may not be ready for those past the first frame.
+ frameState.afterRender.push(() => {
+ this._ready = true;
+ this._readyEvent.raiseEvent(this);
+ // This is here for backwards compatibility and can be removed when readyPromise is removed.
+ this._completeLoad = this._completeLoad && this._completeLoad();
+ if (!this._loader.incrementallyLoadTextures) {
+ this._texturesLoaded = true;
+ this._texturesReadyEvent.raiseEvent(this);
+ this._completeTexturesLoad =
+ this._completeTexturesLoad && this._completeTexturesLoad();
+ }
+ });
// Don't render until the next frame after the ready promise is resolved
return;
}
+ if (this._loader.incrementallyLoadTextures && !this._texturesLoaded) {
+ // TODO: Check if textures are actually ready
+ // Re-run the pipeline so texture memory statistics are re-computed
+ this.resetDrawCommands();
+
+ this._texturesLoaded = true;
+ this._texturesReadyEvent.raiseEvent(this);
+ this._completeTexturesLoad =
+ this._completeTexturesLoad && this._completeTexturesLoad();
+ }
+
updatePickIds(this);
// Update the scene graph and draw commands for any changes in model's properties
@@ -1825,8 +1947,12 @@ Model.prototype.update = function (frameState) {
function processLoader(model, frameState) {
if (!model._resourcesLoaded || !model._texturesLoaded) {
- model._loader.process(frameState);
+ // Ensures frames continue to render in requestRender mode while resources are processing
+ frameState.afterRender.push(() => true);
+ return model._loader.process(frameState);
}
+
+ return true;
}
function updateCustomShader(model, frameState) {
@@ -2635,6 +2761,11 @@ Model.prototype.destroyModelResources = function () {
* @returns {Model} The newly created model.
*/
Model.fromGltf = function (options) {
+ deprecationWarning(
+ "Model.fromGltf",
+ "Model.fromGltf was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync instead."
+ );
+
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
//>>includeStart('debug', pragmas.debug);
@@ -2688,6 +2819,179 @@ Model.fromGltf = function (options) {
return model;
};
+/**
+ *
+ * Asynchronously creates a model from a glTF asset. This function returns a promise that resolves when the model is ready to render, i.e., when the external binary, image,
+ * and shader files are downloaded and the WebGL resources are created.
+ *
+ *
+ * The model can be a traditional glTF asset with a .gltf extension or a Binary glTF using the .glb extension.
+ *
+ * @param {Object} options Object with the following properties:
+ * @param {String|Resource} options.url The url to the .gltf or .glb file.
+ * @param {String|Resource} [options.basePath=''] The base path that paths in the glTF JSON are relative to.
+ * @param {Boolean} [options.show=true] Whether or not to render the model.
+ * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the model from model to world coordinates.
+ * @param {Number} [options.scale=1.0] A uniform scale applied to this model.
+ * @param {Number} [options.minimumPixelSize=0.0] The approximate minimum pixel size of the model regardless of zoom.
+ * @param {Number} [options.maximumScale] The maximum scale size of a model. An upper limit for minimumPixelSize.
+ * @param {Object} [options.id] A user-defined object to return when the model is picked with {@link Scene#pick}.
+ * @param {Boolean} [options.allowPicking=true] When true, each primitive is pickable with {@link Scene#pick}.
+ * @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded.
+ * @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded.
+ * @param {Boolean} [options.clampAnimations=true] Determines if the model's animations should hold a pose over frames where no keyframes are specified.
+ * @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the model casts or receives shadows from light sources.
+ * @param {Boolean} [options.releaseGltfJson=false] When true, the glTF JSON is released once the glTF is loaded. This is is especially useful for cases like 3D Tiles, where each .gltf model is unique and caching the glTF JSON is not effective.
+ * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for each draw command in the model.
+ * @param {Boolean} [options.enableDebugWireframe=false] For debugging only. This must be set to true for debugWireframe to work in WebGL1. This cannot be set after the model has loaded.
+ * @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the model in wireframe. Will only work for WebGL1 if enableDebugWireframe is set to true.
+ * @param {Boolean} [options.cull=true] Whether or not to cull the model using frustum/horizon culling. If the model is part of a 3D Tiles tileset, this property will always be false, since the 3D Tiles culling system is used.
+ * @param {Boolean} [options.opaquePass=Pass.OPAQUE] The pass to use in the {@link DrawCommand} for the opaque portions of the model.
+ * @param {Axis} [options.upAxis=Axis.Y] The up-axis of the glTF model.
+ * @param {Axis} [options.forwardAxis=Axis.Z] The forward-axis of the glTF model.
+ * @param {CustomShader} [options.customShader] A custom shader. This will add user-defined GLSL code to the vertex and fragment shaders. Using custom shaders with a {@link Cesium3DTileStyle} may lead to undefined behavior.
+ * @param {Cesium3DTileContent} [options.content] The tile content this model belongs to. This property will be undefined if model is not loaded as part of a tileset.
+ * @param {HeightReference} [options.heightReference=HeightReference.NONE] Determines how the model is drawn relative to terrain.
+ * @param {Scene} [options.scene] Must be passed in for models that use the height reference property.
+ * @param {DistanceDisplayCondition} [options.distanceDisplayCondition] The condition specifying at what distance from the camera that this model will be displayed.
+ * @param {Color} [options.color] A color that blends with the model's rendered color.
+ * @param {ColorBlendMode} [options.colorBlendMode=ColorBlendMode.HIGHLIGHT] Defines how the color blends with the model.
+ * @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the colorBlendMode is MIX. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two.
+ * @param {Color} [options.silhouetteColor=Color.RED] The silhouette color. If more than 256 models have silhouettes enabled, there is a small chance that overlapping models will have minor artifacts.
+ * @param {Number} [options.silhouetteSize=0.0] The size of the silhouette in pixels.
+ * @param {Boolean} [options.enableShowOutline=true] Whether to enable outlines for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set false to avoid post-processing geometry at load time. When false, the showOutlines and outlineColor options are ignored.
+ * @param {Boolean} [options.showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed.
+ * @param {Color} [options.outlineColor=Color.BLACK] The color to use when rendering outlines.
+ * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model.
+ * @param {Cartesian3} [options.lightColor] The light color when shading the model. When undefined the scene's light color is used instead.
+ * @param {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting on this model.
+ * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if the model's color is translucent.
+ * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas.
+ * @param {Boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen.
+ * @param {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this model.
+ * @param {Boolean} [options.projectTo2D=false] Whether to accurately project the model's positions in 2D. If this is true, the model will be projected accurately to 2D, but it will use more memory to do so. If this is false, the model will use less memory and will still render in 2D / CV mode, but its positions may be inaccurate. This disables minimumPixelSize and prevents future modification to the model matrix. This also cannot be set after the model has loaded.
+ * @param {String|Number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
+ * @param {String|Number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
+ * @param {Object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation and lighting.
+ * @param {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this model. This cannot be set after the model has loaded.
+ *
+ * @returns {Promise} A promise that resolves to the created model when it is ready to render.
+ *
+ * @exception {RuntimeError} The model failed to load.
+ *
+ * @example
+ * // Load a model and add it to the scene
+ * try {
+ * const model = await Cesium.Model.fromGltfAsync({
+ * url: "../../SampleData/models/CesiumMan/Cesium_Man.glb"
+ * });
+ * viewer.scene.primitives.add(model);
+ * } catch (error) {
+ * console.log(`Failed to load model. ${error}`);
+ * }
+ *
+ * @example
+ * // Position a model with modelMatrix and display it with a minimum size of 128 pixels
+ * const position = Cesium.Cartesian3.fromDegrees(
+ * -123.0744619,
+ * 44.0503706,
+ * 5000.0
+ * );
+ * const headingPositionRoll = new Cesium.HeadingPitchRoll();
+ * const fixedFrameTransform = Cesium.Transforms.localFrameToFixedFrameGenerator(
+ * "north",
+ * "west"
+ * );
+ * try {
+ * const model = await Cesium.Model.fromGltfAsync({
+ * url: "../../SampleData/models/CesiumAir/Cesium_Air.glb",
+ * modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
+ * position,
+ * headingPositionRoll,
+ * Cesium.Ellipsoid.WGS84,
+ * fixedFrameTransform
+ * ),
+ * minimumPixelSize: 128,
+ * });
+ * viewer.scene.primitives.add(model);
+ * } catch (error) {
+ * console.log(`Failed to load model. ${error}`);
+ * }
+ */
+Model.fromGltfAsync = async function (options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ //>>includeStart('debug', pragmas.debug);
+ if (!defined(options.url) && !defined(options.gltf)) {
+ throw new DeveloperError("options.url is required.");
+ }
+ //>>includeEnd('debug');
+
+ // options.gltf is used internally for 3D Tiles. It can be a Resource, a URL
+ // to a glTF/glb file, a binary glTF buffer, or a JSON object containing the
+ // glTF contents.
+ const gltf = defaultValue(options.url, options.gltf);
+
+ const loaderOptions = {
+ releaseGltfJson: options.releaseGltfJson,
+ asynchronous: options.asynchronous,
+ incrementallyLoadTextures: options.incrementallyLoadTextures,
+ upAxis: options.upAxis,
+ forwardAxis: options.forwardAxis,
+ loadAttributesFor2D: options.projectTo2D,
+ loadIndicesForWireframe: options.enableDebugWireframe,
+ loadPrimitiveOutline: options.enableShowOutline,
+ loadForClassification: defined(options.classificationType),
+ };
+
+ const basePath = defaultValue(options.basePath, "");
+ const baseResource = Resource.createIfNeeded(basePath);
+
+ if (defined(gltf.asset)) {
+ loaderOptions.gltfJson = gltf;
+ loaderOptions.baseResource = baseResource;
+ loaderOptions.gltfResource = baseResource;
+ } else if (gltf instanceof Uint8Array) {
+ loaderOptions.typedArray = gltf;
+ loaderOptions.baseResource = baseResource;
+ loaderOptions.gltfResource = baseResource;
+ } else {
+ loaderOptions.gltfResource = Resource.createIfNeeded(gltf);
+ }
+
+ const loader = new GltfLoader(loaderOptions);
+
+ const is3DTiles = defined(options.content);
+ const type = is3DTiles ? ModelType.TILE_GLTF : ModelType.GLTF;
+
+ const resource = loaderOptions.gltfResource;
+
+ const modelOptions = makeModelOptions(loader, type, options);
+ modelOptions.resource = resource;
+
+ try {
+ // This load the gltf JSON and ensures the gltf is valid
+ // Further resource loading is handled synchronously in loader.process(), and requires
+ // hooking into model's update() as the frameState is needed
+ await loader.load();
+
+ const model = new Model(modelOptions);
+
+ const resourceCredits = model._resource.credits;
+ if (defined(resourceCredits)) {
+ const length = resourceCredits.length;
+ for (let i = 0; i < length; i++) {
+ model._resourceCredits.push(resourceCredits[i]);
+ }
+ }
+
+ return model;
+ } catch (error) {
+ loader.destroy();
+ throw ModelUtility.getError("model", resource, error);
+ }
+};
+
/*
* @private
*/
diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js
index 54c98c4b4aad..51744b06bf2a 100644
--- a/packages/engine/Source/Scene/Model/Model3DTileContent.js
+++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js
@@ -276,6 +276,7 @@ Model3DTileContent.fromGltf = function (tileset, tile, resource, gltf) {
modelOptions.classificationType = classificationType;
+ // This should be removed when readyPromise is deprecated across 3D Tiles functions
const model = Model.fromGltf(modelOptions);
content._model = model;
// Include the animation setup in the ready promise to avoid an uncaught exception
@@ -319,13 +320,8 @@ Model3DTileContent.fromB3dm = function (
const model = Model.fromB3dm(modelOptions);
content._model = model;
- // Include the animation setup in the ready promise to avoid an uncaught exception
- content._readyPromise = model.readyPromise.then(function (model) {
- model.activeAnimations.addAll({
- loop: ModelAnimationLoop.REPEAT,
- });
- return model;
- });
+ // This should be removed when readyPromise is deprecated across 3D Tiles functions
+ content._readyPromise = model._readyPromise;
return content;
};
@@ -354,13 +350,8 @@ Model3DTileContent.fromI3dm = function (
const model = Model.fromI3dm(modelOptions);
content._model = model;
- // Include the animation setup in the ready promise to avoid an uncaught exception
- content._readyPromise = model.readyPromise.then(function (model) {
- model.activeAnimations.addAll({
- loop: ModelAnimationLoop.REPEAT,
- });
- return model;
- });
+ // This should be removed when readyPromise is deprecated across 3D Tiles functions
+ content._readyPromise = model._readyPromise;
return content;
};
@@ -388,7 +379,8 @@ Model3DTileContent.fromPnts = function (
);
const model = Model.fromPnts(modelOptions);
content._model = model;
- content._readyPromise = model.readyPromise;
+ // This should be removed when readyPromise is deprecated across 3D Tiles functions
+ content._readyPromise = model._readyPromise;
return content;
};
@@ -409,7 +401,8 @@ Model3DTileContent.fromGeoJson = function (tileset, tile, resource, geoJson) {
);
const model = Model.fromGeoJson(modelOptions);
content._model = model;
- content._readyPromise = model.readyPromise;
+ // This should be removed when readyPromise is deprecated across 3D Tiles functions
+ content._readyPromise = model._readyPromise;
return content;
};
diff --git a/packages/engine/Source/Scene/Model/ModelAnimationCollection.js b/packages/engine/Source/Scene/Model/ModelAnimationCollection.js
index 8c1c0eb76972..8cd93901fed5 100644
--- a/packages/engine/Source/Scene/Model/ModelAnimationCollection.js
+++ b/packages/engine/Source/Scene/Model/ModelAnimationCollection.js
@@ -174,7 +174,7 @@ ModelAnimationCollection.prototype.add = function (options) {
//>>includeStart('debug', pragmas.debug);
if (!model.ready) {
throw new DeveloperError(
- "Animations are not loaded. Wait for Model.readyPromise to resolve."
+ "Animations are not loaded. Wait for Model.ready to be true."
);
}
//>>includeEnd('debug');
@@ -258,7 +258,7 @@ ModelAnimationCollection.prototype.addAll = function (options) {
//>>includeStart('debug', pragmas.debug);
if (!model.ready) {
throw new DeveloperError(
- "Animations are not loaded. Wait for Model.readyPromise to resolve."
+ "Animations are not loaded. Wait for Model.ready to be true."
);
}
diff --git a/packages/engine/Source/Scene/Model/ModelUtility.js b/packages/engine/Source/Scene/Model/ModelUtility.js
index b99f9e8076bd..f9f0c052b65c 100644
--- a/packages/engine/Source/Scene/Model/ModelUtility.js
+++ b/packages/engine/Source/Scene/Model/ModelUtility.js
@@ -20,29 +20,27 @@ function ModelUtility() {}
/**
* Create a function for reporting when a model fails to load
*
- * @param {Model} model The model to report about
* @param {String} type The type of object to report about
* @param {String} path The URI of the model file
- * @returns {Function} An error function that throws an error for the failed model
+ * @param {Error} [error] The error which caused the failure
+ * @returns {RuntimeError} An error for the failed model
*
* @private
*/
-ModelUtility.getFailedLoadFunction = function (model, type, path) {
- return function (error) {
- let message = `Failed to load ${type}: ${path}`;
- if (defined(error)) {
- message += `\n${error.message}`;
- }
+ModelUtility.getError = function (type, path, error) {
+ let message = `Failed to load ${type}: ${path}`;
+ if (defined(error) && defined(error.message)) {
+ message += `\n${error.message}`;
+ }
- const runtimeError = new RuntimeError(message);
- if (defined(error)) {
- // the original call stack is often more useful than the new error's stack,
- // so add the information here
- runtimeError.stack = `Original stack:\n${error.stack}\nHandler stack:\n${runtimeError.stack}`;
- }
+ const runtimeError = new RuntimeError(message);
+ if (defined(error)) {
+ // the original call stack is often more useful than the new error's stack,
+ // so add the information here
+ runtimeError.stack = `Original stack:\n${error.stack}\nHandler stack:\n${runtimeError.stack}`;
+ }
- return Promise.reject(runtimeError);
- };
+ return runtimeError;
};
/**
@@ -372,6 +370,8 @@ ModelUtility.supportedExtensions = {
* a {@link RuntimeError} with the extension name.
*
* @param {Array} extensionsRequired The extensionsRequired array in the glTF.
+ *
+ * @exception {RuntimeError} Unsupported glTF Extension
*/
ModelUtility.checkSupportedExtensions = function (extensionsRequired) {
const length = extensionsRequired.length;
diff --git a/packages/engine/Source/Scene/ResourceCache.js b/packages/engine/Source/Scene/ResourceCache.js
index 862b39aeeaa1..649e34294d01 100644
--- a/packages/engine/Source/Scene/ResourceCache.js
+++ b/packages/engine/Source/Scene/ResourceCache.js
@@ -69,20 +69,17 @@ ResourceCache.get = function (cacheKey) {
};
/**
- * Loads a resource and adds it to the cache.
+ * Adds it to the cache.
*
- * @param {Object} options Object with the following properties:
- * @param {ResourceLoader} options.resourceLoader The resource.
+ * @param {ResourceLoader} resourceLoader The resource.
+ * @returns {ResourceLoader} The resource.
*
- * @exception {DeveloperError} Resource with this cacheKey is already in the cach
+ * @exception {DeveloperError} Resource with this cacheKey is already in the cache
* @private
*/
-ResourceCache.load = function (options) {
- options = defaultValue(options, defaultValue.EMPTY_OBJECT);
- const resourceLoader = options.resourceLoader;
-
+ResourceCache.add = function (resourceLoader) {
//>>includeStart('debug', pragmas.debug);
- Check.typeOf.object("options.resourceLoader", resourceLoader);
+ Check.typeOf.object("resourceLoader", resourceLoader);
//>>includeEnd('debug');
const cacheKey = resourceLoader.cacheKey;
@@ -99,7 +96,7 @@ ResourceCache.load = function (options) {
ResourceCache.cacheEntries[cacheKey] = new CacheEntry(resourceLoader);
- resourceLoader.load();
+ return resourceLoader;
};
/**
@@ -136,18 +133,18 @@ ResourceCache.unload = function (resourceLoader) {
};
/**
- * Loads a schema from the cache.
+ * Gets an existing schema loader from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} [options.schema] An object that explicitly defines a schema JSON. Mutually exclusive with options.resource.
* @param {Resource} [options.resource] The {@link Resource} pointing to the schema JSON. Mutually exclusive with options.schema.
*
- * @returns {MetadataSchemaLoader} The schema resource.
+ * @returns {MetadataSchemaLoader} The cached schema resource.
*
* @exception {DeveloperError} One of options.schema and options.resource must be defined.
* @private
*/
-ResourceCache.loadSchema = function (options) {
+ResourceCache.getSchemaLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const schema = options.schema;
const resource = options.resource;
@@ -176,25 +173,21 @@ ResourceCache.loadSchema = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: schemaLoader,
- });
-
- return schemaLoader;
+ return ResourceCache.add(schemaLoader);
};
/**
- * Load an embedded buffer from the cache.
+ * Gets an existing embedded buffer loader from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Resource} options.parentResource The {@link Resource} containing the embedded buffer.
* @param {Number} options.bufferId A unique identifier of the embedded buffer within the parent resource.
* @param {Uint8Array} [options.typedArray] The typed array containing the embedded buffer contents.
*
- * @returns {BufferLoader} The buffer loader.
+ * @returns {BufferLoader} The cached buffer loader.
* @private
*/
-ResourceCache.loadEmbeddedBuffer = function (options) {
+ResourceCache.getEmbeddedBufferLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const parentResource = options.parentResource;
const bufferId = options.bufferId;
@@ -224,23 +217,19 @@ ResourceCache.loadEmbeddedBuffer = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: bufferLoader,
- });
-
- return bufferLoader;
+ return ResourceCache.add(bufferLoader);
};
/**
- * Loads an external buffer from the cache.
+ * Gets an existing external buffer from loader the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Resource} options.resource The {@link Resource} pointing to the external buffer.
*
- * @returns {BufferLoader} The buffer loader.
+ * @returns {BufferLoader} The cached buffer loader.
* @private
*/
-ResourceCache.loadExternalBuffer = function (options) {
+ResourceCache.getExternalBufferLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const resource = options.resource;
@@ -262,15 +251,11 @@ ResourceCache.loadExternalBuffer = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: bufferLoader,
- });
-
- return bufferLoader;
+ return ResourceCache.add(bufferLoader);
};
/**
- * Loads a glTF JSON from the cache.
+ * Gets an existing glTF JSON loader from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
@@ -278,10 +263,10 @@ ResourceCache.loadExternalBuffer = function (options) {
* @param {Uint8Array} [options.typedArray] The typed array containing the glTF contents.
* @param {Object} [options.gltfJson] The parsed glTF JSON contents.
*
- * @returns {GltfJsonLoader} The glTF JSON.
+ * @returns {GltfJsonLoader} The cached glTF JSON loader.
* @private
*/
-ResourceCache.loadGltfJson = function (options) {
+ResourceCache.getGltfJsonLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltfResource = options.gltfResource;
const baseResource = options.baseResource;
@@ -311,15 +296,11 @@ ResourceCache.loadGltfJson = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: gltfJsonLoader,
- });
-
- return gltfJsonLoader;
+ return ResourceCache.add(gltfJsonLoader);
};
/**
- * Loads a glTF buffer view from the cache.
+ * Gets an existing glTF buffer view from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -327,10 +308,10 @@ ResourceCache.loadGltfJson = function (options) {
* @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
* @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
*
- * @returns {GltfBufferViewLoader} The buffer view loader.
+ * @returns {GltfBufferViewLoader>} The cached buffer view loader.
* @private
*/
-ResourceCache.loadBufferView = function (options) {
+ResourceCache.getBufferViewLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const bufferViewId = options.bufferViewId;
@@ -365,15 +346,11 @@ ResourceCache.loadBufferView = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: bufferViewLoader,
- });
-
- return bufferViewLoader;
+ return ResourceCache.add(bufferViewLoader);
};
/**
- * Loads Draco data from the cache.
+ * Gets an existing Draco data from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -381,10 +358,10 @@ ResourceCache.loadBufferView = function (options) {
* @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
* @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
*
- * @returns {GltfDracoLoader} The Draco loader.
+ * @returns {GltfDracoLoader} The cached Draco loader.
* @private
*/
-ResourceCache.loadDraco = function (options) {
+ResourceCache.getDracoLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const draco = options.draco;
@@ -419,15 +396,11 @@ ResourceCache.loadDraco = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: dracoLoader,
- });
-
- return dracoLoader;
+ return ResourceCache.add(dracoLoader);
};
/**
- * Loads a glTF vertex buffer from the cache.
+ * Gets an existing glTF vertex buffer from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -446,10 +419,10 @@ ResourceCache.loadDraco = function (options) {
* @exception {DeveloperError} When options.draco is defined options.attributeSemantic must also be defined.
* @exception {DeveloperError} When options.draco is defined options.accessorId must also be defined.
*
- * @returns {GltfVertexBufferLoader} The vertex buffer loader.
+ * @returns {GltfVertexBufferLoader} The cached vertex buffer loader.
* @private
*/
-ResourceCache.loadVertexBuffer = function (options) {
+ResourceCache.getVertexBufferLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const gltfResource = options.gltfResource;
@@ -539,18 +512,7 @@ ResourceCache.loadVertexBuffer = function (options) {
loadTypedArray: loadTypedArray,
});
- ResourceCache.load({
- resourceLoader: vertexBufferLoader,
- });
-
- const promise = ResourceCache.statistics.addGeometryLoader(
- vertexBufferLoader
- );
-
- // Needed for unit testing
- ResourceCache.cacheEntries[cacheKey]._statisticsPromise = promise;
-
- return vertexBufferLoader;
+ return ResourceCache.add(vertexBufferLoader);
};
function hasDracoCompression(draco, semantic) {
@@ -562,7 +524,7 @@ function hasDracoCompression(draco, semantic) {
}
/**
- * Loads a glTF index buffer from the cache.
+ * Gets an existing glTF index buffer from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -574,10 +536,10 @@ function hasDracoCompression(draco, semantic) {
* @param {Boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
* @param {Boolean} [options.loadBuffer=false] Load index buffer as a GPU index buffer.
* @param {Boolean} [options.loadTypedArray=false] Load index buffer as a typed array.
- * @returns {GltfIndexBufferLoader} The index buffer loader.
+ * @returns {GltfIndexBufferLoader} The cached index buffer loader.
* @private
*/
-ResourceCache.loadIndexBuffer = function (options) {
+ResourceCache.getIndexBufferLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const accessorId = options.accessorId;
@@ -631,19 +593,11 @@ ResourceCache.loadIndexBuffer = function (options) {
loadTypedArray: loadTypedArray,
});
- ResourceCache.load({
- resourceLoader: indexBufferLoader,
- });
- const promise = ResourceCache.statistics.addGeometryLoader(indexBufferLoader);
-
- // Needed for unit testing
- ResourceCache.cacheEntries[cacheKey]._statisticsPromise = promise;
-
- return indexBufferLoader;
+ return ResourceCache.add(indexBufferLoader);
};
/**
- * Loads a glTF image from the cache.
+ * Gets an existing glTF image from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -651,10 +605,10 @@ ResourceCache.loadIndexBuffer = function (options) {
* @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
* @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
*
- * @returns {GltfImageLoader} The image loader.
+ * @returns {GltfImageLoader} The cached image loader.
* @private
*/
-ResourceCache.loadImage = function (options) {
+ResourceCache.getImageLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const imageId = options.imageId;
@@ -689,15 +643,11 @@ ResourceCache.loadImage = function (options) {
cacheKey: cacheKey,
});
- ResourceCache.load({
- resourceLoader: imageLoader,
- });
-
- return imageLoader;
+ return ResourceCache.add(imageLoader);
};
/**
- * Loads a glTF texture from the cache.
+ * Gets an existing glTF texture from the cache, or creates a new loader if one does not already exist.
*
* @param {Object} options Object with the following properties:
* @param {Object} options.gltf The glTF JSON.
@@ -708,10 +658,10 @@ ResourceCache.loadImage = function (options) {
* @param {FrameState} options.frameState The frame state.
* @param {Boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
*
- * @returns {GltfTextureLoader} The texture loader.
+ * @returns {GltfTextureLoader} The cached texture loader.
* @private
*/
-ResourceCache.loadTexture = function (options) {
+ResourceCache.getTextureLoader = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const gltf = options.gltf;
const textureInfo = options.textureInfo;
@@ -755,15 +705,7 @@ ResourceCache.loadTexture = function (options) {
asynchronous: asynchronous,
});
- ResourceCache.load({
- resourceLoader: textureLoader,
- });
- const promise = ResourceCache.statistics.addTextureLoader(textureLoader);
-
- // Needed for unit testing
- ResourceCache.cacheEntries[cacheKey]._statisticsPromise = promise;
-
- return textureLoader;
+ return ResourceCache.add(textureLoader);
};
/**
diff --git a/packages/engine/Source/Scene/ResourceCacheStatistics.js b/packages/engine/Source/Scene/ResourceCacheStatistics.js
index 70fef98fbc39..cdc954298a81 100644
--- a/packages/engine/Source/Scene/ResourceCacheStatistics.js
+++ b/packages/engine/Source/Scene/ResourceCacheStatistics.js
@@ -47,14 +47,12 @@ ResourceCacheStatistics.prototype.clear = function () {
};
/**
- * Track the resources for a vertex or index buffer loader. This is implemented
- * asynchronously since resources may not be immediately available to count.
+ * Track the resources for a vertex or index buffer loader. This should be called after a loader is ready; that
+ * is it has been loaded and processed.
* This method handles the following cases gracefully:
*
*
If the loader is added twice, its resources will not be double-counted
*
If the geometry has a CPU copy of the GPU buffer, it will be added to the count
- *
If the resource loading failed, its resources will not be counted
- *
If removeLoader() was called before the loader promise resolves, its resources will not be counted
*
* @param {GltfVertexBufferLoader|GltfIndexBufferLoader} loader The geometry buffer with resources to track
* @returns {Promise} A promise that resolves once the count is updated.
@@ -75,51 +73,33 @@ ResourceCacheStatistics.prototype.addGeometryLoader = function (loader) {
this._geometrySizes[cacheKey] = 0;
- const that = this;
- return loader.promise
- .then(function (loader) {
- // loader was unloaded before its promise resolved
- if (!that._geometrySizes.hasOwnProperty(cacheKey)) {
- return;
- }
-
- const buffer = loader.buffer;
- const typedArray = loader.typedArray;
-
- let totalSize = 0;
-
- if (defined(buffer)) {
- totalSize += buffer.sizeInBytes;
- }
-
- if (defined(typedArray)) {
- totalSize += typedArray.byteLength;
- }
-
- that.geometryByteLength += totalSize;
- that._geometrySizes[cacheKey] = totalSize;
- })
- .catch(function () {
- // If the resource failed to load, remove it from the cache
- delete that._geometrySizes[cacheKey];
- });
+ const buffer = loader.buffer;
+ const typedArray = loader.typedArray;
+
+ let totalSize = 0;
+
+ if (defined(buffer)) {
+ totalSize += buffer.sizeInBytes;
+ }
+
+ if (defined(typedArray)) {
+ totalSize += typedArray.byteLength;
+ }
+
+ this.geometryByteLength += totalSize;
+ this._geometrySizes[cacheKey] = totalSize;
};
/**
- * Track the resources for a texture loader. This is implemented
- * asynchronously since resources may not be immediately available to count.
- * This method handles the following cases gracefully:
- *
- *
If the loader is added twice, its resources will not be double-counted
- *
If the resource loading failed, its resources will not be counted
- *
If removeLoader() was called before the loader promise resolves, its resources will not be counted
- * The promise remains undefined until the tile's content is requested.
- *
- *
- * @type {Promise}
- * @readonly
- *
- * @private
- */
- contentReadyToProcessPromise: {
- get: function () {
- return this._contentReadyToProcessPromise;
- },
- },
-
- /**
- * Gets the promise that will be resolved when the tile's content is ready to render.
- *
- * The promise remains undefined until the tile's content is requested.
- *
- *
- * @type {Promise}
- * @readonly
- *
- * @private
- */
- contentReadyPromise: {
- get: function () {
- return this._contentReadyPromise;
- },
- },
-
/**
* Returns the number of draw commands used by this tile.
*
@@ -1084,13 +1046,13 @@ function createPriorityFunction(tile) {
* The request may not be made if the Cesium Request Scheduler can't prioritize it.
*
*
- * @return {number} The number of requests that were attempted but not scheduled.
+ * @return {Promise|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled.
* @private
*/
Cesium3DTile.prototype.requestContent = function () {
// empty contents don't require any HTTP requests
if (this.hasEmptyContent) {
- return 0;
+ return;
}
if (this.hasMultipleContents) {
@@ -1112,7 +1074,7 @@ Cesium3DTile.prototype.requestContent = function () {
*
* @private
* @param {Cesium3DTile} tile
- * @returns {number}
+ * @returns {Promise|Promise|undefined} A promise that resolves to the tile content once loaded, or a promise that resolves to undefined if the request was cancelled mid-flight, or undefined if the request cannot be scheduled this frame
*/
function requestMultipleContents(tile) {
let multipleContents = tile._content;
@@ -1134,87 +1096,44 @@ function requestMultipleContents(tile) {
tile._content = multipleContents;
}
- const backloggedRequestCount = multipleContents.requestInnerContents();
- if (backloggedRequestCount > 0) {
- return backloggedRequestCount;
+ const promise = multipleContents.requestInnerContents();
+
+ if (!defined(promise)) {
+ // Request could not all be scheduled this frame
+ return;
}
tile._contentState = Cesium3DTileContentState.LOADING;
- const contentReadyToProcessPromise = multipleContents.contentsFetchedPromise.then(
- function () {
- if (
- tile._contentState !== Cesium3DTileContentState.LOADING ||
- !defined(multipleContents.readyPromise)
- ) {
- // The tile or one of the inner content requests was canceled,
- // short circuit.
- return;
- }
-
+ return promise
+ .then(async (content) => {
if (tile.isDestroyed()) {
- multipleContentFailed(tile, tileset);
+ // Tile is unloaded before the content can process
return;
}
- tile._contentState = Cesium3DTileContentState.PROCESSING;
- return multipleContents;
- }
- );
- tile._contentReadyToProcessPromise = contentReadyToProcessPromise;
- tile._contentReadyPromise = contentReadyToProcessPromise
- .then(function (content) {
+ // Tile was canceled, try again later
if (!defined(content)) {
- // request was canceled, short circuit.
return;
}
- return multipleContents.readyPromise;
+ tile._contentState = Cesium3DTileContentState.PROCESSING;
+ return multipleContents;
})
- .then(function (content) {
- if (!defined(content)) {
- // tile was canceled, short circuit.
- return;
- }
-
+ .catch((error) => {
if (tile.isDestroyed()) {
- multipleContentFailed(tile, tileset);
+ // Tile is unloaded before the content can process
return;
}
- // Refresh style for expired content
- tile._selectedFrame = 0;
- tile.lastStyleTime = 0.0;
-
- JulianDate.now(tile._loadTimestamp);
- tile._contentState = Cesium3DTileContentState.READY;
- return content;
- })
- .catch(function () {
- multipleContentFailed(tile, tileset);
+ tile._contentState = Cesium3DTileContentState.FAILED;
+ throw error;
});
-
- return 0;
-}
-
-/**
- * @private
- * @param {Cesium3DTile} tile
- * @param {Cesium3DTileset} tileset
- */
-function multipleContentFailed(tile, tileset) {
- // note: The Multiple3DTileContent handles decrementing the number of pending
- // requests if the state is LOADING.
- if (tile._contentState === Cesium3DTileContentState.PROCESSING) {
- --tileset.statistics.numberOfTilesProcessing;
- }
-
- tile._contentState = Cesium3DTileContentState.FAILED;
}
/**
* @private
* @param {Cesium3DTile} tile
- * @returns {number}
+ * @returns {Promise|Promise|undefined} A promise that resolves to the tile content once loaded; a promise that resolves to undefined if the tile was destroyed before processing can happen or the request was cancelled mid-flight; or undefined if the request cannot be scheduled this frame.
*/
function requestSingleContent(tile) {
// it is important to clone here. The fetchArrayBuffer() below uses
@@ -1238,90 +1157,81 @@ function requestSingleContent(tile) {
tile._request = request;
resource.request = request;
-
+ const tileset = tile._tileset;
const promise = resource.fetchArrayBuffer();
if (!defined(promise)) {
- return 1;
+ ++tileset.statistics.numberOfAttemptedRequests;
+ return;
}
const previousState = tile._contentState;
- const tileset = tile._tileset;
tile._contentState = Cesium3DTileContentState.LOADING;
++tileset.statistics.numberOfPendingRequests;
- const contentReadyToProcessPromise = promise.then(function (arrayBuffer) {
+
+ return (async () => {
+ let arrayBuffer;
+ try {
+ arrayBuffer = await promise;
+ } catch (error) {
+ --tileset.statistics.numberOfPendingRequests;
+ if (tile.isDestroyed()) {
+ // Tile is unloaded before the content can process
+ return;
+ }
+
+ if (request.cancelled || request.state === RequestState.CANCELLED) {
+ // Cancelled due to low priority - try again later.
+ tile._contentState = previousState;
+ ++tileset.statistics.numberOfAttemptedRequests;
+ return;
+ }
+
+ tile._contentState = Cesium3DTileContentState.FAILED;
+ throw error;
+ }
+
if (tile.isDestroyed()) {
- // Tile is unloaded before the content finishes loading
- singleContentFailed(tile, tileset);
+ --tileset.statistics.numberOfPendingRequests;
+ // Tile is unloaded before the content can process
return;
}
- const content = makeContent(tile, arrayBuffer);
-
- if (expired) {
- tile.expireDate = undefined;
+ if (request.cancelled || request.state === RequestState.CANCELLED) {
+ // Cancelled due to low priority - try again later.
+ tile._contentState = previousState;
+ --tileset.statistics.numberOfPendingRequests;
+ ++tileset.statistics.numberOfAttemptedRequests;
+ return;
}
- tile._content = content;
- tile._contentState = Cesium3DTileContentState.PROCESSING;
- return content;
- });
- tile._contentReadyToProcessPromise = contentReadyToProcessPromise;
- tile._contentReadyPromise = contentReadyToProcessPromise
- .then(function (content) {
- if (!defined(content)) {
- return;
- }
-
+ try {
+ const content = await makeContent(tile, arrayBuffer);
--tileset.statistics.numberOfPendingRequests;
- return content.readyPromise;
- })
- .then(function (content) {
- if (!defined(content)) {
- return;
- }
if (tile.isDestroyed()) {
- // Tile is unloaded before the content finishes processing
- singleContentFailed(tile, tileset);
+ // Tile is unloaded before the content can process
return;
}
- updateExpireDate(tile);
- // Refresh style for expired content
- tile._selectedFrame = 0;
- tile.lastStyleTime = 0.0;
+ if (expired) {
+ tile.expireDate = undefined;
+ }
+
+ tile._content = content;
+ tile._contentState = Cesium3DTileContentState.PROCESSING;
- JulianDate.now(tile._loadTimestamp);
- tile._contentState = Cesium3DTileContentState.READY;
return content;
- })
- .catch(function (error) {
- if (request.state === RequestState.CANCELLED) {
- // Cancelled due to low priority - try again later.
- tile._contentState = previousState;
- --tileset.statistics.numberOfPendingRequests;
- ++tileset.statistics.numberOfAttemptedRequests;
- return Promise.reject("Cancelled");
+ } catch (error) {
+ --tileset.statistics.numberOfPendingRequests;
+ if (tile.isDestroyed()) {
+ // Tile is unloaded before the content can process
+ return;
}
- singleContentFailed(tile, tileset);
- return Promise.reject(error);
- });
- return 0;
-}
-
-/**
- * @private
- * @param {Cesium3DTile} tile
- * @param {Cesium3DTileset} tileset
- */
-function singleContentFailed(tile, tileset) {
- if (tile._contentState === Cesium3DTileContentState.PROCESSING) {
- --tileset.statistics.numberOfTilesProcessing;
- } else {
- --tileset.statistics.numberOfPendingRequests;
- }
- tile._contentState = Cesium3DTileContentState.FAILED;
+ tile._contentState = Cesium3DTileContentState.FAILED;
+ throw error;
+ }
+ })();
}
/**
@@ -1332,10 +1242,10 @@ function singleContentFailed(tile, tileset) {
*
* @param {Cesium3DTile} tile The tile
* @param {ArrayBuffer} arrayBuffer The downloaded payload containing data for the content
- * @return {Cesium3DTileContent} A content object
+ * @return {Promise} A content object
* @private
*/
-function makeContent(tile, arrayBuffer) {
+async function makeContent(tile, arrayBuffer) {
const preprocessed = preprocess3DTileContent(arrayBuffer);
// Vector and Geometry tile rendering do not support the skip LOD optimization.
@@ -1358,21 +1268,29 @@ function makeContent(tile, arrayBuffer) {
let content;
const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType];
+ if (tile.isDestroyed()) {
+ return;
+ }
+
if (defined(preprocessed.binaryPayload)) {
- content = contentFactory(
- tileset,
- tile,
- tile._contentResource,
- preprocessed.binaryPayload.buffer,
- 0
+ content = await Promise.resolve(
+ contentFactory(
+ tileset,
+ tile,
+ tile._contentResource,
+ preprocessed.binaryPayload.buffer,
+ 0
+ )
);
} else {
// JSON formats
- content = contentFactory(
- tileset,
- tile,
- tile._contentResource,
- preprocessed.jsonPayload
+ content = await Promise.resolve(
+ contentFactory(
+ tileset,
+ tile,
+ tile._contentResource,
+ preprocessed.jsonPayload
+ )
);
}
@@ -1426,8 +1344,6 @@ Cesium3DTile.prototype.unloadContent = function () {
this._content = this._content && this._content.destroy();
this._contentState = Cesium3DTileContentState.UNLOADED;
- this._contentReadyToProcessPromise = undefined;
- this._contentReadyPromise = undefined;
this.lastStyleTime = 0.0;
this.clippingPlanesDirty = this._clippingPlanesState === 0;
@@ -1999,7 +1915,11 @@ function updateContent(tile, tileset, frameState) {
if (!tile.hasMultipleContents && defined(expiredContent)) {
if (!tile.contentReady) {
// Render the expired content while the content loads
- expiredContent.update(tileset, frameState);
+ try {
+ expiredContent.update(tileset, frameState);
+ } catch (error) {
+ // Eat error for expired content
+ }
return;
}
@@ -2008,7 +1928,17 @@ function updateContent(tile, tileset, frameState) {
tile._expiredContent = undefined;
}
- tile.content.update(tileset, frameState);
+ if (!defined(tile.content)) {
+ // Implicit placeholder tile
+ return;
+ }
+
+ try {
+ tile.content.update(tileset, frameState);
+ } catch (error) {
+ tile._contentState = Cesium3DTileContentState.FAILED;
+ throw error;
+ }
}
/**
@@ -2073,10 +2003,37 @@ const scratchCommandList = [];
* @private
*/
Cesium3DTile.prototype.process = function (tileset, frameState) {
+ if (!this.contentExpired && !this.contentReady && this._content.ready) {
+ updateExpireDate(this);
+
+ // Refresh style for expired content
+ this._selectedFrame = 0;
+ this.lastStyleTime = 0.0;
+
+ JulianDate.now(this._loadTimestamp);
+ this._contentState = Cesium3DTileContentState.READY;
+
+ if (!this.hasTilesetContent && !this.hasImplicitContent) {
+ // RESEARCH_IDEA: ability to unload tiles (without content) for an
+ // external tileset when all the tiles are unloaded.
+ tileset._statistics.incrementLoadCounts(this.content);
+ ++tileset._statistics.numberOfTilesWithContentReady;
+ ++tileset._statistics.numberOfLoadedTilesTotal;
+
+ // Add to the tile cache. Previously expired tiles are already in the cache and won't get re-added.
+ tileset._cache.add(this);
+ }
+ }
+
const savedCommandList = frameState.commandList;
frameState.commandList = scratchCommandList;
- this._content.update(tileset, frameState);
+ try {
+ this._content.update(tileset, frameState);
+ } catch (error) {
+ this._contentState = Cesium3DTileContentState.FAILED;
+ throw error;
+ }
scratchCommandList.length = 0;
frameState.commandList = savedCommandList;
diff --git a/packages/engine/Source/Scene/Cesium3DTileContent.js b/packages/engine/Source/Scene/Cesium3DTileContent.js
index c5464c418e0b..714cd0adc3e3 100644
--- a/packages/engine/Source/Scene/Cesium3DTileContent.js
+++ b/packages/engine/Source/Scene/Cesium3DTileContent.js
@@ -146,12 +146,28 @@ Object.defineProperties(Cesium3DTileContent.prototype, {
},
/**
- * Gets the promise that will be resolved when the tile's content is ready to render.
+ * Returns true when the tile's content is ready to render; otherwise false
+ *
+ * @memberof Cesium3DTileContent.prototype
+ *
+ * @type {boolean}
+ * @readonly
+ */
+ ready: {
+ // eslint-disable-next-line getter-return
+ get: function () {
+ DeveloperError.throwInstantiationError();
+ },
+ },
+
+ /**
+ * Returns true when the tile's content is ready to render; otherwise false
*
* @memberof Cesium3DTileContent.prototype
*
* @type {Promise}
* @readonly
+ * @deprecated
*/
readyPromise: {
// eslint-disable-next-line getter-return
diff --git a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
index 23c6b39e8928..6e2186c31ca4 100644
--- a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
+++ b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js
@@ -41,7 +41,7 @@ const Cesium3DTileContentFactory = {
},
cmpt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
// Send in the factory in order to avoid a cyclical dependency
- return new Composite3DTileContent(
+ return Composite3DTileContent.fromTileType(
tileset,
tile,
resource,
@@ -51,7 +51,7 @@ const Cesium3DTileContentFactory = {
);
},
externalTileset: function (tileset, tile, resource, json) {
- return new Tileset3DTileContent(tileset, tile, resource, json);
+ return Tileset3DTileContent.fromJson(tileset, tile, resource, json);
},
geom: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return new Geometry3DTileContent(
@@ -67,12 +67,13 @@ const Cesium3DTileContentFactory = {
tileset,
tile,
resource,
+
arrayBuffer,
byteOffset
);
},
subt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
- return new Implicit3DTileContent(
+ return Implicit3DTileContent.fromSubtreeJson(
tileset,
tile,
resource,
@@ -82,7 +83,7 @@ const Cesium3DTileContentFactory = {
);
},
subtreeJson: function (tileset, tile, resource, json) {
- return new Implicit3DTileContent(tileset, tile, resource, json);
+ return Implicit3DTileContent.fromSubtreeJson(tileset, tile, resource, json);
},
glb: function (tileset, tile, resource, arrayBuffer, byteOffset) {
const arrayBufferByteLength = arrayBuffer.byteLength;
@@ -92,11 +93,9 @@ const Cesium3DTileContentFactory = {
const dataView = new DataView(arrayBuffer, byteOffset);
const byteLength = dataView.getUint32(8, true);
const glb = new Uint8Array(arrayBuffer, byteOffset, byteLength);
- // This should be replace with fromGltfAsync when readyPromise is deprecated across 3D Tiles functions
return Model3DTileContent.fromGltf(tileset, tile, resource, glb);
},
gltf: function (tileset, tile, resource, json) {
- // This should be replace with fromGltfAsync when readyPromise is deprecated across 3D Tiles functions
return Model3DTileContent.fromGltf(tileset, tile, resource, json);
},
geoJson: function (tileset, tile, resource, json) {
diff --git a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
index baea53463069..02e1904a612e 100644
--- a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
+++ b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js
@@ -381,15 +381,15 @@ function getVoxelPromise(implicitTileset, tileCoordinates) {
});
}
-function getSubtreePromise(provider, subtreeCoord) {
+async function getSubtreePromise(provider, subtreeCoord) {
const implicitTileset = provider._implicitTileset;
const subtreeCache = provider._subtreeCache;
// First load the subtree to check if the tile is available.
// If the subtree has been requested previously it might still be in the cache
- const subtree = subtreeCache.find(subtreeCoord);
+ let subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
- return subtree.readyPromise;
+ return subtree;
}
const subtreeRelative = implicitTileset.subtreeUriTemplate.getDerivedResource(
@@ -401,26 +401,25 @@ function getSubtreePromise(provider, subtreeCoord) {
url: subtreeRelative.url,
});
- return subtreeResource.fetchArrayBuffer().then(function (arrayBuffer) {
- // Check one more time if the subtree is in the cache.
- // This could happen if there are two in-flight tile requests from the same
- // subtree and one finishes before the other.
- let subtree = subtreeCache.find(subtreeCoord);
- if (defined(subtree)) {
- return subtree.readyPromise;
- }
+ const arrayBuffer = await subtreeResource.fetchArrayBuffer();
+ // Check one more time if the subtree is in the cache.
+ // This could happen if there are two in-flight tile requests from the same
+ // subtree and one finishes before the other.
+ subtree = subtreeCache.find(subtreeCoord);
+ if (defined(subtree)) {
+ return subtree;
+ }
- const preprocessed = preprocess3DTileContent(arrayBuffer);
- subtree = new ImplicitSubtree(
- subtreeResource,
- preprocessed.jsonPayload,
- preprocessed.binaryPayload,
- implicitTileset,
- subtreeCoord
- );
- subtreeCache.addSubtree(subtree);
- return subtree.readyPromise;
- });
+ const preprocessed = preprocess3DTileContent(arrayBuffer);
+ subtree = await ImplicitSubtree.fromSubtreeJson(
+ subtreeResource,
+ preprocessed.jsonPayload,
+ preprocessed.binaryPayload,
+ implicitTileset,
+ subtreeCoord
+ );
+ subtreeCache.addSubtree(subtree);
+ return subtree;
}
/** @inheritdoc */
diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js
index b8c4639a030a..d5c7674a03ae 100644
--- a/packages/engine/Source/Scene/Cesium3DTileset.js
+++ b/packages/engine/Source/Scene/Cesium3DTileset.js
@@ -2298,14 +2298,26 @@ function requestContent(tileset, tile) {
}
const { statistics } = tileset;
- const { contentExpired } = tile;
- const attemptedRequests = tile.requestContent();
+ const contentExpired = tile.contentExpired;
- if (attemptedRequests > 0) {
- statistics.numberOfAttemptedRequests += attemptedRequests;
+ const promise = tile.requestContent();
+ if (!defined(promise)) {
return;
}
+ promise
+ .then((content) => {
+ if (!defined(content) || tile.isDestroyed() || tileset.isDestroyed()) {
+ return;
+ }
+
+ tileset._processingQueue.push(tile);
+ ++statistics.numberOfTilesProcessing;
+ })
+ .catch((error) => {
+ handleTileFailure(error, tileset, tile);
+ });
+
if (contentExpired) {
if (tile.hasTilesetContent || tile.hasImplicitContent) {
destroySubtree(tileset, tile);
@@ -2316,15 +2328,6 @@ function requestContent(tileset, tile) {
}
tileset._requestedTilesInFlight.push(tile);
-
- tile.contentReadyToProcessPromise
- .then(addToProcessingQueue(tileset, tile))
- .catch(function (e) {
- // Any error will propagate through contentReadyPromise and will be handled below
- });
- tile.contentReadyPromise
- .then(handleTileSuccess(tileset, tile))
- .catch(handleTileFailure(tileset, tile));
}
function sortRequestByPriority(a, b) {
@@ -2444,43 +2447,30 @@ function requestTiles(tileset) {
/**
* @private
+ * @param {Error} error
* @param {Cesium3DTileset} tileset
* @param {Cesium3DTile} tile
- * @returns {Function}
*/
-function addToProcessingQueue(tileset, tile) {
- return function () {
- tileset._processingQueue.push(tile);
-
- ++tileset._statistics.numberOfTilesProcessing;
- };
-}
+function handleTileFailure(error, tileset, tile) {
+ if (tileset.isDestroyed()) {
+ return;
+ }
-/**
- * @private
- * @param {Cesium3DTileset} tileset
- * @param {Cesium3DTile} tile
- * @returns {Function}
- */
-function handleTileFailure(tileset, tile) {
- return function (error) {
- if (tile._contentState !== Cesium3DTileContentState.FAILED) {
- // If the tile has not failed, the request has been rejected this frame, but will be retried. Do not bubble up the error.
- return;
- }
+ let url;
+ if (!tile.isDestroyed()) {
+ url = tile._contentResource.url;
+ }
- const url = tile._contentResource.url;
- const message = defined(error.message) ? error.message : error.toString();
- if (tileset.tileFailed.numberOfListeners > 0) {
- tileset.tileFailed.raiseEvent({
- url: url,
- message: message,
- });
- } else {
- console.log(`A 3D tile failed to load: ${url}`);
- console.log(`Error: ${message}`);
- }
- };
+ const message = defined(error.message) ? error.message : error.toString();
+ if (tileset.tileFailed.numberOfListeners > 0) {
+ tileset.tileFailed.raiseEvent({
+ url: url,
+ message: message,
+ });
+ } else {
+ console.log(`A 3D tile failed to load: ${url}`);
+ console.log(`Error: ${message}`);
+ }
}
/**
@@ -2490,27 +2480,7 @@ function handleTileFailure(tileset, tile) {
* @returns {Function}
*/
function handleTileSuccess(tileset, tile) {
- return function (content) {
- --tileset._statistics.numberOfTilesProcessing;
-
- if (!defined(content)) {
- // A request was cancelled. Do not update successful statistics. The request will be retried.
- return;
- }
-
- if (!tile.hasTilesetContent && !tile.hasImplicitContent) {
- // RESEARCH_IDEA: ability to unload tiles (without content) for an
- // external tileset when all the tiles are unloaded.
- tileset._statistics.incrementLoadCounts(tile.content);
- ++tileset._statistics.numberOfTilesWithContentReady;
- ++tileset._statistics.numberOfLoadedTilesTotal;
-
- // Add to the tile cache. Previously expired tiles are already in the cache and won't get re-added.
- tileset._cache.add(tile);
- }
-
- tileset.tileLoad.raiseEvent(tile);
- };
+ tileset.tileLoad.raiseEvent(tile);
}
/**
@@ -2523,7 +2493,10 @@ function filterProcessingQueue(tileset) {
let removeCount = 0;
for (let i = 0; i < tiles.length; ++i) {
const tile = tiles[i];
- if (tile._contentState !== Cesium3DTileContentState.PROCESSING) {
+ if (
+ tile.isDestroyed() ||
+ tile._contentState !== Cesium3DTileContentState.PROCESSING
+ ) {
++removeCount;
continue;
}
@@ -2543,8 +2516,21 @@ function filterProcessingQueue(tileset) {
function processTiles(tileset, frameState) {
filterProcessingQueue(tileset);
const tiles = tileset._processingQueue;
+ const statistics = tileset._statistics;
+ let tile;
for (let i = 0; i < tiles.length; ++i) {
- tiles[i].process(tileset, frameState);
+ tile = tiles[i];
+ try {
+ tile.process(tileset, frameState);
+
+ if (tile.contentReady) {
+ --statistics.numberOfTilesProcessing;
+ handleTileSuccess(tileset, tile, tile.content);
+ }
+ } catch (error) {
+ --statistics.numberOfTilesProcessing;
+ handleTileFailure(error, tileset, tile);
+ }
}
}
diff --git a/packages/engine/Source/Scene/Composite3DTileContent.js b/packages/engine/Source/Scene/Composite3DTileContent.js
index d1fc612bd182..0c224a38cfad 100644
--- a/packages/engine/Source/Scene/Composite3DTileContent.js
+++ b/packages/engine/Source/Scene/Composite3DTileContent.js
@@ -17,14 +17,7 @@ import RuntimeError from "../Core/RuntimeError.js";
*
* @private
*/
-function Composite3DTileContent(
- tileset,
- tile,
- resource,
- arrayBuffer,
- byteOffset,
- factory
-) {
+function Composite3DTileContent(tileset, tile, resource) {
this._tileset = tileset;
this._tile = tile;
this._resource = resource;
@@ -32,8 +25,7 @@ function Composite3DTileContent(
this._metadata = undefined;
this._group = undefined;
-
- this._readyPromise = initialize(this, arrayBuffer, byteOffset, factory);
+ this._ready = false;
}
Object.defineProperties(Composite3DTileContent.prototype, {
@@ -130,9 +122,9 @@ Object.defineProperties(Composite3DTileContent.prototype, {
},
},
- readyPromise: {
+ ready: {
get: function () {
- return this._readyPromise;
+ return this._ready;
},
},
@@ -210,7 +202,14 @@ Object.defineProperties(Composite3DTileContent.prototype, {
const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
-function initialize(content, arrayBuffer, byteOffset, factory) {
+Composite3DTileContent.fromTileType = async function (
+ tileset,
+ tile,
+ resource,
+ arrayBuffer,
+ byteOffset,
+ factory
+) {
byteOffset = defaultValue(byteOffset, 0);
const uint8Array = new Uint8Array(arrayBuffer);
@@ -231,14 +230,13 @@ function initialize(content, arrayBuffer, byteOffset, factory) {
const tilesLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
- const contentPromises = [];
+ const content = new Composite3DTileContent(tileset, tile, resource);
// For caching purposes, models within the composite tile must be
// distinguished. To do this, add a query parameter ?compositeIndex=i.
// Since composite tiles may contain other composite tiles, check for an
// existing prefix and separate them with underscores. e.g.
// ?compositeIndex=0_1_1
- const resource = content._resource;
let prefix = resource.queryParameters.compositeIndex;
if (defined(prefix)) {
// We'll be adding another value at the end, so add an underscore.
@@ -265,15 +263,16 @@ function initialize(content, arrayBuffer, byteOffset, factory) {
});
if (defined(contentFactory)) {
- const innerContent = contentFactory(
- content._tileset,
- content._tile,
- childResource,
- arrayBuffer,
- byteOffset
+ const innerContent = await Promise.resolve(
+ contentFactory(
+ content._tileset,
+ content._tile,
+ childResource,
+ arrayBuffer,
+ byteOffset
+ )
);
content._contents.push(innerContent);
- contentPromises.push(innerContent.readyPromise);
} else {
throw new RuntimeError(
`Unknown tile content type, ${tileType}, inside Composite tile`
@@ -283,10 +282,8 @@ function initialize(content, arrayBuffer, byteOffset, factory) {
byteOffset += tileByteLength;
}
- return Promise.all(contentPromises).then(function () {
- return content;
- });
-}
+ return content;
+};
/**
* Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent
@@ -326,9 +323,13 @@ Composite3DTileContent.prototype.applyStyle = function (style) {
Composite3DTileContent.prototype.update = function (tileset, frameState) {
const contents = this._contents;
const length = contents.length;
+ let ready = true;
for (let i = 0; i < length; ++i) {
contents[i].update(tileset, frameState);
+ ready = ready && contents[i].ready;
}
+
+ this._ready = ready;
};
Composite3DTileContent.prototype.isDestroyed = function () {
diff --git a/packages/engine/Source/Scene/Empty3DTileContent.js b/packages/engine/Source/Scene/Empty3DTileContent.js
index 59f31a58b1f9..13f427b0e02b 100644
--- a/packages/engine/Source/Scene/Empty3DTileContent.js
+++ b/packages/engine/Source/Scene/Empty3DTileContent.js
@@ -64,9 +64,9 @@ Object.defineProperties(Empty3DTileContent.prototype, {
},
},
- readyPromise: {
+ ready: {
get: function () {
- return undefined;
+ return true;
},
},
diff --git a/packages/engine/Source/Scene/Geometry3DTileContent.js b/packages/engine/Source/Scene/Geometry3DTileContent.js
index 572cfe4fb68b..f9ea775d9108 100644
--- a/packages/engine/Source/Scene/Geometry3DTileContent.js
+++ b/packages/engine/Source/Scene/Geometry3DTileContent.js
@@ -42,7 +42,8 @@ function Geometry3DTileContent(
this.featurePropertiesDirty = false;
this._group = undefined;
- this._readyPromise = initialize(this, arrayBuffer, byteOffset);
+ this._ready = false;
+ initialize(this, arrayBuffer, byteOffset);
}
Object.defineProperties(Geometry3DTileContent.prototype, {
@@ -96,9 +97,9 @@ Object.defineProperties(Geometry3DTileContent.prototype, {
},
},
- readyPromise: {
+ ready: {
get: function () {
- return this._readyPromise;
+ return this._ready;
},
},
@@ -224,7 +225,7 @@ function getBatchIds(featureTableJson, featureTableBinary) {
if (atLeastOneDefined && atLeastOneUndefined) {
throw new RuntimeError(
- "If one group of batch ids is defined, then all batch ids must be defined."
+ "If one group of batch ids is defined, then all batch ids must be defined"
);
}
@@ -290,7 +291,7 @@ function initialize(content, arrayBuffer, byteOffset) {
byteOffset += sizeOfUint32;
if (byteLength === 0) {
- content._readyPromise.resolve(content);
+ this._ready = true;
return;
}
@@ -451,9 +452,7 @@ function initialize(content, arrayBuffer, byteOffset) {
boundingVolume: content.tile.boundingVolume.boundingVolume,
});
- return content._geometries.readyPromise.then(function () {
- return content;
- });
+ return content;
}
return Promise.resolve(content);
@@ -509,8 +508,10 @@ Geometry3DTileContent.prototype.update = function (tileset, frameState) {
this._geometries.debugWireframe = this._tileset.debugWireframe;
this._geometries.update(frameState);
}
- if (defined(this._batchTable) && this._geometries._ready) {
+
+ if (defined(this._batchTable) && this._geometries.ready) {
this._batchTable.update(tileset, frameState);
+ this._ready = true;
}
};
diff --git a/packages/engine/Source/Scene/GltfBufferViewLoader.js b/packages/engine/Source/Scene/GltfBufferViewLoader.js
index 4a79f097b39a..a0bb01d6d378 100644
--- a/packages/engine/Source/Scene/GltfBufferViewLoader.js
+++ b/packages/engine/Source/Scene/GltfBufferViewLoader.js
@@ -212,7 +212,7 @@ function getBufferLoader(bufferViewLoader) {
* @private
*/
GltfBufferViewLoader.prototype.unload = function () {
- if (defined(this._bufferLoader)) {
+ if (defined(this._bufferLoader) && !this._bufferLoader.isDestroyed()) {
this._resourceCache.unload(this._bufferLoader);
}
diff --git a/packages/engine/Source/Scene/GltfImageLoader.js b/packages/engine/Source/Scene/GltfImageLoader.js
index 873b0cf1ec7d..91ae40022b81 100644
--- a/packages/engine/Source/Scene/GltfImageLoader.js
+++ b/packages/engine/Source/Scene/GltfImageLoader.js
@@ -300,7 +300,10 @@ function loadImageFromUri(resource) {
* @private
*/
GltfImageLoader.prototype.unload = function () {
- if (defined(this._bufferViewLoader)) {
+ if (
+ defined(this._bufferViewLoader) &&
+ !this._bufferViewLoader.isDestroyed()
+ ) {
this._resourceCache.unload(this._bufferViewLoader);
}
diff --git a/packages/engine/Source/Scene/GltfIndexBufferLoader.js b/packages/engine/Source/Scene/GltfIndexBufferLoader.js
index 563545e21ae6..ca6ace4e0533 100644
--- a/packages/engine/Source/Scene/GltfIndexBufferLoader.js
+++ b/packages/engine/Source/Scene/GltfIndexBufferLoader.js
@@ -394,7 +394,10 @@ GltfIndexBufferLoader.prototype.unload = function () {
const resourceCache = this._resourceCache;
- if (defined(this._bufferViewLoader)) {
+ if (
+ defined(this._bufferViewLoader) &&
+ !this._bufferViewLoader.isDestroyed()
+ ) {
resourceCache.unload(this._bufferViewLoader);
}
diff --git a/packages/engine/Source/Scene/GltfJsonLoader.js b/packages/engine/Source/Scene/GltfJsonLoader.js
index 782f3231e3c6..616c28a42651 100644
--- a/packages/engine/Source/Scene/GltfJsonLoader.js
+++ b/packages/engine/Source/Scene/GltfJsonLoader.js
@@ -281,7 +281,9 @@ GltfJsonLoader.prototype.unload = function () {
const bufferLoaders = this._bufferLoaders;
const bufferLoadersLength = bufferLoaders.length;
for (let i = 0; i < bufferLoadersLength; ++i) {
- this._resourceCache.unload(bufferLoaders[i]);
+ bufferLoaders[i] =
+ !bufferLoaders[i].isDestroyed() &&
+ this._resourceCache.unload(bufferLoaders[i]);
}
this._bufferLoaders.length = 0;
diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js
index a32ad01bbefd..24db24b33bc9 100644
--- a/packages/engine/Source/Scene/GltfLoader.js
+++ b/packages/engine/Source/Scene/GltfLoader.js
@@ -260,6 +260,8 @@ function GltfLoader(options) {
this._geometryCallbacks = [];
this._structuralMetadataLoader = undefined;
this._loadResourcesPromise = undefined;
+ this._resourcesLoaded = false;
+ this._texturesLoaded = false;
// In some cases where geometry post-processing is needed (like generating
// outlines) new attributes are added that may have GPU resources attached.
@@ -295,11 +297,7 @@ Object.defineProperties(GltfLoader.prototype, {
*
* @memberof GltfLoader.prototype
*
-<<<<<<< HEAD
* @type {ModelComponents.Components}
-=======
- * @type {string}
->>>>>>> no-ready-promises
* @readonly
* @private
*/
@@ -326,7 +324,7 @@ Object.defineProperties(GltfLoader.prototype, {
},
},
/**
- * true if textures are loaded separately from the other glTF resources.
+ * Returns true if textures are loaded separately from the other glTF resources.
*
* @memberof GltfLoader.prototype
*
@@ -339,16 +337,24 @@ Object.defineProperties(GltfLoader.prototype, {
return this._incrementallyLoadTextures;
},
},
+ /**
+ * true if textures are loaded, useful when incrementallyLoadTextures is true
+ *
+ * @memberof GltfLoader.prototype
+ *
+ * @type {boolean}
+ * @readonly
+ * @private
+ */
+ texturesLoaded: {
+ get: function () {
+ return this._texturesLoaded;
+ },
+ },
});
/**
-<<<<<<< HEAD
* Loads the gltf object
-=======
- * Loads the resource.
- * @returns {Promise} A promise which resolves to the loader when the resource loading is completed.
- * @private
->>>>>>> no-ready-promises
*/
async function loadGltfJson(loader) {
loader._state = GltfLoaderState.LOADING;
@@ -425,7 +431,7 @@ async function loadResources(loader, frameState) {
// frame. Any promise failures are collected, and will be handled synchronously in process(). Also note that it's fine to call process before a loader is ready
// to process or after it has failed; nothing will happen.
const gltf = loader.gltfJson;
- parse(loader, gltf, supportedImageFormats, frameState);
+ const promise = parse(loader, gltf, supportedImageFormats, frameState);
// All resource loaders have been created, so we can begin processing
loader._state = GltfLoaderState.PROCESSING;
@@ -437,6 +443,8 @@ async function loadResources(loader, frameState) {
ResourceCache.unload(loader._gltfJsonLoader);
loader._gltfJsonLoader = undefined;
}
+
+ return promise;
}
/**
@@ -550,12 +558,15 @@ GltfLoader.prototype._process = function (frameState) {
processLoaders(this, frameState);
}
- if (this._state === GltfLoaderState.POST_PROCESSING) {
+ if (
+ this._resourcesLoaded &&
+ this._state === GltfLoaderState.POST_PROCESSING
+ ) {
postProcessGeometry(this, frameState.context);
this._state = GltfLoaderState.PROCESSED;
}
- if (this._state === GltfLoaderState.PROCESSED) {
+ if (this._resourcesLoaded && this._state === GltfLoaderState.PROCESSED) {
// The buffer views can be unloaded once the data is copied.
unloadBufferViewLoaders(this);
@@ -601,6 +612,7 @@ GltfLoader.prototype._processTextures = function (frameState) {
}
this._textureState = GltfLoaderState.READY;
+ this._texturesLoaded = true;
return true;
};
@@ -619,11 +631,13 @@ GltfLoader.prototype.process = function (frameState) {
this._state === GltfLoaderState.LOADED &&
!defined(this._loadResourcesPromise)
) {
- this._loadResourcesPromise = loadResources(this, frameState).catch(
- (error) => {
+ this._loadResourcesPromise = loadResources(this, frameState)
+ .then(() => {
+ this._resourcesLoaded = true;
+ })
+ .catch((error) => {
this._processError = error;
- }
- );
+ });
}
if (defined(this._processError)) {
@@ -642,11 +656,11 @@ GltfLoader.prototype.process = function (frameState) {
throw error;
}
- let ready = false;
if (this._state === GltfLoaderState.FAILED) {
- return ready;
+ return false;
}
+ let ready = false;
try {
ready = this._process(frameState);
} catch (error) {
@@ -2646,18 +2660,16 @@ function parse(loader, gltf, supportedImageFormats, frameState) {
readyPromises.push.apply(readyPromises, loader._texturesPromises);
}
- Promise.all(readyPromises).catch((error) => {
- // This error will be thrown synchronously during the next call
- // to process()
- loader._processError = error;
- });
+ return Promise.all(readyPromises);
}
function unloadTextures(loader) {
const textureLoaders = loader._textureLoaders;
const textureLoadersLength = textureLoaders.length;
for (let i = 0; i < textureLoadersLength; ++i) {
- ResourceCache.unload(textureLoaders[i]);
+ textureLoaders[i] =
+ !textureLoaders[i].isDestroyed() &&
+ ResourceCache.unload(textureLoaders[i]);
}
loader._textureLoaders.length = 0;
}
@@ -2666,7 +2678,9 @@ function unloadBufferViewLoaders(loader) {
const bufferViewLoaders = loader._bufferViewLoaders;
const bufferViewLoadersLength = bufferViewLoaders.length;
for (let i = 0; i < bufferViewLoadersLength; ++i) {
- ResourceCache.unload(bufferViewLoaders[i]);
+ bufferViewLoaders[i] =
+ !bufferViewLoaders[i].isDestroyed() &&
+ ResourceCache.unload(bufferViewLoaders[i]);
}
loader._bufferViewLoaders.length = 0;
}
@@ -2675,7 +2689,9 @@ function unloadGeometry(loader) {
const geometryLoaders = loader._geometryLoaders;
const geometryLoadersLength = geometryLoaders.length;
for (let i = 0; i < geometryLoadersLength; ++i) {
- ResourceCache.unload(geometryLoaders[i]);
+ geometryLoaders[i] =
+ !geometryLoaders[i].isDestroyed() &&
+ ResourceCache.unload(geometryLoaders[i]);
}
loader._geometryLoaders.length = 0;
}
@@ -2693,7 +2709,10 @@ function unloadGeneratedAttributes(loader) {
}
function unloadStructuralMetadata(loader) {
- if (defined(loader._structuralMetadataLoader)) {
+ if (
+ defined(loader._structuralMetadataLoader) &&
+ !loader._structuralMetadataLoader.isDestroyed()
+ ) {
loader._structuralMetadataLoader.destroy();
loader._structuralMetadataLoader = undefined;
}
diff --git a/packages/engine/Source/Scene/GltfTextureLoader.js b/packages/engine/Source/Scene/GltfTextureLoader.js
index f97d1ec2f27a..277c077bb42a 100644
--- a/packages/engine/Source/Scene/GltfTextureLoader.js
+++ b/packages/engine/Source/Scene/GltfTextureLoader.js
@@ -369,7 +369,7 @@ GltfTextureLoader.prototype.unload = function () {
this._texture.destroy();
}
- if (defined(this._imageLoader)) {
+ if (defined(this._imageLoader) && !this._imageLoader.isDestroyed()) {
this._resourceCache.unload(this._imageLoader);
}
diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js
index 9835fa6c3177..52cc5e5dc4a8 100644
--- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js
+++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js
@@ -452,7 +452,10 @@ GltfVertexBufferLoader.prototype.unload = function () {
const resourceCache = this._resourceCache;
- if (defined(this._bufferViewLoader)) {
+ if (
+ defined(this._bufferViewLoader) &&
+ !this._bufferViewLoader.isDestroyed()
+ ) {
resourceCache.unload(this._bufferViewLoader);
}
diff --git a/packages/engine/Source/Scene/Implicit3DTileContent.js b/packages/engine/Source/Scene/Implicit3DTileContent.js
index fc6bef81a2f1..8f93e9dc5de3 100644
--- a/packages/engine/Source/Scene/Implicit3DTileContent.js
+++ b/packages/engine/Source/Scene/Implicit3DTileContent.js
@@ -26,6 +26,7 @@ import parseBoundingVolumeSemantics from "./parseBoundingVolumeSemantics.js";
*
* Implements the {@link Cesium3DTileContent} interface.
*
+ * This object is normally not instantiated directly, use {@link Implicit3DTileContent.fromSubtreeJson}.
*
* @alias Implicit3DTileContent
* @constructor
@@ -42,20 +43,10 @@ import parseBoundingVolumeSemantics from "./parseBoundingVolumeSemantics.js";
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
-function Implicit3DTileContent(
- tileset,
- tile,
- resource,
- json,
- arrayBuffer,
- byteOffset
-) {
+function Implicit3DTileContent(tileset, tile, resource) {
//>>includeStart('debug', pragmas.debug);
Check.defined("tile.implicitTileset", tile.implicitTileset);
Check.defined("tile.implicitCoordinates", tile.implicitCoordinates);
- if (defined(json) === defined(arrayBuffer)) {
- throw new DeveloperError("One of json and arrayBuffer must be defined.");
- }
//>>includeEnd('debug');
const implicitTileset = tile.implicitTileset;
@@ -81,7 +72,7 @@ function Implicit3DTileContent(
);
this._url = subtreeResource.getUrlComponent(true);
- this._readyPromise = initialize(this, json, arrayBuffer, byteOffset);
+ this._ready = false;
}
Object.defineProperties(Implicit3DTileContent.prototype, {
@@ -127,9 +118,9 @@ Object.defineProperties(Implicit3DTileContent.prototype, {
},
},
- readyPromise: {
+ ready: {
get: function () {
- return this._readyPromise;
+ return this._ready;
},
},
@@ -188,33 +179,58 @@ Object.defineProperties(Implicit3DTileContent.prototype, {
* Initialize the implicit content by parsing the subtree resource and setting
* up a promise chain to expand the immediate subtree.
*
- * @param {Implicit3DTileContent} content The implicit content
+ * @param {Cesium3DTileset} tileset The tileset this content belongs to
+ * @param {Cesium3DTile} tile The tile this content belongs to.
+ * @param {Resource} resource The resource for the tileset
* @param {object} [json] The JSON containing the subtree. Mutually exclusive with arrayBuffer.
* @param {ArrayBuffer} [arrayBuffer] The ArrayBuffer containing a subtree binary. Mutually exclusive with json.
* @param {number} [byteOffset=0] The byte offset into the arrayBuffer
+ * @return {Promise}
+ *
+ * @exception {DeveloperError} One of json and arrayBuffer must be defined.
+ *
* @private
*/
-function initialize(content, json, arrayBuffer, byteOffset) {
+Implicit3DTileContent.fromSubtreeJson = async function (
+ tileset,
+ tile,
+ resource,
+ json,
+ arrayBuffer,
+ byteOffset
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("tile.implicitTileset", tile.implicitTileset);
+ Check.defined("tile.implicitCoordinates", tile.implicitCoordinates);
+ if (defined(json) === defined(arrayBuffer)) {
+ throw new DeveloperError("One of json and arrayBuffer must be defined.");
+ }
+ //>>includeEnd('debug');
+
byteOffset = defaultValue(byteOffset, 0);
let uint8Array;
if (defined(arrayBuffer)) {
uint8Array = new Uint8Array(arrayBuffer, byteOffset);
}
- const subtree = new ImplicitSubtree(
- content._resource,
+ const implicitTileset = tile.implicitTileset;
+ const implicitCoordinates = tile.implicitCoordinates;
+
+ const subtree = await ImplicitSubtree.fromSubtreeJson(
+ resource,
json,
uint8Array,
- content._implicitTileset,
- content._implicitCoordinates
+ implicitTileset,
+ implicitCoordinates
);
+ const content = new Implicit3DTileContent(tileset, tile, resource);
+
content._implicitSubtree = subtree;
- return subtree.readyPromise.then(function () {
- expandSubtree(content, subtree);
- return content;
- });
-}
+ expandSubtree(content, subtree);
+ content._ready = true;
+ return content;
+};
/**
* Expand a single subtree placeholder tile. This transcodes the subtree into
diff --git a/packages/engine/Source/Scene/ImplicitSubtree.js b/packages/engine/Source/Scene/ImplicitSubtree.js
index ea8060f1673d..97133ffa17db 100644
--- a/packages/engine/Source/Scene/ImplicitSubtree.js
+++ b/packages/engine/Source/Scene/ImplicitSubtree.js
@@ -22,34 +22,24 @@ import ResourceCache from "./ResourceCache.js";
* Subtrees also handle content metadata and metadata about the subtree itself.
*
*
+ * This object is normally not instantiated directly, use {@link ImplicitSubtree.fromSubtreeJson}.
+ *
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata#implicit-tile-properties|Implicit Tile Properties in the 3DTILES_metadata specification}
+ * @see ImplicitSubtree.fromSubtreeJson
*
* @alias ImplicitSubtree
* @constructor
*
* @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
- * @param {object} [json] The JSON object for this subtree. Mutually exclusive with subtreeView.
- * @param {Uint8Array} [subtreeView] The contents of a subtree binary in a Uint8Array. Mutually exclusive with json.
* @param {ImplicitTileset} implicitTileset The implicit tileset. This includes information about the size of subtrees
* @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
*
- * @exception {DeveloperError} One of json and subtreeView must be defined.
- *
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
-function ImplicitSubtree(
- resource,
- json,
- subtreeView,
- implicitTileset,
- implicitCoordinates
-) {
+function ImplicitSubtree(resource, implicitTileset, implicitCoordinates) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("resource", resource);
- if (defined(json) === defined(subtreeView)) {
- throw new DeveloperError("One of json and subtreeView must be defined.");
- }
Check.typeOf.object("implicitTileset", implicitTileset);
Check.typeOf.object("implicitCoordinates", implicitCoordinates);
//>>includeEnd('debug');
@@ -77,21 +67,21 @@ function ImplicitSubtree(
this._tileJumpBuffer = undefined;
this._contentJumpBuffers = [];
- this._readyPromise = initialize(this, json, subtreeView, implicitTileset);
+ this._ready = false;
}
Object.defineProperties(ImplicitSubtree.prototype, {
/**
- * A promise that resolves once all necessary availability buffers
+ * Returns true once all necessary availability buffers
* are loaded.
*
- * @type {Promise}
+ * @type {boolean}
* @readonly
* @private
*/
- readyPromise: {
+ ready: {
get: function () {
- return this._readyPromise;
+ return this._ready;
},
},
@@ -309,16 +299,40 @@ ImplicitSubtree.prototype.getParentMortonIndex = function (mortonIndex) {
/**
* Parse all relevant information out of the subtree. This fetches any
- * external buffers that are used by the implicit tileset. When finished,
- * it resolves/rejects subtree.readyPromise.
+ * external buffers that are used by the implicit tileset.
*
- * @param {ImplicitSubtree} subtree The subtree
+ * @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
* @param {object} [json] The JSON object for this subtree. If parsing from a binary subtree file, this will be undefined.
* @param {Uint8Array} [subtreeView] The contents of the subtree binary
* @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to.
+ * @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
+ * @return {Promise} The created subtree
* @private
+ *
+ * @exception {DeveloperError} One of json and subtreeView must be defined.
*/
-function initialize(subtree, json, subtreeView, implicitTileset) {
+ImplicitSubtree.fromSubtreeJson = async function (
+ resource,
+ json,
+ subtreeView,
+ implicitTileset,
+ implicitCoordinates
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("resource", resource);
+ if (defined(json) === defined(subtreeView)) {
+ throw new DeveloperError("One of json and subtreeView must be defined.");
+ }
+ Check.typeOf.object("implicitTileset", implicitTileset);
+ Check.typeOf.object("implicitCoordinates", implicitCoordinates);
+ //>>includeEnd('debug');
+
+ const subtree = new ImplicitSubtree(
+ resource,
+ implicitTileset,
+ implicitCoordinates
+ );
+
let chunks;
if (defined(json)) {
chunks = {
@@ -409,26 +423,25 @@ function initialize(subtree, json, subtreeView, implicitTileset) {
markActiveMetadataBufferViews(contentPropertyTableJson, bufferViewHeaders);
}
- return requestActiveBuffers(subtree, bufferHeaders, chunks.binary).then(
- function (buffersU8) {
- const bufferViewsU8 = parseActiveBufferViews(
- bufferViewHeaders,
- buffersU8
- );
- parseAvailability(subtree, subtreeJson, implicitTileset, bufferViewsU8);
+ const buffersU8 = await requestActiveBuffers(
+ subtree,
+ bufferHeaders,
+ chunks.binary
+ );
+ const bufferViewsU8 = parseActiveBufferViews(bufferViewHeaders, buffersU8);
+ parseAvailability(subtree, subtreeJson, implicitTileset, bufferViewsU8);
- if (defined(tilePropertyTableJson)) {
- parseTileMetadataTable(subtree, implicitTileset, bufferViewsU8);
- makeTileJumpBuffer(subtree);
- }
+ if (defined(tilePropertyTableJson)) {
+ parseTileMetadataTable(subtree, implicitTileset, bufferViewsU8);
+ makeTileJumpBuffer(subtree);
+ }
- parseContentMetadataTables(subtree, implicitTileset, bufferViewsU8);
- makeContentJumpBuffers(subtree);
+ parseContentMetadataTables(subtree, implicitTileset, bufferViewsU8);
+ makeContentJumpBuffers(subtree);
- return subtree;
- }
- );
-}
+ subtree._ready = true;
+ return subtree;
+};
/**
* A helper object for storing the two parts of the subtree binary
@@ -708,7 +721,7 @@ function requestActiveBuffers(subtree, bufferHeaders, internalBuffer) {
});
}
-function requestExternalBuffer(subtree, bufferHeader) {
+async function requestExternalBuffer(subtree, bufferHeader) {
const baseResource = subtree._resource;
const bufferResource = baseResource.getDerivedResource({
url: bufferHeader.uri,
@@ -719,9 +732,17 @@ function requestExternalBuffer(subtree, bufferHeader) {
});
subtree._bufferLoader = bufferLoader;
- return bufferLoader.load().then(function (bufferLoader) {
- return bufferLoader.typedArray;
- });
+ try {
+ await bufferLoader.load();
+ } catch (error) {
+ if (bufferLoader.isDestroyed()) {
+ return;
+ }
+
+ throw error;
+ }
+
+ return bufferLoader.typedArray;
}
/**
diff --git a/packages/engine/Source/Scene/Model/B3dmLoader.js b/packages/engine/Source/Scene/Model/B3dmLoader.js
index ebdb1bbf7115..7385f18a690d 100644
--- a/packages/engine/Source/Scene/Model/B3dmLoader.js
+++ b/packages/engine/Source/Scene/Model/B3dmLoader.js
@@ -129,34 +129,17 @@ if (defined(Object.create)) {
Object.defineProperties(B3dmLoader.prototype, {
/**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
+ * true if textures are loaded, useful when incrementallyLoadTextures is true
*
* @memberof B3dmLoader.prototype
*
- * @type {Promise|undefined}
+ * @type {boolean}
* @readonly
* @private
*/
- promise: {
+ texturesLoaded: {
get: function () {
- return this._promise;
- },
- },
-
- /**
- * A promise that resolves when all textures are loaded.
- * When incrementallyLoadTextures is true this may resolve after
- * promise resolves.
- *
- * @memberof B3dmLoader.prototype
- *
- * @type {Promise}
- * @readonly
- * @private
- */
- texturesLoadedPromise: {
- get: function () {
- return this._gltfLoader.texturesLoadedPromise;
+ return this._gltfLoader?.texturesLoaded;
},
},
/**
@@ -196,6 +179,10 @@ Object.defineProperties(B3dmLoader.prototype, {
* @private
*/
B3dmLoader.prototype.load = function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
const b3dm = B3dmParser.parse(this._arrayBuffer, this._byteOffset);
let batchLength = b3dm.batchLength;
@@ -246,37 +233,21 @@ B3dmLoader.prototype.load = function () {
this._state = B3dmLoaderState.LOADING;
const that = this;
- gltfLoader.load();
- this._promise = gltfLoader.promise
+ this._promise = gltfLoader
+ .load()
.then(function () {
if (that.isDestroyed()) {
return;
}
- const components = gltfLoader.components;
-
- // Combine the RTC_CENTER transform from the b3dm and the CESIUM_RTC
- // transform from the glTF. In practice usually only one or the
- // other is supplied. If they don't exist the transforms will
- // be identity matrices.
- components.transform = Matrix4.multiplyTransformation(
- that._transform,
- components.transform,
- components.transform
- );
- createStructuralMetadata(that, components);
- that._components = components;
-
- // Now that we have the parsed components, we can release the array buffer
- that._arrayBuffer = undefined;
-
- that._state = B3dmLoaderState.READY;
+ that._state = B3dmLoaderState.PROCESSING;
return that;
})
.catch(function (error) {
if (that.isDestroyed()) {
return;
}
+
return handleError(that, error);
});
@@ -296,13 +267,38 @@ B3dmLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- if (this._state === B3dmLoaderState.LOADING) {
- this._state = B3dmLoaderState.PROCESSING;
+ if (this._state === B3dmLoaderState.READY) {
+ return true;
+ }
+
+ if (this._state !== B3dmLoaderState.PROCESSING) {
+ return false;
}
- if (this._state === B3dmLoaderState.PROCESSING) {
- this._gltfLoader.process(frameState);
+ const ready = this._gltfLoader.process(frameState);
+ if (!ready) {
+ return false;
}
+
+ const components = this._gltfLoader.components;
+
+ // Combine the RTC_CENTER transform from the b3dm and the CESIUM_RTC
+ // transform from the glTF. In practice usually only one or the
+ // other is supplied. If they don't exist the transforms will
+ // be identity matrices.
+ components.transform = Matrix4.multiplyTransformation(
+ this._transform,
+ components.transform,
+ components.transform
+ );
+ createStructuralMetadata(this, components);
+ this._components = components;
+
+ // Now that we have the parsed components, we can release the array buffer
+ this._arrayBuffer = undefined;
+
+ this._state = B3dmLoaderState.READY;
+ return true;
};
function createStructuralMetadata(loader, components) {
@@ -368,7 +364,7 @@ function processNode(node) {
}
B3dmLoader.prototype.unload = function () {
- if (defined(this._gltfLoader)) {
+ if (defined(this._gltfLoader) && !this._gltfLoader.isDestroyed()) {
this._gltfLoader.unload();
}
diff --git a/packages/engine/Source/Scene/Model/GeoJsonLoader.js b/packages/engine/Source/Scene/Model/GeoJsonLoader.js
index 239edbb679cc..5bc67d5ec9ca 100644
--- a/packages/engine/Source/Scene/Model/GeoJsonLoader.js
+++ b/packages/engine/Source/Scene/Model/GeoJsonLoader.js
@@ -48,8 +48,6 @@ function GeoJsonLoader(options) {
//>>includeEnd('debug');
this._geoJson = options.geoJson;
- this._promise = undefined;
- this._process = function (loader, frameState) {};
this._components = undefined;
}
@@ -59,20 +57,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(GeoJsonLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource has not yet started loading.
- *
- * @memberof GeoJsonLoader.prototype
- *
- * @type {Promise|Undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource.
*
@@ -109,21 +93,7 @@ Object.defineProperties(GeoJsonLoader.prototype, {
* @private
*/
GeoJsonLoader.prototype.load = function () {
- const loader = this;
- const promise = new Promise(function (resolve) {
- loader._process = function (loader, frameState) {
- if (defined(loader._components)) {
- return;
- }
-
- loader._components = parse(loader._geoJson, frameState);
- loader._geoJson = undefined;
- resolve(loader);
- };
- });
-
- this._promise = promise;
- return promise;
+ return Promise.resolve(this);
};
/**
@@ -137,7 +107,13 @@ GeoJsonLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- this._process(this, frameState);
+ if (defined(this._components)) {
+ return true;
+ }
+
+ this._components = parse(this._geoJson, frameState);
+ this._geoJson = undefined;
+ return true;
};
function ParsedFeature() {
diff --git a/packages/engine/Source/Scene/Model/I3dmLoader.js b/packages/engine/Source/Scene/Model/I3dmLoader.js
index 0bb74d099817..ceffc90f272e 100644
--- a/packages/engine/Source/Scene/Model/I3dmLoader.js
+++ b/packages/engine/Source/Scene/Model/I3dmLoader.js
@@ -116,9 +116,6 @@ function I3dmLoader(options) {
this._promise = undefined;
this._gltfLoader = undefined;
- this._gltfLoaderPromise = undefined;
- this._process = function (loader, frameState) {};
- this._postProcess = function (loader, frameState) {};
// Instanced attributes are initially parsed as typed arrays, but if they
// do not need to be further processed (e.g. turned into transform matrices),
@@ -140,34 +137,17 @@ if (defined(Object.create)) {
Object.defineProperties(I3dmLoader.prototype, {
/**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
+ * true if textures are loaded, useful when incrementallyLoadTextures is true
*
- * @memberof I3dmLoader.prototype
- *
- * @type {Promise|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
-
- /**
- * A promise that resolves when all textures are loaded.
- * When incrementallyLoadTextures is true this may resolve after
- * promise resolves.
+ * @memberof I3dmLoader.prototype
*
- * @memberof I3dmLoader.prototype
- *
- * @type {Promise}
+ * @type {boolean}
* @readonly
* @private
*/
- texturesLoadedPromise: {
+ texturesLoaded: {
get: function () {
- return this._gltfLoader.texturesLoadedPromise;
+ return this._gltfLoader?.texturesLoaded;
},
},
/**
@@ -208,6 +188,10 @@ Object.defineProperties(I3dmLoader.prototype, {
* @private
*/
I3dmLoader.prototype.load = function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
// Parse the i3dm into its various sections.
const i3dm = I3dmParser.parse(this._arrayBuffer, this._byteOffset);
@@ -282,53 +266,21 @@ I3dmLoader.prototype.load = function () {
this._gltfLoader = gltfLoader;
this._state = I3dmLoaderState.LOADING;
- gltfLoader.load();
-
- const that = this;
- const processPromise = new Promise(function (resolve) {
- that._process = function (loader, frameState) {
- loader._gltfLoader.process(frameState);
- };
-
- that._postProcess = function (loader, frameState) {
- const gltfLoader = loader._gltfLoader;
- const components = gltfLoader.components;
-
- // Combine the RTC_CENTER transform from the i3dm and the CESIUM_RTC
- // transform from the glTF. In practice CESIUM_RTC is not set for
- // instanced models but multiply the transforms just in case.
- components.transform = Matrix4.multiplyTransformation(
- loader._transform,
- components.transform,
- components.transform
- );
-
- createInstances(loader, components, frameState);
- createStructuralMetadata(loader, components);
- loader._components = components;
-
- // Now that we have the parsed components, we can release the array buffer
- loader._arrayBuffer = undefined;
-
- loader._state = I3dmLoaderState.READY;
- resolve(loader);
- };
- });
-
- this._promise = gltfLoader.promise
- .then(function () {
- if (that.isDestroyed()) {
+ this._promise = gltfLoader
+ .load()
+ .then(() => {
+ if (this.isDestroyed()) {
return;
}
- that._state = I3dmLoaderState.POST_PROCESSING;
- return processPromise;
+ this._state = I3dmLoaderState.PROCESSING;
+ return this;
})
- .catch(function (error) {
- if (that.isDestroyed()) {
+ .catch((error) => {
+ if (this.isDestroyed()) {
return;
}
- return handleError(that, error);
+ throw handleError(this, error);
});
return this._promise;
@@ -338,8 +290,7 @@ function handleError(i3dmLoader, error) {
i3dmLoader.unload();
i3dmLoader._state = I3dmLoaderState.FAILED;
const errorMessage = "Failed to load i3dm";
- error = i3dmLoader.getError(errorMessage, error);
- return Promise.reject(error);
+ return i3dmLoader.getError(errorMessage, error);
}
I3dmLoader.prototype.process = function (frameState) {
@@ -347,17 +298,40 @@ I3dmLoader.prototype.process = function (frameState) {
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
- if (this._state === I3dmLoaderState.LOADING) {
- this._state = I3dmLoaderState.PROCESSING;
+ if (this._state === I3dmLoaderState.READY) {
+ return true;
}
+ const gltfLoader = this._gltfLoader;
+ let ready = false;
if (this._state === I3dmLoaderState.PROCESSING) {
- this._process(this, frameState);
+ ready = gltfLoader.process(frameState);
}
- if (this._state === I3dmLoaderState.POST_PROCESSING) {
- this._postProcess(this, frameState);
+ if (!ready) {
+ return false;
}
+
+ const components = gltfLoader.components;
+
+ // Combine the RTC_CENTER transform from the i3dm and the CESIUM_RTC
+ // transform from the glTF. In practice CESIUM_RTC is not set for
+ // instanced models but multiply the transforms just in case.
+ components.transform = Matrix4.multiplyTransformation(
+ this._transform,
+ components.transform,
+ components.transform
+ );
+
+ createInstances(this, components, frameState);
+ createStructuralMetadata(this, components);
+ this._components = components;
+
+ // Now that we have the parsed components, we can release the array buffer
+ this._arrayBuffer = undefined;
+
+ this._state = I3dmLoaderState.READY;
+ return true;
};
function createStructuralMetadata(loader, components) {
@@ -895,7 +869,7 @@ I3dmLoader.prototype.isUnloaded = function () {
};
I3dmLoader.prototype.unload = function () {
- if (defined(this._gltfLoader)) {
+ if (defined(this._gltfLoader) && !this._gltfLoader.isDestroyed()) {
this._gltfLoader.unload();
}
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index 2dbbfd094326..d7fbad7f790e 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -534,7 +534,7 @@ function handleError(model, error) {
return;
}
- console.error(error);
+ console.log(error);
}
function createModelFeatureTables(model, structuralMetadata) {
@@ -679,6 +679,21 @@ Object.defineProperties(Model.prototype, {
},
},
+ /**
+ * Returns true if textures are loaded separately from the other glTF resources.
+ *
+ * @memberof GltfLoader.prototype
+ *
+ * @type {boolean}
+ * @readonly
+ * @private
+ */
+ incrementallyLoadTextures: {
+ get: function () {
+ return defaultValue(this._loader.incrementallyLoadTextures, false);
+ },
+ },
+
/**
* Gets an event that, if {@link Model.incrementallyLoadTextures} is true, is raised when the model textures are loaded and ready for rendering, i.e. when the external resources
* have been downloaded and the WebGL resources are created. Event listeners
@@ -711,7 +726,7 @@ Object.defineProperties(Model.prototype, {
get: function () {
deprecationWarning(
"Model.readyPromise",
- "Model.readyPromise was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync and Model.readyEvent instead."
+ "Model.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Model.fromGltfAsync and Model.readyEvent instead."
);
return this._readyPromise;
},
@@ -734,7 +749,7 @@ Object.defineProperties(Model.prototype, {
get: function () {
deprecationWarning(
"Model.texturesLoadedPromise",
- "Model.texturesLoadedPromise was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync and Model.texturesReadyEvent instead."
+ "Model.texturesLoadedPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Model.fromGltfAsync and Model.texturesReadyEvent instead."
);
return this._texturesLoadedPromise;
},
@@ -1925,8 +1940,11 @@ Model.prototype.update = function (frameState) {
return;
}
- if (this._loader.incrementallyLoadTextures && !this._texturesLoaded) {
- // TODO: Check if textures are actually ready
+ if (
+ this._loader.incrementallyLoadTextures &&
+ !this._texturesLoaded &&
+ this._loader.texturesLoaded
+ ) {
// Re-run the pipeline so texture memory statistics are re-computed
this.resetDrawCommands();
@@ -2763,7 +2781,7 @@ Model.prototype.destroyModelResources = function () {
Model.fromGltf = function (options) {
deprecationWarning(
"Model.fromGltf",
- "Model.fromGltf was deprecated in CesiumJS 1.102. It will be removed in 1.104. Use Model.fromGltfAsync instead."
+ "Model.fromGltf was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Model.fromGltfAsync instead."
);
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
@@ -2995,7 +3013,7 @@ Model.fromGltfAsync = async function (options) {
/*
* @private
*/
-Model.fromB3dm = function (options) {
+Model.fromB3dm = async function (options) {
const loaderOptions = {
b3dmResource: options.resource,
arrayBuffer: options.arrayBuffer,
@@ -3013,15 +3031,22 @@ Model.fromB3dm = function (options) {
const loader = new B3dmLoader(loaderOptions);
- const modelOptions = makeModelOptions(loader, ModelType.TILE_B3DM, options);
- const model = new Model(modelOptions);
- return model;
+ try {
+ await loader.load();
+
+ const modelOptions = makeModelOptions(loader, ModelType.TILE_B3DM, options);
+ const model = new Model(modelOptions);
+ return model;
+ } catch (error) {
+ loader.destroy();
+ throw error;
+ }
};
/**
* @private
*/
-Model.fromPnts = function (options) {
+Model.fromPnts = async function (options) {
const loaderOptions = {
arrayBuffer: options.arrayBuffer,
byteOffset: options.byteOffset,
@@ -3029,15 +3054,21 @@ Model.fromPnts = function (options) {
};
const loader = new PntsLoader(loaderOptions);
- const modelOptions = makeModelOptions(loader, ModelType.TILE_PNTS, options);
- const model = new Model(modelOptions);
- return model;
+ try {
+ await loader.load();
+ const modelOptions = makeModelOptions(loader, ModelType.TILE_PNTS, options);
+ const model = new Model(modelOptions);
+ return model;
+ } catch (error) {
+ loader.destroy();
+ throw error;
+ }
};
/*
* @private
*/
-Model.fromI3dm = function (options) {
+Model.fromI3dm = async function (options) {
const loaderOptions = {
i3dmResource: options.resource,
arrayBuffer: options.arrayBuffer,
@@ -3053,15 +3084,22 @@ Model.fromI3dm = function (options) {
};
const loader = new I3dmLoader(loaderOptions);
- const modelOptions = makeModelOptions(loader, ModelType.TILE_I3DM, options);
- const model = new Model(modelOptions);
- return model;
+ try {
+ await loader.load();
+
+ const modelOptions = makeModelOptions(loader, ModelType.TILE_I3DM, options);
+ const model = new Model(modelOptions);
+ return model;
+ } catch (error) {
+ loader.destroy();
+ throw error;
+ }
};
/*
* @private
*/
-Model.fromGeoJson = function (options) {
+Model.fromGeoJson = async function (options) {
const loaderOptions = {
geoJson: options.geoJson,
};
diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js
index 51744b06bf2a..52c51879f4b8 100644
--- a/packages/engine/Source/Scene/Model/Model3DTileContent.js
+++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js
@@ -14,6 +14,8 @@ import Model from "./Model.js";
*
* Implements the {@link Cesium3DTileContent} interface.
*
+ * This object is normally not instantiated directly, use {@link Model3DTileContent.fromGltf}, {@link Model3DTileContent.fromB3dm}, {@link Model3DTileContent.fromI3dm}, {@link Model3DTileContent.fromPnts}, or {@link Model3DTileContent.fromGeoJson}.
+ *
* @alias Model3DTileContent
* @constructor
* @private
@@ -24,9 +26,9 @@ function Model3DTileContent(tileset, tile, resource) {
this._resource = resource;
this._model = undefined;
- this._readyPromise = undefined;
this._metadata = undefined;
this._group = undefined;
+ this._ready = false;
}
Object.defineProperties(Model3DTileContent.prototype, {
@@ -83,9 +85,9 @@ Object.defineProperties(Model3DTileContent.prototype, {
},
},
- readyPromise: {
+ ready: {
get: function () {
- return this._readyPromise;
+ return this._ready;
},
},
@@ -244,6 +246,15 @@ Model3DTileContent.prototype.update = function (tileset, frameState) {
}
model.update(frameState);
+
+ if (!this._ready && model.ready) {
+ // Animation can only be added once the model is ready
+ model.activeAnimations.addAll({
+ loop: ModelAnimationLoop.REPEAT,
+ });
+
+ this._ready = true;
+ }
};
Model3DTileContent.prototype.isDestroyed = function () {
@@ -255,7 +266,7 @@ Model3DTileContent.prototype.destroy = function () {
return destroyObject(this);
};
-Model3DTileContent.fromGltf = function (tileset, tile, resource, gltf) {
+Model3DTileContent.fromGltf = async function (tileset, tile, resource, gltf) {
const content = new Model3DTileContent(tileset, tile, resource);
const additionalOptions = {
@@ -276,21 +287,13 @@ Model3DTileContent.fromGltf = function (tileset, tile, resource, gltf) {
modelOptions.classificationType = classificationType;
- // This should be removed when readyPromise is deprecated across 3D Tiles functions
- const model = Model.fromGltf(modelOptions);
+ const model = await Model.fromGltfAsync(modelOptions);
content._model = model;
- // Include the animation setup in the ready promise to avoid an uncaught exception
- content._readyPromise = model.readyPromise.then(function (model) {
- model.activeAnimations.addAll({
- loop: ModelAnimationLoop.REPEAT,
- });
- return model;
- });
return content;
};
-Model3DTileContent.fromB3dm = function (
+Model3DTileContent.fromB3dm = async function (
tileset,
tile,
resource,
@@ -318,15 +321,13 @@ Model3DTileContent.fromB3dm = function (
modelOptions.classificationType = classificationType;
- const model = Model.fromB3dm(modelOptions);
+ const model = await Model.fromB3dm(modelOptions);
content._model = model;
- // This should be removed when readyPromise is deprecated across 3D Tiles functions
- content._readyPromise = model._readyPromise;
return content;
};
-Model3DTileContent.fromI3dm = function (
+Model3DTileContent.fromI3dm = async function (
tileset,
tile,
resource,
@@ -348,15 +349,13 @@ Model3DTileContent.fromI3dm = function (
additionalOptions
);
- const model = Model.fromI3dm(modelOptions);
+ const model = await Model.fromI3dm(modelOptions);
content._model = model;
- // This should be removed when readyPromise is deprecated across 3D Tiles functions
- content._readyPromise = model._readyPromise;
return content;
};
-Model3DTileContent.fromPnts = function (
+Model3DTileContent.fromPnts = async function (
tileset,
tile,
resource,
@@ -377,15 +376,18 @@ Model3DTileContent.fromPnts = function (
content,
additionalOptions
);
- const model = Model.fromPnts(modelOptions);
+ const model = await Model.fromPnts(modelOptions);
content._model = model;
- // This should be removed when readyPromise is deprecated across 3D Tiles functions
- content._readyPromise = model._readyPromise;
return content;
};
-Model3DTileContent.fromGeoJson = function (tileset, tile, resource, geoJson) {
+Model3DTileContent.fromGeoJson = async function (
+ tileset,
+ tile,
+ resource,
+ geoJson
+) {
const content = new Model3DTileContent(tileset, tile, resource);
const additionalOptions = {
@@ -399,10 +401,8 @@ Model3DTileContent.fromGeoJson = function (tileset, tile, resource, geoJson) {
content,
additionalOptions
);
- const model = Model.fromGeoJson(modelOptions);
+ const model = await Model.fromGeoJson(modelOptions);
content._model = model;
- // This should be removed when readyPromise is deprecated across 3D Tiles functions
- content._readyPromise = model._readyPromise;
return content;
};
diff --git a/packages/engine/Source/Scene/Model/PntsLoader.js b/packages/engine/Source/Scene/Model/PntsLoader.js
index ac6013e1cd7f..2565dd01a420 100644
--- a/packages/engine/Source/Scene/Model/PntsLoader.js
+++ b/packages/engine/Source/Scene/Model/PntsLoader.js
@@ -66,7 +66,7 @@ function PntsLoader(options) {
this._decodedAttributes = undefined;
this._promise = undefined;
- this._process = function (frameState) {};
+ this._error = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._buffers = [];
@@ -81,20 +81,6 @@ if (defined(Object.create)) {
}
Object.defineProperties(PntsLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
- *
- * @memberof PntsLoader.prototype
- *
- * @type {Promise|undefined}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise;
- },
- },
/**
* The cache key of the resource
*
@@ -148,28 +134,36 @@ Object.defineProperties(PntsLoader.prototype, {
* @private
*/
PntsLoader.prototype.load = function () {
+ if (defined(this._promise)) {
+ return this._promise;
+ }
+
this._parsedContent = PntsParser.parse(this._arrayBuffer, this._byteOffset);
this._state = ResourceLoaderState.PROCESSING;
- const loader = this;
- this._promise = new Promise(function (resolve, reject) {
- loader._process = function (frameState) {
- if (loader._state === ResourceLoaderState.PROCESSING) {
- if (defined(loader._decodePromise)) {
- return;
- }
-
- const decodePromise = decodeDraco(loader, frameState.context);
- if (defined(decodePromise)) {
- decodePromise.then(resolve).catch(reject);
- }
- }
- };
- });
+ this._promise = Promise.resolve(this);
};
PntsLoader.prototype.process = function (frameState) {
- this._process(frameState);
+ if (defined(this._error)) {
+ const error = this._error;
+ this._error = undefined;
+ throw error;
+ }
+
+ if (this._state === ResourceLoaderState.READY) {
+ return true;
+ }
+
+ if (this._state === ResourceLoaderState.PROCESSING) {
+ if (defined(this._decodePromise)) {
+ return false;
+ }
+
+ this._decodePromise = decodeDraco(this, frameState.context);
+ }
+
+ return false;
};
function decodeDraco(loader, context) {
@@ -207,7 +201,8 @@ function decodeDraco(loader, context) {
loader.unload();
loader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load Draco pnts";
- return Promise.reject(loader.getError(errorMessage, error));
+ // This error will be thrown next time process is called;
+ loader._error = loader.getError(errorMessage, error);
});
}
diff --git a/packages/engine/Source/Scene/Multiple3DTileContent.js b/packages/engine/Source/Scene/Multiple3DTileContent.js
index 51b20bb93a45..c5ebb7e8a819 100644
--- a/packages/engine/Source/Scene/Multiple3DTileContent.js
+++ b/packages/engine/Source/Scene/Multiple3DTileContent.js
@@ -54,6 +54,7 @@ function Multiple3DTileContent(tileset, tile, tilesetResource, contentsJson) {
const contentCount = this._innerContentHeaders.length;
this._arrayFetchPromises = new Array(contentCount);
this._requests = new Array(contentCount);
+ this._ready = false;
this._innerContentResources = new Array(contentCount);
this._serverKeys = new Array(contentCount);
@@ -70,9 +71,6 @@ function Multiple3DTileContent(tileset, tile, tilesetResource, contentsJson) {
this._innerContentResources[i] = contentResource;
this._serverKeys[i] = serverKey;
}
-
- // undefined until the first time requests are scheduled
- this._contentsFetchedPromise = undefined;
}
Object.defineProperties(Multiple3DTileContent.prototype, {
@@ -213,35 +211,13 @@ Object.defineProperties(Multiple3DTileContent.prototype, {
},
},
- /**
- * Part of the {@link Cesium3DTileContent} interface. This behaves
- * differently for Multiple3DTileContent: it returns
- * undefined if the inner contents have not yet been created. This
- * is handled specially in {@link Cesium3DTile}.
- *
- * @memberof Multiple3DTileContent.prototype
- *
- * @type {Promise|undefined}
- * @readonly
- *
- * @private
- */
- readyPromise: {
+ ready: {
get: function () {
- // The contents haven't been created yet, short-circuit. The tile
- // will handle this as a special case
if (!this._contentsCreated) {
- return undefined;
+ return false;
}
- const readyPromises = this._contents.map(function (content) {
- return content.readyPromise;
- });
-
- const that = this;
- return Promise.all(readyPromises).then(function () {
- return that;
- });
+ return this._ready;
},
},
@@ -338,21 +314,6 @@ Object.defineProperties(Multiple3DTileContent.prototype, {
});
},
},
-
- /**
- * A promise that resolves when all of the inner contents have been fetched.
- * This promise is undefined until the first frame where all array buffer
- * requests have been scheduled.
- * @memberof Multiple3DTileContent.prototype
- *
- * @type {Promise}
- * @private
- */
- contentsFetchedPromise: {
- get: function () {
- return this._contentsFetchedPromise;
- },
- },
});
function updatePendingRequests(multipleContents, deltaRequestCount) {
@@ -386,7 +347,7 @@ function cancelPendingRequests(multipleContents, originalContentState) {
* requests are successfully scheduled.
*
*
- * @return {number} The number of attempted requests that were unable to be scheduled.
+ * @return {Promise|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled.
* @private
*/
Multiple3DTileContent.prototype.requestInnerContents = function () {
@@ -395,26 +356,26 @@ Multiple3DTileContent.prototype.requestInnerContents = function () {
// if we can schedule all the requests at once. If not, no requests are
// scheduled
if (!canScheduleAllRequests(this._serverKeys)) {
- return this._serverKeys.length;
+ this.tileset.statistics.numberOfAttemptedRequests += this._serverKeys.length;
+ return;
}
const contentHeaders = this._innerContentHeaders;
updatePendingRequests(this, contentHeaders.length);
+ const originalCancelCount = this._cancelCount;
for (let i = 0; i < contentHeaders.length; i++) {
// The cancel count is needed to avoid a race condition where a content
// is canceled multiple times.
this._arrayFetchPromises[i] = requestInnerContent(
this,
i,
- this._cancelCount,
+ originalCancelCount,
this._tile._contentState
);
}
- this._contentsFetchedPromise = createInnerContents(this);
-
- return 0;
+ return createInnerContents(this);
};
/**
@@ -476,28 +437,39 @@ function requestInnerContent(
const promise = contentResource.fetchArrayBuffer();
if (!defined(promise)) {
- return Promise.resolve(undefined);
+ return;
}
return promise
.then(function (arrayBuffer) {
- // Short circuit if another inner content was canceled.
+ // Pending requests have already been canceled.
if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
+ return;
+ }
+
+ if (
+ contentResource.request.cancelled ||
+ contentResource.request.state === RequestState.CANCELLED
+ ) {
+ cancelPendingRequests(multipleContents, originalContentState);
+ return;
}
updatePendingRequests(multipleContents, -1);
return arrayBuffer;
})
.catch(function (error) {
- // Short circuit if another inner content was canceled.
+ // Pending requests have already been canceled.
if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
+ return;
}
- if (contentResource.request.state === RequestState.CANCELLED) {
+ if (
+ contentResource.request.cancelled ||
+ contentResource.request.state === RequestState.CANCELLED
+ ) {
cancelPendingRequests(multipleContents, originalContentState);
- return undefined;
+ return;
}
updatePendingRequests(multipleContents, -1);
@@ -505,42 +477,54 @@ function requestInnerContent(
});
}
-function createInnerContents(multipleContents) {
+async function createInnerContents(multipleContents) {
const originalCancelCount = multipleContents._cancelCount;
- return Promise.all(multipleContents._arrayFetchPromises).then(function (
- arrayBuffers
- ) {
- // Short circuit if inner contents were canceled
- if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
+ const arrayBuffers = await Promise.all(multipleContents._arrayFetchPromises);
+ // Request have been cancelled
+ if (originalCancelCount < multipleContents._cancelCount) {
+ return;
+ }
+
+ const createPromise = async (arrayBuffer, i) => {
+ if (!defined(arrayBuffer)) {
+ // Content was not fetched. The error was handled in
+ // the fetch promise. Return undefined to indicate partial failure.
+ return;
}
- const contents = arrayBuffers.map(function (arrayBuffer, i) {
- if (!defined(arrayBuffer)) {
- // Content was not fetched. The error was handled in
- // the fetch promise. Return undefined to indicate partial failure.
- return undefined;
- }
+ try {
+ const contents = await createInnerContent(
+ multipleContents,
+ arrayBuffer,
+ i
+ );
+ return contents;
+ } catch (error) {
+ handleInnerContentFailed(multipleContents, i, error);
+ }
+ };
- return createInnerContent(multipleContents, arrayBuffer, i);
- });
+ const promises = [];
+ for (let i = 0; i < arrayBuffers.length; ++i) {
+ const arrayBuffer = arrayBuffers[i];
+ promises.push(createPromise(arrayBuffer, i));
+ }
- // Even if we had a partial success, mark that we finished creating
- // contents
- multipleContents._contentsCreated = true;
- multipleContents._contents = contents.filter(defined);
- });
+ // Even if we had a partial success, mark that we finished creating
+ // contents
+ const contents = await Promise.all(promises);
+ multipleContents._contentsCreated = true;
+ multipleContents._contents = contents.filter(defined);
+ return contents;
}
-function createInnerContent(multipleContents, arrayBuffer, index) {
+async function createInnerContent(multipleContents, arrayBuffer, index) {
const preprocessed = preprocess3DTileContent(arrayBuffer);
if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) {
- const error = new RuntimeError(
+ throw new RuntimeError(
"External tilesets are disallowed inside multiple contents"
);
-
- return handleInnerContentFailed(multipleContents, index, error);
}
multipleContents._disableSkipLevelOfDetail =
@@ -555,16 +539,20 @@ function createInnerContent(multipleContents, arrayBuffer, index) {
let content;
const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType];
if (defined(preprocessed.binaryPayload)) {
- content = contentFactory(
- tileset,
- tile,
- resource,
- preprocessed.binaryPayload.buffer,
- 0
+ content = await Promise.resolve(
+ contentFactory(
+ tileset,
+ tile,
+ resource,
+ preprocessed.binaryPayload.buffer,
+ 0
+ )
);
} else {
// JSON formats
- content = contentFactory(tileset, tile, resource, preprocessed.jsonPayload);
+ content = await Promise.resolve(
+ contentFactory(tileset, tile, resource, preprocessed.jsonPayload)
+ );
}
const contentHeader = multipleContents._innerContentHeaders[index];
@@ -653,9 +641,13 @@ Multiple3DTileContent.prototype.applyStyle = function (style) {
Multiple3DTileContent.prototype.update = function (tileset, frameState) {
const contents = this._contents;
const length = contents.length;
+ let ready = true;
for (let i = 0; i < length; ++i) {
contents[i].update(tileset, frameState);
+ ready = ready && contents[i].ready;
}
+
+ this._ready = ready;
};
Multiple3DTileContent.prototype.isDestroyed = function () {
diff --git a/packages/engine/Source/Scene/ResourceCache.js b/packages/engine/Source/Scene/ResourceCache.js
index 1aaaedbd05bf..21d8e8c4a664 100644
--- a/packages/engine/Source/Scene/ResourceCache.js
+++ b/packages/engine/Source/Scene/ResourceCache.js
@@ -532,19 +532,11 @@ function hasDracoCompression(draco, semantic) {
* @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
* @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
* @param {FrameState} options.frameState The frame state.
-<<<<<<< HEAD
- * @param {Object} [options.draco] The Draco extension object.
- * @param {Boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
- * @param {Boolean} [options.loadBuffer=false] Load index buffer as a GPU index buffer.
- * @param {Boolean} [options.loadTypedArray=false] Load index buffer as a typed array.
- * @returns {GltfIndexBufferLoader} The cached index buffer loader.
-=======
* @param {object} [options.draco] The Draco extension object.
* @param {boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
* @param {boolean} [options.loadBuffer=false] Load index buffer as a GPU index buffer.
* @param {boolean} [options.loadTypedArray=false] Load index buffer as a typed array.
- * @returns {GltfIndexBufferLoader} The index buffer loader.
->>>>>>> no-ready-promises
+ * @returns {GltfIndexBufferLoader} The cached index buffer loader.
* @private
*/
ResourceCache.getIndexBufferLoader = function (options) {
diff --git a/packages/engine/Source/Scene/ResourceCacheStatistics.js b/packages/engine/Source/Scene/ResourceCacheStatistics.js
index 08aaf513dadf..3492d757be18 100644
--- a/packages/engine/Source/Scene/ResourceCacheStatistics.js
+++ b/packages/engine/Source/Scene/ResourceCacheStatistics.js
@@ -55,7 +55,6 @@ ResourceCacheStatistics.prototype.clear = function () {
*
If the geometry has a CPU copy of the GPU buffer, it will be added to the count
});
}
- primitives[0].primitive.readyPromise.then(function (model) {
- // Play and loop all animations at half-speed
- model.activeAnimations.addAll({
- multiplier: 0.5,
- loop: Cesium.ModelAnimationLoop.REPEAT,
- });
-
- // Zoom to model
+ primitives[0].primitive.readyEvent.addEventListener((model) => {
+ // Zoom to first model
r = 2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.5;
const center = model.boundingSphere.center;
diff --git a/Apps/Sandcastle/gallery/Manually Controlled Animation.html b/Apps/Sandcastle/gallery/Manually Controlled Animation.html
index 7ee0b66ed6a8..dca08d980e75 100644
--- a/Apps/Sandcastle/gallery/Manually Controlled Animation.html
+++ b/Apps/Sandcastle/gallery/Manually Controlled Animation.html
@@ -109,36 +109,25 @@
}
// Add our model.
- const modelPrimitive = viewer.scene.primitives.add(
- Cesium.Model.fromGltf({
- url: "../../SampleData/models/CesiumMan/Cesium_Man.glb",
- scale: 4,
- })
- );
- const modelLabel = viewer.entities.add({
- position: position,
- orientation: new Cesium.VelocityOrientationProperty(position), // Automatically set the model's orientation to the direction it's facing.
- label: {
- text: new Cesium.CallbackProperty(updateSpeedLabel, false),
- font: "20px sans-serif",
- showBackground: true,
- distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
- 0.0,
- 100.0
- ),
- eyeOffset: new Cesium.Cartesian3(0, 7.2, 0),
- },
- });
+ try {
+ const modelPrimitive = viewer.scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: "../../SampleData/models/CesiumMan/Cesium_Man.glb",
+ scale: 4,
+ })
+ );
- modelPrimitive.readyPromise.then(function (model) {
- model.activeAnimations.addAll({
- loop: Cesium.ModelAnimationLoop.REPEAT,
- animationTime: function (duration) {
- return distance.getValue(viewer.clock.currentTime) / duration;
- },
- multiplier: 0.25,
+ modelPrimitive.readyEvent.addEventListener(() => {
+ modelPrimitive.activeAnimations.addAll({
+ loop: Cesium.ModelAnimationLoop.REPEAT,
+ animationTime: function (duration) {
+ return distance.getValue(viewer.clock.currentTime) / duration;
+ },
+ multiplier: 0.25,
+ });
});
- const rot = new Cesium.Matrix3();
+
+ const rotation = new Cesium.Matrix3();
viewer.scene.preUpdate.addEventListener(function () {
const time = viewer.clock.currentTime;
const pos = position.getValue(time);
@@ -148,10 +137,31 @@
pos,
vel,
viewer.scene.globe.ellipsoid,
- rot
+ rotation
+ );
+ Cesium.Matrix4.fromRotationTranslation(
+ rotation,
+ pos,
+ modelPrimitive.modelMatrix
);
- Cesium.Matrix4.fromRotationTranslation(rot, pos, model.modelMatrix);
});
+ } catch (error) {
+ window.alert(error);
+ }
+
+ const modelLabel = viewer.entities.add({
+ position: position,
+ orientation: new Cesium.VelocityOrientationProperty(position), // Automatically set the model's orientation to the direction it's facing.
+ label: {
+ text: new Cesium.CallbackProperty(updateSpeedLabel, false),
+ font: "20px sans-serif",
+ showBackground: true,
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
+ 0.0,
+ 100.0
+ ),
+ eyeOffset: new Cesium.Cartesian3(0, 7.2, 0),
+ },
});
viewer.trackedEntity = modelLabel;
modelLabel.viewFrom = new Cesium.Cartesian3(-30.0, -10.0, 10.0);
diff --git a/Apps/Sandcastle/gallery/development/3D Models Articulations.html b/Apps/Sandcastle/gallery/development/3D Models Articulations.html
index d276ccb100e4..97a795c2b986 100644
--- a/Apps/Sandcastle/gallery/development/3D Models Articulations.html
+++ b/Apps/Sandcastle/gallery/development/3D Models Articulations.html
@@ -107,16 +107,16 @@
new Cesium.HeadingPitchRoll()
);
- const model = scene.primitives.add(
- Cesium.Model.fromGltf({
- url: modelUrl,
- modelMatrix: modelMatrix,
- minimumPixelSize: 128,
- })
- );
+ try {
+ const model = scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: modelUrl,
+ modelMatrix: modelMatrix,
+ minimumPixelSize: 128,
+ })
+ );
- model.readyPromise
- .then(function (model) {
+ model.readyEvent.addEventListener(() => {
const camera = viewer.camera;
// Zoom to model
@@ -176,10 +176,10 @@
};
});
viewModel.selectedArticulation = viewModel.articulations[0];
- })
- .catch(function (error) {
- console.error(error);
});
+ } catch (error) {
+ console.log(`Error loading model: ${error}`);
+ }
//Sandcastle_End
};
diff --git a/Apps/Sandcastle/gallery/development/3D Models Node Explorer.html b/Apps/Sandcastle/gallery/development/3D Models Node Explorer.html
index 84165f2d7a49..7c13672c0299 100644
--- a/Apps/Sandcastle/gallery/development/3D Models Node Explorer.html
+++ b/Apps/Sandcastle/gallery/development/3D Models Node Explorer.html
@@ -302,16 +302,16 @@
new Cesium.HeadingPitchRoll()
);
- const model = scene.primitives.add(
- Cesium.Model.fromGltf({
- url: modelUrl,
- modelMatrix: modelMatrix,
- minimumPixelSize: 128,
- })
- );
+ try {
+ const model = scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: modelUrl,
+ modelMatrix: modelMatrix,
+ minimumPixelSize: 128,
+ })
+ );
- model.readyPromise
- .then(function (model) {
+ model.readyEvent.addEventListener(() => {
const camera = viewer.camera;
// Zoom to model
@@ -363,10 +363,10 @@
new Cesium.Matrix4()
);
});
- })
- .catch(function (error) {
- window.alert(error);
});
+ } catch (error) {
+ window.alert(error);
+ }
//Sandcastle_End
};
diff --git a/Apps/Sandcastle/gallery/development/3D Models.html b/Apps/Sandcastle/gallery/development/3D Models.html
index d182236569ff..6fc693c9a412 100644
--- a/Apps/Sandcastle/gallery/development/3D Models.html
+++ b/Apps/Sandcastle/gallery/development/3D Models.html
@@ -160,7 +160,7 @@
model.colorBlendAmount = Number(newValue);
});
- function createModel(url, height, heading, pitch, roll) {
+ async function createModel(url, height, heading, pitch, roll) {
height = Cesium.defaultValue(height, 0.0);
heading = Cesium.defaultValue(heading, 0.0);
pitch = Cesium.defaultValue(pitch, 0.0);
@@ -178,16 +178,16 @@
);
scene.primitives.removeAll(); // Remove previous model
- model = scene.primitives.add(
- Cesium.Model.fromGltf({
- url: url,
- modelMatrix: modelMatrix,
- minimumPixelSize: 128,
- })
- );
+ try {
+ model = scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: url,
+ modelMatrix: modelMatrix,
+ minimumPixelSize: 128,
+ })
+ );
- model.readyPromise
- .then(function (model) {
+ model.readyEvent.addEventListener(() => {
model.color = Cesium.Color.fromAlpha(
getColor(viewModel.color),
Number(viewModel.alpha)
@@ -218,10 +218,10 @@
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 2.0)
);
- })
- .catch(function (error) {
- window.alert(error);
});
+ } catch (error) {
+ window.alert(error);
+ }
}
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
diff --git a/Apps/Sandcastle/gallery/development/Display Conditions.html b/Apps/Sandcastle/gallery/development/Display Conditions.html
index f3838c402087..1c38d42e364d 100644
--- a/Apps/Sandcastle/gallery/development/Display Conditions.html
+++ b/Apps/Sandcastle/gallery/development/Display Conditions.html
@@ -78,7 +78,7 @@
);
}
- function addPointAndModel() {
+ async function addPointAndModel() {
Sandcastle.declare(addPointAndModel);
const position = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
@@ -94,16 +94,22 @@
),
});
- scene.primitives.add(
- Cesium.Model.fromGltf({
- url: "../../SampleData/models/GroundVehicle/GroundVehicle.glb",
- modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(position),
- distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
- 0.0,
- 250.5
- ),
- })
- );
+ try {
+ scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: "../../SampleData/models/GroundVehicle/GroundVehicle.glb",
+ modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(
+ position
+ ),
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
+ 0.0,
+ 250.5
+ ),
+ })
+ );
+ } catch (error) {
+ window.alert(error);
+ }
}
Sandcastle.addToolbarMenu([
diff --git a/Apps/Sandcastle/gallery/development/Multiple Shadows.html b/Apps/Sandcastle/gallery/development/Multiple Shadows.html
index a87df91fcc86..e72b532ec680 100644
--- a/Apps/Sandcastle/gallery/development/Multiple Shadows.html
+++ b/Apps/Sandcastle/gallery/development/Multiple Shadows.html
@@ -72,28 +72,30 @@
shadowMap.enabled = true;
- const model = scene.primitives.add(
- Cesium.Model.fromGltf({
- url:
- "../../SampleData/models/ShadowTester/Shadow_Tester_Point.glb",
- modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
- center,
- new Cesium.HeadingPitchRoll(heading, 0.0, 0.0)
- ),
- })
- );
-
- model.readyPromise
- .then(function (model) {
- // Play and loop all animations at half-speed
- model.activeAnimations.addAll({
- multiplier: 0.5,
- loop: Cesium.ModelAnimationLoop.REPEAT,
+ (async () => {
+ try {
+ const model = scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url:
+ "../../SampleData/models/ShadowTester/Shadow_Tester_Point.glb",
+ modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
+ center,
+ new Cesium.HeadingPitchRoll(heading, 0.0, 0.0)
+ ),
+ })
+ );
+
+ model.readyEvent.addEventListener(() => {
+ // Play and loop all animations at half-speed
+ model.activeAnimations.addAll({
+ multiplier: 0.5,
+ loop: Cesium.ModelAnimationLoop.REPEAT,
+ });
});
- })
- .catch(function (error) {
+ } catch (error) {
window.alert(error);
- });
+ }
+ })();
return shadowMap;
}
diff --git a/Apps/Sandcastle/gallery/development/Shadows.html b/Apps/Sandcastle/gallery/development/Shadows.html
index 67a7ac804203..f6d39e4adcbe 100644
--- a/Apps/Sandcastle/gallery/development/Shadows.html
+++ b/Apps/Sandcastle/gallery/development/Shadows.html
@@ -913,32 +913,32 @@
});
}
- function createModel(url, origin) {
+ async function createModel(url, origin) {
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
new Cesium.HeadingPitchRoll()
);
- const model = scene.primitives.add(
- Cesium.Model.fromGltf({
- url: url,
- modelMatrix: modelMatrix,
- })
- );
+ try {
+ const model = scene.primitives.add(
+ await Cesium.Model.fromGltfAsync({
+ url: url,
+ modelMatrix: modelMatrix,
+ })
+ );
- model.readyPromise
- .then(function (model) {
+ model.readyEvent.addEventListener(() => {
// Play and loop all animations at half-speed
model.activeAnimations.addAll({
multiplier: 0.5,
loop: Cesium.ModelAnimationLoop.REPEAT,
});
- })
- .catch(function (error) {
- window.alert(error);
});
- return model;
+ return model;
+ } catch (error) {
+ window.alert(error);
+ }
}
function createBoxRTC(origin) {
diff --git a/CHANGES.md b/CHANGES.md
index c3ffcc9b3e98..ee6040c27b50 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,7 +6,7 @@
##### Additions :tada:
-- Added `ArcGisMapServerImageryProvider.fromUrl`, `ArcGISTiledElevationTerrainProvider.fromUrl`, `BingMapsImageryProvider.fromUrl`, `CesiumTerrainProvider.fromUrl`, `GoogleEarthEnterpriseMetadata.fromUrl`, `GoogleEarthEnterpriseImageryProvider.fromMetadata`, `GoogleEarthEnterpriseMapsProvider.fromUrl`, `GoogleEarthEnterpriseTerrainProvider.fromMetadata`, `ImageryLayer.fromProviderAsync`, `IonImageryProvider.fromAssetId`, `SingleTileImageryProvider.fromUrl`, `Terrain`, `TileMapServiceImageryProvider.fromUrl`, `VRTheWorldTerrainProvider.fromUrl`, and `createWorldTerrainAsync` for better async flow and error handling.
+- Added `ArcGisMapServerImageryProvider.fromUrl`, `ArcGISTiledElevationTerrainProvider.fromUrl`, `BingMapsImageryProvider.fromUrl`, `CesiumTerrainProvider.fromUrl`, `GoogleEarthEnterpriseMetadata.fromUrl`, `GoogleEarthEnterpriseImageryProvider.fromMetadata`, `GoogleEarthEnterpriseMapsProvider.fromUrl`, `GoogleEarthEnterpriseTerrainProvider.fromMetadata`, `ImageryLayer.fromProviderAsync`, `IonImageryProvider.fromAssetId`, `SingleTileImageryProvider.fromUrl`, `Terrain`, `TileMapServiceImageryProvider.fromUrl`, `VRTheWorldTerrainProvider.fromUrl`, `createWorldTerrainAsync`, `Model.fromGltfAsync`, , `Model.readyEvent`, `Model.errorEvent`, and `Model.texturesReadyEvent` for better async flow and error handling.
##### Fixes :wrench:
@@ -45,6 +45,21 @@
- `GoogleEarthEnterpriseTerrainProvider` constructor parameters `options.url` and `options.metadata`, `GoogleEarthEnterpriseTerrainProvider.ready`, and `GoogleEarthEnterpriseTerrainProvider.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `GoogleEarthEnterpriseTerrainProvider.fromMetadata` instead.
- `VRTheWorldTerrainProvider` constructor parameter `options.url`, `VRTheWorldTerrainProvider.ready`, and `VRTheWorldTerrainProvider.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `VRTheWorldTerrainProvider.fromUrl` instead.
- `createWorldTerrain` was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use createWorldTerrainAsync instead.
+- `Model.fromGltf`, `Model.readyPromise`, and `Model.texturesLoadedPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `Model.fromGltfAsync`, `Model.readyEvent`, `Model.errorEvent`, and `Model.texturesReadyEvent` instead. For example:
+
+ ```js
+ try {
+ const model = await Cesium.Model.fromGltfAsync({
+ url: "../../SampleData/models/CesiumMan/Cesium_Man.glb",
+ });
+ viewer.scene.primitives.add(model);
+ model.readyEvent.addEventListener(() => {
+ // model is ready for rendering
+ });
+ } catch (error) {
+ console.log(`Failed to load model. ${error}`);
+ }
+ ```
#### @cesium/widgets
diff --git a/Documentation/Contributors/TestingGuide/README.md b/Documentation/Contributors/TestingGuide/README.md
index 2488dae152ca..fdf75033ccb8 100644
--- a/Documentation/Contributors/TestingGuide/README.md
+++ b/Documentation/Contributors/TestingGuide/README.md
@@ -678,9 +678,18 @@ Make external requests that assume the tests are being used with an Internet con
(For an introduction to promises, see [JavaScript Promises - There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)).
-For asynchronous testing, Jasmine's `it` function uses a `done` callback. For better integration with CesiumJS's asynchronous patterns, CesiumJS replaces `it` with a function that can return promises.
+Jasmine also has support for running specs that require testing asynchronous operations. The functions that you pass to `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, and `it` can be declared `async`. These functions can also return promises. There are also cases where asynchronous functions that explicitly return promises should be tested. See the [Asynchronous Work tutorial](https://jasmine.github.io/tutorials/async) for more information.
-Here is a simplified example of a test from [ModelSpec.js](https://github.com/CesiumGS/cesium/blob/main/Specs/Scene/Model/ModelSpec.js):
+Here is a simplified example of `beforeAll` from [sampleTerrainSpec.js](https://github.com/CesiumGS/cesium/blob/main/packages/engine/Specs/Core/sampleTerrainSpec.js):
+
+```javascript
+let worldTerrain;
+beforeAll(async function () {
+ worldTerrain = await createWorldTerrainAsync();
+});
+```
+
+Here is a simplified example of a test from [ModelSpec.js](https://github.com/CesiumGS/cesium/blob/main/packages/engine/Specs/Scene/Model/ModelSpec.js):
```javascript
const modelUrl = "./Data/Models/glTF-2.0/Box/glTF/Box.gltf";
@@ -694,59 +703,50 @@ afterAll(function () {
scene.destroyForSpecs();
});
-it("renders glTF model", function () {
- return loadAndZoomToModel({ gltf: modelUrl }, scene).then(function (model) {
- expect(scene).toRenderAndCall(function (rgba) {
- expect(rgba[0]).toBeGreaterThan(0);
- expect(rgba[1]).toBeGreaterThan(0);
- expect(rgba[2]).toBeGreaterThan(0);
- expect(rgba[3]).toBe(255);
- });
+it("renders glTF model", async function () {
+ const model = await loadAndZoomToModelAsync({ gltf: modelUrl }, scene);
+ expect(scene).toRenderAndCall(function (rgba) {
+ expect(rgba[0]).toBeGreaterThan(0);
+ expect(rgba[1]).toBeGreaterThan(0);
+ expect(rgba[2]).toBeGreaterThan(0);
+ expect(rgba[3]).toBe(255);
});
});
```
-Given a model's url and other options, [`loadAndZoomToModel`](https://github.com/CesiumGS/cesium/blob/main/Specs/Scene/Model/loadAndZoomToModel.js) loads a model, configures the camera, and returns a promise that resolves when a model's `readyPromise` resolves.
+Given a model's url and other options, [`loadAndZoomToModelAsync`](https://github.com/CesiumGS/cesium/blob/main/paclages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js) loads a model, configures the camera, and returns a promise that resolves when a model is ready for rendering.
-Since loading a model requires asynchronous requests and creating WebGL resources that may be spread over several frames, CesiumJS's [`pollToPromise`](https://github.com/CesiumGS/cesium/blob/main/Specs/pollToPromise.js) is used to return a promise that resolves when the model is ready, which occurs by rendering the scene in an implicit loop (hence the name "poll") until `model.readyPromise` resolves or the `timeout` is reached. `loadAndZoomToModel` uses `pollToPromise` to wait until the model is finished loading.
+Since loading a model requires asynchronous requests and creating WebGL resources that may be spread over several frames, CesiumJS's [`pollToPromise`](https://github.com/CesiumGS/cesium/blob/main/Specs/pollToPromise.js) is used to return a promise that resolves when the model is ready, which occurs by rendering the scene in an implicit loop (hence the name "poll") until `model.ready` is `true` or the `timeout` is reached.
-`pollToPromise` is also used in many places where a test needs to wait for an asynchronous event before testing its expectations. Here is an excerpt from [BillboardCollectionSpec.js](https://github.com/CesiumGS/cesium/blob/main/Specs/Scene/BillboardCollectionSpec.js):
+`pollToPromise` is also used in many places where a test needs to wait for an asynchronous event before testing its expectations. Here is an excerpt from [BillboardCollectionSpec.js](https://github.com/CesiumGS/cesium/blob/main/packages/engine/Specs/Scene/BillboardCollectionSpec.js):
```javascript
-it("can create a billboard using a URL", function () {
+it("can create a billboard using a URL", async function () {
const b = billboards.add({
image: "./Data/Images/Green.png",
});
expect(b.ready).toEqual(false);
- return pollToPromise(function () {
+ await pollToPromise(function () {
return b.ready;
- }).then(function () {
- expect(scene).toRender([0, 255, 0, 255]);
});
+
+ expect(scene).toRender([0, 255, 0, 255]);
});
```
-Here a billboard is loaded using a url to image. Internally, `Billboard` makes an asynchronous request for the image and then sets its `ready` property to `true`. The function passed to `pollToPromise` just returns the value of `ready`; it does not need to render the scene to progressively complete the request like `Model`. Finally, the resolve function (passed to `then`) verifies that the billboard is green.
+Here a billboard is loaded using a url to image. Internally, `Billboard` makes an asynchronous request for the image and then sets its `ready` property to `true`. The function passed to `pollToPromise` just returns the value of `ready`; it does not need to render the scene to progressively complete the request like `Model`. Finally, the test verifies that the billboard is green.
-To test if a promises rejects, we call `fail` in the resolve function and put the expectation in the reject function. Here is an excerpt from [ArcGisMapServerImageryProviderSpec.js](https://github.com/CesiumGS/cesium/blob/main/Specs/Scene/ArcGisMapServerImageryProviderSpec.js):
+To test if a promises rejects, we use `expectAsync` and provide the expected error type and message. Here is an excerpt from [ArcGISTiledElevationTerrainProviderSpec.js](https://github.com/CesiumGS/cesium/blob/main/packages/engine/Specs/Core/ArcGISTiledElevationTerrainProviderSpec.js):
```javascript
-it("rejects readyPromise on error", function () {
- const baseUrl = "//tiledArcGisMapServer.invalid";
-
- const provider = new ArcGisMapServerImageryProvider({
- url: baseUrl,
- });
+it("fromUrl throws if the SRS is not supported", async function () {
+ const baseUrl = "made/up/url";
+ metadata.spatialReference.latestWkid = 1234;
- return provider.readyPromise
- .then(function () {
- fail("should not resolve");
- })
- .catch(function (e) {
- expect(e.message).toContain(baseUrl);
- expect(provider.ready).toBe(false);
- });
+ await expectAsync(
+ ArcGISTiledElevationTerrainProvider.fromUrl(baseUrl)
+ ).toBeRejectedWithError(RuntimeError, "Invalid spatial reference");
});
```
diff --git a/Documentation/CustomShaderGuide/README.md b/Documentation/CustomShaderGuide/README.md
index 7a95e34cac73..681559dbdf39 100644
--- a/Documentation/CustomShaderGuide/README.md
+++ b/Documentation/CustomShaderGuide/README.md
@@ -73,7 +73,7 @@ const tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({
}));
// Applying to a model directly
-const model = Cesium.Model.fromGltf({,
+const model = await Cesium.Model.fromGltfAsync({,
url: "http://example.com/model.gltf",
customShader: customShader
});
From 0a498caa36c5dcf533daebbd04933bff0b05bb28 Mon Sep 17 00:00:00 2001
From: Gabby Getz
Date: Mon, 20 Mar 2023 11:57:06 -0400
Subject: [PATCH 04/18] Deprecate Cesium3DTileset ready and readyPromise
---
.../gallery/3D Tiles Adjust Height.html | 41 +-
Apps/Sandcastle/gallery/3D Tiles BIM.html | 52 +-
.../3D Tiles Batch Table Hierarchy.html | 32 +-
CHANGES.md | 20 +-
.../PerformanceTestingGuide/README.md | 25 +-
.../Contributors/TestingGuide/README.md | 10 +
Specs/Cesium3DTilesTester.js | 22 +-
packages/engine/Source/Core/IonResource.js | 12 +-
.../DataSources/Cesium3DTilesetVisualizer.js | 63 +-
.../Source/Scene/Cesium3DTileStyleEngine.js | 2 +-
.../engine/Source/Scene/Cesium3DTileset.js | 681 +++++++++++-------
packages/engine/Source/Scene/I3SLayer.js | 63 +-
.../engine/Source/Scene/createOsmBuildings.js | 6 +
.../Source/Scene/createOsmBuildingsAsync.js | 84 +++
.../Cesium3DTilesetVisualizerSpec.js | 93 +--
.../engine/Specs/Scene/Cesium3DTilesetSpec.js | 358 ++++-----
packages/engine/Specs/Scene/I3SLayerSpec.js | 80 +-
.../Specs/Scene/Vector3DTileContentSpec.js | 8 +-
.../Cesium3DTilesInspectorViewModel.js | 3 +-
packages/widgets/Source/Viewer/Viewer.js | 3 +-
.../Cesium3DTilesInspectorViewModelSpec.js | 38 +-
packages/widgets/Specs/Viewer/ViewerSpec.js | 239 +++---
22 files changed, 1035 insertions(+), 900 deletions(-)
create mode 100644 packages/engine/Source/Scene/createOsmBuildingsAsync.js
diff --git a/Apps/Sandcastle/gallery/3D Tiles Adjust Height.html b/Apps/Sandcastle/gallery/3D Tiles Adjust Height.html
index c8e8f6c139ac..31de2f9fcbd1 100644
--- a/Apps/Sandcastle/gallery/3D Tiles Adjust Height.html
+++ b/Apps/Sandcastle/gallery/3D Tiles Adjust Height.html
@@ -60,8 +60,6 @@
shadows: true,
});
- viewer.scene.globe.depthTestAgainstTerrain = true;
-
const viewModel = {
height: 0,
};
@@ -71,31 +69,30 @@
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
- const tileset = new Cesium.Cesium3DTileset({
- url: "../../SampleData/Cesium3DTiles/Tilesets/Tileset/tileset.json",
- });
-
- tileset.readyPromise
- .then(function (tileset) {
- viewer.scene.primitives.add(tileset);
- viewer.zoomTo(
- tileset,
- new Cesium.HeadingPitchRange(
- 0.0,
- -0.5,
- tileset.boundingSphere.radius * 2.0
- )
- );
- })
- .catch(function (error) {
- console.log(error);
- });
+ let tileset;
+ try {
+ tileset = await Cesium.Cesium3DTileset.fromUrl(
+ "../../SampleData/Cesium3DTiles/Tilesets/Tileset/tileset.json"
+ );
+ viewer.scene.primitives.add(tileset);
+ viewer.scene.globe.depthTestAgainstTerrain = true;
+ viewer.zoomTo(
+ tileset,
+ new Cesium.HeadingPitchRange(
+ 0.0,
+ -0.5,
+ tileset.boundingSphere.radius * 2.0
+ )
+ );
+ } catch (error) {
+ console.log(`Error loading tileset: ${error}`);
+ }
Cesium.knockout
.getObservable(viewModel, "height")
.subscribe(function (height) {
height = Number(height);
- if (isNaN(height)) {
+ if (isNaN(height) || !Cesium.defined(tileset)) {
return;
}
diff --git a/Apps/Sandcastle/gallery/3D Tiles BIM.html b/Apps/Sandcastle/gallery/3D Tiles BIM.html
index 2f403e730916..06df8cba02b3 100644
--- a/Apps/Sandcastle/gallery/3D Tiles BIM.html
+++ b/Apps/Sandcastle/gallery/3D Tiles BIM.html
@@ -43,29 +43,6 @@
"2022-08-01T00:00:00Z"
);
- const tileset = scene.primitives.add(
- new Cesium.Cesium3DTileset({
- url: Cesium.IonResource.fromAssetId(1240402),
- })
- );
-
- tileset.readyPromise
- .then(function (tileset) {
- viewer.zoomTo(
- tileset,
- new Cesium.HeadingPitchRange(
- 0.5,
- -0.2,
- tileset.boundingSphere.radius * 4.0
- )
- );
- })
- .catch(function (error) {
- console.log(error);
- });
-
- tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE;
-
let selectedFeature;
let picking = false;
@@ -209,13 +186,30 @@
}
}
- tileset.tileLoad.addEventListener(function (tile) {
- processTileFeatures(tile, loadFeature);
- });
+ try {
+ const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(1240402);
+ scene.primitives.add(tileset);
+
+ viewer.zoomTo(
+ tileset,
+ new Cesium.HeadingPitchRange(
+ 0.5,
+ -0.2,
+ tileset.boundingSphere.radius * 4.0
+ )
+ );
+
+ tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE;
+ tileset.tileLoad.addEventListener(function (tile) {
+ processTileFeatures(tile, loadFeature);
+ });
- tileset.tileUnload.addEventListener(function (tile) {
- processTileFeatures(tile, unloadFeature);
- });
+ tileset.tileUnload.addEventListener(function (tile) {
+ processTileFeatures(tile, unloadFeature);
+ });
+ } catch (error) {
+ console.log(`Error loading tileset: ${error}`);
+ }
//Sandcastle_End
};
if (typeof Cesium !== "undefined") {
diff --git a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html
index de0497ca1f44..5e7a1731d76d 100644
--- a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html
+++ b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html
@@ -95,13 +95,27 @@
const viewer = new Cesium.Viewer("cesiumContainer");
viewer.clock.currentTime = new Cesium.JulianDate(2457522.154792);
- const tileset = new Cesium.Cesium3DTileset({
- url:
- "../../SampleData/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json",
- });
+ let tileset;
+ try {
+ tileset = await Cesium.Cesium3DTileset.fromUrl(
+ "../../SampleData/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json"
+ );
+
+ viewer.scene.primitives.add(tileset);
+ viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0.0, -0.3, 0.0));
+ } catch (error) {
+ console.log(`Error loading tileset: ${error}`);
+ }
- viewer.scene.primitives.add(tileset);
- viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0.0, -0.3, 0.0));
+ function setStyle(style) {
+ return function () {
+ if (!Cesium.defined(tileset)) {
+ return;
+ }
+
+ tileset.style = new Cesium.Cesium3DTileStyle(style);
+ };
+ }
const styles = [];
function addStyle(name, style) {
@@ -167,12 +181,6 @@
},
});
- function setStyle(style) {
- return function () {
- tileset.style = new Cesium.Cesium3DTileStyle(style);
- };
- }
-
const styleOptions = [];
for (let i = 0; i < styles.length; ++i) {
const style = styles[i];
diff --git a/CHANGES.md b/CHANGES.md
index ee6040c27b50..9dff17ef1645 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,11 +2,24 @@
### 1.104 - 2023-04-03
+#### Major Announcements :loudspeaker:
+
+- Starting with CesiumJS 1.104 The `readyPromise` pattern has been deprecated across the API. It will be removed in CesiumJS 1.107. This has been done to facilitate better asynchronous flow and error management. For example:
+
+```js
+try {
+ const tileset = await Cesium.Cesium3DTileset.fromUrl(url);
+ viewer.scene.primitives.add(tileset);
+} catch (error) {
+ console.log(`Failed to load tileset: ${error}`);
+}
+```
+
#### @cesium/engine
##### Additions :tada:
-- Added `ArcGisMapServerImageryProvider.fromUrl`, `ArcGISTiledElevationTerrainProvider.fromUrl`, `BingMapsImageryProvider.fromUrl`, `CesiumTerrainProvider.fromUrl`, `GoogleEarthEnterpriseMetadata.fromUrl`, `GoogleEarthEnterpriseImageryProvider.fromMetadata`, `GoogleEarthEnterpriseMapsProvider.fromUrl`, `GoogleEarthEnterpriseTerrainProvider.fromMetadata`, `ImageryLayer.fromProviderAsync`, `IonImageryProvider.fromAssetId`, `SingleTileImageryProvider.fromUrl`, `Terrain`, `TileMapServiceImageryProvider.fromUrl`, `VRTheWorldTerrainProvider.fromUrl`, `createWorldTerrainAsync`, `Model.fromGltfAsync`, , `Model.readyEvent`, `Model.errorEvent`, and `Model.texturesReadyEvent` for better async flow and error handling.
+- Added `ArcGisMapServerImageryProvider.fromUrl`, `ArcGISTiledElevationTerrainProvider.fromUrl`, `BingMapsImageryProvider.fromUrl`, `CesiumTerrainProvider.fromUrl`, `GoogleEarthEnterpriseMetadata.fromUrl`, `GoogleEarthEnterpriseImageryProvider.fromMetadata`, `GoogleEarthEnterpriseMapsProvider.fromUrl`, `GoogleEarthEnterpriseTerrainProvider.fromMetadata`, `ImageryLayer.fromProviderAsync`, `IonImageryProvider.fromAssetId`, `SingleTileImageryProvider.fromUrl`, `Terrain`, `TileMapServiceImageryProvider.fromUrl`, `VRTheWorldTerrainProvider.fromUrl`, `createWorldTerrainAsync`, `Cesium3DTileset.fromUrl`, `createOsmBuildingsAsync`, `Model.fromGltfAsync`, , `Model.readyEvent`, `Model.errorEvent`, and `Model.texturesReadyEvent` for better async flow and error handling.
##### Fixes :wrench:
@@ -44,9 +57,10 @@
- `GoogleEarthEnterpriseMetadata` constructor parameter `options.url` and `GoogleEarthEnterpriseMetadata.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `GoogleEarthEnterpriseMetadata.fromUrl` instead.
- `GoogleEarthEnterpriseTerrainProvider` constructor parameters `options.url` and `options.metadata`, `GoogleEarthEnterpriseTerrainProvider.ready`, and `GoogleEarthEnterpriseTerrainProvider.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `GoogleEarthEnterpriseTerrainProvider.fromMetadata` instead.
- `VRTheWorldTerrainProvider` constructor parameter `options.url`, `VRTheWorldTerrainProvider.ready`, and `VRTheWorldTerrainProvider.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `VRTheWorldTerrainProvider.fromUrl` instead.
-- `createWorldTerrain` was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use createWorldTerrainAsync instead.
+- `createWorldTerrain` was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use `createWorldTerrainAsync` instead.
+- `Cesium3DTileset` constructor parameter `options.url`, `Cesium3DTileset.ready`, and `Cesium3DTileset.readyPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `Cesium3DTileset.fromUrl` instead.
+- `createOsmBuildings` was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use `createOsmBuildingsAsync` instead.
- `Model.fromGltf`, `Model.readyPromise`, and `Model.texturesLoadedPromise` were deprecated in CesiumJS 1.104. They will be removed in 1.107. Use `Model.fromGltfAsync`, `Model.readyEvent`, `Model.errorEvent`, and `Model.texturesReadyEvent` instead. For example:
-
```js
try {
const model = await Cesium.Model.fromGltfAsync({
diff --git a/Documentation/Contributors/PerformanceTestingGuide/README.md b/Documentation/Contributors/PerformanceTestingGuide/README.md
index 9244c10bc869..decea410bfee 100644
--- a/Documentation/Contributors/PerformanceTestingGuide/README.md
+++ b/Documentation/Contributors/PerformanceTestingGuide/README.md
@@ -127,37 +127,26 @@ class Timer {
To time how long it takes for the tileset.json to be fetched and the
`Cesium3DTileset` to be initialized (not including the tile content), start
-the timer just before creating the tileset, and stop it in the `readyPromise`
+the timer just before creating the tileset with `Cesium3DTileset.fromUrl`, and stop it after the asynchronous function has completed.
```js
tilesetTimer.start();
-const tileset = new Cesium.Cesium3DTileset({
- url,
-});
-tileset.maximumScreenSpaceError = 4;
-// and any other initialization code
-
-viewer.scene.primitives.add(tileset);
-
-tileset.readyPromise.then(() => {
- tilesetTimer.stop();
- tilesetTimer.print();
-});
+const tileset = await Cesium.Cesium3DTileset.fromUrl(url);
+tilesetTimer.stop();
+tilesetTimer.print();
```
### How to measure tile load time
To time how long it takes for all the tiles in the current camera view to load
-(not including the tileset load time), start the timer in the ready promise
-and stop it in the `initialTilesLoaded` event handler. This event is used
+(not including the tileset load time), after the tileset has been created with `Cesium3DTileset.fromUrl` and stop it in the `initialTilesLoaded` event handler. This event is used
because we only care about our initial fixed camera view. `allTilesLoaded`, in
contrast, may trigger multiple times if the camera moves, which is undesireable
here.
```js
-tileset.readyPromise.then(() => {
- tileTimer.start();
-});
+const tileset = await Cesium.Cesium3DTileset.fromUrl(url);
+tileTimer.start();
// This callback is fired the first time that all the visible tiles are loaded.
// It indicates the end of the test.
diff --git a/Documentation/Contributors/TestingGuide/README.md b/Documentation/Contributors/TestingGuide/README.md
index fdf75033ccb8..6b367b69ebdc 100644
--- a/Documentation/Contributors/TestingGuide/README.md
+++ b/Documentation/Contributors/TestingGuide/README.md
@@ -750,6 +750,16 @@ it("fromUrl throws if the SRS is not supported", async function () {
});
```
+Since developer errors are removed for release builds, CesiumJS's `toBeRejectedWithDeveloperError` matcher is used to verify asynchronous Developer Errors. Here is an excerpt from [Cesium3DTilesetSpec.js](https://github.com/CesiumGS/cesium/blob/main/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js):
+
+```javascript
+it("fromUrl throws without url", async function () {
+ await expectAsync(Cesium3DTileset.fromUrl()).toBeRejectedWithDeveloperError(
+ "url is required, actual value was undefined"
+ );
+});
+```
+
### Mocks
To isolate testing, mock objects can be used to simulate real objects. Here is an excerpt from [SceneSpec.js](https://github.com/CesiumGS/cesium/blob/main/Specs/Scene/SceneSpec.js);
diff --git a/Specs/Cesium3DTilesTester.js b/Specs/Cesium3DTilesTester.js
index 04318ceeadf7..b52d0e73cea8 100644
--- a/Specs/Cesium3DTilesTester.js
+++ b/Specs/Cesium3DTilesTester.js
@@ -102,27 +102,19 @@ Cesium3DTilesTester.waitForTilesLoaded = function (scene, tileset) {
});
};
-Cesium3DTilesTester.waitForReady = function (scene, tileset) {
- return pollToPromise(function () {
- scene.renderForSpecs();
- return tileset.ready;
- }).then(function () {
- return tileset;
- });
-};
-
-Cesium3DTilesTester.loadTileset = function (scene, url, options) {
+Cesium3DTilesTester.loadTileset = async function (scene, url, options) {
options = defaultValue(options, {});
- options.url = url;
options.cullRequestsWhileMoving = defaultValue(
options.cullRequestsWhileMoving,
false
);
+
+ const tileset = await Cesium3DTileset.fromUrl(url, options);
+
// Load all visible tiles
- const tileset = scene.primitives.add(new Cesium3DTileset(options));
- return tileset.readyPromise.then(function () {
- return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset);
- });
+ scene.primitives.add(tileset);
+ await Cesium3DTilesTester.waitForTilesLoaded(scene, tileset);
+ return tileset;
};
Cesium3DTilesTester.createContentForMockTile = async function (
diff --git a/packages/engine/Source/Core/IonResource.js b/packages/engine/Source/Core/IonResource.js
index 137be3899ee1..91ce926c34a9 100644
--- a/packages/engine/Source/Core/IonResource.js
+++ b/packages/engine/Source/Core/IonResource.js
@@ -16,7 +16,7 @@ import RuntimeError from "./RuntimeError.js";
* @augments Resource
*
* @param {object} endpoint The result of the Cesium ion asset endpoint service.
- * @param {Resource} endpointResource The resource used to retreive the endpoint.
+ * @param {Resource} endpointResource The resource used to retrieve the endpoint.
*
* @see Ion
* @see IonImageryProvider
@@ -87,8 +87,14 @@ if (defined(Object.create)) {
* @returns {Promise} A Promise to am instance representing the Cesium ion Asset.
*
* @example
- * //Load a Cesium3DTileset with asset ID of 124624234
- * viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ url: Cesium.IonResource.fromAssetId(124624234) }));
+ * // Load a Cesium3DTileset with asset ID of 124624234
+ * try {
+ * const resource = await Cesium.IonResource.fromAssetId(124624234);
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(resource);
+ * scene.primitives.add(tileset);
+ * } catch (error) {
+ * console.error(`Error creating tileset: ${error}`);
+ * }
*
* @example
* //Load a CZML file with asset ID of 10890
diff --git a/packages/engine/Source/DataSources/Cesium3DTilesetVisualizer.js b/packages/engine/Source/DataSources/Cesium3DTilesetVisualizer.js
index f0c5a9f794df..950c365b847d 100644
--- a/packages/engine/Source/DataSources/Cesium3DTilesetVisualizer.js
+++ b/packages/engine/Source/DataSources/Cesium3DTilesetVisualizer.js
@@ -65,7 +65,7 @@ Cesium3DTilesetVisualizer.prototype.update = function (time) {
const tilesetGraphics = entity._tileset;
let resource;
- let tilesetData = tilesetHash[entity.id];
+ const tilesetData = tilesetHash[entity.id];
const show =
entity.isShowing &&
entity.isAvailable(time) &&
@@ -86,28 +86,21 @@ Cesium3DTilesetVisualizer.prototype.update = function (time) {
continue;
}
- let tileset = defined(tilesetData)
+ const tileset = defined(tilesetData)
? tilesetData.tilesetPrimitive
: undefined;
- if (!defined(tileset) || resource.url !== tilesetData.url) {
+ if (!defined(tilesetData) || resource.url !== tilesetData.url) {
if (defined(tileset)) {
primitives.removeAndDestroy(tileset);
- delete tilesetHash[entity.id];
}
- tileset = new Cesium3DTileset({
- url: resource,
- });
- tileset.id = entity;
- primitives.add(tileset);
-
- tilesetData = {
- tilesetPrimitive: tileset,
- url: resource.url,
- loadFail: false,
- };
- tilesetHash[entity.id] = tilesetData;
-
- checkLoad(tileset, entity, tilesetHash);
+
+ delete tilesetHash[entity.id];
+
+ createTileset(resource, tilesetHash, entity, primitives);
+ }
+
+ if (!defined(tileset)) {
+ continue;
}
tileset.show = true;
@@ -180,12 +173,12 @@ Cesium3DTilesetVisualizer.prototype.getBoundingSphere = function (
}
const primitive = tilesetData.tilesetPrimitive;
- if (!defined(primitive) || !primitive.show) {
- return BoundingSphereState.FAILED;
+ if (!defined(primitive)) {
+ return BoundingSphereState.PENDING;
}
- if (!primitive.ready) {
- return BoundingSphereState.PENDING;
+ if (!primitive.show) {
+ return BoundingSphereState.FAILED;
}
BoundingSphere.clone(primitive.boundingSphere, result);
@@ -235,15 +228,33 @@ Cesium3DTilesetVisualizer.prototype._onCollectionChanged = function (
function removeTileset(visualizer, entity, tilesetHash, primitives) {
const tilesetData = tilesetHash[entity.id];
if (defined(tilesetData)) {
- primitives.removeAndDestroy(tilesetData.tilesetPrimitive);
+ if (defined(tilesetData.tilesetPrimitive)) {
+ primitives.removeAndDestroy(tilesetData.tilesetPrimitive);
+ }
delete tilesetHash[entity.id];
}
}
-function checkLoad(primitive, entity, tilesetHash) {
- primitive.readyPromise.catch(function (error) {
+async function createTileset(resource, tilesetHash, entity, primitives) {
+ tilesetHash[entity.id] = {
+ url: resource.url,
+ loadFail: false,
+ };
+
+ try {
+ const tileset = await Cesium3DTileset.fromUrl(resource);
+ tileset.id = entity;
+ primitives.add(tileset);
+
+ if (!defined(tilesetHash[entity.id])) {
+ return;
+ }
+
+ tilesetHash[entity.id].tilesetPrimitive = tileset;
+ } catch (error) {
console.error(error);
tilesetHash[entity.id].loadFail = true;
- });
+ }
}
+
export default Cesium3DTilesetVisualizer;
diff --git a/packages/engine/Source/Scene/Cesium3DTileStyleEngine.js b/packages/engine/Source/Scene/Cesium3DTileStyleEngine.js
index 91aa8a313646..bd6eac5a1136 100644
--- a/packages/engine/Source/Scene/Cesium3DTileStyleEngine.js
+++ b/packages/engine/Source/Scene/Cesium3DTileStyleEngine.js
@@ -33,7 +33,7 @@ Cesium3DTileStyleEngine.prototype.resetDirty = function () {
};
Cesium3DTileStyleEngine.prototype.applyStyle = function (tileset) {
- if (!tileset.ready) {
+ if (!defined(tileset.root)) {
return;
}
diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js
index d5c7674a03ae..3c885a72c8ea 100644
--- a/packages/engine/Source/Scene/Cesium3DTileset.js
+++ b/packages/engine/Source/Scene/Cesium3DTileset.js
@@ -10,10 +10,10 @@ import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import deprecationWarning from "../Core/deprecationWarning.js";
import destroyObject from "../Core/destroyObject.js";
-import DeveloperError from "../Core/DeveloperError.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import Event from "../Core/Event.js";
import ImageBasedLighting from "./ImageBasedLighting.js";
+import IonResource from "../Core/IonResource.js";
import JulianDate from "../Core/JulianDate.js";
import ManagedArray from "../Core/ManagedArray.js";
import CesiumMath from "../Core/Math.js";
@@ -53,113 +53,131 @@ import TileBoundingRegion from "./TileBoundingRegion.js";
import TileBoundingSphere from "./TileBoundingSphere.js";
import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js";
+/**
+ * @typedef {Object} Cesium3DTileset.ConstructorOptions
+ *
+ * Initialization options for the Cesium3DTileset constructor
+ *
+ * @property {boolean} [options.show=true] Determines if the tileset will be shown.
+ * @property {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] A 4x4 transformation matrix that transforms the tileset's root tile.
+ * @property {Axis} [options.modelUpAxis=Axis.Y] Which axis is considered up when loading models for tile contents.
+ * @property {Axis} [options.modelForwardAxis=Axis.X] Which axis is considered forward when loading models for tile contents.
+ * @property {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the tileset casts or receives shadows from light sources.
+ * @property {number} [options.maximumScreenSpaceError=16] The maximum screen space error used to drive level of detail refinement.
+ * @property {number} [options.maximumMemoryUsage=512] The maximum amount of memory in MB that can be used by the tileset.
+ * @property {boolean} [options.cullWithChildrenBounds=true] Optimization option. Whether to cull tiles using the union of their children bounding volumes.
+ * @property {boolean} [options.cullRequestsWhileMoving=true] Optimization option. Don't request tiles that will likely be unused when they come back because of the camera's movement. This optimization only applies to stationary tilesets.
+ * @property {number} [options.cullRequestsWhileMovingMultiplier=60.0] Optimization option. Multiplier used in culling requests while moving. Larger is more aggressive culling, smaller less aggressive culling.
+ * @property {boolean} [options.preloadWhenHidden=false] Preload tiles when tileset.show is false. Loads tiles as if the tileset is visible but does not render them.
+ * @property {boolean} [options.preloadFlightDestinations=true] Optimization option. Preload tiles at the camera's flight destination while the camera is in flight.
+ * @property {boolean} [options.preferLeaves=false] Optimization option. Prefer loading of leaves first.
+ * @property {boolean} [options.dynamicScreenSpaceError=false] Optimization option. Reduce the screen space error for tiles that are further away from the camera.
+ * @property {number} [options.dynamicScreenSpaceErrorDensity=0.00278] Density used to adjust the dynamic screen space error, similar to fog density.
+ * @property {number} [options.dynamicScreenSpaceErrorFactor=4.0] A factor used to increase the computed dynamic screen space error.
+ * @property {number} [options.dynamicScreenSpaceErrorHeightFalloff=0.25] A ratio of the tileset's height at which the density starts to falloff.
+ * @property {number} [options.progressiveResolutionHeightFraction=0.3] Optimization option. If between (0.0, 0.5], tiles at or above the screen space error for the reduced screen resolution of progressiveResolutionHeightFraction*screenHeight will be prioritized first. This can help get a quick layer of tiles down while full resolution tiles continue to load.
+ * @property {boolean} [options.foveatedScreenSpaceError=true] Optimization option. Prioritize loading tiles in the center of the screen by temporarily raising the screen space error for tiles around the edge of the screen. Screen space error returns to normal once all the tiles in the center of the screen as determined by the {@link Cesium3DTileset#foveatedConeSize} are loaded.
+ * @property {number} [options.foveatedConeSize=0.1] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control the cone size that determines which tiles are deferred. Tiles that are inside this cone are loaded immediately. Tiles outside the cone are potentially deferred based on how far outside the cone they are and their screen space error. This is controlled by {@link Cesium3DTileset#foveatedInterpolationCallback} and {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation}. Setting this to 0.0 means the cone will be the line formed by the camera position and its view direction. Setting this to 1.0 means the cone encompasses the entire field of view of the camera, disabling the effect.
+ * @property {number} [options.foveatedMinimumScreenSpaceErrorRelaxation=0.0] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control the starting screen space error relaxation for tiles outside the foveated cone. The screen space error will be raised starting with tileset value up to {@link Cesium3DTileset#maximumScreenSpaceError} based on the provided {@link Cesium3DTileset#foveatedInterpolationCallback}.
+ * @property {Cesium3DTileset.foveatedInterpolationCallback} [options.foveatedInterpolationCallback=Math.lerp] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control how much to raise the screen space error for tiles outside the foveated cone, interpolating between {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation} and {@link Cesium3DTileset#maximumScreenSpaceError}
+ * @property {number} [options.foveatedTimeDelay=0.2] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control how long in seconds to wait after the camera stops moving before deferred tiles start loading in. This time delay prevents requesting tiles around the edges of the screen when the camera is moving. Setting this to 0.0 will immediately request all tiles in any given view.
+ * @property {boolean} [options.skipLevelOfDetail=false] Optimization option. Determines if level of detail skipping should be applied during the traversal.
+ * @property {number} [options.baseScreenSpaceError=1024] When skipLevelOfDetail is true, the screen space error that must be reached before skipping levels of detail.
+ * @property {number} [options.skipScreenSpaceErrorFactor=16] When skipLevelOfDetail is true, a multiplier defining the minimum screen space error to skip. Used in conjunction with skipLevels to determine which tiles to load.
+ * @property {number} [options.skipLevels=1] When skipLevelOfDetail is true, a constant defining the minimum number of levels to skip when loading tiles. When it is 0, no levels are skipped. Used in conjunction with skipScreenSpaceErrorFactor to determine which tiles to load.
+ * @property {boolean} [options.immediatelyLoadDesiredLevelOfDetail=false] When skipLevelOfDetail is true, only tiles that meet the maximum screen space error will ever be downloaded. Skipping factors are ignored and just the desired tiles are loaded.
+ * @property {boolean} [options.loadSiblings=false] When skipLevelOfDetail is true, determines whether siblings of visible tiles are always downloaded during traversal.
+ * @property {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset.
+ * @property {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this tileset. See {@link Cesium3DTileset#classificationType} for details about restrictions and limitations.
+ * @property {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid determining the size and shape of the globe.
+ * @property {object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting.
+ * @property {Cartesian3} [options.lightColor] The light color when shading models. When undefined the scene's light color is used instead.
+ * @property {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting for this tileset.
+ * @property {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled.
+ * @property {boolean} [options.enableShowOutline=true] Whether to enable outlines for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set to false to avoid the additional processing of geometry at load time. When false, the showOutlines and outlineColor options are ignored.
+ * @property {boolean} [options.showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed.
+ * @property {Color} [options.outlineColor=Color.BLACK] The color to use when rendering outlines.
+ * @property {boolean} [options.vectorClassificationOnly=false] Indicates that only the tileset's vector tiles should be used for classification.
+ * @property {boolean} [options.vectorKeepDecodedPositions=false] Whether vector tiles should keep decoded positions in memory. This is used with {@link Cesium3DTileFeature.getPolylinePositions}.
+ * @property {string|number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
+ * @property {string|number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
+ * @property {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this tileset on screen.
+ * @property {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset.
+ * @property {boolean} [options.projectTo2D=false] Whether to accurately project the tileset to 2D. If this is true, the tileset will be projected accurately to 2D, but it will use more memory to do so. If this is false, the tileset will use less memory and will still render in 2D / CV mode, but its projected positions may be inaccurate. This cannot be set after the tileset has loaded.
+ * @property {string} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value.
+ * @property {boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering.
+ * @property {boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile.
+ * @property {boolean} [options.enableDebugWireframe] For debugging only. This must be true for debugWireframe to work in WebGL1. This cannot be set after the tileset has loaded.
+ * @property {boolean} [options.debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe.
+ * @property {boolean} [options.debugShowBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile.
+ * @property {boolean} [options.debugShowContentBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile's content.
+ * @property {boolean} [options.debugShowViewerRequestVolume=false] For debugging only. When true, renders the viewer request volume for each tile.
+ * @property {boolean} [options.debugShowGeometricError=false] For debugging only. When true, draws labels to indicate the geometric error of each tile.
+ * @property {boolean} [options.debugShowRenderingStatistics=false] For debugging only. When true, draws labels to indicate the number of commands, points, triangles and features for each tile.
+ * @property {boolean} [options.debugShowMemoryUsage=false] For debugging only. When true, draws labels to indicate the texture and geometry memory in megabytes used by each tile.
+ * @property {boolean} [options.debugShowUrl=false] For debugging only. When true, draws labels to indicate the url of each tile.
+ */
+
+/**
+ * @typedef {Cesium3DTileset.ConstructorOptions} Cesium3DTileset.DeprecatedConstructorOptions
+ * @property {Resource|string|Promise|Promise} options.url The url to a tileset JSON file. Deprecated.
+ */
+
/**
* A {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles tileset},
* used for streaming massive heterogeneous 3D geospatial datasets.
*
+ *
+ * This object is normally not instantiated directly, use {@link Cesium3DTileset.fromUrl}.
+ *
+ *
* @alias Cesium3DTileset
* @constructor
*
- * @param {object} options Object with the following properties:
- * @param {Resource|string|Promise|Promise} options.url The url to a tileset JSON file.
- * @param {boolean} [options.show=true] Determines if the tileset will be shown.
- * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] A 4x4 transformation matrix that transforms the tileset's root tile.
- * @param {Axis} [options.modelUpAxis=Axis.Y] Which axis is considered up when loading models for tile contents.
- * @param {Axis} [options.modelForwardAxis=Axis.X] Which axis is considered forward when loading models for tile contents.
- * @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the tileset casts or receives shadows from light sources.
- * @param {number} [options.maximumScreenSpaceError=16] The maximum screen space error used to drive level of detail refinement.
- * @param {number} [options.maximumMemoryUsage=512] The maximum amount of memory in MB that can be used by the tileset.
- * @param {boolean} [options.cullWithChildrenBounds=true] Optimization option. Whether to cull tiles using the union of their children bounding volumes.
- * @param {boolean} [options.cullRequestsWhileMoving=true] Optimization option. Don't request tiles that will likely be unused when they come back because of the camera's movement. This optimization only applies to stationary tilesets.
- * @param {number} [options.cullRequestsWhileMovingMultiplier=60.0] Optimization option. Multiplier used in culling requests while moving. Larger is more aggressive culling, smaller less aggressive culling.
- * @param {boolean} [options.preloadWhenHidden=false] Preload tiles when tileset.show is false. Loads tiles as if the tileset is visible but does not render them.
- * @param {boolean} [options.preloadFlightDestinations=true] Optimization option. Preload tiles at the camera's flight destination while the camera is in flight.
- * @param {boolean} [options.preferLeaves=false] Optimization option. Prefer loading of leaves first.
- * @param {boolean} [options.dynamicScreenSpaceError=false] Optimization option. Reduce the screen space error for tiles that are further away from the camera.
- * @param {number} [options.dynamicScreenSpaceErrorDensity=0.00278] Density used to adjust the dynamic screen space error, similar to fog density.
- * @param {number} [options.dynamicScreenSpaceErrorFactor=4.0] A factor used to increase the computed dynamic screen space error.
- * @param {number} [options.dynamicScreenSpaceErrorHeightFalloff=0.25] A ratio of the tileset's height at which the density starts to falloff.
- * @param {number} [options.progressiveResolutionHeightFraction=0.3] Optimization option. If between (0.0, 0.5], tiles at or above the screen space error for the reduced screen resolution of progressiveResolutionHeightFraction*screenHeight will be prioritized first. This can help get a quick layer of tiles down while full resolution tiles continue to load.
- * @param {boolean} [options.foveatedScreenSpaceError=true] Optimization option. Prioritize loading tiles in the center of the screen by temporarily raising the screen space error for tiles around the edge of the screen. Screen space error returns to normal once all the tiles in the center of the screen as determined by the {@link Cesium3DTileset#foveatedConeSize} are loaded.
- * @param {number} [options.foveatedConeSize=0.1] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control the cone size that determines which tiles are deferred. Tiles that are inside this cone are loaded immediately. Tiles outside the cone are potentially deferred based on how far outside the cone they are and their screen space error. This is controlled by {@link Cesium3DTileset#foveatedInterpolationCallback} and {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation}. Setting this to 0.0 means the cone will be the line formed by the camera position and its view direction. Setting this to 1.0 means the cone encompasses the entire field of view of the camera, disabling the effect.
- * @param {number} [options.foveatedMinimumScreenSpaceErrorRelaxation=0.0] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control the starting screen space error relaxation for tiles outside the foveated cone. The screen space error will be raised starting with tileset value up to {@link Cesium3DTileset#maximumScreenSpaceError} based on the provided {@link Cesium3DTileset#foveatedInterpolationCallback}.
- * @param {Cesium3DTileset.foveatedInterpolationCallback} [options.foveatedInterpolationCallback=Math.lerp] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control how much to raise the screen space error for tiles outside the foveated cone, interpolating between {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation} and {@link Cesium3DTileset#maximumScreenSpaceError}
- * @param {number} [options.foveatedTimeDelay=0.2] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control how long in seconds to wait after the camera stops moving before deferred tiles start loading in. This time delay prevents requesting tiles around the edges of the screen when the camera is moving. Setting this to 0.0 will immediately request all tiles in any given view.
- * @param {boolean} [options.skipLevelOfDetail=false] Optimization option. Determines if level of detail skipping should be applied during the traversal.
- * @param {number} [options.baseScreenSpaceError=1024] When skipLevelOfDetail is true, the screen space error that must be reached before skipping levels of detail.
- * @param {number} [options.skipScreenSpaceErrorFactor=16] When skipLevelOfDetail is true, a multiplier defining the minimum screen space error to skip. Used in conjunction with skipLevels to determine which tiles to load.
- * @param {number} [options.skipLevels=1] When skipLevelOfDetail is true, a constant defining the minimum number of levels to skip when loading tiles. When it is 0, no levels are skipped. Used in conjunction with skipScreenSpaceErrorFactor to determine which tiles to load.
- * @param {boolean} [options.immediatelyLoadDesiredLevelOfDetail=false] When skipLevelOfDetail is true, only tiles that meet the maximum screen space error will ever be downloaded. Skipping factors are ignored and just the desired tiles are loaded.
- * @param {boolean} [options.loadSiblings=false] When skipLevelOfDetail is true, determines whether siblings of visible tiles are always downloaded during traversal.
- * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset.
- * @param {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this tileset. See {@link Cesium3DTileset#classificationType} for details about restrictions and limitations.
- * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid determining the size and shape of the globe.
- * @param {object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting.
- * @param {Cartesian3} [options.lightColor] The light color when shading models. When undefined the scene's light color is used instead.
- * @param {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting for this tileset.
- * @param {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled.
- * @param {boolean} [options.enableShowOutline=true] Whether to enable outlines for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set to false to avoid the additional processing of geometry at load time. When false, the showOutlines and outlineColor options are ignored.
- * @param {boolean} [options.showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed.
- * @param {Color} [options.outlineColor=Color.BLACK] The color to use when rendering outlines.
- * @param {boolean} [options.vectorClassificationOnly=false] Indicates that only the tileset's vector tiles should be used for classification.
- * @param {boolean} [options.vectorKeepDecodedPositions=false] Whether vector tiles should keep decoded positions in memory. This is used with {@link Cesium3DTileFeature.getPolylinePositions}.
- * @param {string|number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
- * @param {string|number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
- * @param {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this tileset on screen.
- * @param {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset.
- * @param {boolean} [options.projectTo2D=false] Whether to accurately project the tileset to 2D. If this is true, the tileset will be projected accurately to 2D, but it will use more memory to do so. If this is false, the tileset will use less memory and will still render in 2D / CV mode, but its projected positions may be inaccurate. This cannot be set after the tileset has loaded.
- * @param {string} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value.
- * @param {boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering.
- * @param {boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile.
- * @param {boolean} [options.enableDebugWireframe] For debugging only. This must be true for debugWireframe to work in WebGL1. This cannot be set after the tileset has loaded.
- * @param {boolean} [options.debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe.
- * @param {boolean} [options.debugShowBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile.
- * @param {boolean} [options.debugShowContentBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile's content.
- * @param {boolean} [options.debugShowViewerRequestVolume=false] For debugging only. When true, renders the viewer request volume for each tile.
- * @param {boolean} [options.debugShowGeometricError=false] For debugging only. When true, draws labels to indicate the geometric error of each tile.
- * @param {boolean} [options.debugShowRenderingStatistics=false] For debugging only. When true, draws labels to indicate the number of commands, points, triangles and features for each tile.
- * @param {boolean} [options.debugShowMemoryUsage=false] For debugging only. When true, draws labels to indicate the texture and geometry memory in megabytes used by each tile.
- * @param {boolean} [options.debugShowUrl=false] For debugging only. When true, draws labels to indicate the url of each tile.
+ * @param {Cesium3DTileset.DeprecatedConstructorOptions} options An object describing initialization options
*
* @exception {DeveloperError} The tileset must be 3D Tiles version 0.0 or 1.0.
*
* @example
- * const tileset = scene.primitives.add(new Cesium.Cesium3DTileset({
- * url : 'http://localhost:8002/tilesets/Seattle/tileset.json'
- * }));
+ * try {
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json"
+ * });
+ * scene.primitives.add(tileset);
+ * } catch (error) {
+ * console.error(`Error creating tileset: ${error}`);
+ * }
*
* @example
* // Common setting for the skipLevelOfDetail optimization
- * const tileset = scene.primitives.add(new Cesium.Cesium3DTileset({
- * url : 'http://localhost:8002/tilesets/Seattle/tileset.json',
- * skipLevelOfDetail : true,
- * baseScreenSpaceError : 1024,
- * skipScreenSpaceErrorFactor : 16,
- * skipLevels : 1,
- * immediatelyLoadDesiredLevelOfDetail : false,
- * loadSiblings : false,
- * cullWithChildrenBounds : true
- * }));
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json", {
+ * skipLevelOfDetail: true,
+ * baseScreenSpaceError: 1024,
+ * skipScreenSpaceErrorFactor: 16,
+ * skipLevels: 1,
+ * immediatelyLoadDesiredLevelOfDetail: false,
+ * loadSiblings: false,
+ * cullWithChildrenBounds: true
+ * });
+ * scene.primitives.add(tileset);
*
* @example
* // Common settings for the dynamicScreenSpaceError optimization
- * const tileset = scene.primitives.add(new Cesium.Cesium3DTileset({
- * url : 'http://localhost:8002/tilesets/Seattle/tileset.json',
- * dynamicScreenSpaceError : true,
- * dynamicScreenSpaceErrorDensity : 0.00278,
- * dynamicScreenSpaceErrorFactor : 4.0,
- * dynamicScreenSpaceErrorHeightFalloff : 0.25
- * }));
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json", {
+ * dynamicScreenSpaceError: true,
+ * dynamicScreenSpaceErrorDensity: 0.00278,
+ * dynamicScreenSpaceErrorFactor: 4.0,
+ * dynamicScreenSpaceErrorHeightFalloff: 0.25
+ * });
+ * scene.primitives.add(tileset);
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles specification}
*/
function Cesium3DTileset(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
- //>>includeStart('debug', pragmas.debug);
- Check.defined("options.url", options.url);
- //>>includeEnd('debug');
-
this._url = undefined;
this._basePath = undefined;
this._root = undefined;
@@ -974,114 +992,123 @@ function Cesium3DTileset(options) {
}
this._instanceFeatureIdLabel = instanceFeatureIdLabel;
- this._schemaLoader = undefined;
-
- const that = this;
- let resource;
- this._readyPromise = Promise.resolve(options.url)
- .then(function (url) {
- let basePath;
- resource = Resource.createIfNeeded(url);
- that._resource = resource;
-
- // ion resources have a credits property we can use for additional attribution.
- that._credits = resource.credits;
-
- if (resource.extension === "json") {
- basePath = resource.getBaseUri(true);
- } else if (resource.isDataUri) {
- basePath = "";
- }
+ if (defined(options.url)) {
+ deprecationWarning(
+ "Cesium3DTileset options.url",
+ "Cesium3DTileset constructor parameter options.url was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTileset.fromUrl instead."
+ );
+ const that = this;
+ let resource;
+ this._readyPromise = Promise.resolve(options.url)
+ .then(function (url) {
+ let basePath;
+ resource = Resource.createIfNeeded(url);
+ that._resource = resource;
+
+ // ion resources have a credits property we can use for additional attribution.
+ that._credits = resource.credits;
+
+ if (resource.extension === "json") {
+ basePath = resource.getBaseUri(true);
+ } else if (resource.isDataUri) {
+ basePath = "";
+ }
- that._url = resource.url;
- that._basePath = basePath;
+ that._url = resource.url;
+ that._basePath = basePath;
- return Cesium3DTileset.loadJson(resource);
- })
- .then(function (tilesetJson) {
- if (that.isDestroyed()) {
- return;
- }
-
- // This needs to be called before loadTileset() so tile metadata
- // can be initialized synchronously in the Cesium3DTile constructor
- return processMetadataExtension(that, tilesetJson);
- })
- .then(function (tilesetJson) {
- if (that.isDestroyed()) {
- return;
- }
+ return Cesium3DTileset.loadJson(resource);
+ })
+ .then(function (tilesetJson) {
+ if (that.isDestroyed()) {
+ return;
+ }
- // Set these before loading the tileset since _geometricError
- // and _scaledGeometricError get accessed during tile creation
- that._geometricError = tilesetJson.geometricError;
- that._scaledGeometricError = tilesetJson.geometricError;
-
- that._root = that.loadTileset(resource, tilesetJson);
-
- // Handle legacy gltfUpAxis option
- const gltfUpAxis = defined(tilesetJson.asset.gltfUpAxis)
- ? Axis.fromName(tilesetJson.asset.gltfUpAxis)
- : Axis.Y;
- const modelUpAxis = defaultValue(options.modelUpAxis, gltfUpAxis);
- const modelForwardAxis = defaultValue(options.modelForwardAxis, Axis.X);
-
- const asset = tilesetJson.asset;
- that._asset = asset;
- that._properties = tilesetJson.properties;
- that._extensionsUsed = tilesetJson.extensionsUsed;
- that._extensions = tilesetJson.extensions;
- that._modelUpAxis = modelUpAxis;
- that._modelForwardAxis = modelForwardAxis;
- that._extras = tilesetJson.extras;
-
- const extras = asset.extras;
- if (
- defined(extras) &&
- defined(extras.cesium) &&
- defined(extras.cesium.credits)
- ) {
- const extraCredits = extras.cesium.credits;
- let credits = that._credits;
- if (!defined(credits)) {
- credits = [];
- that._credits = credits;
+ // This needs to be called before loadTileset() so tile metadata
+ // can be initialized synchronously in the Cesium3DTile constructor
+ return processMetadataExtension(resource, tilesetJson).then(
+ (metadata) => {
+ that._metadataExtension = metadata;
+ return tilesetJson;
+ }
+ );
+ })
+ .then(function (tilesetJson) {
+ if (that.isDestroyed()) {
+ return;
}
- for (let i = 0; i < extraCredits.length; ++i) {
- const credit = extraCredits[i];
- credits.push(new Credit(credit.html, that._showCreditsOnScreen));
+
+ // Set these before loading the tileset since _geometricError
+ // and _scaledGeometricError get accessed during tile creation
+ that._geometricError = tilesetJson.geometricError;
+ that._scaledGeometricError = tilesetJson.geometricError;
+
+ that._root = that.loadTileset(resource, tilesetJson);
+
+ // Handle legacy gltfUpAxis option
+ const gltfUpAxis = defined(tilesetJson.asset.gltfUpAxis)
+ ? Axis.fromName(tilesetJson.asset.gltfUpAxis)
+ : Axis.Y;
+ const modelUpAxis = defaultValue(options.modelUpAxis, gltfUpAxis);
+ const modelForwardAxis = defaultValue(options.modelForwardAxis, Axis.X);
+
+ const asset = tilesetJson.asset;
+ that._asset = asset;
+ that._properties = tilesetJson.properties;
+ that._extensionsUsed = tilesetJson.extensionsUsed;
+ that._extensions = tilesetJson.extensions;
+ that._modelUpAxis = modelUpAxis;
+ that._modelForwardAxis = modelForwardAxis;
+ that._extras = tilesetJson.extras;
+
+ const extras = asset.extras;
+ if (
+ defined(extras) &&
+ defined(extras.cesium) &&
+ defined(extras.cesium.credits)
+ ) {
+ const extraCredits = extras.cesium.credits;
+ let credits = that._credits;
+ if (!defined(credits)) {
+ credits = [];
+ that._credits = credits;
+ }
+ for (let i = 0; i < extraCredits.length; ++i) {
+ const credit = extraCredits[i];
+ credits.push(new Credit(credit.html, that._showCreditsOnScreen));
+ }
}
- }
- // Save the original, untransformed bounding volume position so we can apply
- // the tile transform and model matrix at run time
- const boundingVolume = that._root.createBoundingVolume(
- tilesetJson.root.boundingVolume,
- Matrix4.IDENTITY
- );
- const clippingPlanesOrigin = boundingVolume.boundingSphere.center;
- // If this origin is above the surface of the earth
- // we want to apply an ENU orientation as our best guess of orientation.
- // Otherwise, we assume it gets its position/orientation completely from the
- // root tile transform and the tileset's model matrix
- const originCartographic = that._ellipsoid.cartesianToCartographic(
- clippingPlanesOrigin
- );
- if (
- defined(originCartographic) &&
- originCartographic.height >
- ApproximateTerrainHeights._defaultMinTerrainHeight
- ) {
- that._initialClippingPlanesOriginMatrix = Transforms.eastNorthUpToFixedFrame(
+ // Save the original, untransformed bounding volume position so we can apply
+ // the tile transform and model matrix at run time
+ const boundingVolume = that._root.createBoundingVolume(
+ tilesetJson.root.boundingVolume,
+ Matrix4.IDENTITY
+ );
+ const clippingPlanesOrigin = boundingVolume.boundingSphere.center;
+ // If this origin is above the surface of the earth
+ // we want to apply an ENU orientation as our best guess of orientation.
+ // Otherwise, we assume it gets its position/orientation completely from the
+ // root tile transform and the tileset's model matrix
+ const originCartographic = that._ellipsoid.cartesianToCartographic(
clippingPlanesOrigin
);
- }
- that._clippingPlanesOriginMatrix = Matrix4.clone(
- that._initialClippingPlanesOriginMatrix
- );
+ if (
+ defined(originCartographic) &&
+ originCartographic.height >
+ ApproximateTerrainHeights._defaultMinTerrainHeight
+ ) {
+ that._initialClippingPlanesOriginMatrix = Transforms.eastNorthUpToFixedFrame(
+ clippingPlanesOrigin
+ );
+ }
+ that._clippingPlanesOriginMatrix = Matrix4.clone(
+ that._initialClippingPlanesOriginMatrix
+ );
- return that;
- });
+ return that;
+ });
+ }
}
Object.defineProperties(Cesium3DTileset.prototype, {
@@ -1108,19 +1135,9 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*
* @type {object}
* @readonly
- *
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
*/
asset: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
return this._asset;
},
},
@@ -1132,19 +1149,9 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*
* @type {object}
* @readonly
- *
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
*/
extensions: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
return this._extensions;
},
},
@@ -1177,8 +1184,6 @@ Object.defineProperties(Cesium3DTileset.prototype, {
* @type {object}
* @readonly
*
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
- *
* @example
* console.log(`Maximum building height: ${tileset.properties.height.maximum}`);
* console.log(`Minimum building height: ${tileset.properties.height.minimum}`);
@@ -1188,21 +1193,12 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*/
properties: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
return this._properties;
},
},
/**
* When true, the tileset's root tile is loaded and the tileset is ready to render.
- * This is set to true right before {@link Cesium3DTileset#readyPromise} is resolved.
*
* @memberof Cesium3DTileset.prototype
*
@@ -1213,6 +1209,10 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*/
ready: {
get: function () {
+ deprecationWarning(
+ "Cesium3DTileset.ready",
+ "Cesium3DTileset.ready was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTileset.fromUrl instead."
+ );
return defined(this._root);
},
},
@@ -1227,6 +1227,7 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*
* @type {Promise}
* @readonly
+ * @deprecated
*
* @example
* tileset.readyPromise.then(function(tileset) {
@@ -1241,6 +1242,10 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*/
readyPromise: {
get: function () {
+ deprecationWarning(
+ "Cesium3DTileset.readyPromise",
+ "Cesium3DTileset.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTileset.fromUrl instead."
+ );
return this._readyPromise;
},
},
@@ -1537,19 +1542,9 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*
* @type {Cesium3DTile}
* @readonly
- *
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
*/
root: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
return this._root;
},
},
@@ -1562,28 +1557,16 @@ Object.defineProperties(Cesium3DTileset.prototype, {
* @type {BoundingSphere}
* @readonly
*
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
- *
* @example
- * const tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({
- * url : 'http://localhost:8002/tilesets/Seattle/tileset.json'
- * }));
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl("http://localhost:8002/tilesets/Seattle/tileset.json");
*
- * tileset.readyPromise.then(function(tileset) {
- * // Set the camera to view the newly added tileset
- * viewer.camera.viewBoundingSphere(tileset.boundingSphere, new Cesium.HeadingPitchRange(0, -0.5, 0));
- * });
+ * viewer.scene.primitives.add(tileset);
+ *
+ * // Set the camera to view the newly added tileset
+ * viewer.camera.viewBoundingSphere(tileset.boundingSphere, new Cesium.HeadingPitchRange(0, -0.5, 0));
*/
boundingSphere: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
this._root.updateTransform(this._modelMatrix);
return this._root.boundingSphere;
},
@@ -1806,8 +1789,6 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*
* @memberof Cesium3DTileset.prototype
*
- * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.
- *
* @type {*}
* @readonly
*
@@ -1815,14 +1796,6 @@ Object.defineProperties(Cesium3DTileset.prototype, {
*/
extras: {
get: function () {
- //>>includeStart('debug', pragmas.debug);
- if (!this.ready) {
- throw new DeveloperError(
- "The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true."
- );
- }
- //>>includeEnd('debug');
-
return this._extras;
},
},
@@ -1978,6 +1951,174 @@ Object.defineProperties(Cesium3DTileset.prototype, {
},
});
+/**
+ * Creates a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles tileset},
+ * used for streaming massive heterogeneous 3D geospatial datasets, from a Cesium ion asset ID.
+ *
+ * @param {number} assetId The Cesium ion asset id.
+ * @param {Cesium3DTileset.ConstructorOptions} options An object describing initialization options
+ *
+ * @exception {DeveloperError} The tileset must be 3D Tiles version 0.0 or 1.0.
+ *
+ * @example
+ * //Load a Cesium3DTileset with asset ID of 124624234
+ * try {
+ * const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(124624234);
+ * scene.primitives.add(tileset);
+ * } catch (error) {
+ * console.error(`Error creating tileset: ${error}`);
+ * }
+ */
+Cesium3DTileset.fromIonAssetId = async function (url, options) {
+ const resource = await IonResource.fromAssetId(124624234);
+ return Cesium3DTileset.fromUrl(resource);
+};
+
+/**
+ * Creates a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles tileset},
+ * used for streaming massive heterogeneous 3D geospatial datasets.
+ *
+ * @param {Resource|string} url The url to a tileset JSON file.
+ * @param {Cesium3DTileset.ConstructorOptions} options An object describing initialization options
+ *
+ * @exception {DeveloperError} The tileset must be 3D Tiles version 0.0 or 1.0.
+ *
+ * @example
+ * try {
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json"
+ * });
+ * scene.primitives.add(tileset);
+ * } catch (error) {
+ * console.error(`Error creating tileset: ${error}`);
+ * }
+ *
+ * @example
+ * // Common setting for the skipLevelOfDetail optimization
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json", {
+ * skipLevelOfDetail: true,
+ * baseScreenSpaceError: 1024,
+ * skipScreenSpaceErrorFactor: 16,
+ * skipLevels: 1,
+ * immediatelyLoadDesiredLevelOfDetail: false,
+ * loadSiblings: false,
+ * cullWithChildrenBounds: true
+ * });
+ * scene.primitives.add(tileset);
+ *
+ * @example
+ * // Common settings for the dynamicScreenSpaceError optimization
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json", {
+ * dynamicScreenSpaceError: true,
+ * dynamicScreenSpaceErrorDensity: 0.00278,
+ * dynamicScreenSpaceErrorFactor: 4.0,
+ * dynamicScreenSpaceErrorHeightFalloff: 0.25
+ * });
+ * scene.primitives.add(tileset);
+ */
+Cesium3DTileset.fromUrl = async function (url, options) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("url", url);
+ //>>includeEnd('debug');
+
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ const resource = Resource.createIfNeeded(url);
+ let basePath;
+ if (resource.extension === "json") {
+ basePath = resource.getBaseUri(true);
+ } else if (resource.isDataUri) {
+ basePath = "";
+ }
+
+ const tilesetJson = await Cesium3DTileset.loadJson(resource);
+ const metadataExtension = await processMetadataExtension(
+ resource,
+ tilesetJson
+ );
+
+ const tileset = new Cesium3DTileset(options);
+ tileset._resource = resource;
+ tileset._url = resource.url;
+ tileset._basePath = basePath;
+ tileset._metadataExtension = metadataExtension;
+ // Set these before loading the tileset since _geometricError
+ // and _scaledGeometricError get accessed during tile creation
+ tileset._geometricError = tilesetJson.geometricError;
+ tileset._scaledGeometricError = tilesetJson.geometricError;
+
+ const asset = tilesetJson.asset;
+ tileset._asset = asset;
+ tileset._extras = tilesetJson.extras;
+
+ let credits = resource.credits;
+ if (!defined(credits)) {
+ credits = [];
+ }
+
+ const assetExtras = asset.extras;
+ if (
+ defined(assetExtras) &&
+ defined(assetExtras.cesium) &&
+ defined(assetExtras.cesium.credits)
+ ) {
+ const extraCredits = assetExtras.cesium.credits;
+ for (let i = 0; i < extraCredits.length; ++i) {
+ const credit = extraCredits[i];
+ credits.push(new Credit(credit.html, tileset._showCreditsOnScreen));
+ }
+ }
+ tileset._credits = credits;
+
+ // Handle legacy gltfUpAxis option
+ const gltfUpAxis = defined(tilesetJson.asset.gltfUpAxis)
+ ? Axis.fromName(tilesetJson.asset.gltfUpAxis)
+ : Axis.Y;
+ const modelUpAxis = defaultValue(options.modelUpAxis, gltfUpAxis);
+ const modelForwardAxis = defaultValue(options.modelForwardAxis, Axis.X);
+
+ tileset._properties = tilesetJson.properties;
+ tileset._extensionsUsed = tilesetJson.extensionsUsed;
+ tileset._extensions = tilesetJson.extensions;
+ tileset._modelUpAxis = modelUpAxis;
+ tileset._modelForwardAxis = modelForwardAxis;
+
+ tileset._root = tileset.loadTileset(resource, tilesetJson);
+
+ // Save the original, untransformed bounding volume position so we can apply
+ // the tile transform and model matrix at run time
+ const boundingVolume = tileset._root.createBoundingVolume(
+ tilesetJson.root.boundingVolume,
+ Matrix4.IDENTITY
+ );
+ const clippingPlanesOrigin = boundingVolume.boundingSphere.center;
+ // If this origin is above the surface of the earth
+ // we want to apply an ENU orientation as our best guess of orientation.
+ // Otherwise, we assume it gets its position/orientation completely from the
+ // root tile transform and the tileset's model matrix
+ const originCartographic = tileset._ellipsoid.cartesianToCartographic(
+ clippingPlanesOrigin
+ );
+ if (
+ defined(originCartographic) &&
+ originCartographic.height >
+ ApproximateTerrainHeights._defaultMinTerrainHeight
+ ) {
+ tileset._initialClippingPlanesOriginMatrix = Transforms.eastNorthUpToFixedFrame(
+ clippingPlanesOrigin
+ );
+ }
+ tileset._clippingPlanesOriginMatrix = Matrix4.clone(
+ tileset._initialClippingPlanesOriginMatrix
+ );
+
+ tileset._readyPromise = Promise.resolve(tileset);
+ tileset._ready = true;
+ return tileset;
+};
+
/**
* Provides a hook to override the method used to request the tileset json
* useful when fetching tilesets from remote servers
@@ -2146,17 +2287,17 @@ function makeTile(tileset, baseResource, tileHeader, parentTile) {
*
* @param {Cesium3DTileset} tileset The tileset
* @param {object} tilesetJson The tileset JSON
- * @return {Promise