Stabilize WebGL title and timeline texture flow

This commit is contained in:
2026-06-17 08:31:46 +02:00
parent ef358c5cfd
commit c19ebe3089
5 changed files with 211 additions and 76 deletions
+88 -23
View File
@@ -40,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
const generatedTextureCanvases = {};
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
const reflectionPixelRatio = 1;
const reflectionPixelRatio = 0.72;
const pageTextureWidth = 3072;
const reflectionTargetSize = new THREE.Vector2();
const pageRaycaster = new THREE.Raycaster();
@@ -80,13 +80,13 @@ let tableDustTexture = null;
let tableGreaseTexture = null;
const tableTopY = -0.02;
const bookTableContactClearance = 0.002;
const tableReflectionBaseWidth = 2048;
const tableReflectionBaseHeight = 1152;
const tableReflectionBaseWidth = 1536;
const tableReflectionBaseHeight = 864;
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
colorSpace: THREE.SRGBColorSpace,
depthBuffer: true,
stencilBuffer: false,
samples: renderer.capabilities.isWebGL2 ? 8 : 0
samples: renderer.capabilities.isWebGL2 ? 4 : 0
});
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
@@ -105,7 +105,7 @@ const reflectionUp = new THREE.Vector3();
const candleShadowSources = [];
const candleWorldPosition = new THREE.Vector3();
const flameWorldPosition = new THREE.Vector3();
const bookShadowMapSize = 1536;
const bookShadowMapSize = 1024;
const bookShadowTargets = Array.from({ length: 3 }, () => {
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
colorSpace: THREE.NoColorSpace,
@@ -126,6 +126,10 @@ const bookShadowBiasMatrix = new THREE.Matrix4().set(
0, 0, 0.5, 0.5,
0, 0, 0, 1
);
const dynamicBufferRefreshIntervalMs = 1000 / 30;
const flipDynamicBufferGraceMs = 180;
let lastBookShadowRefreshAt = -Infinity;
let lastTableReflectionRefreshAt = -Infinity;
const bookShadowDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking
});
@@ -158,6 +162,7 @@ configureScenePostprocessing();
const clock = new THREE.Clock();
const targetFrameDurationMs = 1000 / 60;
const minRenderFrameIntervalMs = targetFrameDurationMs * 0.5;
let lastRenderFrameAt = 0;
let fpsDisplay = null;
let fpsWindowStartedAt = performance.now();
@@ -626,7 +631,8 @@ window.BookLabDebug = {
preparedPageTextureCount: textureStoreState.preparedTextureCount || 0,
singlePageTextureStore: true,
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
mirrorRefreshesEveryFrame: true,
mirrorRefreshesAtFps: Math.round(1000 / dynamicBufferRefreshIntervalMs),
mirrorDefersDuringFlipStartMs: flipDynamicBufferGraceMs,
mirrorRefreshesWhenStaticDirty: true,
lastFlipTexturePreflight
};
@@ -668,6 +674,30 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
const incomingSpreadIndex = Math.max(0, Number(detail.spreadIndex || 0));
const latestBlockId = Math.max(0, Number(detail.latestBlockId || 0));
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
if (
window.BookPlaybackTimeline?.ownsPageFlipCommit === true
&& detail.visibility !== 'future-ready'
&& latestBlockId > 0
) {
markPageTextureTiming('spreadUpdate:timeline-owned-state-only', {
incomingSpreadIndex,
visibleSpreadIndex: bookPaginationState.spreadIndex,
latestBlockId,
latestRenderedBlockId
});
bookPaginationState = {
...bookPaginationState,
spreadCount: Math.max(1, Number(detail.spreadCount || bookPaginationState.spreadCount || 1)),
writtenPageLimit: Math.max(
Math.max(0, Number(bookPaginationState.writtenPageLimit || 0)),
Math.max(0, Number(detail.writtenPageLimit || 0))
)
};
growBookIfWritableLimitReached();
syncBookControls();
if (pendingRightPageFlip) tryStartPendingRightPageFlip('spread-updated');
return;
}
if (
latestBlockId > latestRenderedBlockId
&& detail.visibility !== 'future-ready'
@@ -2464,6 +2494,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
visualElapsedMs: activeStartedAt ? Math.max(0, performance.now() - activeStartedAt) : 0,
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
blockIds: revealBlockIds,
pageMeta: revealDetail.pageMeta ? { ...revealDetail.pageMeta } : null,
baseTexture,
pageFlipAfterReveal: revealDetail.pageFlipAfterReveal === true,
fastForwarding: false,
@@ -2602,6 +2633,14 @@ function syncFlipRevealShaderFromSource(sourceSide, targetMaterial = materials.f
return true;
}
function revealStateMatchesPage(side, pageMeta = null) {
const statePageIndex = Number(pageRevealState[side]?.pageMeta?.pageIndex);
const expectedPageIndex = Number(pageMeta?.pageIndex);
return Number.isFinite(statePageIndex)
&& Number.isFinite(expectedPageIndex)
&& Math.max(0, Math.round(statePageIndex)) === Math.max(0, Math.round(expectedPageIndex));
}
function getRevealDebugState() {
return ['left', 'right'].reduce((state, side) => {
const shader = getPageRevealShader(side);
@@ -2622,7 +2661,7 @@ function getRevealDebugState() {
}, {});
}
function clearPageReveal(side, reason = 'clear') {
function clearPageReveal(side, reason = 'clear', options = {}) {
const previousState = pageRevealState[side];
pageRevealClearLog.push({
side,
@@ -2646,7 +2685,7 @@ function clearPageReveal(side, reason = 'clear') {
shader.uniforms.bookRevealRegionCount.value = 0;
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = 0;
}
previousState?.baseTexture?.dispose?.();
if (options.preserveBaseTexture !== true) previousState?.baseTexture?.dispose?.();
}
function startPageRevealForBlock(blockId) {
@@ -2885,7 +2924,7 @@ async function startPageFlip(direction, options = {}) {
if (activeFlips.length || !currentProceduralBookModel) return false;
if (!options.force && !canPageFlip(direction)) return false;
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
const prewarm = await prewarmFlipTextures(direction, targetSpread);
const prewarm = options.prewarm || options.flipPlan?.prewarm || await prewarmFlipTextures(direction, targetSpread);
return startPageFlipPrepared(direction, {
...options,
targetSpread,
@@ -2922,7 +2961,7 @@ function startPageFlipPrepared(direction, options = {}) {
async function startFastPageFlip(direction, options = {}) {
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
const prewarm = await prewarmFlipTextures(direction, targetSpread);
const prewarm = options.prewarm || options.flipPlan?.prewarm || await prewarmFlipTextures(direction, targetSpread);
return startFastPageFlipPrepared(direction, {
...options,
targetSpread,
@@ -3040,14 +3079,16 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
}
materials.flipPageSurface.map = sourceTexture;
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
materials.flipPageSurface.userData.sourceRevealSide = pageRevealState[sourceSide] ? sourceSide : null;
materials.flipPageBackSurface.userData.sourceRevealSide = null;
materials.flipPageSurface.userData.sourceRevealSide = revealStateMatchesPage(sourceSide, sourcePageMeta) ? sourceSide : null;
materials.flipPageBackSurface.userData.sourceRevealSide = revealStateMatchesPage(targetBackSide, targetBackPageMeta) ? targetBackSide : null;
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
materials.flipPageBackSurface.roughnessMap = materials.pageSurface.roughnessMap;
materials.flipPageSurface.needsUpdate = true;
materials.flipPageBackSurface.needsUpdate = true;
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
syncFlipRevealShaderFromSource(targetBackSide, materials.flipPageBackSurface);
flip.sourceTexture = sourceTexture;
flip.sourcePageMeta = sourcePageMeta ? { ...sourcePageMeta } : null;
flip.backTexture = backTexture || getBlankPageTexture();
@@ -3070,14 +3111,14 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
if (flip.direction > 0) {
const blankTexture = getBlankPageTexture();
if (blankTexture && materials.rightPage.map !== blankTexture) {
clearPageReveal('right', 'page-flip-start');
clearPageReveal('right', 'page-flip-start', { preserveBaseTexture: sourceSide === 'right' });
materials.rightPage.map = blankTexture;
materials.rightPage.needsUpdate = true;
}
} else if (flip.direction < 0) {
const blankTexture = getBlankPageTexture();
if (blankTexture && materials.leftPage.map !== blankTexture) {
clearPageReveal('left', 'page-flip-start');
clearPageReveal('left', 'page-flip-start', { preserveBaseTexture: sourceSide === 'left' });
materials.leftPage.map = blankTexture;
materials.leftPage.needsUpdate = true;
}
@@ -3086,7 +3127,6 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
...lastFlipTexturePreflight,
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
});
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
return true;
}
@@ -3094,7 +3134,7 @@ function resolveCurrentFlipSourceTexture(side) {
const pageMeta = currentPageMeta?.[side] || null;
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
const material = side === 'left' ? materials.leftPage : materials.rightPage;
if (pageRevealState[side]) return material?.map || null;
if (revealStateMatchesPage(side, pageMeta)) return material?.map || null;
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
if (resident) return resident;
return material?.map || null;
@@ -3222,7 +3262,8 @@ function applyResidentSpreadTextures(spreadIndex, reason = 'resident-spread') {
return;
}
const material = side === 'left' ? materials.leftPage : materials.rightPage;
clearPageReveal(side, reason);
const activeRevealForPage = revealStateMatchesPage(side, pageMeta);
if (!activeRevealForPage) clearPageReveal(side, reason);
if (material.map !== texture) {
material.map = texture;
material.needsUpdate = true;
@@ -3385,8 +3426,8 @@ function createFlippingPageGeometry(surface, direction = 1) {
const targetSide = -sourceSide;
const topPageSide = direction > 0 ? targetSide : sourceSide;
const bottomPageSide = direction > 0 ? sourceSide : targetSide;
const topMaterialIndex = direction > 0 ? 1 : 0;
const bottomMaterialIndex = direction > 0 ? 0 : 1;
const topMaterialIndex = 0;
const bottomMaterialIndex = 1;
const push = (point, yOffset, uv) => {
const index = positions.length / 3;
positions.push(point.x, point.y + yOffset, point.z);
@@ -4553,7 +4594,7 @@ function renderMirrorDebugView() {
function animate(now = performance.now()) {
const elapsedSinceLastFrame = lastRenderFrameAt ? now - lastRenderFrameAt : targetFrameDurationMs;
if (lastRenderFrameAt && elapsedSinceLastFrame < targetFrameDurationMs) {
if (lastRenderFrameAt && elapsedSinceLastFrame < minRenderFrameIntervalMs) {
requestAnimationFrame(animate);
return;
}
@@ -4603,12 +4644,36 @@ function animate(now = performance.now()) {
updateCandleShadowUniforms();
lastFrameTiming.update = performance.now() - updateStartedAt;
renderedFrameCount += 1;
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
const shadowStartedAt = performance.now();
updateBookShadowMaps();
const forceDynamicBufferRefresh = staticSceneBuffersDirty && activeFlips.length === 0;
const newestFlipAge = activeFlips.length
? Math.min(...activeFlips.map(flip => Math.max(0, now - Number(flip.startTime || now))))
: Infinity;
const deferDynamicBuffersForFlipStart = activeFlips.length > 0 && newestFlipAge < flipDynamicBufferGraceMs;
const shadowRefreshDue = !deferDynamicBuffersForFlipStart && (
forceDynamicBufferRefresh || now - lastBookShadowRefreshAt >= dynamicBufferRefreshIntervalMs
);
const reflectionRefreshDue = !deferDynamicBuffersForFlipStart && (
forceDynamicBufferRefresh || now - lastTableReflectionRefreshAt >= dynamicBufferRefreshIntervalMs
);
const bothHeavyPassesDue = shadowRefreshDue && reflectionRefreshDue && !forceDynamicBufferRefresh;
const refreshShadowsThisFrame = shadowRefreshDue && (
!bothHeavyPassesDue || lastBookShadowRefreshAt <= lastTableReflectionRefreshAt
);
const refreshReflectionThisFrame = reflectionRefreshDue && (
!bothHeavyPassesDue || !refreshShadowsThisFrame
);
if (refreshShadowsThisFrame) {
updateBookShadowMaps();
lastBookShadowRefreshAt = now;
}
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
const reflectionStartedAt = performance.now();
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
updateTableReflection();
if (refreshReflectionThisFrame) {
updateTableReflection();
lastTableReflectionRefreshAt = now;
}
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
const renderStartedAt = performance.now();
if (tableDebugMode === tableDebugModes.mirror) {