Render continuously and optimize the glTF viewer
Remove vsync and dirty-flag frame gating, optimize PBR sampling and lighting, add a direct-to-canvas loadout, and preserve camera state on viewer resize. Add regression coverage and update rendering documentation. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b382-1e79-712b-abd1-d29201c9bb5e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+53
-17
@@ -19,6 +19,7 @@ const elements = {
|
||||
|
||||
let activeViewer;
|
||||
let loading = false;
|
||||
let resizeTimer;
|
||||
|
||||
function setPrompt(message, detail, error = false) {
|
||||
elements.message.textContent = message;
|
||||
@@ -34,14 +35,17 @@ function resetPrompt() {
|
||||
);
|
||||
}
|
||||
|
||||
function canvasSize() {
|
||||
const scale = Math.min(devicePixelRatio || 1, 2);
|
||||
return [
|
||||
Math.max(1, Math.round(elements.dropZone.clientWidth * scale)),
|
||||
Math.max(1, Math.round(elements.dropZone.clientHeight * scale)),
|
||||
];
|
||||
}
|
||||
|
||||
function replaceCanvas() {
|
||||
const canvas = elements.canvas.cloneNode();
|
||||
const scale = Math.min(devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.max(1, Math.round(elements.dropZone.clientWidth * scale));
|
||||
canvas.height = Math.max(
|
||||
1,
|
||||
Math.round(elements.dropZone.clientHeight * scale),
|
||||
);
|
||||
[canvas.width, canvas.height] = canvasSize();
|
||||
elements.canvas.replaceWith(canvas);
|
||||
elements.canvas = canvas;
|
||||
return canvas;
|
||||
@@ -49,18 +53,12 @@ function replaceCanvas() {
|
||||
|
||||
function disposeViewer() {
|
||||
if (!activeViewer) return;
|
||||
cancelAnimationFrame(activeViewer.dirtyFrame);
|
||||
activeViewer.stopProfile?.();
|
||||
activeViewer.camera?.detachControls();
|
||||
activeViewer.scene.dispose();
|
||||
activeViewer = undefined;
|
||||
}
|
||||
|
||||
function markDirtyEveryFrame(viewer) {
|
||||
viewer.signals[5] = 1;
|
||||
viewer.dirtyFrame = requestAnimationFrame(() => markDirtyEveryFrame(viewer));
|
||||
}
|
||||
|
||||
function rotate([rx, ry, rz, rw], [x, y, z]) {
|
||||
const tx = 2 * (ry * z - rz * y);
|
||||
const ty = 2 * (rz * x - rx * z);
|
||||
@@ -189,7 +187,7 @@ function isGltf(file) {
|
||||
return /\.(?:glb|gltf)$/iu.test(file.name);
|
||||
}
|
||||
|
||||
async function openFile(file) {
|
||||
async function openFile(file, cameraState) {
|
||||
if (loading) return;
|
||||
if (!file || !isGltf(file)) {
|
||||
setPrompt("That file is not glTF", "Drop a .glb or .gltf file.", true);
|
||||
@@ -224,6 +222,7 @@ async function openFile(file) {
|
||||
scene = new Scene(canvas, {
|
||||
arenaBytes: 384 * 1024 * 1024,
|
||||
hdr: true,
|
||||
directToCanvas: true,
|
||||
clearColor: [0.025, 0.035, 0.045, 1],
|
||||
});
|
||||
await scene.ready;
|
||||
@@ -262,7 +261,13 @@ async function openFile(file) {
|
||||
];
|
||||
const camera = new ArcRotateCamera(
|
||||
scene,
|
||||
cameraOptions(scene, meshes, canvas, bounds),
|
||||
cameraState
|
||||
? {
|
||||
...cameraState,
|
||||
aspect: canvas.width / canvas.height,
|
||||
controls: { element: canvas, pointer: true },
|
||||
}
|
||||
: cameraOptions(scene, meshes, canvas, bounds),
|
||||
);
|
||||
await Promise.all([...lights.map((light) => light.ready), camera.ready]);
|
||||
|
||||
@@ -278,12 +283,11 @@ async function openFile(file) {
|
||||
else elements.renderTime.removeAttribute("title");
|
||||
|
||||
activeViewer = {
|
||||
file,
|
||||
camera,
|
||||
scene,
|
||||
stopProfile,
|
||||
signals: scene.core.array("signals").row(0),
|
||||
};
|
||||
markDirtyEveryFrame(activeViewer);
|
||||
resetPrompt();
|
||||
elements.prompt.hidden = true;
|
||||
} catch (error) {
|
||||
@@ -293,9 +297,37 @@ async function openFile(file) {
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
loading = false;
|
||||
scheduleResize();
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleResize() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (loading || !activeViewer) return;
|
||||
const [width, height] = canvasSize();
|
||||
if (elements.canvas.width === width && elements.canvas.height === height)
|
||||
return;
|
||||
const { file, camera, scene } = activeViewer;
|
||||
const row = scene.core.array("cameras").row(camera.cameraId);
|
||||
// Core's surface size is fixed at initialization. Recreate only after resizing settles,
|
||||
// preserving orbit, pan, zoom and clipping rather than asking for the file again.
|
||||
void openFile(file, {
|
||||
alpha: camera.alpha,
|
||||
beta: camera.beta,
|
||||
radius: camera.radius,
|
||||
targetPosition: Array.from(row.subarray(16, 19)),
|
||||
fov: camera.fov,
|
||||
near: camera.near,
|
||||
far: camera.far,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(scheduleResize);
|
||||
resizeObserver.observe(elements.dropZone);
|
||||
addEventListener("resize", scheduleResize);
|
||||
|
||||
elements.prompt.addEventListener("click", () => {
|
||||
if (!loading) elements.filePicker.click();
|
||||
});
|
||||
@@ -326,5 +358,9 @@ elements.dropZone.addEventListener("drop", (event) => {
|
||||
void openFile(file);
|
||||
});
|
||||
|
||||
addEventListener("beforeunload", disposeViewer);
|
||||
addEventListener("beforeunload", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeObserver.disconnect();
|
||||
disposeViewer();
|
||||
});
|
||||
resetPrompt();
|
||||
|
||||
Reference in New Issue
Block a user