Stabilize WebGL flip reveal handoff
This commit is contained in:
+111
-25
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user