Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/dev/serializers/src/glTF/2.0/glTFExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
106 changes: 74 additions & 32 deletions packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ const MaxSpecularPower = 1024;
const White = Color3.White() as DeepImmutable<Color3>;
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
Expand All @@ -52,8 +63,8 @@ interface IPBRMetallicRoughness {
baseColor: Color3;
metallic: Nullable<number>;
roughness: Nullable<number>;
metallicRoughnessTextureData?: Nullable<ArrayBuffer>;
baseColorTextureData?: Nullable<ArrayBuffer>;
metallicRoughnessTextureData?: Nullable<IImageData>;
baseColorTextureData?: Nullable<IImageData>;
}

function GetFileExtensionFromMimeType(mimeType: ImageMimeType): string {
Expand All @@ -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<Nullable<{ data: ArrayBuffer; mimeType: string }>> {
async function GetCachedImageAsync(babylonTexture: BaseTexture): Promise<Nullable<IImageData>> {
const internalTexture = babylonTexture.getInternalTexture();
if (!internalTexture || internalTexture.source !== InternalTextureSource.Url) {
return null;
Expand Down Expand Up @@ -108,7 +136,7 @@ async function GetCachedImageAsync(babylonTexture: BaseTexture): Promise<Nullabl
mimeType = GetMimeType(buffer.src) || mimeType;
}

if (data && mimeType) {
if (data && IsSupportedMimeType(mimeType)) {
return { data, mimeType };
}

Expand Down Expand Up @@ -321,8 +349,29 @@ export class GLTFMaterialExporter {
await this._exporter._extensionsPostExportMaterialAsync("exportMaterial", glTFMaterial, babylonMaterial);
}

private async _getImageDataAsync(buffer: Uint8Array, width: number, height: number, mimeType: ImageMimeType): Promise<ArrayBuffer> {
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<IImageData> {
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;
}
}

/**
Expand Down Expand Up @@ -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<BaseTexture>,
specularGlossinessTexture: Nullable<BaseTexture>,
factors: IPBRSpecularGlossiness,
mimeType: ImageMimeType
factors: IPBRSpecularGlossiness
): Promise<IPBRMetallicRoughness> {
const promises = new Array<Promise<void>>();
if (!(diffuseTexture || specularGlossinessTexture)) {
Expand Down Expand Up @@ -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;
})
);
Expand Down Expand Up @@ -816,7 +863,6 @@ export class GLTFMaterialExporter {
pbrMetallicRoughness: IMaterialPbrMetallicRoughness,
hasUVs: boolean
): Promise<IPBRMetallicRoughness> {
const mimeType = ImageMimeType.PNG;
const specGloss: IPBRSpecularGlossiness = {
diffuseColor: babylonPBRMaterial._albedoColor,
specularColor: babylonPBRMaterial._reflectivityColor,
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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}`;
Expand All @@ -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);
Expand Down
Loading