Skip to content

Commit 0af8c55

Browse files
authored
DumpData: Refactor for glTF export in BN (#17365)
This PR adds the glue code to enable DumpData usage in BN with the glTF exporter. Changes: - Factor out core image encoding to a separate function matching NativeEncoding's own exposed function name. This follows the current nativeOverride pattern for module-level functions (see the MathHelper class.) - Remove the `bitmaprenderer` path. I initially planned for Native to polyfill this, but that turned out to be not a good idea. - Fix case in glTF exporter where exporting images to a specific format could lead to invalid images if the browser or platform didn't support it.
1 parent 2c784ce commit 0af8c55

File tree

5 files changed

+116
-100
lines changed

5 files changed

+116
-100
lines changed

packages/dev/core/src/Misc/decorators.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ declare const _native: any;
114114
export function nativeOverride<T extends (...params: any[]) => boolean>(
115115
target: any,
116116
propertyKey: string,
117-
descriptor: TypedPropertyDescriptor<(...params: Parameters<T>) => unknown>,
117+
descriptor: TypedPropertyDescriptor<(...params: Parameters<T>) => any>,
118118
predicate?: T
119119
) {
120120
// Cache the original JS function for later.

packages/dev/core/src/Misc/dumpTools.ts

Lines changed: 69 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { Clamp } from "../Maths/math.scalar.functions";
99
import type { AbstractEngine } from "../Engines/abstractEngine";
1010
import { EngineStore } from "../Engines/engineStore";
1111
import { Logger } from "./logger";
12+
import { EncodeArrayBufferToBase64 } from "./stringTools";
13+
import { nativeOverride } from "./decorators";
1214

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

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

4140
const options = {
@@ -85,6 +84,58 @@ async function _GetDumpResourcesAsync() {
8584
return await ResourcesPromise;
8685
}
8786

87+
class EncodingHelper {
88+
/**
89+
* Encodes image data to the given mime type.
90+
* This is put into a helper class so we can apply the nativeOverride decorator to it.
91+
* @internal
92+
*/
93+
@nativeOverride
94+
public static async EncodeImageAsync(pixelData: ArrayBufferView, width: number, height: number, mimeType?: string, invertY?: boolean, quality?: number): Promise<Blob> {
95+
const resources = await _GetDumpResourcesAsync();
96+
97+
const dumpEngine = resources.dumpEngine;
98+
dumpEngine.engine.setSize(width, height, true);
99+
100+
// Create the image
101+
const texture = dumpEngine.engine.createRawTexture(pixelData, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST);
102+
103+
dumpEngine.renderer.setViewport();
104+
dumpEngine.renderer.applyEffectWrapper(dumpEngine.wrapper);
105+
dumpEngine.wrapper.effect._bindTexture("textureSampler", texture);
106+
dumpEngine.renderer.draw();
107+
108+
texture.dispose();
109+
110+
return await new Promise<Blob>((resolve, reject) => {
111+
Tools.ToBlob(
112+
resources.canvas,
113+
(blob) => {
114+
if (!blob) {
115+
reject(new Error("EncodeImageAsync: Failed to convert canvas to blob."));
116+
} else {
117+
resolve(blob);
118+
}
119+
},
120+
mimeType,
121+
quality
122+
);
123+
});
124+
}
125+
}
126+
127+
/**
128+
* Encodes pixel data to an image
129+
* @param pixelData 8-bit RGBA pixel data
130+
* @param width the width of the image
131+
* @param height the height of the image
132+
* @param mimeType the requested MIME type
133+
* @param invertY true to invert the image in the Y direction
134+
* @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.
135+
* @returns a promise that resolves to the encoded image data. Note that the `blob.type` may differ from `mimeType` if it was not supported.
136+
*/
137+
export const EncodeImageAsync = EncodingHelper.EncodeImageAsync;
138+
88139
/**
89140
* Dumps the current bound framebuffer
90141
* @param width defines the rendering width
@@ -168,63 +219,23 @@ export async function DumpDataAsync(
168219
data = data2;
169220
}
170221

171-
const resources = await _GetDumpResourcesAsync();
222+
const blob = await EncodingHelper.EncodeImageAsync(data, width, height, mimeType, invertY, quality);
172223

173-
// Keep the async render + read from the shared canvas atomic
174-
// eslint-disable-next-line no-async-promise-executor
175-
return await new Promise<string | ArrayBuffer>(async (resolve) => {
176-
if (resources.dumpEngine) {
177-
const dumpEngine = resources.dumpEngine;
178-
dumpEngine.engine.setSize(width, height, true);
179-
180-
// Create the image
181-
const texture = dumpEngine.engine.createRawTexture(data, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST);
182-
183-
dumpEngine.renderer.setViewport();
184-
dumpEngine.renderer.applyEffectWrapper(dumpEngine.wrapper);
185-
dumpEngine.wrapper.effect._bindTexture("textureSampler", texture);
186-
dumpEngine.renderer.draw();
224+
if (fileName !== undefined) {
225+
Tools.DownloadBlob(blob, fileName);
226+
}
187227

188-
texture.dispose();
189-
} else {
190-
const ctx = resources.canvas.getContext("bitmaprenderer") as ImageBitmapRenderingContext;
191-
resources.canvas.width = width;
192-
resources.canvas.height = height;
228+
if (blob.type !== mimeType) {
229+
Logger.Warn(`DumpData: The requested mimeType '${mimeType}' is not supported. The result has mimeType '${blob.type}' instead.`);
230+
}
193231

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

198-
ctx.transferFromImageBitmap(imageBitmap);
199-
}
234+
if (toArrayBuffer) {
235+
return buffer;
236+
}
200237

201-
Tools.ToBlob(
202-
resources.canvas,
203-
(blob) => {
204-
if (!blob) {
205-
throw new Error("DumpData: Failed to convert canvas to blob.");
206-
}
207-
208-
if (fileName !== undefined) {
209-
Tools.DownloadBlob(blob, fileName);
210-
}
211-
212-
const fileReader = new FileReader();
213-
fileReader.onload = (event: any) => {
214-
const result = event.target!.result as string | ArrayBuffer;
215-
resolve(result);
216-
};
217-
218-
if (toArrayBuffer) {
219-
fileReader.readAsArrayBuffer(blob);
220-
} else {
221-
fileReader.readAsDataURL(blob);
222-
}
223-
},
224-
mimeType,
225-
quality
226-
);
227-
});
238+
return `data:${mimeType};base64,${EncodeArrayBufferToBase64(buffer)}`;
228239
}
229240

230241
/**

packages/dev/core/src/Misc/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ export * from "./snapshotRenderingHelper";
7070
// eslint-disable-next-line import/export
7171
export * from "./observableCoroutine";
7272
export * from "./copyTextureToTexture";
73-
/** @deprecated Use individual exports */
74-
export { DumpTools } from "./dumpTools";
73+
export { DumpTools, EncodeImageAsync } from "./dumpTools";
7574
export * from "./greasedLineTools";
7675
export * from "./equirectangularCapture";
7776
export * from "./decorators.serialization";

packages/dev/serializers/src/glTF/2.0/glTFExporter.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {
1515
ITextureInfo,
1616
ISkin,
1717
ICamera,
18-
ImageMimeType,
1918
} from "babylonjs-gltf2interface";
2019
import { AccessorComponentType, AccessorType, CameraType } from "babylonjs-gltf2interface";
2120
import type { FloatArray, IndicesArray, Nullable } from "core/types";
@@ -243,7 +242,7 @@ export class GLTFExporter {
243242
public readonly _textures: ITexture[] = [];
244243

245244
public readonly _babylonScene: Scene;
246-
public readonly _imageData: { [fileName: string]: { data: ArrayBuffer; mimeType: ImageMimeType } } = {};
245+
public readonly _imageData: { [fileName: string]: Blob } = {};
247246

248247
/**
249248
* Baked animation sample rate
@@ -561,7 +560,7 @@ export class GLTFExporter {
561560

562561
if (this._imageData) {
563562
for (const image in this._imageData) {
564-
container.files[image] = new Blob([this._imageData[image].data], { type: this._imageData[image].mimeType });
563+
container.files[image] = this._imageData[image];
565564
}
566565
}
567566

0 commit comments

Comments
 (0)