@babylonjs/loaders
Version:
For usage documentation please visit https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes/.
1,059 lines (1,058 loc) • 117 kB
JavaScript
import { Deferred } from "@babylonjs/core/Misc/deferred.js";
import { Quaternion, Vector3, Matrix, TmpVectors } from "@babylonjs/core/Maths/math.vector.js";
import { Color3 } from "@babylonjs/core/Maths/math.color.js";
import { Tools } from "@babylonjs/core/Misc/tools.js";
import { Camera } from "@babylonjs/core/Cameras/camera.js";
import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera.js";
import { Bone } from "@babylonjs/core/Bones/bone.js";
import { Skeleton } from "@babylonjs/core/Bones/skeleton.js";
import { Material } from "@babylonjs/core/Materials/material.js";
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial.js";
import { Texture } from "@babylonjs/core/Materials/Textures/texture.js";
import { TransformNode } from "@babylonjs/core/Meshes/transformNode.js";
import { Buffer, VertexBuffer } from "@babylonjs/core/Buffers/buffer.js";
import { Geometry } from "@babylonjs/core/Meshes/geometry.js";
import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh.js";
import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
import { MorphTarget } from "@babylonjs/core/Morph/morphTarget.js";
import { MorphTargetManager } from "@babylonjs/core/Morph/morphTargetManager.js";
import { GLTFFileLoader, GLTFLoaderState, GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "../glTFFileLoader.js";
import { DecodeBase64UrlToBinary, GetMimeType, IsBase64DataUrl, LoadFileError } from "@babylonjs/core/Misc/fileTools.js";
import { Logger } from "@babylonjs/core/Misc/logger.js";
import { BoundingInfo } from "@babylonjs/core/Culling/boundingInfo.js";
import { registeredGLTFExtensions, registerGLTFExtension, unregisterGLTFExtension } from "./glTFLoaderExtensionRegistry.js";
import { GetMappingForKey } from "./Extensions/objectModelMapping.js";
import { deepMerge } from "@babylonjs/core/Misc/deepMerger.js";
import { GetTypedArrayConstructor } from "@babylonjs/core/Buffers/bufferUtils.js";
import { Lazy } from "@babylonjs/core/Misc/lazy.js";
// Caching these dynamic imports gives a surprising perf boost (compared to importing them directly each time).
const LazyAnimationGroupModulePromise = new Lazy(() => import("@babylonjs/core/Animations/animationGroup.js"));
const LazyLoaderAnimationModulePromise = new Lazy(() => import("./glTFLoaderAnimation.js"));
export { GLTFFileLoader };
/**
* Helper class for working with arrays when loading the glTF asset
*/
export class ArrayItem {
/**
* Gets an item from the given array.
* @param context The context when loading the asset
* @param array The array to get the item from
* @param index The index to the array
* @returns The array item
*/
static Get(context, array, index) {
if (!array || index == undefined || !array[index]) {
throw new Error(`${context}: Failed to find index (${index})`);
}
return array[index];
}
/**
* Gets an item from the given array or returns null if not available.
* @param array The array to get the item from
* @param index The index to the array
* @returns The array item or null
*/
static TryGet(array, index) {
if (!array || index == undefined || !array[index]) {
return null;
}
return array[index];
}
/**
* Assign an `index` field to each item of the given array.
* @param array The array of items
*/
static Assign(array) {
if (array) {
for (let index = 0; index < array.length; index++) {
array[index].index = index;
}
}
}
}
/** @internal */
export function LoadBoundingInfoFromPositionAccessor(accessor) {
if (accessor.min && accessor.max) {
const minArray = accessor.min;
const maxArray = accessor.max;
const minVector = TmpVectors.Vector3[0].copyFromFloats(minArray[0], minArray[1], minArray[2]);
const maxVector = TmpVectors.Vector3[1].copyFromFloats(maxArray[0], maxArray[1], maxArray[2]);
if (accessor.normalized && accessor.componentType !== 5126 /* AccessorComponentType.FLOAT */) {
let divider = 1;
switch (accessor.componentType) {
case 5120 /* AccessorComponentType.BYTE */:
divider = 127.0;
break;
case 5121 /* AccessorComponentType.UNSIGNED_BYTE */:
divider = 255.0;
break;
case 5122 /* AccessorComponentType.SHORT */:
divider = 32767.0;
break;
case 5123 /* AccessorComponentType.UNSIGNED_SHORT */:
divider = 65535.0;
break;
}
const oneOverDivider = 1 / divider;
minVector.scaleInPlace(oneOverDivider);
maxVector.scaleInPlace(oneOverDivider);
}
return new BoundingInfo(minVector, maxVector);
}
return null;
}
/**
* The glTF 2.0 loader
*/
export class GLTFLoader {
/**
* Registers a loader extension.
* @param name The name of the loader extension.
* @param factory The factory function that creates the loader extension.
* @deprecated Please use registerGLTFExtension instead.
*/
static RegisterExtension(name, factory) {
registerGLTFExtension(name, false, factory);
}
/**
* Unregisters a loader extension.
* @param name The name of the loader extension.
* @returns A boolean indicating whether the extension has been unregistered
* @deprecated Please use unregisterGLTFExtension instead.
*/
static UnregisterExtension(name) {
return unregisterGLTFExtension(name);
}
/**
* The object that represents the glTF JSON.
*/
get gltf() {
if (!this._gltf) {
throw new Error("glTF JSON is not available");
}
return this._gltf;
}
/**
* The BIN chunk of a binary glTF.
*/
get bin() {
return this._bin;
}
/**
* The parent file loader.
*/
get parent() {
return this._parent;
}
/**
* The Babylon scene when loading the asset.
*/
get babylonScene() {
if (!this._babylonScene) {
throw new Error("Scene is not available");
}
return this._babylonScene;
}
/**
* The root Babylon node when loading the asset.
*/
get rootBabylonMesh() {
return this._rootBabylonMesh;
}
/**
* The root url when loading the asset.
*/
get rootUrl() {
return this._rootUrl;
}
/**
* @internal
*/
constructor(parent) {
/** @internal */
this._completePromises = new Array();
/** @internal */
this._assetContainer = null;
/** Storage */
this._babylonLights = [];
/** @internal */
this._disableInstancedMesh = 0;
/** @internal */
this._allMaterialsDirtyRequired = false;
/** @internal */
this._skipStartAnimationStep = false;
this._extensions = new Array();
this._disposed = false;
this._rootUrl = null;
this._fileName = null;
this._uniqueRootUrl = null;
this._bin = null;
this._rootBabylonMesh = null;
this._defaultBabylonMaterialData = {};
this._postSceneLoadActions = new Array();
this._parent = parent;
}
/** @internal */
dispose() {
if (this._disposed) {
return;
}
this._disposed = true;
this._completePromises.length = 0;
this._extensions.forEach((extension) => extension.dispose && extension.dispose());
this._extensions.length = 0;
this._gltf = null; // TODO
this._bin = null;
this._babylonScene = null; // TODO
this._rootBabylonMesh = null;
this._defaultBabylonMaterialData = {};
this._postSceneLoadActions.length = 0;
this._parent.dispose();
}
/**
* @internal
*/
async importMeshAsync(meshesNames, scene, container, data, rootUrl, onProgress, fileName = "") {
// eslint-disable-next-line github/no-then
return await Promise.resolve().then(async () => {
this._babylonScene = scene;
this._assetContainer = container;
this._loadData(data);
let nodes = null;
if (meshesNames) {
const nodeMap = {};
if (this._gltf.nodes) {
for (const node of this._gltf.nodes) {
if (node.name) {
nodeMap[node.name] = node.index;
}
}
}
const names = meshesNames instanceof Array ? meshesNames : [meshesNames];
nodes = names.map((name) => {
const node = nodeMap[name];
if (node === undefined) {
throw new Error(`Failed to find node '${name}'`);
}
return node;
});
}
return await this._loadAsync(rootUrl, fileName, nodes, () => {
return {
meshes: this._getMeshes(),
particleSystems: [],
skeletons: this._getSkeletons(),
animationGroups: this._getAnimationGroups(),
lights: this._babylonLights,
transformNodes: this._getTransformNodes(),
geometries: this._getGeometries(),
spriteManagers: [],
};
});
});
}
/**
* @internal
*/
async loadAsync(scene, data, rootUrl, onProgress, fileName = "") {
this._babylonScene = scene;
this._loadData(data);
return await this._loadAsync(rootUrl, fileName, null, () => undefined);
}
async _loadAsync(rootUrl, fileName, nodes, resultFunc) {
return await Promise.resolve()
.then(async () => {
this._rootUrl = rootUrl;
this._uniqueRootUrl = !rootUrl.startsWith("file:") && fileName ? rootUrl : `${rootUrl}${Date.now()}/`;
this._fileName = fileName;
this._allMaterialsDirtyRequired = false;
await this._loadExtensionsAsync();
const loadingToReadyCounterName = `${GLTFLoaderState[GLTFLoaderState.LOADING]} => ${GLTFLoaderState[GLTFLoaderState.READY]}`;
const loadingToCompleteCounterName = `${GLTFLoaderState[GLTFLoaderState.LOADING]} => ${GLTFLoaderState[GLTFLoaderState.COMPLETE]}`;
this._parent._startPerformanceCounter(loadingToReadyCounterName);
this._parent._startPerformanceCounter(loadingToCompleteCounterName);
this._parent._setState(GLTFLoaderState.LOADING);
this._extensionsOnLoading();
const promises = new Array();
// Block the marking of materials dirty until the scene is loaded.
const oldBlockMaterialDirtyMechanism = this._babylonScene.blockMaterialDirtyMechanism;
this._babylonScene.blockMaterialDirtyMechanism = true;
if (!this.parent.loadOnlyMaterials) {
if (nodes) {
promises.push(this.loadSceneAsync("/nodes", { nodes: nodes, index: -1 }));
}
else if (this._gltf.scene != undefined || (this._gltf.scenes && this._gltf.scenes[0])) {
const scene = ArrayItem.Get(`/scene`, this._gltf.scenes, this._gltf.scene || 0);
promises.push(this.loadSceneAsync(`/scenes/${scene.index}`, scene));
}
}
if (!this.parent.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) {
for (let m = 0; m < this._gltf.materials.length; ++m) {
const material = this._gltf.materials[m];
const context = "/materials/" + m;
const babylonDrawMode = Material.TriangleFillMode;
promises.push(this._loadMaterialAsync(context, material, null, babylonDrawMode, () => { }));
}
}
// Restore the blocking of material dirty.
if (this._allMaterialsDirtyRequired) {
// This can happen if we add a light for instance as it will impact the whole scene.
// This automatically resets everything if needed.
this._babylonScene.blockMaterialDirtyMechanism = oldBlockMaterialDirtyMechanism;
}
else {
// By default a newly created material is dirty so there is no need to flag the full scene as dirty.
// For perf reasons, we then bypass blockMaterialDirtyMechanism as this would "dirty" the entire scene.
this._babylonScene._forceBlockMaterialDirtyMechanism(oldBlockMaterialDirtyMechanism);
}
if (this._parent.compileMaterials) {
promises.push(this._compileMaterialsAsync());
}
if (this._parent.compileShadowGenerators) {
promises.push(this._compileShadowGeneratorsAsync());
}
const resultPromise = Promise.all(promises).then(() => {
if (this._rootBabylonMesh && this._rootBabylonMesh !== this._parent.customRootNode) {
this._rootBabylonMesh.setEnabled(true);
}
// Making sure we enable enough lights to have all lights together
for (const material of this._babylonScene.materials) {
const mat = material;
if (mat.maxSimultaneousLights !== undefined) {
mat.maxSimultaneousLights = Math.max(mat.maxSimultaneousLights, this._babylonScene.lights.length);
}
}
this._extensionsOnReady();
this._parent._setState(GLTFLoaderState.READY);
if (!this._skipStartAnimationStep) {
this._startAnimations();
}
return resultFunc();
});
return await resultPromise.then((result) => {
this._parent._endPerformanceCounter(loadingToReadyCounterName);
Tools.SetImmediate(() => {
if (!this._disposed) {
Promise.all(this._completePromises).then(() => {
this._parent._endPerformanceCounter(loadingToCompleteCounterName);
this._parent._setState(GLTFLoaderState.COMPLETE);
this._parent.onCompleteObservable.notifyObservers(undefined);
this._parent.onCompleteObservable.clear();
this.dispose();
}, (error) => {
this._parent.onErrorObservable.notifyObservers(error);
this._parent.onErrorObservable.clear();
this.dispose();
});
}
});
return result;
});
})
.catch((error) => {
if (!this._disposed) {
this._parent.onErrorObservable.notifyObservers(error);
this._parent.onErrorObservable.clear();
this.dispose();
}
throw error;
});
}
_loadData(data) {
this._gltf = data.json;
this._setupData();
if (data.bin) {
const buffers = this._gltf.buffers;
if (buffers && buffers[0] && !buffers[0].uri) {
const binaryBuffer = buffers[0];
if (binaryBuffer.byteLength < data.bin.byteLength - 3 || binaryBuffer.byteLength > data.bin.byteLength) {
Logger.Warn(`Binary buffer length (${binaryBuffer.byteLength}) from JSON does not match chunk length (${data.bin.byteLength})`);
}
this._bin = data.bin;
}
else {
Logger.Warn("Unexpected BIN chunk");
}
}
}
_setupData() {
ArrayItem.Assign(this._gltf.accessors);
ArrayItem.Assign(this._gltf.animations);
ArrayItem.Assign(this._gltf.buffers);
ArrayItem.Assign(this._gltf.bufferViews);
ArrayItem.Assign(this._gltf.cameras);
ArrayItem.Assign(this._gltf.images);
ArrayItem.Assign(this._gltf.materials);
ArrayItem.Assign(this._gltf.meshes);
ArrayItem.Assign(this._gltf.nodes);
ArrayItem.Assign(this._gltf.samplers);
ArrayItem.Assign(this._gltf.scenes);
ArrayItem.Assign(this._gltf.skins);
ArrayItem.Assign(this._gltf.textures);
if (this._gltf.nodes) {
const nodeParents = {};
for (const node of this._gltf.nodes) {
if (node.children) {
for (const index of node.children) {
nodeParents[index] = node.index;
}
}
}
const rootNode = this._createRootNode();
for (const node of this._gltf.nodes) {
const parentIndex = nodeParents[node.index];
node.parent = parentIndex === undefined ? rootNode : this._gltf.nodes[parentIndex];
}
}
}
async _loadExtensionsAsync() {
const extensionPromises = [];
registeredGLTFExtensions.forEach((registeredExtension, name) => {
// Don't load explicitly disabled extensions.
if (this.parent.extensionOptions[name]?.enabled === false) {
// But warn if the disabled extension is used by the model.
if (registeredExtension.isGLTFExtension && this.isExtensionUsed(name)) {
Logger.Warn(`Extension ${name} is used but has been explicitly disabled.`);
}
}
// Load loader extensions that are not a glTF extension, as well as extensions that are glTF extensions and are used by the model.
else if (!registeredExtension.isGLTFExtension || this.isExtensionUsed(name)) {
extensionPromises.push((async () => {
const extension = await registeredExtension.factory(this);
if (extension.name !== name) {
Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`);
}
this._parent.onExtensionLoadedObservable.notifyObservers(extension);
return extension;
})());
}
});
this._extensions.push(...(await Promise.all(extensionPromises)));
this._extensions.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE));
this._parent.onExtensionLoadedObservable.clear();
if (this._gltf.extensionsRequired) {
for (const name of this._gltf.extensionsRequired) {
const available = this._extensions.some((extension) => extension.name === name && extension.enabled);
if (!available) {
if (this.parent.extensionOptions[name]?.enabled === false) {
throw new Error(`Required extension ${name} is disabled`);
}
throw new Error(`Required extension ${name} is not available`);
}
}
}
}
_createRootNode() {
if (this._parent.customRootNode !== undefined) {
this._rootBabylonMesh = this._parent.customRootNode;
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
_babylonTransformNode: this._rootBabylonMesh === null ? undefined : this._rootBabylonMesh,
index: -1,
};
}
this._babylonScene._blockEntityCollection = !!this._assetContainer;
const rootMesh = new Mesh("__root__", this._babylonScene);
this._rootBabylonMesh = rootMesh;
this._rootBabylonMesh._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
this._rootBabylonMesh.setEnabled(false);
const rootNode = {
// eslint-disable-next-line @typescript-eslint/naming-convention
_babylonTransformNode: this._rootBabylonMesh,
index: -1,
};
switch (this._parent.coordinateSystemMode) {
case GLTFLoaderCoordinateSystemMode.AUTO: {
if (!this._babylonScene.useRightHandedSystem) {
rootNode.rotation = [0, 1, 0, 0];
rootNode.scale = [1, 1, -1];
GLTFLoader._LoadTransform(rootNode, this._rootBabylonMesh);
}
break;
}
case GLTFLoaderCoordinateSystemMode.FORCE_RIGHT_HANDED: {
this._babylonScene.useRightHandedSystem = true;
break;
}
default: {
throw new Error(`Invalid coordinate system mode (${this._parent.coordinateSystemMode})`);
}
}
this._parent.onMeshLoadedObservable.notifyObservers(rootMesh);
return rootNode;
}
/**
* Loads a glTF scene.
* @param context The context when loading the asset
* @param scene The glTF scene property
* @returns A promise that resolves when the load is complete
*/
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/promise-function-async
loadSceneAsync(context, scene) {
const extensionPromise = this._extensionsLoadSceneAsync(context, scene);
if (extensionPromise) {
return extensionPromise;
}
const promises = new Array();
this.logOpen(`${context} ${scene.name || ""}`);
if (scene.nodes) {
for (const index of scene.nodes) {
const node = ArrayItem.Get(`${context}/nodes/${index}`, this._gltf.nodes, index);
promises.push(this.loadNodeAsync(`/nodes/${node.index}`, node, (babylonMesh) => {
babylonMesh.parent = this._rootBabylonMesh;
}));
}
}
for (const action of this._postSceneLoadActions) {
action();
}
promises.push(this._loadAnimationsAsync());
this.logClose();
return Promise.all(promises).then(() => { });
}
_forEachPrimitive(node, callback) {
if (node._primitiveBabylonMeshes) {
for (const babylonMesh of node._primitiveBabylonMeshes) {
callback(babylonMesh);
}
}
}
_getGeometries() {
const geometries = [];
const nodes = this._gltf.nodes;
if (nodes) {
for (const node of nodes) {
this._forEachPrimitive(node, (babylonMesh) => {
const geometry = babylonMesh.geometry;
if (geometry && geometries.indexOf(geometry) === -1) {
geometries.push(geometry);
}
});
}
}
return geometries;
}
_getMeshes() {
const meshes = [];
// Root mesh is always first, if available.
if (this._rootBabylonMesh instanceof AbstractMesh) {
meshes.push(this._rootBabylonMesh);
}
const nodes = this._gltf.nodes;
if (nodes) {
for (const node of nodes) {
this._forEachPrimitive(node, (babylonMesh) => {
meshes.push(babylonMesh);
});
}
}
return meshes;
}
_getTransformNodes() {
const transformNodes = [];
const nodes = this._gltf.nodes;
if (nodes) {
for (const node of nodes) {
if (node._babylonTransformNode && node._babylonTransformNode.getClassName() === "TransformNode") {
transformNodes.push(node._babylonTransformNode);
}
if (node._babylonTransformNodeForSkin) {
transformNodes.push(node._babylonTransformNodeForSkin);
}
}
}
return transformNodes;
}
_getSkeletons() {
const skeletons = [];
const skins = this._gltf.skins;
if (skins) {
for (const skin of skins) {
if (skin._data) {
skeletons.push(skin._data.babylonSkeleton);
}
}
}
return skeletons;
}
_getAnimationGroups() {
const animationGroups = [];
const animations = this._gltf.animations;
if (animations) {
for (const animation of animations) {
if (animation._babylonAnimationGroup) {
animationGroups.push(animation._babylonAnimationGroup);
}
}
}
return animationGroups;
}
_startAnimations() {
switch (this._parent.animationStartMode) {
case GLTFLoaderAnimationStartMode.NONE: {
// do nothing
break;
}
case GLTFLoaderAnimationStartMode.FIRST: {
const babylonAnimationGroups = this._getAnimationGroups();
if (babylonAnimationGroups.length !== 0) {
babylonAnimationGroups[0].start(true);
}
break;
}
case GLTFLoaderAnimationStartMode.ALL: {
const babylonAnimationGroups = this._getAnimationGroups();
for (const babylonAnimationGroup of babylonAnimationGroups) {
babylonAnimationGroup.start(true);
}
break;
}
default: {
Logger.Error(`Invalid animation start mode (${this._parent.animationStartMode})`);
return;
}
}
}
/**
* Loads a glTF node.
* @param context The context when loading the asset
* @param node The glTF node property
* @param assign A function called synchronously after parsing the glTF properties
* @returns A promise that resolves with the loaded Babylon mesh when the load is complete
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
loadNodeAsync(context, node, assign = () => { }) {
const extensionPromise = this._extensionsLoadNodeAsync(context, node, assign);
if (extensionPromise) {
return extensionPromise;
}
if (node._babylonTransformNode) {
throw new Error(`${context}: Invalid recursive node hierarchy`);
}
const promises = new Array();
this.logOpen(`${context} ${node.name || ""}`);
const loadNode = (babylonTransformNode) => {
GLTFLoader.AddPointerMetadata(babylonTransformNode, context);
GLTFLoader._LoadTransform(node, babylonTransformNode);
if (node.camera != undefined) {
const camera = ArrayItem.Get(`${context}/camera`, this._gltf.cameras, node.camera);
promises.push(this.loadCameraAsync(`/cameras/${camera.index}`, camera, (babylonCamera) => {
babylonCamera.parent = babylonTransformNode;
if (!this._babylonScene.useRightHandedSystem) {
babylonTransformNode.scaling.x = -1; // Cancelling root node scaling for handedness so the view matrix does not end up flipped.
}
}));
}
if (node.children) {
for (const index of node.children) {
const childNode = ArrayItem.Get(`${context}/children/${index}`, this._gltf.nodes, index);
promises.push(this.loadNodeAsync(`/nodes/${childNode.index}`, childNode, (childBabylonMesh) => {
childBabylonMesh.parent = babylonTransformNode;
}));
}
}
assign(babylonTransformNode);
};
const hasMesh = node.mesh != undefined;
const hasSkin = this._parent.loadSkins && node.skin != undefined;
if (!hasMesh || hasSkin) {
const nodeName = node.name || `node${node.index}`;
this._babylonScene._blockEntityCollection = !!this._assetContainer;
const transformNode = new TransformNode(nodeName, this._babylonScene);
transformNode._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
if (node.mesh == undefined) {
node._babylonTransformNode = transformNode;
}
else {
node._babylonTransformNodeForSkin = transformNode;
}
loadNode(transformNode);
}
if (hasMesh) {
if (hasSkin) {
// See https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins (second implementation note)
// This code path will place the skinned mesh as a sibling of the skeleton root node without loading the
// transform, which effectively ignores the transform of the skinned mesh, as per spec.
const mesh = ArrayItem.Get(`${context}/mesh`, this._gltf.meshes, node.mesh);
promises.push(this._loadMeshAsync(`/meshes/${mesh.index}`, node, mesh, (babylonTransformNode) => {
const babylonTransformNodeForSkin = node._babylonTransformNodeForSkin;
// Merge the metadata from the skin node to the skinned mesh in case a loader extension added metadata.
babylonTransformNode.metadata = deepMerge(babylonTransformNodeForSkin.metadata, babylonTransformNode.metadata || {});
const skin = ArrayItem.Get(`${context}/skin`, this._gltf.skins, node.skin);
promises.push(this._loadSkinAsync(`/skins/${skin.index}`, node, skin, (babylonSkeleton) => {
this._forEachPrimitive(node, (babylonMesh) => {
babylonMesh.skeleton = babylonSkeleton;
});
// Wait until all the nodes are parented before parenting the skinned mesh.
this._postSceneLoadActions.push(() => {
if (skin.skeleton != undefined) {
// Place the skinned mesh node as a sibling of the skeleton root node.
// Handle special case when the parent of the skeleton root is the skinned mesh.
const parentNode = ArrayItem.Get(`/skins/${skin.index}/skeleton`, this._gltf.nodes, skin.skeleton).parent;
if (node.index === parentNode.index) {
babylonTransformNode.parent = babylonTransformNodeForSkin.parent;
}
else {
babylonTransformNode.parent = parentNode._babylonTransformNode;
}
}
else {
babylonTransformNode.parent = this._rootBabylonMesh;
}
this._parent.onSkinLoadedObservable.notifyObservers({ node: babylonTransformNodeForSkin, skinnedNode: babylonTransformNode });
});
}));
}));
}
else {
const mesh = ArrayItem.Get(`${context}/mesh`, this._gltf.meshes, node.mesh);
promises.push(this._loadMeshAsync(`/meshes/${mesh.index}`, node, mesh, loadNode));
}
}
this.logClose();
return Promise.all(promises).then(() => {
this._forEachPrimitive(node, (babylonMesh) => {
const asMesh = babylonMesh;
if (!asMesh.isAnInstance && asMesh.geometry && asMesh.geometry.useBoundingInfoFromGeometry) {
// simply apply the world matrices to the bounding info - the extends are already ok
babylonMesh._updateBoundingInfo();
}
else {
babylonMesh.refreshBoundingInfo(true, true);
}
});
return node._babylonTransformNode;
});
}
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
_loadMeshAsync(context, node, mesh, assign) {
const primitives = mesh.primitives;
if (!primitives || !primitives.length) {
throw new Error(`${context}: Primitives are missing`);
}
if (primitives[0].index == undefined) {
ArrayItem.Assign(primitives);
}
const promises = new Array();
this.logOpen(`${context} ${mesh.name || ""}`);
const name = node.name || `node${node.index}`;
if (primitives.length === 1) {
const primitive = mesh.primitives[0];
promises.push(this._loadMeshPrimitiveAsync(`${context}/primitives/${primitive.index}`, name, node, mesh, primitive, (babylonMesh) => {
node._babylonTransformNode = babylonMesh;
node._primitiveBabylonMeshes = [babylonMesh];
}));
}
else {
this._babylonScene._blockEntityCollection = !!this._assetContainer;
node._babylonTransformNode = new TransformNode(name, this._babylonScene);
node._babylonTransformNode._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
node._primitiveBabylonMeshes = [];
for (const primitive of primitives) {
promises.push(this._loadMeshPrimitiveAsync(`${context}/primitives/${primitive.index}`, `${name}_primitive${primitive.index}`, node, mesh, primitive, (babylonMesh) => {
babylonMesh.parent = node._babylonTransformNode;
node._primitiveBabylonMeshes.push(babylonMesh);
}));
}
}
assign(node._babylonTransformNode);
this.logClose();
return Promise.all(promises).then(() => {
return node._babylonTransformNode;
});
}
/**
* @internal Define this method to modify the default behavior when loading data for mesh primitives.
* @param context The context when loading the asset
* @param name The mesh name when loading the asset
* @param node The glTF node when loading the asset
* @param mesh The glTF mesh when loading the asset
* @param primitive The glTF mesh primitive property
* @param assign A function called synchronously after parsing the glTF properties
* @returns A promise that resolves with the loaded mesh when the load is complete or null if not handled
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
_loadMeshPrimitiveAsync(context, name, node, mesh, primitive, assign) {
const extensionPromise = this._extensionsLoadMeshPrimitiveAsync(context, name, node, mesh, primitive, assign);
if (extensionPromise) {
return extensionPromise;
}
this.logOpen(`${context}`);
const shouldInstance = this._disableInstancedMesh === 0 && this._parent.createInstances && node.skin == undefined && !mesh.primitives[0].targets;
let babylonAbstractMesh;
let promise;
if (shouldInstance && primitive._instanceData) {
this._babylonScene._blockEntityCollection = !!this._assetContainer;
babylonAbstractMesh = primitive._instanceData.babylonSourceMesh.createInstance(name);
babylonAbstractMesh._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
promise = primitive._instanceData.promise;
}
else {
const promises = new Array();
this._babylonScene._blockEntityCollection = !!this._assetContainer;
const babylonMesh = new Mesh(name, this._babylonScene);
babylonMesh._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
babylonMesh.sideOrientation = this._babylonScene.useRightHandedSystem ? Material.CounterClockWiseSideOrientation : Material.ClockWiseSideOrientation;
this._createMorphTargets(context, node, mesh, primitive, babylonMesh);
promises.push(this._loadVertexDataAsync(context, primitive, babylonMesh).then(async (babylonGeometry) => {
return await this._loadMorphTargetsAsync(context, primitive, babylonMesh, babylonGeometry).then(() => {
if (this._disposed) {
return;
}
this._babylonScene._blockEntityCollection = !!this._assetContainer;
babylonGeometry.applyToMesh(babylonMesh);
babylonGeometry._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
});
}));
const babylonDrawMode = GLTFLoader._GetDrawMode(context, primitive.mode);
if (primitive.material == undefined) {
let babylonMaterial = this._defaultBabylonMaterialData[babylonDrawMode];
if (!babylonMaterial) {
babylonMaterial = this._createDefaultMaterial("__GLTFLoader._default", babylonDrawMode);
this._parent.onMaterialLoadedObservable.notifyObservers(babylonMaterial);
this._defaultBabylonMaterialData[babylonDrawMode] = babylonMaterial;
}
babylonMesh.material = babylonMaterial;
}
else if (!this.parent.skipMaterials) {
const material = ArrayItem.Get(`${context}/material`, this._gltf.materials, primitive.material);
promises.push(this._loadMaterialAsync(`/materials/${material.index}`, material, babylonMesh, babylonDrawMode, (babylonMaterial) => {
babylonMesh.material = babylonMaterial;
}));
}
promise = Promise.all(promises);
if (shouldInstance) {
primitive._instanceData = {
babylonSourceMesh: babylonMesh,
promise: promise,
};
}
babylonAbstractMesh = babylonMesh;
}
GLTFLoader.AddPointerMetadata(babylonAbstractMesh, context);
this._parent.onMeshLoadedObservable.notifyObservers(babylonAbstractMesh);
assign(babylonAbstractMesh);
this.logClose();
return promise.then(() => {
return babylonAbstractMesh;
});
}
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
_loadVertexDataAsync(context, primitive, babylonMesh) {
const extensionPromise = this._extensionsLoadVertexDataAsync(context, primitive, babylonMesh);
if (extensionPromise) {
return extensionPromise;
}
const attributes = primitive.attributes;
if (!attributes) {
throw new Error(`${context}: Attributes are missing`);
}
const promises = new Array();
const babylonGeometry = new Geometry(babylonMesh.name, this._babylonScene);
if (primitive.indices == undefined) {
babylonMesh.isUnIndexed = true;
}
else {
const accessor = ArrayItem.Get(`${context}/indices`, this._gltf.accessors, primitive.indices);
promises.push(this._loadIndicesAccessorAsync(`/accessors/${accessor.index}`, accessor).then((data) => {
babylonGeometry.setIndices(data);
}));
}
const loadAttribute = (name, kind, callback) => {
if (attributes[name] == undefined) {
return;
}
babylonMesh._delayInfo = babylonMesh._delayInfo || [];
if (babylonMesh._delayInfo.indexOf(kind) === -1) {
babylonMesh._delayInfo.push(kind);
}
const accessor = ArrayItem.Get(`${context}/attributes/${name}`, this._gltf.accessors, attributes[name]);
promises.push(this._loadVertexAccessorAsync(`/accessors/${accessor.index}`, accessor, kind).then((babylonVertexBuffer) => {
if (babylonVertexBuffer.getKind() === VertexBuffer.PositionKind && !this.parent.alwaysComputeBoundingBox && !babylonMesh.skeleton) {
const babylonBoundingInfo = LoadBoundingInfoFromPositionAccessor(accessor);
if (babylonBoundingInfo) {
babylonGeometry._boundingInfo = babylonBoundingInfo;
babylonGeometry.useBoundingInfoFromGeometry = true;
}
}
babylonGeometry.setVerticesBuffer(babylonVertexBuffer, accessor.count);
}));
if (kind == VertexBuffer.MatricesIndicesExtraKind) {
babylonMesh.numBoneInfluencers = 8;
}
if (callback) {
callback(accessor);
}
};
loadAttribute("POSITION", VertexBuffer.PositionKind);
loadAttribute("NORMAL", VertexBuffer.NormalKind);
loadAttribute("TANGENT", VertexBuffer.TangentKind);
loadAttribute("TEXCOORD_0", VertexBuffer.UVKind);
loadAttribute("TEXCOORD_1", VertexBuffer.UV2Kind);
loadAttribute("TEXCOORD_2", VertexBuffer.UV3Kind);
loadAttribute("TEXCOORD_3", VertexBuffer.UV4Kind);
loadAttribute("TEXCOORD_4", VertexBuffer.UV5Kind);
loadAttribute("TEXCOORD_5", VertexBuffer.UV6Kind);
loadAttribute("JOINTS_0", VertexBuffer.MatricesIndicesKind);
loadAttribute("WEIGHTS_0", VertexBuffer.MatricesWeightsKind);
loadAttribute("JOINTS_1", VertexBuffer.MatricesIndicesExtraKind);
loadAttribute("WEIGHTS_1", VertexBuffer.MatricesWeightsExtraKind);
loadAttribute("COLOR_0", VertexBuffer.ColorKind, (accessor) => {
if (accessor.type === "VEC4" /* AccessorType.VEC4 */) {
babylonMesh.hasVertexAlpha = true;
}
});
return Promise.all(promises).then(() => {
return babylonGeometry;
});
}
_createMorphTargets(context, node, mesh, primitive, babylonMesh) {
if (!primitive.targets || !this._parent.loadMorphTargets) {
return;
}
if (node._numMorphTargets == undefined) {
node._numMorphTargets = primitive.targets.length;
}
else if (primitive.targets.length !== node._numMorphTargets) {
throw new Error(`${context}: Primitives do not have the same number of targets`);
}
const targetNames = mesh.extras ? mesh.extras.targetNames : null;
this._babylonScene._blockEntityCollection = !!this._assetContainer;
babylonMesh.morphTargetManager = new MorphTargetManager(this._babylonScene);
babylonMesh.morphTargetManager._parentContainer = this._assetContainer;
this._babylonScene._blockEntityCollection = false;
babylonMesh.morphTargetManager.areUpdatesFrozen = true;
for (let index = 0; index < primitive.targets.length; index++) {
const weight = node.weights ? node.weights[index] : mesh.weights ? mesh.weights[index] : 0;
const name = targetNames ? targetNames[index] : `morphTarget${index}`;
babylonMesh.morphTargetManager.addTarget(new MorphTarget(name, weight, babylonMesh.getScene()));
// TODO: tell the target whether it has positions, normals, tangents
}
}
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
_loadMorphTargetsAsync(context, primitive, babylonMesh, babylonGeometry) {
if (!primitive.targets || !this._parent.loadMorphTargets) {
return Promise.resolve();
}
const promises = new Array();
const morphTargetManager = babylonMesh.morphTargetManager;
for (let index = 0; index < morphTargetManager.numTargets; index++) {
const babylonMorphTarget = morphTargetManager.getTarget(index);
promises.push(this._loadMorphTargetVertexDataAsync(`${context}/targets/${index}`, babylonGeometry, primitive.targets[index], babylonMorphTarget));
}
return Promise.all(promises).then(() => {
morphTargetManager.areUpdatesFrozen = false;
});
}
async _loadMorphTargetVertexDataAsync(context, babylonGeometry, attributes, babylonMorphTarget) {
const promises = new Array();
const loadAttribute = (attribute, kind, setData) => {
if (attributes[attribute] == undefined) {
return;
}
const babylonVertexBuffer = babylonGeometry.getVertexBuffer(kind);
if (!babylonVertexBuffer) {
return;
}
const accessor = ArrayItem.Get(`${context}/${attribute}`, this._gltf.accessors, attributes[attribute]);
promises.push(this._loadFloatAccessorAsync(`/accessors/${accessor.index}`, accessor).then((data) => {
setData(babylonVertexBuffer, data);
}));
};
loadAttribute("POSITION", VertexBuffer.PositionKind, (babylonVertexBuffer, data) => {
const positions = new Float32Array(data.length);
babylonVertexBuffer.forEach(data.length, (value, index) => {
positions[index] = data[index] + value;
});
babylonMorphTarget.setPositions(positions);
});
loadAttribute("NORMAL", VertexBuffer.NormalKind, (babylonVertexBuffer, data) => {
const normals = new Float32Array(data.length);
babylonVertexBuffer.forEach(normals.length, (value, index) => {
normals[index] = data[index] + value;
});
babylonMorphTarget.setNormals(normals);
});
loadAttribute("TANGENT", VertexBuffer.TangentKind, (babylonVertexBuffer, data) => {
const tangents = new Float32Array((data.length / 3) * 4);
let dataIndex = 0;
babylonVertexBuffer.forEach((data.length / 3) * 4, (value, index) => {
// Tangent data for morph targets is stored as xyz delta.
// The vertexData.tangent is stored as xyzw.
// So we need to skip every fourth vertexData.tangent.
if ((index + 1) % 4 !== 0) {
tangents[dataIndex] = data[dataIndex] + value;
dataIndex++;
}
});
babylonMorphTarget.setTangents(tangents);
});
loadAttribute("TEXCOORD_0", VertexBuffer.UVKind, (babylonVertexBuffer, data) => {
const uvs = new Float32Array(data.length);
babylonVertexBuffer.forEach(data.length, (value, index) => {
uvs[index] = data[index] + value;
});
babylonMorphTarget.setUVs(uvs);
});
loadAttribute("TEXCOORD_1", VertexBuffer.UV2Kind, (babylonVertexBuffer, data) => {
const uvs = new Float32Array(data.length);
babylonVertexBuffer.forEach(data.length, (value, index) => {
uvs[index] = data[index] + value;
});
babylonMorphTarget.setUV2s(uvs);
});
loadAttribute("COLOR_0", VertexBuffer.ColorKind, (babylonVertexBuffer, data) => {
let colors = null;
const componentSize = babylonVertexBuffer.getSize();
if (componentSize === 3) {
colors = new Float32Array((data.length / 3) * 4);
babylonVertexBuffer.forEach(data.length, (value, index) => {
const pixid = Math.floor(index / 3);
const channel = index % 3;
colors[4 * pixid + channel] = data[3 * pixid + channel] + value;
});
for (let i = 0; i < data.length / 3; ++i) {
colors[4 * i + 3] = 1;
}
}
else if (componentSize === 4) {
colors = new Float32Array(data.length);
babylonVertexBuffer.forEach(data.length, (value, index) => {
colors[index] = data[index] + value;
});
}
else {
throw new Error(`${context}: Invalid number of components (${componentSize}) for COLOR_0 attribute`);
}
babylonMorphTarget.setColors(colors);
});
return await Promise.all(promises).then(() => { });
}
static _LoadTransform(node, babylonNode) {
// Ignore the TRS of skinned nodes.
// See https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins (second implementation note)
if (node.skin != undefined) {
return;
}
let position = Vector3.Zero();
let rotation = Quaternion.Identity();
let scaling = Vector3.One();
if (node.matrix) {
const matrix = Matrix.FromArray(node.matrix);
matrix.decompose(scaling, rotation, position);
}
else {
if (node.translation) {
position = Vector3.FromArray(node.translation);
}
if (node.rotation) {
rotation = Quaternion.FromArray(node.rotation);
}
if (node.scale) {
scaling = Vector3.FromArray(node.scale);