UNPKG

@babylonjs/loaders

Version:

For usage documentation please visit https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes/.

1,059 lines (1,058 loc) 117 kB
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);