Skip to content
2 changes: 1 addition & 1 deletion packages/dev/core/src/Misc/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ declare const _native: any;
export function nativeOverride<T extends (...params: any[]) => boolean>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<(...params: Parameters<T>) => unknown>,
descriptor: TypedPropertyDescriptor<(...params: Parameters<T>) => any>,
predicate?: T
) {
// Cache the original JS function for later.
Expand Down
128 changes: 70 additions & 58 deletions packages/dev/core/src/Misc/dumpTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { Clamp } from "../Maths/math.scalar.functions";
import type { AbstractEngine } from "../Engines/abstractEngine";
import { EngineStore } from "../Engines/engineStore";
import { Logger } from "./logger";
import { EncodeArrayBufferToBase64 } from "./stringTools";
import { nativeOverride } from "./decorators";

type DumpResources = {
canvas: HTMLCanvasElement | OffscreenCanvas;
dumpEngine?: {
dumpEngine: {
engine: ThinEngine;
renderer: EffectRenderer;
wrapper: EffectWrapper;
Expand All @@ -28,14 +30,11 @@ async function _CreateDumpResourcesAsync(): Promise<DumpResources> {
Logger.Warn("DumpData: OffscreenCanvas will be used for dumping data. This may result in lossy alpha values.");
}

// If WebGL via ThinEngine is not available (e.g. Native), use the BitmapRenderer.
// If WebGL via ThinEngine is not available, we cannot encode the data.
// If https://github.com/whatwg/html/issues/10142 is resolved, we can migrate to just BitmapRenderer and avoid an engine dependency altogether.
const { ThinEngine: thinEngineClass } = await import("../Engines/thinEngine");
if (!thinEngineClass.IsSupported) {
if (!canvas.getContext("bitmaprenderer")) {
throw new Error("DumpData: No WebGL or bitmap rendering context available. Cannot dump data.");
}
return { canvas };
throw new Error("DumpData: No WebGL context available. Cannot dump data.");
}

const options = {
Expand Down Expand Up @@ -85,6 +84,59 @@ async function _GetDumpResourcesAsync() {
return await ResourcesPromise;
}

class EncodingHelper {
/**
* Encodes image data to the given mime type.
* This is put into a helper class so we can apply the nativeOverride decorator to it.
* @internal
*/
@nativeOverride
public static async EncodeImageAsync(pixelData: ArrayBufferView, width: number, height: number, mimeType?: string, invertY?: boolean, quality?: number): Promise<Blob> {
const resources = await _GetDumpResourcesAsync();

// TODO: No need to promisify this whole thing
return await new Promise<Blob>((resolve, reject) => {
const dumpEngine = resources.dumpEngine;
dumpEngine.engine.setSize(width, height, true);

// Create the image
const texture = dumpEngine.engine.createRawTexture(pixelData, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST);

dumpEngine.renderer.setViewport();
dumpEngine.renderer.applyEffectWrapper(dumpEngine.wrapper);
dumpEngine.wrapper.effect._bindTexture("textureSampler", texture);
dumpEngine.renderer.draw();

texture.dispose();

Tools.ToBlob(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a ToBlobAsync that is promise returning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. It wouldn't be as simple as replacing with ToBlob with ToBlobAsync, though, as the canvas draw + toBlob must be atomic. Guess we could use an AsyncLock?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean. We can chat, or if you want to just leave it as is that's ok too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synced offline. My previous comment was probably incorrect, and we should be able to directly replace with something like ToBlobAsync.

My question is-- where to put it? ATM, ToBlob exists as a static method in Tools.

Maybe this can be put off for another PR.

resources.canvas,
(blob) => {
if (!blob) {
reject(new Error("EncodeImageAsync: Failed to convert canvas to blob."));
} else {
resolve(blob);
}
},
mimeType,
quality
);
});
}
}

/**
* Encodes pixel data to an image
* @param pixelData 8-bit RGBA pixel data
* @param width the width of the image
* @param height the height of the image
* @param mimeType the requested MIME type
* @param invertY true to invert the image in the Y direction
* @param quality the quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter.
* @returns a promise that resolves to the encoded image data. Note that the `blob.type` may differ from `mimeType` if it was not supported.
*/
export const EncodeImageAsync = EncodingHelper.EncodeImageAsync;

/**
* Dumps the current bound framebuffer
* @param width defines the rendering width
Expand Down Expand Up @@ -168,63 +220,23 @@ export async function DumpDataAsync(
data = data2;
}

const resources = await _GetDumpResourcesAsync();
const blob = await EncodingHelper.EncodeImageAsync(data, width, height, mimeType, invertY, quality);

// Keep the async render + read from the shared canvas atomic
// eslint-disable-next-line no-async-promise-executor
return await new Promise<string | ArrayBuffer>(async (resolve) => {
if (resources.dumpEngine) {
const dumpEngine = resources.dumpEngine;
dumpEngine.engine.setSize(width, height, true);

// Create the image
const texture = dumpEngine.engine.createRawTexture(data, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST);

dumpEngine.renderer.setViewport();
dumpEngine.renderer.applyEffectWrapper(dumpEngine.wrapper);
dumpEngine.wrapper.effect._bindTexture("textureSampler", texture);
dumpEngine.renderer.draw();
if (fileName !== undefined) {
Tools.DownloadBlob(blob, fileName);
}

texture.dispose();
} else {
const ctx = resources.canvas.getContext("bitmaprenderer") as ImageBitmapRenderingContext;
resources.canvas.width = width;
resources.canvas.height = height;
if (blob.type !== mimeType) {
Logger.Warn(`DumpData: The requested mimeType '${mimeType}' is not supported. The result has mimeType '${blob.type}' instead.`);
}

const imageData = new ImageData(width, height); // ImageData(data, sw, sh) ctor not yet widely implemented
imageData.data.set(data as Uint8ClampedArray);
const imageBitmap = await createImageBitmap(imageData, { premultiplyAlpha: "none", imageOrientation: invertY ? "flipY" : "from-image" });
const buffer = await blob.arrayBuffer();

ctx.transferFromImageBitmap(imageBitmap);
}
if (toArrayBuffer) {
return buffer;
}

Tools.ToBlob(
resources.canvas,
(blob) => {
if (!blob) {
throw new Error("DumpData: Failed to convert canvas to blob.");
}

if (fileName !== undefined) {
Tools.DownloadBlob(blob, fileName);
}

const fileReader = new FileReader();
fileReader.onload = (event: any) => {
const result = event.target!.result as string | ArrayBuffer;
resolve(result);
};

if (toArrayBuffer) {
fileReader.readAsArrayBuffer(blob);
} else {
fileReader.readAsDataURL(blob);
}
},
mimeType,
quality
);
});
return `data:${mimeType};base64,${EncodeArrayBufferToBase64(buffer)}`;
}

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/dev/core/src/Misc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ export * from "./snapshotRenderingHelper";
// eslint-disable-next-line import/export
export * from "./observableCoroutine";
export * from "./copyTextureToTexture";
/** @deprecated Use individual exports */
export { DumpTools } from "./dumpTools";
export * from "./dumpTools";
export * from "./greasedLineTools";
export * from "./equirectangularCapture";
export * from "./decorators.serialization";
Expand Down
5 changes: 2 additions & 3 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 Down Expand Up @@ -243,7 +242,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]: Blob } = {};

/**
* Baked animation sample rate
Expand Down Expand Up @@ -561,7 +560,7 @@ export class GLTFExporter {

if (this._imageData) {
for (const image in this._imageData) {
container.files[image] = new Blob([this._imageData[image].data], { type: this._imageData[image].mimeType });
container.files[image] = this._imageData[image];
}
}

Expand Down
Loading