import ArticulationStageType from "../Core/ArticulationStageType.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Check from "../Core/Check.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import Credit from "../Core/Credit.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import FeatureDetection from "../Core/FeatureDetection.js"; import InterpolationType from "../Core/InterpolationType.js"; import Matrix4 from "../Core/Matrix4.js"; import PrimitiveType from "../Core/PrimitiveType.js"; import Quaternion from "../Core/Quaternion.js"; import RuntimeError from "../Core/RuntimeError.js"; import Sampler from "../Renderer/Sampler.js"; import getAccessorByteStride from "./GltfPipeline/getAccessorByteStride.js"; import getComponentReader from "./GltfPipeline/getComponentReader.js"; import numberOfComponentsForType from "./GltfPipeline/numberOfComponentsForType.js"; import GltfStructuralMetadataLoader from "./GltfStructuralMetadataLoader.js"; import AttributeType from "./AttributeType.js"; import Axis from "./Axis.js"; import GltfLoaderUtil from "./GltfLoaderUtil.js"; import hasExtension from "./hasExtension.js"; import InstanceAttributeSemantic from "./InstanceAttributeSemantic.js"; import ModelComponents from "./ModelComponents.js"; import PrimitiveLoadPlan from "./PrimitiveLoadPlan.js"; import ResourceCache from "./ResourceCache.js"; import ResourceLoader from "./ResourceLoader.js"; import SupportedImageFormats from "./SupportedImageFormats.js"; import VertexAttributeSemantic from "./VertexAttributeSemantic.js"; import GltfGpmLoader from "./Model/Extensions/Gpm/GltfGpmLoader.js"; import GltfMeshPrimitiveGpmLoader from "./Model/Extensions/Gpm/GltfMeshPrimitiveGpmLoader.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import addAllToArray from "../Core/addAllToArray.js"; import getMeshPrimitives from "./getMeshPrimitives.js"; const { Attribute, Indices, FeatureIdAttribute, FeatureIdTexture, FeatureIdImplicitRange, MorphTarget, Primitive, Instances, Skin, Node, AnimatedPropertyType, AnimationSampler, AnimationTarget, AnimationChannel, Animation, ArticulationStage, Articulation, Asset, Scene, Components, MetallicRoughness, SpecularGlossiness, Specular, Anisotropy, Clearcoat, Material, } = ModelComponents; /** * States of the glTF loading process. These states also apply to * asynchronous texture loading unless otherwise noted * * @enum {number} * * @private */ const GltfLoaderState = { /** * The initial state of the glTF loader before load() is called. * * @type {number} * @constant * * @private */ NOT_LOADED: 0, /** * The state of the loader while waiting for the glTF JSON loader promise * to resolve. * * @type {number} * @constant * * @private */ LOADING: 1, /** * The state of the loader once the glTF JSON is loaded but before * process() is called. * * @type {number} * @constant * * @private */ LOADED: 2, /** * The state of the loader while parsing the glTF and creating GPU resources * as needed. * * @type {number} * @constant * * @private */ PROCESSING: 3, /** * For some features like handling CESIUM_primitive_outlines, the geometry * must be modified after it is loaded. The post-processing state handles * any geometry modification (if needed). *
* This state is not used for asynchronous texture loading. *
* * @type {number} * @constant * * @private */ POST_PROCESSING: 4, /** * Once the processing/post-processing states are finished, the loader * enters the processed state (sometimes from a promise chain). The next * call to process() will advance to the ready state. * * @type {number} * @constant * * @private */ PROCESSED: 5, /** * When the loader reaches the ready state, the loaders' promise will be * resolved. * * @type {number} * @constant * * @private */ READY: 6, /** * If an error occurs at any point, the loader switches to the failed state. * * @type {number} * @constant * * @private */ FAILED: 7, /** * If unload() is called, the loader switches to the unloaded state. * * @type {number} * @constant * * @private */ UNLOADED: 8, }; /** * Loads a glTF model. ** Implements the {@link ResourceLoader} interface. *
* * @alias GltfLoader * @constructor * @augments ResourceLoader * * @param {object} options Object with the following properties: * @param {Resource} options.gltfResource The {@link Resource} containing the glTF. This is often the path of the .gltf or .glb file, but may also be the path of the .b3dm, .i3dm, or .cmpt file containing the embedded glb. .cmpt resources should have a URI fragment indicating the index of the inner content to which the glb belongs in order to individually identify the glb in the cache, e.g. http://example.com/tile.cmpt#index=2. * @param {Resource} [options.baseResource] The {@link Resource} that paths in the glTF JSON are relative to. * @param {Uint8Array} [options.typedArray] The typed array containing the glTF contents, e.g. from a .b3dm, .i3dm, or .cmpt file. * @param {object} [options.gltfJson] A parsed glTF JSON file instead of passing it in as a typed array. * @param {boolean} [options.releaseGltfJson=false] When true, the glTF JSON is released once the glTF is loaded. This 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.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.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the glTF is loaded. * @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 {boolean} [options.loadAttributesAsTypedArray=false] Load all attributes and indices as typed arrays instead of GPU buffers. If the attributes are interleaved in the glTF they will be de-interleaved in the typed array. * @param {boolean} [options.loadAttributesFor2D=false] Iftrue, load the positions buffer and any instanced attribute buffers as typed arrays for accurately projecting models to 2D.
* @param {boolean} [options.enablePick=false] If true, load the positions buffer, any instanced attribute buffers, and index buffer as typed arrays for CPU-enabled picking in WebGL1.
* @param {boolean} [options.loadIndicesForWireframe=false] If true, load the index buffer as both a buffer and typed array. The latter is useful for creating wireframe indices in WebGL1.
* @param {boolean} [options.loadPrimitiveOutline=true] If true, load outlines from 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.
* @param {boolean} [options.loadForClassification=false] If true and if the model has feature IDs, load the feature IDs and indices as typed arrays. This is useful for batching features for classification.
* @param {boolean} [options.renameBatchIdSemantic=false] If true, rename _BATCHID or BATCHID to _FEATURE_ID_0. This is used for .b3dm models
* @private
*/
function GltfLoader(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const {
gltfResource,
typedArray,
releaseGltfJson = false,
asynchronous = true,
incrementallyLoadTextures = true,
upAxis = Axis.Y,
forwardAxis = Axis.Z,
loadAttributesAsTypedArray = false,
loadAttributesFor2D = false,
enablePick = false,
loadIndicesForWireframe = false,
loadPrimitiveOutline = true,
loadForClassification = false,
renameBatchIdSemantic = false,
} = options;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.gltfResource", gltfResource);
//>>includeEnd('debug');
const { baseResource = gltfResource.clone() } = options;
this._gltfJson = options.gltfJson;
this._gltfResource = gltfResource;
this._baseResource = baseResource;
this._typedArray = typedArray;
this._releaseGltfJson = releaseGltfJson;
this._asynchronous = asynchronous;
this._incrementallyLoadTextures = incrementallyLoadTextures;
this._upAxis = upAxis;
this._forwardAxis = forwardAxis;
this._loadAttributesAsTypedArray = loadAttributesAsTypedArray;
this._loadAttributesFor2D = loadAttributesFor2D;
this._enablePick = enablePick;
this._loadIndicesForWireframe = loadIndicesForWireframe;
this._loadPrimitiveOutline = loadPrimitiveOutline;
this._loadForClassification = loadForClassification;
this._renameBatchIdSemantic = renameBatchIdSemantic;
// When loading EXT_feature_metadata, the feature tables and textures
// are now stored as arrays like the newer EXT_structural_metadata extension.
// This requires sorting the dictionary keys for a consistent ordering.
this._sortedPropertyTableIds = undefined;
this._sortedFeatureTextureIds = undefined;
this._gltfJsonLoader = undefined;
this._state = GltfLoaderState.NOT_LOADED;
this._textureState = GltfLoaderState.NOT_LOADED;
this._promise = undefined;
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
// generating outlines)
this._primitiveLoadPlans = [];
// Loaders that need to be processed before the glTF becomes ready
this._loaderPromises = [];
this._textureLoaders = [];
this._texturesPromises = [];
this._textureCallbacks = [];
this._bufferViewLoaders = [];
this._geometryLoaders = [];
this._geometryCallbacks = [];
this._structuralMetadataLoader = undefined;
this._meshPrimitiveGpmLoader = undefined;
this._loadResourcesPromise = undefined;
this._resourcesLoaded = false;
this._texturesLoaded = false;
this._supportedImageFormats = undefined;
// In some cases where geometry post-processing is needed (like generating
// outlines) new attributes are added that may have GPU resources attached.
// The GltfLoader will own the resources and store them here.
this._postProcessBuffers = [];
// Loaded results
this._components = undefined;
}
if (defined(Object.create)) {
GltfLoader.prototype = Object.create(ResourceLoader.prototype);
GltfLoader.prototype.constructor = GltfLoader;
}
Object.defineProperties(GltfLoader.prototype, {
/**
* The cache key of the resource.
*
* @memberof GltfLoader.prototype
*
* @type {string}
* @readonly
* @private
*/
cacheKey: {
get: function () {
return undefined;
},
},
/**
* The loaded components.
*
* @memberof GltfLoader.prototype
*
* @type {ModelComponents.Components}
* @readonly
* @private
*/
components: {
get: function () {
return this._components;
},
},
/**
* The loaded glTF json.
*
* @memberof GltfLoader.prototype
*
* @type {object}
* @readonly
* @private
*/
gltfJson: {
get: function () {
if (defined(this._gltfJsonLoader)) {
return this._gltfJsonLoader.gltf;
}
return this._gltfJson;
},
},
/**
* Returns true if textures are loaded separately from the other glTF resources.
*
* @memberof GltfLoader.prototype
*
* @type {boolean}
* @readonly
* @private
*/
incrementallyLoadTextures: {
get: function () {
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;
},
},
});
/**
* Loads the gltf object
*/
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();
if (
loader.isDestroyed() ||
loader.isUnloaded() ||
gltfJsonLoader.isDestroyed()
) {
return;
}
loader._state = GltfLoaderState.LOADED;
loader._textureState = GltfLoaderState.LOADED;
return loader;
} catch (error) {
if (loader.isDestroyed()) {
return;
}
loader._state = GltfLoaderState.FAILED;
loader._textureState = GltfLoaderState.FAILED;
handleError(loader, error);
}
}
async function loadResources(loader, frameState) {
if (!FeatureDetection.supportsWebP.initialized) {
await FeatureDetection.supportsWebP.initialize();
}
loader._supportedImageFormats = new SupportedImageFormats({
webp: FeatureDetection.supportsWebP(),
basis: frameState.context.supportsBasis,
});
// 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 promise = parse(loader, frameState);
// All resource loaders have been created, so we can begin processing
loader._state = GltfLoaderState.PROCESSING;
loader._textureState = GltfLoaderState.PROCESSING;
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;
}
return promise;
}
/**
* Loads the resource.
* @returns {Promise..materials array in the glTF JSON
* @param {FrameState} frameState
* @returns {ModelComponents.Material}
* @private
*/
function loadMaterial(loader, gltfMaterial, frameState) {
const material = new Material();
const extensions = gltfMaterial.extensions ?? Frozen.EMPTY_OBJECT;
const pbrSpecularGlossiness = extensions.KHR_materials_pbrSpecularGlossiness;
const pbrSpecular = extensions.KHR_materials_specular;
const pbrAnisotropy = extensions.KHR_materials_anisotropy;
const pbrClearcoat = extensions.KHR_materials_clearcoat;
const pbrMetallicRoughness = gltfMaterial.pbrMetallicRoughness;
material.unlit = defined(extensions.KHR_materials_unlit);
if (defined(pbrSpecularGlossiness)) {
material.specularGlossiness = loadSpecularGlossiness(
loader,
pbrSpecularGlossiness,
frameState,
);
} else {
if (defined(pbrMetallicRoughness)) {
material.metallicRoughness = loadMetallicRoughness(
loader,
pbrMetallicRoughness,
frameState,
);
}
if (defined(pbrSpecular) && !material.unlit) {
material.specular = loadSpecular(loader, pbrSpecular, frameState);
}
if (defined(pbrAnisotropy) && !material.unlit) {
material.anisotropy = loadAnisotropy(loader, pbrAnisotropy, frameState);
}
if (defined(pbrClearcoat) && !material.unlit) {
material.clearcoat = loadClearcoat(loader, pbrClearcoat, frameState);
}
}
// Top level textures
if (defined(gltfMaterial.emissiveTexture)) {
material.emissiveTexture = loadTexture(
loader,
gltfMaterial.emissiveTexture,
frameState,
);
}
// Normals aren't used for classification, so don't load the normal texture.
if (defined(gltfMaterial.normalTexture) && !loader._loadForClassification) {
material.normalTexture = loadTexture(
loader,
gltfMaterial.normalTexture,
frameState,
);
}
if (defined(gltfMaterial.occlusionTexture)) {
material.occlusionTexture = loadTexture(
loader,
gltfMaterial.occlusionTexture,
frameState,
);
}
material.emissiveFactor = fromArray(Cartesian3, gltfMaterial.emissiveFactor);
material.alphaMode = gltfMaterial.alphaMode;
material.alphaCutoff = gltfMaterial.alphaCutoff;
material.doubleSided = gltfMaterial.doubleSided;
return material;
}
// for EXT_mesh_features
function loadFeatureIdAttribute(featureIds, positionalLabel) {
const featureIdAttribute = new FeatureIdAttribute();
featureIdAttribute.featureCount = featureIds.featureCount;
featureIdAttribute.nullFeatureId = featureIds.nullFeatureId;
featureIdAttribute.propertyTableId = featureIds.propertyTable;
featureIdAttribute.setIndex = featureIds.attribute;
featureIdAttribute.label = featureIds.label;
featureIdAttribute.positionalLabel = positionalLabel;
return featureIdAttribute;
}
// for backwards compatibility with EXT_feature_metadata
function loadFeatureIdAttributeLegacy(
gltfFeatureIdAttribute,
featureTableId,
featureCount,
positionalLabel,
) {
const featureIdAttribute = new FeatureIdAttribute();
const featureIds = gltfFeatureIdAttribute.featureIds;
featureIdAttribute.featureCount = featureCount;
featureIdAttribute.propertyTableId = featureTableId;
featureIdAttribute.setIndex = getSetIndex(featureIds.attribute);
featureIdAttribute.positionalLabel = positionalLabel;
return featureIdAttribute;
}
// implicit ranges do not exist in EXT_mesh_features and EXT_instance_features,
// but both default to the vertex/instance ID which is like
// an implicit range of {offset: 0, repeat: 1}
function loadDefaultFeatureIds(featureIds, positionalLabel) {
const featureIdRange = new FeatureIdImplicitRange();
featureIdRange.propertyTableId = featureIds.propertyTable;
featureIdRange.featureCount = featureIds.featureCount;
featureIdRange.nullFeatureId = featureIds.nullFeatureId;
featureIdRange.label = featureIds.label;
featureIdRange.positionalLabel = positionalLabel;
featureIdRange.offset = 0;
featureIdRange.repeat = 1;
return featureIdRange;
}
// for backwards compatibility with EXT_feature_metadata
function loadFeatureIdImplicitRangeLegacy(
gltfFeatureIdAttribute,
featureTableId,
featureCount,
positionalLabel,
) {
const featureIdRange = new FeatureIdImplicitRange();
const featureIds = gltfFeatureIdAttribute.featureIds;
featureIdRange.propertyTableId = featureTableId;
featureIdRange.featureCount = featureCount;
// constant/divisor was renamed to offset/repeat
featureIdRange.offset = featureIds.constant ?? 0;
// The default is now undefined
const divisor = featureIds.divisor ?? 0;
featureIdRange.repeat = divisor === 0 ? undefined : divisor;
featureIdRange.positionalLabel = positionalLabel;
return featureIdRange;
}
// for EXT_mesh_features
function loadFeatureIdTexture(
loader,
gltfFeatureIdTexture,
frameState,
positionalLabel,
) {
const featureIdTexture = new FeatureIdTexture();
featureIdTexture.featureCount = gltfFeatureIdTexture.featureCount;
featureIdTexture.nullFeatureId = gltfFeatureIdTexture.nullFeatureId;
featureIdTexture.propertyTableId = gltfFeatureIdTexture.propertyTable;
featureIdTexture.label = gltfFeatureIdTexture.label;
featureIdTexture.positionalLabel = positionalLabel;
const textureInfo = gltfFeatureIdTexture.texture;
featureIdTexture.textureReader = loadTexture(
loader,
textureInfo,
frameState,
Sampler.NEAREST, // Feature ID textures require nearest sampling
);
// Though the new channel index is more future-proof, this implementation
// only supports RGBA textures. At least for now, the string representation
// is more useful for generating shader code.
const channels = defined(textureInfo.channels) ? textureInfo.channels : [0];
const channelString = channels
.map(function (channelIndex) {
return "rgba".charAt(channelIndex);
})
.join("");
featureIdTexture.textureReader.channels = channelString;
return featureIdTexture;
}
// for backwards compatibility with EXT_feature_metadata
function loadFeatureIdTextureLegacy(
loader,
gltfFeatureIdTexture,
featureTableId,
frameState,
featureCount,
positionalLabel,
) {
const featureIdTexture = new FeatureIdTexture();
const featureIds = gltfFeatureIdTexture.featureIds;
const textureInfo = featureIds.texture;
featureIdTexture.featureCount = featureCount;
featureIdTexture.propertyTableId = featureTableId;
featureIdTexture.textureReader = loadTexture(
loader,
textureInfo,
frameState,
Sampler.NEAREST, // Feature ID textures require nearest sampling
);
featureIdTexture.textureReader.channels = featureIds.channels;
featureIdTexture.positionalLabel = positionalLabel;
return featureIdTexture;
}
function loadMorphTarget(
loader,
target,
needsPostProcessing,
primitiveLoadPlan,
frameState,
) {
const morphTarget = new MorphTarget();
// Don't pass in primitive or draco object since morph targets can't be draco compressed
const primitive = undefined;
const draco = undefined;
const spz = undefined;
const hasInstances = false;
for (const semantic in target) {
if (!target.hasOwnProperty(semantic)) {
continue;
}
const accessorId = target[semantic];
const semanticInfo = getSemanticInfo(
loader,
VertexAttributeSemantic,
semantic,
);
const attributePlan = loadVertexAttribute(
loader,
accessorId,
semanticInfo,
primitive,
draco,
spz,
hasInstances,
needsPostProcessing,
frameState,
);
morphTarget.attributes.push(attributePlan.attribute);
// The load plan doesn't need to distinguish morph target attributes from
// regular attributes
primitiveLoadPlan.attributePlans.push(attributePlan);
}
return morphTarget;
}
function fetchSpzExtensionFrom(extensions) {
const gaussianSplatting = extensions?.KHR_gaussian_splatting;
const gsExtensions = gaussianSplatting?.extensions;
const spz = gsExtensions?.KHR_gaussian_splatting_compression_spz_2;
if (defined(spz)) {
return spz;
}
const legacySpz = extensions?.KHR_spz_gaussian_splats_compression;
if (defined(legacySpz)) {
return legacySpz;
}
return undefined;
}
/**
* Load resources associated with a mesh primitive for a glTF node
* @param {GltfLoader} loader
* @param {object} gltfPrimitive One of the primitives in a mesh
* @param {boolean} hasInstances True if the node using this mesh has instances
* @param {FrameState} frameState
* @returns {ModelComponents.Primitive}
* @private
*/
function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) {
const primitive = new Primitive();
const primitivePlan = new PrimitiveLoadPlan(primitive);
loader._primitiveLoadPlans.push(primitivePlan);
const materialId = gltfPrimitive.material;
if (defined(materialId)) {
primitive.material = loadMaterial(
loader,
loader.gltfJson.materials[materialId],
frameState,
);
}
const extensions = gltfPrimitive.extensions ?? Frozen.EMPTY_OBJECT;
let needsPostProcessing = false;
const outlineExtension = extensions.CESIUM_primitive_outline;
if (loader._loadPrimitiveOutline && defined(outlineExtension)) {
needsPostProcessing = true;
primitivePlan.needsOutlines = true;
primitivePlan.outlineIndices = loadPrimitiveOutline(
loader,
outlineExtension,
primitivePlan,
);
}
//support the latest glTF spec and the legacy extension
const spzExtension = fetchSpzExtensionFrom(extensions);
if (defined(spzExtension)) {
needsPostProcessing = true;
primitivePlan.needsGaussianSplats = true;
}
const loadForClassification = loader._loadForClassification;
const draco = extensions.KHR_draco_mesh_compression;
let hasFeatureIds = false;
const attributes = gltfPrimitive.attributes;
if (defined(attributes)) {
for (const semantic in attributes) {
if (!attributes.hasOwnProperty(semantic)) {
continue;
}
const accessorId = attributes[semantic];
const semanticInfo = getSemanticInfo(
loader,
VertexAttributeSemantic,
semantic,
);
const modelSemantic = semanticInfo.modelSemantic;
if (loadForClassification && !isClassificationAttribute(modelSemantic)) {
continue;
}
if (modelSemantic === VertexAttributeSemantic.FEATURE_ID) {
hasFeatureIds = true;
}
const attributePlan = loadVertexAttribute(
loader,
accessorId,
semanticInfo,
gltfPrimitive,
draco,
spzExtension,
hasInstances,
needsPostProcessing,
frameState,
);
primitivePlan.attributePlans.push(attributePlan);
primitive.attributes.push(attributePlan.attribute);
}
}
const targets = gltfPrimitive.targets;
// Morph targets are disabled for classification models.
if (defined(targets) && !loadForClassification) {
for (let i = 0; i < targets.length; ++i) {
primitive.morphTargets.push(
loadMorphTarget(
loader,
targets[i],
needsPostProcessing,
primitivePlan,
frameState,
),
);
}
}
const indices = gltfPrimitive.indices;
if (defined(indices)) {
const indicesPlan = loadIndices(
loader,
indices,
gltfPrimitive,
draco,
hasFeatureIds,
needsPostProcessing,
frameState,
);
if (defined(indicesPlan)) {
primitivePlan.indicesPlan = indicesPlan;
primitive.indices = indicesPlan.indices;
}
}
// With the latest revision, feature IDs are defined in EXT_mesh_features
// while EXT_structural_metadata is for defining property textures and
// property mappings. In the legacy EXT_feature_metadata, these concepts
// were all in one extension.
const structuralMetadata = extensions.EXT_structural_metadata;
const meshFeatures = extensions.EXT_mesh_features;
const featureMetadataLegacy = extensions.EXT_feature_metadata;
const hasFeatureMetadataLegacy = defined(featureMetadataLegacy);
// Load feature Ids
if (defined(meshFeatures)) {
loadPrimitiveFeatures(loader, primitive, meshFeatures, frameState);
} else if (hasFeatureMetadataLegacy) {
loadPrimitiveFeaturesLegacy(
loader,
primitive,
featureMetadataLegacy,
frameState,
);
}
// Load structural metadata
if (defined(structuralMetadata)) {
loadPrimitiveMetadata(primitive, structuralMetadata);
} else if (hasFeatureMetadataLegacy) {
loadPrimitiveMetadataLegacy(loader, primitive, featureMetadataLegacy);
}
const primitiveType = gltfPrimitive.mode;
if (loadForClassification && primitiveType !== PrimitiveType.TRIANGLES) {
throw new RuntimeError(
"Only triangle meshes can be used for classification.",
);
}
primitive.primitiveType = primitiveType;
return primitive;
}
function loadPrimitiveOutline(loader, outlineExtension) {
const accessorId = outlineExtension.indices;
const accessor = loader.gltfJson.accessors[accessorId];
const useQuaternion = false;
return loadAccessor(loader, accessor, useQuaternion);
}
// For EXT_mesh_features
function loadPrimitiveFeatures(
loader,
primitive,
meshFeaturesExtension,
frameState,
) {
let featureIdsArray;
if (
defined(meshFeaturesExtension) &&
defined(meshFeaturesExtension.featureIds)
) {
featureIdsArray = meshFeaturesExtension.featureIds;
} else {
featureIdsArray = [];
}
for (let i = 0; i < featureIdsArray.length; i++) {
const featureIds = featureIdsArray[i];
const label = `featureId_${i}`;
let featureIdComponent;
if (defined(featureIds.texture)) {
featureIdComponent = loadFeatureIdTexture(
loader,
featureIds,
frameState,
label,
);
} else if (defined(featureIds.attribute)) {
featureIdComponent = loadFeatureIdAttribute(featureIds, label);
} else {
// default to vertex ID, in other words an implicit range with
// offset: 0, repeat: 1
featureIdComponent = loadDefaultFeatureIds(featureIds, label);
}
primitive.featureIds.push(featureIdComponent);
}
}
// For EXT_feature_metadata
function loadPrimitiveFeaturesLegacy(
loader,
primitive,
metadataExtension,
frameState,
) {
// For looking up the featureCount for each set of feature IDs
const { featureTables } = loader.gltfJson.extensions.EXT_feature_metadata;
let nextFeatureIdIndex = 0;
// Feature ID Attributes
const featureIdAttributes = metadataExtension.featureIdAttributes;
if (defined(featureIdAttributes)) {
for (let i = 0; i < featureIdAttributes.length; ++i) {
const featureIdAttribute = featureIdAttributes[i];
const featureTableId = featureIdAttribute.featureTable;
const propertyTableId =
loader._sortedPropertyTableIds.indexOf(featureTableId);
const featureCount = featureTables[featureTableId].count;
const label = `featureId_${nextFeatureIdIndex}`;
nextFeatureIdIndex++;
let featureIdComponent;
if (defined(featureIdAttribute.featureIds.attribute)) {
featureIdComponent = loadFeatureIdAttributeLegacy(
featureIdAttribute,
propertyTableId,
featureCount,
label,
);
} else {
featureIdComponent = loadFeatureIdImplicitRangeLegacy(
featureIdAttribute,
propertyTableId,
featureCount,
label,
);
}
primitive.featureIds.push(featureIdComponent);
}
}
// Feature ID Textures
const featureIdTextures = metadataExtension.featureIdTextures;
if (defined(featureIdTextures)) {
for (let i = 0; i < featureIdTextures.length; ++i) {
const featureIdTexture = featureIdTextures[i];
const featureTableId = featureIdTexture.featureTable;
const propertyTableId =
loader._sortedPropertyTableIds.indexOf(featureTableId);
const featureCount = featureTables[featureTableId].count;
const featureIdLabel = `featureId_${nextFeatureIdIndex}`;
nextFeatureIdIndex++;
const featureIdComponent = loadFeatureIdTextureLegacy(
loader,
featureIdTexture,
propertyTableId,
frameState,
featureCount,
featureIdLabel,
);
// Feature ID textures are added after feature ID attributes in the list
primitive.featureIds.push(featureIdComponent);
}
}
}
// For primitive-level EXT_structural_metadata
function loadPrimitiveMetadata(primitive, structuralMetadataExtension) {
if (!defined(structuralMetadataExtension)) {
return;
}
const { propertyTextures, propertyAttributes } = structuralMetadataExtension;
if (defined(propertyTextures)) {
primitive.propertyTextureIds = propertyTextures;
}
if (defined(propertyAttributes)) {
primitive.propertyAttributeIds = propertyAttributes;
}
}
// For EXT_feature_metadata
function loadPrimitiveMetadataLegacy(loader, primitive, metadataExtension) {
// Feature Textures
if (defined(metadataExtension.featureTextures)) {
// feature textures are now identified by an integer index. To convert the
// string IDs to integers, find their place in the sorted list of feature
// table names
primitive.propertyTextureIds = metadataExtension.featureTextures.map(
function (id) {
return loader._sortedFeatureTextureIds.indexOf(id);
},
);
}
}
function loadInstances(loader, nodeExtensions, frameState) {
const instancingExtension = nodeExtensions.EXT_mesh_gpu_instancing;
const instances = new Instances();
const attributes = instancingExtension.attributes;
if (defined(attributes)) {
for (const semantic in attributes) {
if (!attributes.hasOwnProperty(semantic)) {
continue;
}
const accessorId = attributes[semantic];
instances.attributes.push(
loadInstancedAttribute(
loader,
accessorId,
attributes,
semantic,
frameState,
),
);
}
}
const instancingExtExtensions =
instancingExtension.extensions ?? Frozen.EMPTY_OBJECT;
const instanceFeatures = nodeExtensions.EXT_instance_features;
const featureMetadataLegacy = instancingExtExtensions.EXT_feature_metadata;
if (defined(instanceFeatures)) {
loadInstanceFeatures(instances, instanceFeatures);
} else if (defined(featureMetadataLegacy)) {
loadInstanceFeaturesLegacy(
loader.gltfJson,
instances,
featureMetadataLegacy,
loader._sortedPropertyTableIds,
);
}
return instances;
}
// For EXT_mesh_features
function loadInstanceFeatures(instances, instanceFeaturesExtension) {
// feature IDs are required in EXT_instance_features
const featureIdsArray = instanceFeaturesExtension.featureIds;
for (let i = 0; i < featureIdsArray.length; i++) {
const featureIds = featureIdsArray[i];
const label = `instanceFeatureId_${i}`;
let featureIdComponent;
if (defined(featureIds.attribute)) {
featureIdComponent = loadFeatureIdAttribute(featureIds, label);
} else {
// in EXT_instance_features, the default is to assign IDs by instance
// ID. This can be expressed with offset: 0, repeat: 1
featureIdComponent = loadDefaultFeatureIds(featureIds, label);
}
instances.featureIds.push(featureIdComponent);
}
}
// For backwards-compatibility with EXT_feature_metadata
function loadInstanceFeaturesLegacy(
gltf,
instances,
metadataExtension,
sortedPropertyTableIds,
) {
// For looking up the featureCount for each set of feature IDs
const featureTables = gltf.extensions.EXT_feature_metadata.featureTables;
const featureIdAttributes = metadataExtension.featureIdAttributes;
if (defined(featureIdAttributes)) {
for (let i = 0; i < featureIdAttributes.length; ++i) {
const featureIdAttribute = featureIdAttributes[i];
const featureTableId = featureIdAttribute.featureTable;
const propertyTableId = sortedPropertyTableIds.indexOf(featureTableId);
const featureCount = featureTables[featureTableId].count;
const label = `instanceFeatureId_${i}`;
let featureIdComponent;
if (defined(featureIdAttribute.featureIds.attribute)) {
featureIdComponent = loadFeatureIdAttributeLegacy(
featureIdAttribute,
propertyTableId,
featureCount,
label,
);
} else {
featureIdComponent = loadFeatureIdImplicitRangeLegacy(
featureIdAttribute,
propertyTableId,
featureCount,
label,
);
}
instances.featureIds.push(featureIdComponent);
}
}
}
/**
* Load resources associated with one node from a glTF JSON
* @param {GltfLoader} loader
* @param {object} gltfNode An entry from the .nodes array in the glTF JSON
* @param {FrameState} frameState
* @returns {ModelComponents.Node}
* @private
*/
function loadNode(loader, gltfNode, frameState) {
const node = new Node();
node.name = gltfNode.name;
node.matrix = fromArray(Matrix4, gltfNode.matrix);
node.translation = fromArray(Cartesian3, gltfNode.translation);
node.rotation = fromArray(Quaternion, gltfNode.rotation);
node.scale = fromArray(Cartesian3, gltfNode.scale);
const nodeExtensions = gltfNode.extensions ?? Frozen.EMPTY_OBJECT;
const instancingExtension = nodeExtensions.EXT_mesh_gpu_instancing;
const articulationsExtension = nodeExtensions.AGI_articulations;
if (defined(instancingExtension)) {
if (loader._loadForClassification) {
throw new RuntimeError(
"Models with the EXT_mesh_gpu_instancing extension cannot be used for classification.",
);
}
node.instances = loadInstances(loader, nodeExtensions, frameState);
}
if (defined(articulationsExtension)) {
node.articulationName = articulationsExtension.articulationName;
}
const meshId = gltfNode.mesh;
if (defined(meshId)) {
const mesh = loader.gltfJson.meshes[meshId];
const primitives = getMeshPrimitives(mesh);
for (let i = 0; i < primitives.length; ++i) {
node.primitives.push(
loadPrimitive(
loader,
primitives[i],
defined(node.instances),
frameState,
),
);
}
// If the node has no weights array, it will look for the weights array provided
// by the mesh. If both are undefined, it will default to an array of zero weights.
const morphWeights = gltfNode.weights ?? mesh.weights;
const targets = node.primitives[0].morphTargets;
// Since meshes are not stored as separate components, the mesh weights will still
// be stored at the node level.
node.morphWeights = defined(morphWeights)
? morphWeights.slice()
: new Array(targets.length).fill(0.0);
}
return node;
}
/**
* Load resources associated with the nodes in a glTF JSON
* @param {GltfLoader} loader
* @param {FrameState} frameState
* @returns {ModelComponents.Node[]}
* @private
*/
function loadNodes(loader, frameState) {
const nodeJsons = loader.gltfJson.nodes;
if (!defined(nodeJsons)) {
return [];
}
const loadedNodes = nodeJsons.map(function (nodeJson, i) {
const node = loadNode(loader, nodeJson, frameState);
node.index = i;
return node;
});
for (let i = 0; i < loadedNodes.length; ++i) {
const childrenNodeIds = nodeJsons[i].children;
if (defined(childrenNodeIds)) {
for (let j = 0; j < childrenNodeIds.length; ++j) {
loadedNodes[i].children.push(loadedNodes[childrenNodeIds[j]]);
}
}
}
return loadedNodes;
}
function loadSkin(loader, gltfSkin, nodes) {
const skin = new Skin();
const jointIds = gltfSkin.joints;
skin.joints = jointIds.map((jointId) => nodes[jointId]);
const inverseBindMatricesAccessorId = gltfSkin.inverseBindMatrices;
if (defined(inverseBindMatricesAccessorId)) {
const accessor = loader.gltfJson.accessors[inverseBindMatricesAccessorId];
skin.inverseBindMatrices = loadAccessor(loader, accessor);
} else {
skin.inverseBindMatrices = new Array(jointIds.length).fill(
Matrix4.IDENTITY,
);
}
return skin;
}
function loadSkins(loader, nodes) {
const skinJsons = loader.gltfJson.skins;
// Skins are disabled for classification models.
if (loader._loadForClassification || !defined(skinJsons)) {
return [];
}
const loadedSkins = skinJsons.map(function (skinJson, i) {
const skin = loadSkin(loader, skinJson, nodes);
skin.index = i;
return skin;
});
const nodeJsons = loader.gltfJson.nodes;
for (let i = 0; i < nodes.length; ++i) {
const skinId = nodeJsons[i].skin;
if (defined(skinId)) {
nodes[i].skin = loadedSkins[skinId];
}
}
return loadedSkins;
}
async function loadStructuralMetadata(
loader,
extension,
extensionLegacy,
frameState,
) {
const structuralMetadataLoader = new GltfStructuralMetadataLoader({
gltf: loader.gltfJson,
extension: extension,
extensionLegacy: extensionLegacy,
gltfResource: loader._gltfResource,
baseResource: loader._baseResource,
supportedImageFormats: loader._supportedImageFormats,
frameState: frameState,
asynchronous: loader._asynchronous,
});
loader._structuralMetadataLoader = structuralMetadataLoader;
return structuralMetadataLoader.load();
}
async function loadMeshPrimitiveGpm(loader, gltf, extension, frameState) {
const meshPrimitiveGpmLoader = new GltfMeshPrimitiveGpmLoader({
gltf: gltf,
extension: extension,
gltfResource: loader._gltfResource,
baseResource: loader._baseResource,
supportedImageFormats: loader._supportedImageFormats,
frameState: frameState,
asynchronous: loader._asynchronous,
});
loader._meshPrimitiveGpmLoader = meshPrimitiveGpmLoader;
return meshPrimitiveGpmLoader.load();
}
function loadAnimationSampler(loader, gltfSampler) {
const animationSampler = new AnimationSampler();
const accessors = loader.gltfJson.accessors;
const inputAccessor = accessors[gltfSampler.input];
animationSampler.input = loadAccessor(loader, inputAccessor);
const gltfInterpolation = gltfSampler.interpolation;
animationSampler.interpolation =
InterpolationType[gltfInterpolation] ?? InterpolationType.LINEAR;
const outputAccessor = accessors[gltfSampler.output];
animationSampler.output = loadAccessor(loader, outputAccessor, true);
return animationSampler;
}
function loadAnimationTarget(gltfTarget, nodes) {
const animationTarget = new AnimationTarget();
const nodeIndex = gltfTarget.node;
// If the node isn't defined, the animation channel should be ignored.
// It's easiest to signal this by returning undefined.
if (!defined(nodeIndex)) {
return undefined;
}
animationTarget.node = nodes[nodeIndex];
const path = gltfTarget.path.toUpperCase();
animationTarget.path = AnimatedPropertyType[path];
return animationTarget;
}
function loadAnimationChannel(gltfChannel, samplers, nodes) {
const animationChannel = new AnimationChannel();
const samplerIndex = gltfChannel.sampler;
animationChannel.sampler = samplers[samplerIndex];
animationChannel.target = loadAnimationTarget(gltfChannel.target, nodes);
return animationChannel;
}
function loadAnimation(loader, animationJson, nodes) {
const animation = new Animation();
animation.name = animationJson.name;
const samplers = animationJson.samplers.map(function (samplerJson, i) {
const sampler = loadAnimationSampler(loader, samplerJson);
sampler.index = i;
return sampler;
});
const channels = animationJson.channels.map(function (channelJson) {
return loadAnimationChannel(channelJson, samplers, nodes);
});
animation.samplers = samplers;
animation.channels = channels;
return animation;
}
function loadAnimations(loader, nodes) {
const animationJsons = loader.gltfJson.animations;
// Animations are disabled for classification models.
if (loader._loadForClassification || !defined(animationJsons)) {
return [];
}
const animations = animationJsons.map(function (animationJson, i) {
const animation = loadAnimation(loader, animationJson, nodes);
animation.index = i;
return animation;
});
return animations;
}
function loadArticulationStage(gltfStage) {
const stage = new ArticulationStage();
stage.name = gltfStage.name;
const type = gltfStage.type.toUpperCase();
stage.type = ArticulationStageType[type];
stage.minimumValue = gltfStage.minimumValue;
stage.maximumValue = gltfStage.maximumValue;
stage.initialValue = gltfStage.initialValue;
return stage;
}
function loadArticulation(articulationJson) {
const articulation = new Articulation();
articulation.name = articulationJson.name;
articulation.stages = articulationJson.stages.map(loadArticulationStage);
return articulation;
}
function loadArticulations(gltf) {
const extensions = gltf.extensions ?? Frozen.EMPTY_OBJECT;
const articulationJsons = extensions.AGI_articulations?.articulations;
if (!defined(articulationJsons)) {
return [];
}
return articulationJsons.map(loadArticulation);
}
function getSceneNodeIds(gltf) {
let nodesIds;
if (defined(gltf.scenes) && defined(gltf.scene)) {
nodesIds = gltf.scenes[gltf.scene].nodes;
}
nodesIds = nodesIds ?? gltf.nodes;
nodesIds = defined(nodesIds) ? nodesIds : [];
return nodesIds;
}
function loadScene(gltf, nodes) {
const scene = new Scene();
const sceneNodeIds = getSceneNodeIds(gltf);
scene.nodes = sceneNodeIds.map(function (sceneNodeId) {
return nodes[sceneNodeId];
});
return scene;
}
const scratchCenter = new Cartesian3();
/**
* 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.
*
* @param {GltfLoader} loader
* @param {FrameState} frameState
* @returns {Promise} A Promise that resolves when all loaders are ready
* @private
*/
function parse(loader, frameState) {
const gltf = loader.gltfJson;
const extensions = gltf.extensions ?? Frozen.EMPTY_OBJECT;
const structuralMetadataExtension = extensions.EXT_structural_metadata;
const featureMetadataExtensionLegacy = extensions.EXT_feature_metadata;
const cesiumRtcExtension = extensions.CESIUM_RTC;
if (defined(featureMetadataExtensionLegacy)) {
// If the old EXT_feature_metadata extension is present, sort the IDs of the
// feature tables and feature textures so we don't have to do this once
// per primitive.
//
// This must run before loadNodes so these IDs are available when
// attributes are processed.
const featureTables = featureMetadataExtensionLegacy.featureTables;
const featureTextures = featureMetadataExtensionLegacy.featureTextures;
const allPropertyTableIds = defined(featureTables) ? featureTables : [];
const allFeatureTextureIds = defined(featureTextures)
? featureTextures
: [];
loader._sortedPropertyTableIds = Object.keys(allPropertyTableIds).sort();
loader._sortedFeatureTextureIds = Object.keys(allFeatureTextureIds).sort();
}
const nodes = loadNodes(loader, frameState);
const skins = loadSkins(loader, nodes);
const animations = loadAnimations(loader, nodes);
const articulations = loadArticulations(gltf);
const scene = loadScene(gltf, nodes);
const components = new Components();
const asset = new Asset();
const copyright = gltf.asset.copyright;
if (defined(copyright)) {
const credits = copyright.split(";").map(function (string) {
return new Credit(string.trim());
});
asset.credits = credits;
}
components.asset = asset;
components.scene = scene;
components.nodes = nodes;
components.skins = skins;
components.animations = animations;
components.articulations = articulations;
components.upAxis = loader._upAxis;
components.forwardAxis = loader._forwardAxis;
if (defined(cesiumRtcExtension)) {
// CESIUM_RTC is almost always WGS84 coordinates so no axis conversion needed
const center = Cartesian3.fromArray(
cesiumRtcExtension.center,
0,
scratchCenter,
);
components.transform = Matrix4.fromTranslation(
center,
components.transform,
);
}
loader._components = components;
// Load structural metadata (property tables and property textures)
if (
defined(structuralMetadataExtension) ||
defined(featureMetadataExtensionLegacy)
) {
const promise = loadStructuralMetadata(
loader,
structuralMetadataExtension,
featureMetadataExtensionLegacy,
frameState,
);
loader._loaderPromises.push(promise);
}
// Load NGA_gpm_local from root object
const gpmExtension = extensions.NGA_gpm_local;
if (defined(gpmExtension)) {
const gltfGpmLocal = GltfGpmLoader.load(gpmExtension);
loader._components.extensions["NGA_gpm_local"] = gltfGpmLocal;
}
// Load NGA_gpm_local from mesh primitives
const meshes = gltf.meshes;
if (defined(meshes)) {
for (const mesh of meshes) {
const primitives = mesh.primitives;
if (defined(primitives)) {
for (const primitive of primitives) {
const primitiveExtensions = primitive.extensions;
if (defined(primitiveExtensions)) {
const meshPrimitiveGpmExtension = primitiveExtensions.NGA_gpm_local;
if (defined(meshPrimitiveGpmExtension)) {
const promise = loadMeshPrimitiveGpm(
loader,
gltf,
meshPrimitiveGpmExtension,
frameState,
);
loader._loaderPromises.push(promise);
}
}
}
}
}
}
// Gather promises and handle any errors
const readyPromises = [];
addAllToArray(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) {
addAllToArray(readyPromises, loader._texturesPromises);
}
return Promise.all(readyPromises);
}
function unloadTextures(loader) {
const textureLoaders = loader._textureLoaders;
for (let i = 0; i < textureLoaders.length; ++i) {
textureLoaders[i] =
!textureLoaders[i].isDestroyed() &&
ResourceCache.unload(textureLoaders[i]);
}
loader._textureLoaders.length = 0;
}
function unloadBufferViewLoaders(loader) {
const bufferViewLoaders = loader._bufferViewLoaders;
for (let i = 0; i < bufferViewLoaders.length; ++i) {
bufferViewLoaders[i] =
!bufferViewLoaders[i].isDestroyed() &&
ResourceCache.unload(bufferViewLoaders[i]);
}
loader._bufferViewLoaders.length = 0;
}
function unloadGeometry(loader) {
const geometryLoaders = loader._geometryLoaders;
for (let i = 0; i < geometryLoaders.length; ++i) {
geometryLoaders[i] =
!geometryLoaders[i].isDestroyed() &&
ResourceCache.unload(geometryLoaders[i]);
}
loader._geometryLoaders.length = 0;
}
function unloadGeneratedAttributes(loader) {
const buffers = loader._postProcessBuffers;
for (let i = 0; i < buffers.length; i++) {
const buffer = buffers[i];
if (!buffer.isDestroyed()) {
buffer.destroy();
}
}
buffers.length = 0;
}
function unloadStructuralMetadata(loader) {
if (
defined(loader._structuralMetadataLoader) &&
!loader._structuralMetadataLoader.isDestroyed()
) {
loader._structuralMetadataLoader.destroy();
loader._structuralMetadataLoader = undefined;
}
}
function unloadMeshPrimitiveGpm(loader) {
if (
defined(loader._meshPrimitiveGpmLoader) &&
!loader._meshPrimitiveGpmLoader.isDestroyed()
) {
loader._meshPrimitiveGpmLoader.destroy();
loader._meshPrimitiveGpmLoader = undefined;
}
}
/**
* Returns whether the resource has been unloaded.
* @private
*/
GltfLoader.prototype.isUnloaded = function () {
return this._state === GltfLoaderState.UNLOADED;
};
/**
* Unloads the resource.
* @private
*/
GltfLoader.prototype.unload = function () {
if (defined(this._gltfJsonLoader) && !this._gltfJsonLoader.isDestroyed()) {
ResourceCache.unload(this._gltfJsonLoader);
}
this._gltfJsonLoader = undefined;
unloadTextures(this);
unloadBufferViewLoaders(this);
unloadGeometry(this);
unloadGeneratedAttributes(this);
unloadStructuralMetadata(this);
unloadMeshPrimitiveGpm(this);
this._components = undefined;
this._typedArray = undefined;
this._state = GltfLoaderState.UNLOADED;
};
export default GltfLoader;