diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 05bc3d89ec4..6527e72e448 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -15,7 +15,6 @@ import type { ITextureInfo, ISkin, ICamera, - ImageMimeType, } from "babylonjs-gltf2interface"; import { AccessorComponentType, AccessorType, CameraType } from "babylonjs-gltf2interface"; import type { FloatArray, IndicesArray, Nullable } from "core/types"; @@ -38,6 +37,7 @@ import { EngineStore } from "core/Engines/engineStore"; import type { IGLTFExporterExtensionV2 } from "./glTFExporterExtension"; import { GLTFMaterialExporter } from "./glTFMaterialExporter"; +import type { IImageData } from "./glTFMaterialExporter"; import type { IExportOptions } from "./glTFSerializer"; import { GLTFData } from "./glTFData"; import { @@ -243,7 +243,7 @@ export class GLTFExporter { public readonly _textures: ITexture[] = []; public readonly _babylonScene: Scene; - public readonly _imageData: { [fileName: string]: { data: ArrayBuffer; mimeType: ImageMimeType } } = {}; + public readonly _imageData: { [fileName: string]: IImageData } = {}; /** * Baked animation sample rate diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts index c25b565f30b..3b4a1089aa0 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts @@ -35,6 +35,17 @@ const MaxSpecularPower = 1024; const White = Color3.White() as DeepImmutable; const Black = Color3.BlackReadOnly; +/** + * Simple structure for storing image data + * @internal + */ +export interface IImageData { + /** Binary data */ + data: ArrayBuffer; + /** Media type */ + mimeType: ImageMimeType; +} + /** * Interface for storing specular glossiness factors * @internal @@ -52,8 +63,8 @@ interface IPBRMetallicRoughness { baseColor: Color3; metallic: Nullable; roughness: Nullable; - metallicRoughnessTextureData?: Nullable; - baseColorTextureData?: Nullable; + metallicRoughnessTextureData?: Nullable; + baseColorTextureData?: Nullable; } function GetFileExtensionFromMimeType(mimeType: ImageMimeType): string { @@ -71,12 +82,29 @@ function GetFileExtensionFromMimeType(mimeType: ImageMimeType): string { } } +/** + * @param mimeType the MIME type requested by the user + * @returns true if the given mime type is compatible with glTF + */ +function IsSupportedMimeType(mimeType?: string): mimeType is ImageMimeType { + switch (mimeType) { + case ImageMimeType.JPEG: + case ImageMimeType.PNG: + case ImageMimeType.WEBP: + case ImageMimeType.AVIF: + case ImageMimeType.KTX2: + return true; + default: + return false; + } +} + /** * Gets cached image from a texture, if available. * @param babylonTexture texture to check for cached image * @returns image data if found and directly usable; null otherwise */ -async function GetCachedImageAsync(babylonTexture: BaseTexture): Promise> { +async function GetCachedImageAsync(babylonTexture: BaseTexture): Promise> { const internalTexture = babylonTexture.getInternalTexture(); if (!internalTexture || internalTexture.source !== InternalTextureSource.Url) { return null; @@ -108,7 +136,7 @@ async function GetCachedImageAsync(babylonTexture: BaseTexture): Promise { - return await DumpTools.DumpDataAsync(width, height, buffer, mimeType, undefined, false, true); + /** + * Gets image data from a pixel buffer. + * NOTE: The returned mime type is NOT guaranteed to be the requested mime type. + * @internal + */ + private async _getImageDataAsync(buffer: Uint8Array, width: number, height: number, mimeType: ImageMimeType = ImageMimeType.PNG): Promise { + try { + return { + data: await DumpTools.DumpDataAsync(width, height, buffer, mimeType, undefined, false, true), + mimeType, + }; + } catch (error) { + // It's possible that the requested format isn't supported in this environment, so retry with PNG + if (mimeType !== ImageMimeType.PNG) { + Tools.Warn(`Failed to encode to ${mimeType}. Retrying with PNG.`); + return { + data: await DumpTools.DumpDataAsync(width, height, buffer, ImageMimeType.PNG, undefined, false, true), + mimeType: ImageMimeType.PNG, + }; + } + // Re-throw the error if we were already trying PNG + throw error; + } } /** @@ -370,14 +419,12 @@ export class GLTFMaterialExporter { * @param diffuseTexture texture used to store diffuse information * @param specularGlossinessTexture texture used to store specular and glossiness information * @param factors specular glossiness material factors - * @param mimeType the mime type to use for the texture * @returns pbr metallic roughness interface or null */ private async _convertSpecularGlossinessTexturesToMetallicRoughnessAsync( diffuseTexture: Nullable, specularGlossinessTexture: Nullable, - factors: IPBRSpecularGlossiness, - mimeType: ImageMimeType + factors: IPBRSpecularGlossiness ): Promise { const promises = new Array>(); if (!(diffuseTexture || specularGlossinessTexture)) { @@ -502,14 +549,14 @@ export class GLTFMaterialExporter { if (writeOutMetallicRoughnessTexture) { promises.push( - this._getImageDataAsync(metallicRoughnessBuffer, width, height, mimeType).then((data) => { + this._getImageDataAsync(metallicRoughnessBuffer, width, height).then((data) => { metallicRoughnessFactors.metallicRoughnessTextureData = data; }) ); } if (writeOutBaseColorTexture) { promises.push( - this._getImageDataAsync(baseColorBuffer, width, height, mimeType).then((data) => { + this._getImageDataAsync(baseColorBuffer, width, height).then((data) => { metallicRoughnessFactors.baseColorTextureData = data; }) ); @@ -816,7 +863,6 @@ export class GLTFMaterialExporter { pbrMetallicRoughness: IMaterialPbrMetallicRoughness, hasUVs: boolean ): Promise { - const mimeType = ImageMimeType.PNG; const specGloss: IPBRSpecularGlossiness = { diffuseColor: babylonPBRMaterial._albedoColor, specularColor: babylonPBRMaterial._reflectivityColor, @@ -834,17 +880,17 @@ export class GLTFMaterialExporter { this._exporter._materialNeedsUVsSet.add(babylonPBRMaterial); const samplerIndex = this._exportTextureSampler(albedoTexture || reflectivityTexture); - const metallicRoughnessFactors = await this._convertSpecularGlossinessTexturesToMetallicRoughnessAsync(albedoTexture, reflectivityTexture, specGloss, mimeType); + const metallicRoughnessFactors = await this._convertSpecularGlossinessTexturesToMetallicRoughnessAsync(albedoTexture, reflectivityTexture, specGloss); const textures = this._exporter._textures; if (metallicRoughnessFactors.baseColorTextureData) { - const imageIndex = this._exportImage(`baseColor${textures.length}`, mimeType, metallicRoughnessFactors.baseColorTextureData); + const imageIndex = this._exportImage(`baseColor${textures.length}`, metallicRoughnessFactors.baseColorTextureData); pbrMetallicRoughness.baseColorTexture = this._exportTextureInfo(imageIndex, samplerIndex, albedoTexture?.coordinatesIndex); } if (metallicRoughnessFactors.metallicRoughnessTextureData) { - const imageIndex = this._exportImage(`metallicRoughness${textures.length}`, mimeType, metallicRoughnessFactors.metallicRoughnessTextureData); + const imageIndex = this._exportImage(`metallicRoughness${textures.length}`, metallicRoughnessFactors.metallicRoughnessTextureData); pbrMetallicRoughness.metallicRoughnessTexture = this._exportTextureInfo(imageIndex, samplerIndex, reflectivityTexture?.coordinatesIndex); } @@ -1061,29 +1107,25 @@ export class GLTFMaterialExporter { // Try to get the image from memory first, if applicable const cache = await GetCachedImageAsync(babylonTexture); if (cache && (requestedMimeType === "none" || cache.mimeType === requestedMimeType)) { - return this._exportImage(babylonTexture.name, cache.mimeType as ImageMimeType, cache.data); + return this._exportImage(babylonTexture.name, cache); } // Preserve texture mime type if defined let mimeType = ImageMimeType.PNG; if (requestedMimeType !== "none") { - switch (requestedMimeType) { - case ImageMimeType.JPEG: - case ImageMimeType.PNG: - case ImageMimeType.WEBP: - mimeType = requestedMimeType; - break; - default: - Tools.Warn(`Unsupported media type: ${requestedMimeType}. Exporting texture as PNG.`); - break; + if (IsSupportedMimeType(requestedMimeType)) { + mimeType = requestedMimeType; + } else { + mimeType = ImageMimeType.PNG; + Tools.Warn(`Unsupported media type: ${requestedMimeType}. Exporting texture as PNG.`); } } const size = babylonTexture.getSize(); const pixels = await GetTextureDataAsync(babylonTexture); - const data = await this._getImageDataAsync(pixels, size.width, size.height, mimeType); + const imageData = await this._getImageDataAsync(pixels, size.width, size.height, mimeType); - return this._exportImage(babylonTexture.name, mimeType, data); + return this._exportImage(babylonTexture.name, imageData); })(); internalTextureToImage[internalTextureUniqueId][requestedMimeType] = imageIndexPromise; @@ -1092,22 +1134,22 @@ export class GLTFMaterialExporter { return await imageIndexPromise; } - private _exportImage(name: string, mimeType: ImageMimeType, data: ArrayBuffer): number { + private _exportImage(name: string, imageData: IImageData): number { const images = this._exporter._images; let image: IImage; if (this._exporter._shouldUseGlb) { image = { name: name, - mimeType: mimeType, + mimeType: imageData.mimeType, bufferView: undefined, // Will be updated later by BufferManager }; - const bufferView = this._exporter._bufferManager.createBufferView(new Uint8Array(data)); + const bufferView = this._exporter._bufferManager.createBufferView(new Uint8Array(imageData.data)); this._exporter._bufferManager.setBufferView(image, bufferView); } else { // Build a unique URI const baseName = name.replace(/\.\/|\/|\.\\|\\/g, "_"); - const extension = GetFileExtensionFromMimeType(mimeType); + const extension = GetFileExtensionFromMimeType(imageData.mimeType); let fileName = baseName + extension; if (images.some((image) => image.uri === fileName)) { fileName = `${baseName}_${Tools.RandomId()}${extension}`; @@ -1117,7 +1159,7 @@ export class GLTFMaterialExporter { name: name, uri: fileName, }; - this._exporter._imageData[fileName] = { data: data, mimeType: mimeType }; // Save image data to be written to file later + this._exporter._imageData[fileName] = imageData; // Save image data to be written to file later } images.push(image);