Checkpoint WebGL book reveal optimization
This commit is contained in:
+148
-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=20260607-webgl-typography-a';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260608-webgl-mask-timing-c';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -214,13 +214,31 @@ const leftCanvas = createPageCanvas('left');
|
||||
const rightCanvas = createPageCanvas('right');
|
||||
const leftTexture = new THREE.CanvasTexture(leftCanvas);
|
||||
const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
||||
[leftTexture, rightTexture].forEach((texture) => {
|
||||
function configurePageCanvasTexture(texture) {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.anisotropy = maxTextureAnisotropy;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = true;
|
||||
});
|
||||
texture.generateMipmaps = false;
|
||||
return texture;
|
||||
}
|
||||
[leftTexture, rightTexture].forEach(configurePageCanvasTexture);
|
||||
|
||||
function createPageCanvasTexture(sourceCanvas) {
|
||||
if (!sourceCanvas) return null;
|
||||
const texture = configurePageCanvasTexture(new THREE.CanvasTexture(sourceCanvas));
|
||||
texture.needsUpdate = true;
|
||||
if (typeof renderer?.initTexture === 'function') {
|
||||
renderer.initTexture(texture);
|
||||
texture.needsUpdate = false;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
const preparedPageTextures = {
|
||||
left: new Map(),
|
||||
right: new Map()
|
||||
};
|
||||
const pageRevealState = {
|
||||
left: null,
|
||||
right: null
|
||||
@@ -585,7 +603,7 @@ function configureBookShadowReceiver(material, strength) {
|
||||
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
||||
const isHeadband = material.userData?.isHeadband === true;
|
||||
const pageReveal = material.userData?.bookPageReveal || null;
|
||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${pageReveal ? 'page-reveal-v1' : isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${pageReveal ? 'page-reveal-v2' : isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||
@@ -599,7 +617,9 @@ function configureBookShadowReceiver(material, strength) {
|
||||
shader.uniforms.bookRevealWordRects = { value: Array.from({ length: maxRevealWords }, () => new THREE.Vector4(0, 0, 0, 0)) };
|
||||
shader.uniforms.bookRevealWordTimings = { value: Array.from({ length: maxRevealWords }, () => new THREE.Vector4(0, 1, 0, 0)) };
|
||||
shader.uniforms.bookRevealPaperColor = { value: paperColor.clone() };
|
||||
shader.uniforms.bookRevealSoftness = { value: 0.035 };
|
||||
shader.uniforms.bookRevealBaseMap = { value: leftTexture };
|
||||
shader.uniforms.bookRevealUseBaseMap = { value: 0 };
|
||||
shader.uniforms.bookRevealSoftness = { value: 0.025 };
|
||||
material.userData.bookRevealShader = shader;
|
||||
applyPendingPageReveal(pageReveal.side, shader);
|
||||
}
|
||||
@@ -642,6 +662,8 @@ function configureBookShadowReceiver(material, strength) {
|
||||
uniform vec4 bookRevealWordRects[256];
|
||||
uniform vec4 bookRevealWordTimings[256];
|
||||
uniform vec3 bookRevealPaperColor;
|
||||
uniform sampler2D bookRevealBaseMap;
|
||||
uniform float bookRevealUseBaseMap;
|
||||
uniform float bookRevealSoftness;
|
||||
|
||||
float bookRevealVisibleMask(vec2 uv) {
|
||||
@@ -653,7 +675,7 @@ function configureBookShadowReceiver(material, strength) {
|
||||
float inside = step(0.0, local.x) * step(0.0, local.y) * step(local.x, 1.0) * step(local.y, 1.0);
|
||||
vec4 timing = bookRevealWordTimings[i];
|
||||
float progress = clamp((bookRevealElapsedMs - timing.x) / max(1.0, timing.y), 0.0, 1.0);
|
||||
float scan = clamp((local.x + (1.0 - local.y)) * 0.5, 0.0, 1.0);
|
||||
float scan = clamp(local.x * 0.88 + (1.0 - local.y) * 0.12, 0.0, 1.0);
|
||||
float feather = max(0.0001, bookRevealSoftness);
|
||||
float visible = smoothstep(scan - feather, scan + feather, progress);
|
||||
hidden = max(hidden, inside * (1.0 - visible));
|
||||
@@ -805,8 +827,9 @@ function configureBookShadowReceiver(material, strength) {
|
||||
if (bookRevealActive > 0.5) {
|
||||
float hiddenInk = bookRevealVisibleMask(vMapUv);
|
||||
float luminance = dot(sampledDiffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||
float inkMask = 1.0 - smoothstep(0.26, 0.72, luminance);
|
||||
sampledDiffuseColor.rgb = mix(sampledDiffuseColor.rgb, bookRevealPaperColor, hiddenInk * inkMask);
|
||||
float inkMask = 1.0 - smoothstep(0.52, 0.9, luminance);
|
||||
vec3 revealBaseColor = mix(bookRevealPaperColor, texture2D(bookRevealBaseMap, vMapUv).rgb, bookRevealUseBaseMap);
|
||||
sampledDiffuseColor.rgb = mix(sampledDiffuseColor.rgb, revealBaseColor, clamp(hiddenInk * inkMask * 1.55, 0.0, 1.0));
|
||||
}
|
||||
diffuseColor *= sampledDiffuseColor;
|
||||
#endif`
|
||||
@@ -1653,8 +1676,15 @@ function handlePageCanvases(event) {
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {})
|
||||
revealSides: Object.keys(detail.reveal || {}),
|
||||
preloadOnly: Boolean(detail.preloadOnly)
|
||||
});
|
||||
if (detail.preloadOnly) {
|
||||
if (detail.left) preloadPageTexture('left', detail.left, detail.reveal?.left);
|
||||
if (detail.right) preloadPageTexture('right', detail.right, detail.reveal?.right);
|
||||
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
||||
return;
|
||||
}
|
||||
if (detail.left) {
|
||||
if (detail.reveal?.left) {
|
||||
beginPageReveal('left', detail.left, detail.reveal.left);
|
||||
@@ -1678,10 +1708,59 @@ function handlePageCanvases(event) {
|
||||
markPageTextureTiming('handlePageCanvases:end');
|
||||
}
|
||||
|
||||
function getRevealCacheKey(revealDetail = {}) {
|
||||
const ids = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : [];
|
||||
return ids.map(id => String(id)).join('|') || 'direct';
|
||||
}
|
||||
|
||||
function preloadPageTexture(side, sourceCanvas, revealDetail = {}) {
|
||||
if (!sourceCanvas) return null;
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const baseTexture = revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null;
|
||||
const key = getRevealCacheKey(revealDetail);
|
||||
markPageTextureTiming('preloadTexture:start', {
|
||||
side,
|
||||
key,
|
||||
width: sourceCanvas.width,
|
||||
height: sourceCanvas.height,
|
||||
hasBaseTexture: Boolean(baseTexture)
|
||||
});
|
||||
preparedPageTextures[side].set(key, {
|
||||
texture,
|
||||
baseTexture,
|
||||
sourceCanvas,
|
||||
revealDetail,
|
||||
uploadedAt: performance.now()
|
||||
});
|
||||
if (preparedPageTextures[side].size > 12) {
|
||||
const oldestKey = preparedPageTextures[side].keys().next().value;
|
||||
const oldest = preparedPageTextures[side].get(oldestKey);
|
||||
oldest?.texture?.dispose?.();
|
||||
oldest?.baseTexture?.dispose?.();
|
||||
preparedPageTextures[side].delete(oldestKey);
|
||||
}
|
||||
markPageTextureTiming('preloadTexture:end', { side, key });
|
||||
return texture;
|
||||
}
|
||||
|
||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||
const key = getRevealCacheKey(revealDetail);
|
||||
const prepared = preparedPageTextures[side].get(key);
|
||||
if (!prepared) return null;
|
||||
preparedPageTextures[side].delete(key);
|
||||
markPageTextureTiming('preloadTexture:activate', { side, key });
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function uploadPageTextureDirect(side, sourceCanvas) {
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
markPageTextureTiming('directUpload:start', { side });
|
||||
clearPageReveal(side, 'direct-upload');
|
||||
if (material.map !== texture) {
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
markPageTextureTiming('directUpload:end', { side });
|
||||
}
|
||||
@@ -1689,12 +1768,25 @@ function uploadPageTextureDirect(side, sourceCanvas) {
|
||||
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const shader = getPageRevealShader(side);
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const prepared = takePreparedPageTexture(side, revealDetail);
|
||||
|
||||
markPageTextureTiming('revealUpload:start', {
|
||||
side,
|
||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0
|
||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0,
|
||||
usedPreparedTexture: Boolean(prepared),
|
||||
usedPreparedBaseTexture: Boolean(prepared?.baseTexture)
|
||||
});
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
if (prepared?.texture) {
|
||||
material.map = prepared.texture;
|
||||
} else {
|
||||
if (material.map !== texture) {
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
}
|
||||
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null);
|
||||
|
||||
pageRevealState[side] = {
|
||||
startedAt: revealDetail.startNow ? performance.now() : null,
|
||||
@@ -1702,9 +1794,13 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
lastRevealFrameAt: null,
|
||||
visualElapsedMs: 0,
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : [],
|
||||
baseTexture,
|
||||
fastForwarding: false,
|
||||
fastForwardStartedAt: null,
|
||||
fastForwardStartElapsedMs: 0,
|
||||
fastForwardDurationMs: 260
|
||||
};
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
if (material?.userData) material.userData.pendingPageReveal = revealDetail;
|
||||
if (shader?.uniforms) applyPendingPageReveal(side, shader);
|
||||
else if (material) material.needsUpdate = true;
|
||||
@@ -1725,6 +1821,9 @@ function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
||||
applyPageRevealWords(shader, revealDetail.wordRects || []);
|
||||
shader.uniforms.bookRevealActive.value = 1;
|
||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
const baseTexture = pageRevealState[side]?.baseTexture;
|
||||
if (shader.uniforms.bookRevealBaseMap) shader.uniforms.bookRevealBaseMap.value = baseTexture || (side === 'left' ? leftTexture : rightTexture);
|
||||
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = baseTexture ? 1 : 0;
|
||||
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
||||
side,
|
||||
blockIds: pageRevealState[side]?.blockIds || revealDetail.blockIds || [],
|
||||
@@ -1744,6 +1843,12 @@ function applyPageRevealWords(shader, words = []) {
|
||||
source.forEach((word, index) => {
|
||||
const rect = word.rect || {};
|
||||
const timing = word.timing || {};
|
||||
const nextTiming = source[index + 1]?.timing || {};
|
||||
const delay = Math.max(0, Number(timing.delay || 0));
|
||||
const nextDelay = Number(nextTiming.delay);
|
||||
const allottedDuration = Number.isFinite(nextDelay) && nextDelay > delay
|
||||
? nextDelay - delay
|
||||
: Number(timing.duration || 1);
|
||||
const x = THREE.MathUtils.clamp(Number(rect.x || 0), 0, 1);
|
||||
const y = THREE.MathUtils.clamp(Number(rect.y || 0), 0, 1);
|
||||
const width = THREE.MathUtils.clamp(Number(rect.width || 0), 0, 1);
|
||||
@@ -1755,8 +1860,8 @@ function applyPageRevealWords(shader, words = []) {
|
||||
Math.max(0.0001, height)
|
||||
);
|
||||
timingUniforms[index].set(
|
||||
Math.max(0, Number(timing.delay || 0)),
|
||||
Math.max(1, Number(timing.duration || 1)),
|
||||
delay,
|
||||
Math.max(1, allottedDuration),
|
||||
0,
|
||||
0
|
||||
);
|
||||
@@ -1781,6 +1886,8 @@ function getRevealDebugState() {
|
||||
elapsedMs: Number(uniforms.bookRevealElapsedMs?.value || 0),
|
||||
visualElapsedMs: Number(pageRevealState[side]?.visualElapsedMs || 0),
|
||||
wordCount: Number(uniforms.bookRevealWordCount?.value || 0),
|
||||
usesBaseTexture: Number(uniforms.bookRevealUseBaseMap?.value || 0),
|
||||
fastForwarding: pageRevealState[side]?.fastForwarding === true,
|
||||
started: pageRevealState[side]?.startedAt != null,
|
||||
pendingStart: pageRevealState[side]?.pendingStart === true,
|
||||
durationMs: Number(pageRevealState[side]?.durationMs || 0),
|
||||
@@ -1791,16 +1898,17 @@ function getRevealDebugState() {
|
||||
}
|
||||
|
||||
function clearPageReveal(side, reason = 'clear') {
|
||||
const previousState = pageRevealState[side];
|
||||
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 || []
|
||||
state: previousState ? {
|
||||
started: previousState.startedAt != null,
|
||||
pendingStart: previousState.pendingStart === true,
|
||||
visualElapsedMs: previousState.visualElapsedMs || 0,
|
||||
durationMs: previousState.durationMs,
|
||||
blockIds: previousState.blockIds || []
|
||||
} : null
|
||||
});
|
||||
if (pageRevealClearLog.length > 40) pageRevealClearLog.splice(0, pageRevealClearLog.length - 40);
|
||||
@@ -1811,7 +1919,9 @@ function clearPageReveal(side, reason = 'clear') {
|
||||
shader.uniforms.bookRevealActive.value = 0;
|
||||
shader.uniforms.bookRevealElapsedMs.value = completedRevealElapsedMs;
|
||||
shader.uniforms.bookRevealWordCount.value = 0;
|
||||
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = 0;
|
||||
}
|
||||
previousState?.baseTexture?.dispose?.();
|
||||
}
|
||||
|
||||
function startPageRevealForBlock(blockId) {
|
||||
@@ -1833,7 +1943,10 @@ function fastForwardPageReveals(blockIds = []) {
|
||||
if (!state) return;
|
||||
const matches = ids.size === 0 || state.blockIds.some(blockId => ids.has(String(blockId)));
|
||||
if (!matches) return;
|
||||
clearPageReveal(side, 'fast-forward');
|
||||
state.fastForwarding = true;
|
||||
state.fastForwardStartedAt = performance.now();
|
||||
state.fastForwardStartElapsedMs = Math.max(0, Number(state.visualElapsedMs || 0));
|
||||
state.fastForwardDurationMs = 260;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1860,7 +1973,17 @@ function updatePageRevealAnimations(now) {
|
||||
}
|
||||
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);
|
||||
if (state.fastForwarding) {
|
||||
const fastElapsed = Math.max(0, now - Number(state.fastForwardStartedAt || now));
|
||||
const fastProgress = THREE.MathUtils.clamp(fastElapsed / Math.max(1, Number(state.fastForwardDurationMs || 1)), 0, 1);
|
||||
state.visualElapsedMs = THREE.MathUtils.lerp(
|
||||
Math.max(0, Number(state.fastForwardStartElapsedMs || 0)),
|
||||
state.durationMs,
|
||||
fastProgress
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user