Skip to content

Implementation of a render image dialog #478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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: 4 additions & 0 deletions src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
return msg;
});

events.function('targetSize', () => {
return scene.targetSize;
});

events.on('scene.clear', () => {
scene.clear();
editHistory.clear();
Expand Down
121 changes: 82 additions & 39 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
import { Splat } from './splat';
import { localize } from './ui/localization';

type ImageSettings = {
width: number;
height: number;
transparentBg: boolean;
showDebug: boolean;
};

type VideoSettings = {
startFrame: number;
endFrame: number;
Expand Down Expand Up @@ -36,38 +43,72 @@
const registerRenderEvents = (scene: Scene, events: Events) => {
let compressor: PngCompressor;

events.function('render.image', async () => {
// wait for postrender to fire
const postRender = () => {
return new Promise<boolean>((resolve, reject) => {
const handle = scene.events.on('postrender', async () => {

Check failure on line 49 in src/render.ts

View workflow job for this annotation

GitHub Actions / Lint (18.x)

Async arrow function has no 'await' expression
handle.off();
try {
resolve(true);
} catch (error) {
reject(error);
}
});
});
};

events.function('render.image', async (imageSettings: ImageSettings) => {
events.fire('startSpinner');

try {
const renderTarget = scene.camera.entity.camera.renderTarget;
const texture = renderTarget.colorBuffer;
const data = new Uint8Array(texture.width * texture.height * 4);

await texture.read(0, 0, texture.width, texture.height, { renderTarget, data });
const { width, height, transparentBg, showDebug } = imageSettings;

// construct the png compressor
if (!compressor) {
compressor = new PngCompressor();
// start rendering to offscreen buffer only
scene.camera.startOffscreenMode(width, height);
scene.camera.renderOverlays = showDebug;
if (!transparentBg) {
scene.camera.entity.camera.clearColor.copy(events.invoke('bgClr'));
}

// @ts-ignore
const pixels = new Uint8ClampedArray(data.buffer);
// render the next frame
scene.forceRender = true;

// for render to finish
await postRender();

// cpu-side buffer to read pixels into
const data = new Uint8Array(width * height * 4);

const { renderTarget } = scene.camera.entity.camera;
const { colorBuffer } = renderTarget;

// read the rendered frame
await colorBuffer.read(0, 0, width, height, { renderTarget, data });

// the render buffer contains premultiplied alpha. so apply background color.
const { r, g, b } = events.invoke('bgClr');
for (let i = 0; i < pixels.length; i += 4) {
const a = 255 - pixels[i + 3];
pixels[i + 0] += r * a;
pixels[i + 1] += g * a;
pixels[i + 2] += b * a;
pixels[i + 3] = 255;
if (!transparentBg) {
// @ts-ignore
const pixels = new Uint8ClampedArray(data.buffer);

const { r, g, b } = events.invoke('bgClr');
for (let i = 0; i < pixels.length; i += 4) {
const a = 255 - pixels[i + 3];
pixels[i + 0] += r * a;
pixels[i + 1] += g * a;
pixels[i + 2] += b * a;
pixels[i + 3] = 255;
}
}

// construct the png compressor
if (!compressor) {
compressor = new PngCompressor();
}

const arrayBuffer = await compressor.compress(
new Uint32Array(pixels.buffer),
texture.width,
texture.height
new Uint32Array(data.buffer),
colorBuffer.width,
colorBuffer.height
);

// construct filename
Expand All @@ -76,7 +117,19 @@

// download
downloadFile(arrayBuffer, filename);

return true;
} catch (error) {
await events.invoke('showPopup', {
type: 'error',
header: localize('render.failed'),
message: `'${error.message ?? error}'`
});
} finally {
scene.camera.endOffscreenMode();
scene.camera.renderOverlays = true;
scene.camera.entity.camera.clearColor.set(0, 0, 0, 0);

events.fire('stopSpinner');
}
});
Expand Down Expand Up @@ -131,7 +184,6 @@

// prepare the frame for rendering
const prepareFrame = async (frameTime: number) => {
// go to first frame of the animation
events.fire('timeline.time', frameTime);

// manually update the camera so position and rotation are correct
Expand Down Expand Up @@ -161,9 +213,6 @@
}, 1000);
});
}));

// render during next update
scene.lockedRender = true;
};

// capture the current video frame
Expand Down Expand Up @@ -199,23 +248,17 @@
const duration = (endFrame - startFrame) / animFrameRate;

for (let frameTime = 0; frameTime <= duration; frameTime += 1.0 / frameRate) {
const capturePromise = new Promise<boolean>((resolve, reject) => {
const handle = scene.events.on('postrender', async () => {
handle.off();
try {
await captureFrame(frameTime);
resolve(true);
} catch (error) {
reject(error);
}
});
});

// special case the first frame
await prepareFrame(startFrame + frameTime * animFrameRate);

// render a frame
scene.lockedRender = true;

// wait for render to finish
await postRender();

// wait for capture
await capturePromise;
await captureFrame(frameTime);
}

// Flush and finalize muxer
Expand Down Expand Up @@ -247,4 +290,4 @@
});
};

export { VideoSettings, registerRenderEvents };
export { ImageSettings, VideoSettings, registerRenderEvents };
13 changes: 13 additions & 0 deletions src/ui/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DataPanel } from './data-panel';
import { Events } from '../events';
import { BottomToolbar } from './bottom-toolbar';
import { ColorPanel } from './color-panel';
import { ImageSettingsDialog } from './image-settings-dialog';
import { localize, localizeInit } from './localization';
import { Menu } from './menu';
import { ModeToggle } from './mode-toggle';
Expand Down Expand Up @@ -166,12 +167,16 @@ class EditorUI {
// publish settings
const publishSettingsDialog = new PublishSettingsDialog(events);

// image settings
const imageSettingsDialog = new ImageSettingsDialog(events);

// video settings
const videoSettingsDialog = new VideoSettingsDialog(events);

topContainer.append(popup);
topContainer.append(viewerExportPopup);
topContainer.append(publishSettingsDialog);
topContainer.append(imageSettingsDialog);
topContainer.append(videoSettingsDialog);

appContainer.append(editorContainer);
Expand Down Expand Up @@ -218,6 +223,14 @@ class EditorUI {
}
});

events.function('show.imageSettingsDialog', async () => {
const imageSettings = await imageSettingsDialog.show();

if (imageSettings) {
await events.invoke('render.image', imageSettings);
}
});

events.function('show.videoSettingsDialog', async () => {
const videoSettings = await videoSettingsDialog.show();

Expand Down
Loading
Loading