diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 9dc6a59c54c..8bfad77f17b 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -22,6 +22,7 @@ import { RuntimeError, ErrorCodes } from "../Misc/error"; import type { ISpriteManager } from "../Sprites/spriteManager"; import { RandomGUID } from "../Misc/guid"; import { AbstractEngine } from "../Engines/abstractEngine"; +import { _FetchAsync } from "core/Misc/webRequest.fetch"; /** * Type used for the success callback of ImportMesh @@ -111,6 +112,7 @@ export interface ISceneLoaderPluginExtensions { */ readonly [extension: string]: { readonly isBinary: boolean; + readonly mimeType?: string; }; } @@ -333,6 +335,7 @@ interface IRegisteredPlugin { * Defines if the plugin supports binary data */ isBinary: boolean; + mimeType?: string; } function isFactory(pluginOrFactory: IRegisteredPlugin["plugin"]): pluginOrFactory is ISceneLoaderPluginFactory { @@ -482,7 +485,17 @@ function getDefaultPlugin(): IRegisteredPlugin | undefined { return registeredPlugins[".babylon"]; } -function getPluginForExtension(extension: string): IRegisteredPlugin | undefined { +function getPluginForMimeType(mimeType: string): IRegisteredPlugin | undefined { + for (const registeredPluginKey in registeredPlugins) { + const registeredPlugin = registeredPlugins[registeredPluginKey]; + if (registeredPlugin.mimeType === mimeType) { + return registeredPlugin; + } + } + return undefined; +} + +function getPluginForExtension(extension: string, returnDefault: boolean): IRegisteredPlugin | undefined { const registeredPlugin = registeredPlugins[extension]; if (registeredPlugin) { return registeredPlugin; @@ -492,7 +505,7 @@ function getPluginForExtension(extension: string): IRegisteredPlugin | undefined extension + " files. Trying to use .babylon default plugin. To load from a specific filetype (eg. gltf) see: https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes" ); - return getDefaultPlugin(); + return returnDefault ? getDefaultPlugin() : undefined; } function isPluginForExtensionAvailable(extension: string): boolean { @@ -511,7 +524,7 @@ function getPluginForDirectLoad(data: string): IRegisteredPlugin | undefined { return getDefaultPlugin(); } -function getPluginForFilename(sceneFilename: string): IRegisteredPlugin | undefined { +function getFilenameExtension(sceneFilename: string): string { const queryStringPosition = sceneFilename.indexOf("?"); if (queryStringPosition !== -1) { @@ -520,8 +533,7 @@ function getPluginForFilename(sceneFilename: string): IRegisteredPlugin | undefi const dotPosition = sceneFilename.lastIndexOf("."); - const extension = sceneFilename.substring(dotPosition, sceneFilename.length).toLowerCase(); - return getPluginForExtension(extension); + return sceneFilename.substring(dotPosition, sceneFilename.length).toLowerCase(); } function getDirectLoad(sceneFilename: string): Nullable { @@ -545,7 +557,7 @@ function formatErrorMessage(fileInfo: IFileInfo, message?: string, exception?: a return errorMessage; } -function loadData( +async function loadDataAsync( fileInfo: IFileInfo, scene: Scene, onSuccess: (plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync, data: unknown, responseURL?: string) => void, @@ -555,7 +567,7 @@ function loadData( pluginExtension: Nullable, name: string, pluginOptions: PluginOptions -): Nullable { +): Promise> { const directLoad = getDirectLoad(fileInfo.url); if (fileInfo.rawData && !pluginExtension) { @@ -563,7 +575,28 @@ function loadData( throw "When using ArrayBufferView to load data the file extension must be provided."; } - const registeredPlugin = pluginExtension ? getPluginForExtension(pluginExtension) : directLoad ? getPluginForDirectLoad(fileInfo.url) : getPluginForFilename(fileInfo.url); + const fileExtension = !directLoad && !pluginExtension ? getFilenameExtension(fileInfo.url) : ""; + + let registeredPlugin = pluginExtension + ? getPluginForExtension(pluginExtension, true) + : directLoad + ? getPluginForDirectLoad(fileInfo.url) + : getPluginForExtension(fileExtension, false); + + if (!registeredPlugin && fileExtension) { + if (fileInfo.url && !fileInfo.url.startsWith("blob:")) { + // Fetching head content to get the mime type + const response = await _FetchAsync(fileInfo.url, { method: "HEAD", responseHeaders: ["Content-Type"] }); + const mimeType = response.headerValues ? response.headerValues["Content-Type"] : ""; + if (mimeType) { + registeredPlugin = getPluginForMimeType(mimeType); + } + } + + if (!registeredPlugin) { + registeredPlugin = getDefaultPlugin(); + } + } if (!registeredPlugin) { throw new Error(`No plugin or fallback for ${pluginExtension ?? fileInfo.url}`); @@ -582,8 +615,8 @@ function loadData( // For plugin factories, the plugin is instantiated on each SceneLoader operation. This makes options handling // much simpler as we can just pass the options to the factory, rather than passing options through to every possible // plugin call. Given this, options are only supported for plugins that provide a factory function. - if (isFactory(registeredPlugin.plugin)) { - const pluginFactory = registeredPlugin.plugin; + if (isFactory(registeredPlugin!.plugin)) { + const pluginFactory = registeredPlugin!.plugin; const partialPlugin = pluginFactory.createPlugin(pluginOptions ?? {}); if (partialPlugin instanceof Promise) { partialPlugin.then(callback).catch((error) => { @@ -597,8 +630,8 @@ function loadData( return partialPlugin; } } else { - callback(registeredPlugin.plugin); - return registeredPlugin.plugin; + callback(registeredPlugin!.plugin); + return registeredPlugin!.plugin; } }; @@ -632,7 +665,7 @@ function loadData( return; } - const useArrayBuffer = registeredPlugin.isBinary; + const useArrayBuffer = registeredPlugin!.isBinary; const dataCallback = (data: unknown, responseURL?: string) => { if (scene.isDisposed) { @@ -699,7 +732,7 @@ function loadData( }); } -function getFileInfo(rootUrl: string, sceneSource: SceneSource): Nullable { +function _getFileInfo(rootUrl: string, sceneSource: SceneSource): Nullable { let url: string; let name: string; let file: Nullable = null; @@ -761,12 +794,13 @@ export function registerSceneLoaderPlugin(plugin: ISceneLoaderPlugin | ISceneLoa registeredPlugins[extension.toLowerCase()] = { plugin: plugin, isBinary: extensions[extension].isBinary, + mimeType: extensions[extension].mimeType, }; }); } } -function importMesh( +async function importMeshAsync( meshNames: string | readonly string[] | null | undefined, rootUrl: string, sceneFilename: SceneSource = "", @@ -777,13 +811,13 @@ function importMesh( pluginExtension: Nullable = null, name = "", pluginOptions: PluginOptions = {} -): Nullable { +): Promise> { if (!scene) { Logger.Error("No scene available to import mesh to"); return null; } - const fileInfo = getFileInfo(rootUrl, sceneFilename); + const fileInfo = _getFileInfo(rootUrl, sceneFilename); if (!fileInfo) { return null; } @@ -832,7 +866,7 @@ function importMesh( scene.removePendingData(loadingToken); }; - return loadData( + return await loadDataAsync( fileInfo, scene, (plugin, data, responseURL) => { @@ -894,7 +928,7 @@ function importMeshAsyncCore( pluginOptions?: PluginOptions ): Promise { return new Promise((resolve, reject) => { - importMesh( + importMeshAsync( meshNames, rootUrl, sceneFilename, @@ -932,13 +966,13 @@ function loadScene( pluginExtension: Nullable = null, name = "", pluginOptions: PluginOptions = {} -): Nullable { +): void { if (!engine) { Tools.Error("No engine available"); - return null; + return; } - return append(rootUrl, sceneFilename, new Scene(engine), onSuccess, onProgress, onError, pluginExtension, name, pluginOptions); + appendAsync(rootUrl, sceneFilename, new Scene(engine), onSuccess, onProgress, onError, pluginExtension, name, pluginOptions); } /** @@ -982,7 +1016,7 @@ function loadSceneAsyncCore( }); } -function append( +async function appendAsync( rootUrl: string, sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, @@ -992,13 +1026,13 @@ function append( pluginExtension: Nullable = null, name = "", pluginOptions: PluginOptions = {} -): Nullable { +): Promise> { if (!scene) { Logger.Error("No scene available to append to"); return null; } - const fileInfo = getFileInfo(rootUrl, sceneFilename); + const fileInfo = _getFileInfo(rootUrl, sceneFilename); if (!fileInfo) { return null; } @@ -1054,7 +1088,7 @@ function append( scene.removePendingData(loadingToken); }; - return loadData( + return await loadDataAsync( fileInfo, scene, (plugin, data) => { @@ -1110,7 +1144,7 @@ function appendSceneAsyncCore( pluginOptions?: PluginOptions ): Promise { return new Promise((resolve, reject) => { - append( + appendAsync( rootUrl, sceneFilename, scene, @@ -1128,7 +1162,7 @@ function appendSceneAsyncCore( }); } -function loadAssetContainer( +async function loadAssetContainerCoreAsync( rootUrl: string, sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, @@ -1138,13 +1172,13 @@ function loadAssetContainer( pluginExtension: Nullable = null, name = "", pluginOptions: PluginOptions = {} -): Nullable { +): Promise> { if (!scene) { Logger.Error("No scene available to load asset container to"); return null; } - const fileInfo = getFileInfo(rootUrl, sceneFilename); + const fileInfo = _getFileInfo(rootUrl, sceneFilename); if (!fileInfo) { return null; } @@ -1191,7 +1225,7 @@ function loadAssetContainer( scene.removePendingData(loadingToken); }; - return loadData( + return await loadDataAsync( fileInfo, scene, (plugin, data) => { @@ -1239,10 +1273,10 @@ function loadAssetContainer( */ export function loadAssetContainerAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise { const { rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = options ?? {}; - return loadAssetContainerAsyncCore(rootUrl, source, scene, onProgress, pluginExtension, name, pluginOptions); + return internalLoadAssetContainerAsync(rootUrl, source, scene, onProgress, pluginExtension, name, pluginOptions); } -function loadAssetContainerAsyncCore( +function internalLoadAssetContainerAsync( rootUrl: string, sceneFilename?: SceneSource, scene?: Nullable, @@ -1252,7 +1286,7 @@ function loadAssetContainerAsyncCore( pluginOptions?: PluginOptions ): Promise { return new Promise((resolve, reject) => { - loadAssetContainer( + loadAssetContainerCoreAsync( rootUrl, sceneFilename, scene, @@ -1345,7 +1379,7 @@ function importAnimations( } }; - loadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, name, pluginOptions); + loadAssetContainerCoreAsync(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, name, pluginOptions); } /** @@ -1489,7 +1523,7 @@ export class SceneLoader { * @returns a plugin or null if none works */ public static GetPluginForExtension(extension: string): ISceneLoaderPlugin | ISceneLoaderPluginAsync | ISceneLoaderPluginFactory | undefined { - return getPluginForExtension(extension)?.plugin; + return getPluginForExtension(extension, true)?.plugin; } /** @@ -1520,7 +1554,7 @@ export class SceneLoader { * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin * @param name defines the name of the file, if the data is binary - * @returns The loaded plugin + * @deprecated Please use ImportMeshAsync instead */ public static ImportMesh( meshNames: string | readonly string[] | null | undefined, @@ -1532,8 +1566,8 @@ export class SceneLoader { onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, name?: string - ): Nullable { - return importMesh(meshNames, rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); + ): void { + importMeshAsync(meshNames, rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } /** @@ -1569,7 +1603,7 @@ export class SceneLoader { * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin * @param name defines the filename, if the data is binary - * @returns The loaded plugin + * @deprecated Please use LoadAsync instead */ public static Load( rootUrl: string, @@ -1580,8 +1614,8 @@ export class SceneLoader { onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, name?: string - ): Nullable { - return loadScene(rootUrl, sceneFilename, engine, onSuccess, onProgress, onError, pluginExtension, name); + ) { + loadScene(rootUrl, sceneFilename, engine, onSuccess, onProgress, onError, pluginExtension, name); } /** @@ -1615,7 +1649,7 @@ export class SceneLoader { * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin * @param name defines the name of the file, if the data is binary - * @returns The loaded plugin + * @deprecated Please use AppendAsync instead */ public static Append( rootUrl: string, @@ -1626,8 +1660,8 @@ export class SceneLoader { onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, name?: string - ): Nullable { - return append(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); + ) { + appendAsync(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } /** @@ -1661,7 +1695,7 @@ export class SceneLoader { * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin * @param name defines the filename, if the data is binary - * @returns The loaded plugin + * @deprecated Please use LoadAssetContainerAsync instead */ public static LoadAssetContainer( rootUrl: string, @@ -1672,8 +1706,8 @@ export class SceneLoader { onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, name?: string - ): Nullable { - return loadAssetContainer(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); + ) { + loadAssetContainerCoreAsync(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } /** @@ -1694,7 +1728,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Promise { - return loadAssetContainerAsyncCore(rootUrl, sceneFilename, scene, onProgress, pluginExtension, name); + return internalLoadAssetContainerAsync(rootUrl, sceneFilename, scene, onProgress, pluginExtension, name); } /** @@ -1710,6 +1744,7 @@ export class SceneLoader { * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin * @param name defines the filename, if the data is binary + * @deprecated Please use ImportAnimationsAsync instead */ public static ImportAnimations( rootUrl: string, diff --git a/packages/dev/core/src/Misc/webRequest.fetch.ts b/packages/dev/core/src/Misc/webRequest.fetch.ts new file mode 100644 index 00000000000..db458ee0b28 --- /dev/null +++ b/packages/dev/core/src/Misc/webRequest.fetch.ts @@ -0,0 +1,37 @@ +import { WebRequest } from "./webRequest"; + +/** + * Fetches a resource from the network + * @param url defines the url to fetch the resource from + * @param options defines the options to use when fetching the resource + * @returns a promise that resolves when the resource is fetched + * @internal + */ +export function _FetchAsync( + url: string, + options: Partial<{ method: string; responseHeaders?: string[] }> +): Promise<{ response: Response; headerValues: { [key: string]: string } }> { + const method = options.method || "GET"; + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const headerValues: { [key: string]: string } = {}; + if (options.responseHeaders) { + for (const header of options.responseHeaders) { + headerValues[header] = request.getResponseHeader(header) || ""; + } + } + + resolve({ response: request.response, headerValues: headerValues }); + } else { + reject(`Unable to fetch data from ${url}. Error code: ${request.status}`); + } + } + }); + + request.open(method, url); + request.send(); + }); +} diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.metadata.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.metadata.ts index 2c7dcbcc4cd..873165bf759 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.metadata.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.metadata.ts @@ -8,9 +8,9 @@ export const GLTFFileLoaderMetadata = { extensions: { // eslint-disable-next-line @typescript-eslint/naming-convention - ".gltf": { isBinary: false }, + ".gltf": { isBinary: false, mimeType: "model/gltf+json" }, // eslint-disable-next-line @typescript-eslint/naming-convention - ".glb": { isBinary: true }, + ".glb": { isBinary: true, mimeType: "model/gltf-binary" }, } as const satisfies ISceneLoaderPluginExtensions, canDirectLoad(data: string): boolean { diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index 02fda235916..56b7d793319 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -424,7 +424,7 @@ }, { "title": "Chibi Rex", - "playgroundId": "#NZ2RZY#3", + "playgroundId": "#NZ2RZY#4", "renderCount": 50, "referenceImage": "chibi-rex.png" }, @@ -1622,7 +1622,7 @@ }, { "title": "GLTF ext MSFT_LOD", - "playgroundId": "#2YZFA0#234", + "playgroundId": "#2YZFA0#422", "referenceImage": "gltfMSFTLOD.png" }, { diff --git a/packages/tools/viewer/src/loader/modelLoader.ts b/packages/tools/viewer/src/loader/modelLoader.ts index f364866ceea..0bc637973b8 100644 --- a/packages/tools/viewer/src/loader/modelLoader.ts +++ b/packages/tools/viewer/src/loader/modelLoader.ts @@ -101,7 +101,55 @@ export class ModelLoader { const scene = model.rootMesh.getScene(); - model.loader = SceneLoader.ImportMesh( + SceneLoader.OnPluginActivatedObservable.addOnce((plugin) => { + model.loader = plugin; + if (model.loader.name === "gltf") { + const gltfLoader = model.loader; + gltfLoader.animationStartMode = GLTFLoaderAnimationStartMode.NONE; + gltfLoader.compileMaterials = true; + + if (!modelConfiguration.file) { + gltfLoader.rewriteRootURL = (rootURL, responseURL) => { + return modelConfiguration.root || Tools.GetFolderPath(responseURL || modelConfiguration.url || ""); + }; + } + // if ground is set to "mirror": + if ( + this._configurationContainer && + this._configurationContainer.configuration && + this._configurationContainer.configuration.ground && + typeof this._configurationContainer.configuration.ground === "object" && + this._configurationContainer.configuration.ground.mirror + ) { + gltfLoader.useClipPlane = true; + } + Object.keys(gltfLoader) + .filter((name) => name.indexOf("on") === 0 && name.indexOf("Observable") !== -1) + .forEach((functionName) => { + (gltfLoader as any)[functionName].add((payload: any[]) => { + this._checkAndRun(functionName.replace("Observable", ""), payload); + }); + }); + + gltfLoader.onParsedObservable.add((data) => { + if (data && data.json && (data.json as any)["asset"]) { + model.loadInfo = (data.json as any)["asset"]; + } + }); + + gltfLoader.onCompleteObservable.add(() => { + model.loaderDone = true; + }); + } else { + model.loaderDone = true; + } + + this._checkAndRun("onInit", model.loader, model); + + this._loaders.push(model.loader); + }); + + SceneLoader.ImportMesh( undefined, this._baseUrl, filename, @@ -136,51 +184,6 @@ export class ModelLoader { plugin )!; - if (model.loader.name === "gltf") { - const gltfLoader = model.loader; - gltfLoader.animationStartMode = GLTFLoaderAnimationStartMode.NONE; - gltfLoader.compileMaterials = true; - - if (!modelConfiguration.file) { - gltfLoader.rewriteRootURL = (rootURL, responseURL) => { - return modelConfiguration.root || Tools.GetFolderPath(responseURL || modelConfiguration.url || ""); - }; - } - // if ground is set to "mirror": - if ( - this._configurationContainer && - this._configurationContainer.configuration && - this._configurationContainer.configuration.ground && - typeof this._configurationContainer.configuration.ground === "object" && - this._configurationContainer.configuration.ground.mirror - ) { - gltfLoader.useClipPlane = true; - } - Object.keys(gltfLoader) - .filter((name) => name.indexOf("on") === 0 && name.indexOf("Observable") !== -1) - .forEach((functionName) => { - (gltfLoader as any)[functionName].add((payload: any[]) => { - this._checkAndRun(functionName.replace("Observable", ""), payload); - }); - }); - - gltfLoader.onParsedObservable.add((data) => { - if (data && data.json && (data.json as any)["asset"]) { - model.loadInfo = (data.json as any)["asset"]; - } - }); - - gltfLoader.onCompleteObservable.add(() => { - model.loaderDone = true; - }); - } else { - model.loaderDone = true; - } - - this._checkAndRun("onInit", model.loader, model); - - this._loaders.push(model.loader); - return model; }