Stabilize WebGL reveal timing
This commit is contained in:
+109
-9
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-forced-font-mask';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-reveal-clock';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -161,6 +161,26 @@ let fpsDisplay = null;
|
||||
let fpsWindowStartedAt = performance.now();
|
||||
let fpsWindowFrames = 0;
|
||||
const lastFrameTiming = {};
|
||||
const loaderTimings = {};
|
||||
const pageTextureTimings = [];
|
||||
|
||||
function markLoaderTiming(name) {
|
||||
loaderTimings[name] = performance.now();
|
||||
document.documentElement.dataset.webglLoaderTimings = JSON.stringify(loaderTimings);
|
||||
}
|
||||
|
||||
function markPageTextureTiming(name, detail = {}) {
|
||||
const entry = {
|
||||
name,
|
||||
at: performance.now(),
|
||||
detail
|
||||
};
|
||||
pageTextureTimings.push(entry);
|
||||
if (pageTextureTimings.length > 120) pageTextureTimings.splice(0, pageTextureTimings.length - 120);
|
||||
document.documentElement.dataset.webglPageTextureTimings = JSON.stringify(pageTextureTimings);
|
||||
return entry;
|
||||
}
|
||||
|
||||
const book = new THREE.Group();
|
||||
scene.add(book);
|
||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
||||
@@ -203,6 +223,7 @@ const pageRevealState = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
const pageRevealClearLog = [];
|
||||
await reportLabStep(52, 'Generating leather texture set');
|
||||
const leatherTextures = createLeatherTextures();
|
||||
await reportLabStep(56, 'Generating spine cloth texture set');
|
||||
@@ -389,6 +410,9 @@ window.BookLabDebug = {
|
||||
textures: generatedTextureCanvases,
|
||||
ready: false,
|
||||
renderedFrames: 0,
|
||||
loaderTimings,
|
||||
pageTextureTimings,
|
||||
pageRevealClearLog,
|
||||
get sceneAoPass() {
|
||||
return sceneAoPass;
|
||||
},
|
||||
@@ -444,6 +468,9 @@ window.BookLabDebug = {
|
||||
window.BookTextureRenderer?.publishSpread?.();
|
||||
return true;
|
||||
},
|
||||
getRevealDebugState() {
|
||||
return getRevealDebugState();
|
||||
},
|
||||
getTextureInfo() {
|
||||
return {
|
||||
pageTextureWidth,
|
||||
@@ -1619,6 +1646,11 @@ function syncBookControls() {
|
||||
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {})
|
||||
});
|
||||
if (detail.left) {
|
||||
if (detail.reveal?.left) {
|
||||
beginPageReveal('left', detail.left, detail.reveal.left);
|
||||
@@ -1639,14 +1671,17 @@ function handlePageCanvases(event) {
|
||||
height: leftCanvas.height,
|
||||
source: 'book-texture-renderer'
|
||||
});
|
||||
markPageTextureTiming('handlePageCanvases:end');
|
||||
}
|
||||
|
||||
function uploadPageTextureDirect(side, sourceCanvas) {
|
||||
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
clearPageReveal(side);
|
||||
markPageTextureTiming('directUpload:start', { side });
|
||||
clearPageReveal(side, 'direct-upload');
|
||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||
texture.needsUpdate = true;
|
||||
markPageTextureTiming('directUpload:end', { side });
|
||||
}
|
||||
|
||||
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
@@ -1654,11 +1689,18 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const shader = getPageRevealShader(side);
|
||||
|
||||
markPageTextureTiming('revealUpload:start', {
|
||||
side,
|
||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0
|
||||
});
|
||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||
texture.needsUpdate = true;
|
||||
|
||||
pageRevealState[side] = {
|
||||
startedAt: revealDetail.startNow ? performance.now() : null,
|
||||
pendingStart: false,
|
||||
lastRevealFrameAt: null,
|
||||
visualElapsedMs: 0,
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||
};
|
||||
@@ -1673,6 +1715,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
shaderReady: Boolean(shader?.uniforms),
|
||||
started: pageRevealState[side].startedAt != null
|
||||
});
|
||||
markPageTextureTiming('revealUpload:end', { side });
|
||||
}
|
||||
|
||||
function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
||||
@@ -1729,7 +1772,39 @@ function getPageRevealShader(side) {
|
||||
return material?.userData?.bookRevealShader || null;
|
||||
}
|
||||
|
||||
function clearPageReveal(side) {
|
||||
function getRevealDebugState() {
|
||||
return ['left', 'right'].reduce((state, side) => {
|
||||
const shader = getPageRevealShader(side);
|
||||
const uniforms = shader?.uniforms || {};
|
||||
state[side] = {
|
||||
active: Number(uniforms.bookRevealActive?.value || 0),
|
||||
elapsedMs: Number(uniforms.bookRevealElapsedMs?.value || 0),
|
||||
visualElapsedMs: Number(pageRevealState[side]?.visualElapsedMs || 0),
|
||||
wordCount: Number(uniforms.bookRevealWordCount?.value || 0),
|
||||
started: pageRevealState[side]?.startedAt != null,
|
||||
pendingStart: pageRevealState[side]?.pendingStart === true,
|
||||
durationMs: Number(pageRevealState[side]?.durationMs || 0),
|
||||
blockIds: pageRevealState[side]?.blockIds || []
|
||||
};
|
||||
return state;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function clearPageReveal(side, reason = 'clear') {
|
||||
pageRevealClearLog.push({
|
||||
side,
|
||||
reason,
|
||||
at: performance.now(),
|
||||
state: pageRevealState[side] ? {
|
||||
started: pageRevealState[side].startedAt != null,
|
||||
pendingStart: pageRevealState[side].pendingStart === true,
|
||||
visualElapsedMs: pageRevealState[side].visualElapsedMs || 0,
|
||||
durationMs: pageRevealState[side].durationMs,
|
||||
blockIds: pageRevealState[side].blockIds || []
|
||||
} : null
|
||||
});
|
||||
if (pageRevealClearLog.length > 40) pageRevealClearLog.splice(0, pageRevealClearLog.length - 40);
|
||||
document.documentElement.dataset.webglRevealClearLog = JSON.stringify(pageRevealClearLog);
|
||||
pageRevealState[side] = null;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (shader?.uniforms?.bookRevealActive) {
|
||||
@@ -1745,7 +1820,7 @@ function startPageRevealForBlock(blockId) {
|
||||
const state = pageRevealState[side];
|
||||
if (!state || state.startedAt != null) return;
|
||||
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
||||
state.startedAt = performance.now();
|
||||
state.pendingStart = true;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
});
|
||||
@@ -1758,7 +1833,7 @@ function fastForwardPageReveals(blockIds = []) {
|
||||
if (!state) return;
|
||||
const matches = ids.size === 0 || state.blockIds.some(blockId => ids.has(String(blockId)));
|
||||
if (!matches) return;
|
||||
clearPageReveal(side);
|
||||
clearPageReveal(side, 'fast-forward');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1768,18 +1843,29 @@ function updatePageRevealAnimations(now) {
|
||||
if (!state) return;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (!shader?.uniforms) {
|
||||
clearPageReveal(side);
|
||||
clearPageReveal(side, 'missing-shader');
|
||||
return;
|
||||
}
|
||||
if (state.pendingStart) {
|
||||
state.startedAt = now;
|
||||
state.pendingStart = false;
|
||||
state.lastRevealFrameAt = now;
|
||||
state.visualElapsedMs = 0;
|
||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
return;
|
||||
}
|
||||
if (state.startedAt == null) {
|
||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
return;
|
||||
}
|
||||
const progress = THREE.MathUtils.clamp((now - state.startedAt) / state.durationMs, 0, 1);
|
||||
shader.uniforms.bookRevealElapsedMs.value = Math.max(0, now - state.startedAt);
|
||||
const revealFrameDeltaMs = state.lastRevealFrameAt == null ? 0 : Math.max(0, now - state.lastRevealFrameAt);
|
||||
state.lastRevealFrameAt = now;
|
||||
state.visualElapsedMs = Math.max(0, Number(state.visualElapsedMs || 0)) + Math.min(revealFrameDeltaMs, targetFrameDurationMs);
|
||||
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
|
||||
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
||||
if (progress < 1) return;
|
||||
|
||||
clearPageReveal(side);
|
||||
clearPageReveal(side, 'duration-complete');
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||
detail: {
|
||||
side,
|
||||
@@ -1790,6 +1876,11 @@ function updatePageRevealAnimations(now) {
|
||||
}
|
||||
|
||||
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||
markPageTextureTiming('drawCanvasPageTexture:start', {
|
||||
side,
|
||||
width: canvas?.width || 0,
|
||||
height: canvas?.height || 0
|
||||
});
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#f2ead0';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -1803,6 +1894,7 @@ function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||
|
||||
ctx.drawImage(sourceCanvas, 0, 0, canvas.width, canvas.height);
|
||||
updatePageTextureDebugState(side, canvas, sourceCanvas, true);
|
||||
markPageTextureTiming('drawCanvasPageTexture:end', { side });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2858,12 +2950,20 @@ function loadAiRoomReflection() {
|
||||
}
|
||||
|
||||
function primeSceneForLoader() {
|
||||
markLoaderTiming('primeSceneForLoader:start');
|
||||
updateCameraRig(0);
|
||||
updateCandleShadowUniforms();
|
||||
markLoaderTiming('bookShadowMaps:start');
|
||||
updateBookShadowMaps();
|
||||
markLoaderTiming('bookShadowMaps:end');
|
||||
markLoaderTiming('tableReflection:start');
|
||||
updateTableReflection();
|
||||
markLoaderTiming('tableReflection:end');
|
||||
markLoaderTiming('shaderCompile:start');
|
||||
renderer.compile(scene, camera);
|
||||
markLoaderTiming('shaderCompile:end');
|
||||
staticSceneBuffersDirty = false;
|
||||
markLoaderTiming('primeSceneForLoader:end');
|
||||
}
|
||||
|
||||
function tintAmbientFromCanvas(canvas) {
|
||||
|
||||
Reference in New Issue
Block a user