Stabilize WebGL flip reveal handoff

This commit is contained in:
2026-06-10 15:10:57 +02:00
parent 97eab216b7
commit ef358c5cfd
6 changed files with 186 additions and 41 deletions
+111 -25
View File
@@ -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=20260610-book-timeline-j';
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-l';
const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab';
@@ -430,6 +430,12 @@ materials.flipPageEdge.map = paperTextures.edge;
materials.flipPageEdge.normalMap = paperTextures.normal;
materials.flipPageEdge.roughnessMap = paperTextures.roughness;
materials.flipPageEdge.side = THREE.DoubleSide;
materials.flipPageSurface.userData.bookPageReveal = {
side: 'flipFront'
};
materials.flipPageBackSurface.userData.bookPageReveal = {
side: 'flipBack'
};
materials.leftPage.userData.bookPageReveal = {
side: 'left'
};
@@ -441,6 +447,8 @@ materials.headband.userData.isHeadband = true;
configureHardcoverPaperMaterial(materials.pageBlock);
configureHardcoverPaperMaterial(materials.pageEdge, { useEdgeMap: true });
configureHardcoverPaperMaterial(materials.pageSurface);
configureHardcoverPaperMaterial(materials.flipPageSurface);
configureHardcoverPaperMaterial(materials.flipPageBackSurface);
configureHardcoverPaperMaterial(materials.leftPage);
configureHardcoverPaperMaterial(materials.rightPage);
@@ -2072,30 +2080,36 @@ function syncBottomNavigation() {
function handlePageTextureRecords(event) {
const detail = normalizePageTextureRecordDetail(event.detail || {});
if (detail.pageMeta) {
currentPageMeta = normalizePageMetaPair(detail.pageMeta, currentPageMeta);
const incomingPageMeta = detail.pageMeta
? normalizePageMetaPair(detail.pageMeta, currentPageMeta)
: currentPageMeta;
const effectivePageMeta = detail.phase === 'prepare'
? incomingPageMeta
: incomingPageMeta;
if (detail.phase !== 'prepare' && detail.pageMeta) {
currentPageMeta = incomingPageMeta;
}
markPageTextureTiming('handlePageTextureRecords:start', {
hasLeft: Boolean(detail.left),
hasRight: Boolean(detail.right),
revealSides: Object.keys(detail.reveal || {}),
phase: detail.phase || 'activate',
pageMeta: currentPageMeta
pageMeta: effectivePageMeta
});
const leftReveal = attachRevealPageMeta(detail.reveal?.left, currentPageMeta.left || null);
const rightReveal = attachRevealPageMeta(detail.reveal?.right, currentPageMeta.right || null);
const leftReveal = attachRevealPageMeta(detail.reveal?.left, effectivePageMeta.left || null);
const rightReveal = attachRevealPageMeta(detail.reveal?.right, effectivePageMeta.right || null);
if (detail.phase === 'prepare') {
if (detail.left) {
const texture = preloadPageTexture('left', detail.left, leftReveal, currentPageMeta.left);
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, texture, detail.left, true);
} else if (currentPageMeta.left?.kind === 'blank') {
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, getBlankPageTexture(), null, false);
const texture = preloadPageTexture('left', detail.left, leftReveal, effectivePageMeta.left);
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, texture, detail.left, true);
} else if (effectivePageMeta.left?.kind === 'blank') {
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, getBlankPageTexture(), null, false);
}
if (detail.right) {
const texture = preloadPageTexture('right', detail.right, rightReveal, currentPageMeta.right);
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, texture, detail.right, true);
} else if (currentPageMeta.right?.kind === 'blank') {
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, getBlankPageTexture(), null, false);
const texture = preloadPageTexture('right', detail.right, rightReveal, effectivePageMeta.right);
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, texture, detail.right, true);
} else if (effectivePageMeta.right?.kind === 'blank') {
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, getBlankPageTexture(), null, false);
}
markPageTextureTiming('handlePageTextureRecords:prepare:end');
return;
@@ -2104,21 +2118,21 @@ function handlePageTextureRecords(event) {
if (leftReveal) {
beginPageReveal('left', detail.left, leftReveal);
} else {
uploadPageTextureDirect('left', detail.left, currentPageMeta.left);
uploadPageTextureDirect('left', detail.left, effectivePageMeta.left);
}
}
if (detail.right) {
if (rightReveal) {
beginPageReveal('right', detail.right, rightReveal);
} else {
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
uploadPageTextureDirect('right', detail.right, effectivePageMeta.right);
}
}
if (!detail.left && currentPageMeta.left?.kind === 'blank') {
applyExplicitBlankPageTexture('left', currentPageMeta.left, 'page-texture-records');
if (!detail.left && effectivePageMeta.left?.kind === 'blank') {
applyExplicitBlankPageTexture('left', effectivePageMeta.left, 'page-texture-records');
}
if (!detail.right && currentPageMeta.right?.kind === 'blank') {
applyExplicitBlankPageTexture('right', currentPageMeta.right, 'page-texture-records');
if (!detail.right && effectivePageMeta.right?.kind === 'blank') {
applyExplicitBlankPageTexture('right', effectivePageMeta.right, 'page-texture-records');
}
markStaticSceneBuffersDirty();
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
@@ -2427,6 +2441,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
});
if (prepared?.texture) {
material.map = prepared.texture;
material.needsUpdate = true;
} else {
if (material.map !== texture) {
material.map = texture;
@@ -2549,10 +2564,44 @@ function applyPageRevealRegions(shader, regions = []) {
}
function getPageRevealShader(side) {
const material = side === 'left' ? materials.leftPage : materials.rightPage;
const material = side === 'left'
? materials.leftPage
: side === 'right'
? materials.rightPage
: side === 'flipFront'
? materials.flipPageSurface
: side === 'flipBack'
? materials.flipPageBackSurface
: null;
return material?.userData?.bookRevealShader || null;
}
function syncFlipRevealShaderFromSource(sourceSide, targetMaterial = materials.flipPageSurface) {
if (!sourceSide || !targetMaterial?.userData) return false;
const sourceState = pageRevealState[sourceSide];
const sourceShader = getPageRevealShader(sourceSide);
const targetShader = targetMaterial.userData.bookRevealShader || null;
if (!sourceState || !sourceShader?.uniforms || !targetShader?.uniforms) return false;
const sourceUniforms = sourceShader.uniforms;
const targetUniforms = targetShader.uniforms;
targetUniforms.bookRevealActive.value = sourceUniforms.bookRevealActive?.value || 0;
targetUniforms.bookRevealElapsedMs.value = sourceUniforms.bookRevealElapsedMs?.value || sourceState.visualElapsedMs || 0;
targetUniforms.bookRevealRegionCount.value = sourceUniforms.bookRevealRegionCount?.value || 0;
if (targetUniforms.bookRevealBaseMap) targetUniforms.bookRevealBaseMap.value = sourceUniforms.bookRevealBaseMap?.value || sourceState.baseTexture || targetMaterial.map;
if (targetUniforms.bookRevealUseBaseMap) targetUniforms.bookRevealUseBaseMap.value = sourceUniforms.bookRevealUseBaseMap?.value || 0;
const sourceRects = sourceUniforms.bookRevealRegionRects?.value || [];
const targetRects = targetUniforms.bookRevealRegionRects?.value || [];
const sourceTimings = sourceUniforms.bookRevealRegionTimings?.value || [];
const targetTimings = targetUniforms.bookRevealRegionTimings?.value || [];
for (let index = 0; index < Math.min(sourceRects.length, targetRects.length); index += 1) {
targetRects[index].copy(sourceRects[index]);
}
for (let index = 0; index < Math.min(sourceTimings.length, targetTimings.length); index += 1) {
targetTimings[index].copy(sourceTimings[index]);
}
return true;
}
function getRevealDebugState() {
return ['left', 'right'].reduce((state, side) => {
const shader = getPageRevealShader(side);
@@ -2699,6 +2748,12 @@ function updatePageRevealAnimations(now) {
}
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
if (materials.flipPageSurface.userData.sourceRevealSide === side) {
syncFlipRevealShaderFromSource(side, materials.flipPageSurface);
}
if (materials.flipPageBackSurface.userData.sourceRevealSide === side) {
syncFlipRevealShaderFromSource(side, materials.flipPageBackSurface);
}
if (progress < 1) return;
clearPageReveal(side, 'duration-complete');
@@ -2913,8 +2968,8 @@ function startFastPageFlipPrepared(direction, options = {}) {
function createPageFlip(direction, startTime, duration) {
const sourceSide = direction > 0 ? 1 : -1;
const sourcePageSide = direction > 0 ? 'right' : 'left';
const sourceLine = topVisibleLine(sourceSide);
const destinationLine = topVisibleLine(-sourceSide);
const sourceLine = normalizeFlipLineToVisiblePage(topVisibleLine(sourceSide), sourceSide);
const destinationLine = normalizeFlipLineToVisiblePage(topVisibleLine(-sourceSide), -sourceSide);
if (!sourceLine || !destinationLine) return null;
return {
direction,
@@ -2930,6 +2985,29 @@ function createPageFlip(direction, startTime, duration) {
};
}
function normalizeFlipLineToVisiblePage(line, side) {
if (!line || !currentProceduralBookModel) return line;
const points = Array.isArray(line.points) ? line.points : [];
if (points.length < 2) return line;
const pageStartX = side * Math.max(0, Number(currentProceduralBookModel.spineHalf || 0));
const endpoint = points[points.length - 1];
const sourceStart = points[0];
const sourceSpan = Math.max(0.0001, side * (endpoint.x - sourceStart.x));
const normalizedPoints = points.map((point) => {
const u = THREE.MathUtils.clamp(side * (point.x - sourceStart.x) / sourceSpan, 0, 1);
return {
x: THREE.MathUtils.lerp(pageStartX, endpoint.x, u),
y: point.y
};
});
return {
...line,
anchor: normalizedPoints[0],
points: normalizedPoints,
endpoint: normalizedPoints[normalizedPoints.length - 1]
};
}
function prepareStaticPageForFlip(flip, prewarm = null) {
if (!flip) return false;
const sourceSide = flip.direction > 0 ? 'right' : 'left';
@@ -2962,6 +3040,8 @@ 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.normalMap = materials.pageSurface.normalMap;
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
@@ -3006,15 +3086,17 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
...lastFlipTexturePreflight,
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
});
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
return true;
}
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;
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
if (resident) return resident;
const material = side === 'left' ? materials.leftPage : materials.rightPage;
return material?.map || null;
}
@@ -3366,8 +3448,10 @@ function createFlippingPageGeometry(surface, direction = 1) {
}
function pageUvForSide(side, u, v) {
const inset = THREE.MathUtils.clamp(Number(PROCEDURAL_BOOK.PAGE_TEXTURE_FORE_EDGE_INSET_RATIO || 0), 0, 0.35);
const pageU = THREE.MathUtils.clamp(u / Math.max(0.0001, 1 - inset), 0, 1);
return {
x: side < 0 ? 1 - u : u,
x: side < 0 ? 1 - pageU : pageU,
y: v
};
}
@@ -3462,6 +3546,8 @@ function removeFlipMesh(flip) {
book.remove(flip.mesh);
flip.mesh.geometry.dispose();
flip.mesh = null;
materials.flipPageSurface.userData.sourceRevealSide = null;
materials.flipPageBackSurface.userData.sourceRevealSide = null;
}
function easeInOutCubic(t) {