Queue WebGL book reveal masks
This commit is contained in:
+110
-63
@@ -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-shader-reveal';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-queued-mask-reveal';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -184,17 +184,15 @@ let pendingPageFlips = 0;
|
||||
|
||||
const paperColor = new THREE.Color(0xece4ca);
|
||||
const inkColor = '#1a1009';
|
||||
const maxRevealWords = 128;
|
||||
const completedRevealElapsedMs = 1000000000;
|
||||
|
||||
await reportLabStep(48, 'Preparing high-resolution page textures');
|
||||
const leftCanvas = createPageCanvas('left');
|
||||
const rightCanvas = createPageCanvas('right');
|
||||
const leftRevealCanvas = createPageCanvas('left');
|
||||
const rightRevealCanvas = createPageCanvas('right');
|
||||
const leftTexture = new THREE.CanvasTexture(leftCanvas);
|
||||
const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
||||
const leftRevealTexture = new THREE.CanvasTexture(leftRevealCanvas);
|
||||
const rightRevealTexture = new THREE.CanvasTexture(rightRevealCanvas);
|
||||
[leftTexture, rightTexture, leftRevealTexture, rightRevealTexture].forEach((texture) => {
|
||||
[leftTexture, rightTexture].forEach((texture) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.anisotropy = maxTextureAnisotropy;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
@@ -205,14 +203,6 @@ const pageRevealState = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
const pageRevealCanvases = {
|
||||
left: leftRevealCanvas,
|
||||
right: rightRevealCanvas
|
||||
};
|
||||
const pageRevealTextures = {
|
||||
left: leftRevealTexture,
|
||||
right: rightRevealTexture
|
||||
};
|
||||
await reportLabStep(52, 'Generating leather texture set');
|
||||
const leatherTextures = createLeatherTextures();
|
||||
await reportLabStep(56, 'Generating spine cloth texture set');
|
||||
@@ -357,12 +347,10 @@ const materials = {
|
||||
})
|
||||
};
|
||||
materials.leftPage.userData.bookPageReveal = {
|
||||
side: 'left',
|
||||
texture: leftRevealTexture
|
||||
side: 'left'
|
||||
};
|
||||
materials.rightPage.userData.bookPageReveal = {
|
||||
side: 'right',
|
||||
texture: rightRevealTexture
|
||||
side: 'right'
|
||||
};
|
||||
materials.spineCloth.userData.isSpineCloth = true;
|
||||
materials.headband.userData.isHeadband = true;
|
||||
@@ -475,6 +463,12 @@ window.BookLabDebug = {
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
document.addEventListener('webgl-book:page-canvases', handlePageCanvases);
|
||||
document.addEventListener('webgl-book:page-reveal-start', (event) => {
|
||||
startPageRevealForBlock(event.detail?.blockId);
|
||||
});
|
||||
document.addEventListener('webgl-book:page-reveal-fast-forward', (event) => {
|
||||
fastForwardPageReveals(event.detail?.blockIds || []);
|
||||
});
|
||||
installBookControls();
|
||||
installCameraControls();
|
||||
resize();
|
||||
@@ -568,10 +562,12 @@ function configureBookShadowReceiver(material, strength) {
|
||||
shader.uniforms.bookShadowReceiverStrength = { value: strength };
|
||||
shader.uniforms.bookTableTopY = { value: tableTopY };
|
||||
if (pageReveal) {
|
||||
shader.uniforms.bookRevealMap = { value: pageReveal.texture };
|
||||
shader.uniforms.bookRevealActive = { value: 0 };
|
||||
shader.uniforms.bookRevealProgress = { value: 1 };
|
||||
shader.uniforms.bookRevealBounds = { value: new THREE.Vector4(0, 0, 1, 1) };
|
||||
shader.uniforms.bookRevealElapsedMs = { value: completedRevealElapsedMs };
|
||||
shader.uniforms.bookRevealWordCount = { value: 0 };
|
||||
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 };
|
||||
material.userData.bookRevealShader = shader;
|
||||
}
|
||||
@@ -608,18 +604,29 @@ function configureBookShadowReceiver(material, strength) {
|
||||
uniform vec2 bookShadowMapTexelSize;
|
||||
uniform float bookShadowReceiverStrength;
|
||||
uniform float bookTableTopY;
|
||||
${pageReveal ? `uniform sampler2D bookRevealMap;
|
||||
uniform float bookRevealActive;
|
||||
uniform float bookRevealProgress;
|
||||
uniform vec4 bookRevealBounds;
|
||||
${pageReveal ? `uniform float bookRevealActive;
|
||||
uniform float bookRevealElapsedMs;
|
||||
uniform int bookRevealWordCount;
|
||||
uniform vec4 bookRevealWordRects[128];
|
||||
uniform vec4 bookRevealWordTimings[128];
|
||||
uniform vec3 bookRevealPaperColor;
|
||||
uniform float bookRevealSoftness;
|
||||
|
||||
float bookRevealMask(vec2 uv) {
|
||||
vec2 local = (uv - bookRevealBounds.xy) / max(bookRevealBounds.zw, vec2(0.0001));
|
||||
float inside = step(0.0, local.x) * step(0.0, local.y) * step(local.x, 1.0) * step(local.y, 1.0);
|
||||
float diagonal = clamp((local.x + (1.0 - local.y)) * 0.5, 0.0, 1.0);
|
||||
float feather = max(0.0001, bookRevealSoftness);
|
||||
return inside * smoothstep(diagonal - feather, diagonal + feather, bookRevealProgress);
|
||||
float bookRevealVisibleMask(vec2 uv) {
|
||||
float hidden = 0.0;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (i >= bookRevealWordCount) break;
|
||||
vec4 rect = bookRevealWordRects[i];
|
||||
vec2 local = (uv - rect.xy) / max(rect.zw, vec2(0.0001));
|
||||
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 feather = max(0.0001, bookRevealSoftness);
|
||||
float visible = smoothstep(scan - feather, scan + feather, progress);
|
||||
hidden = max(hidden, inside * (1.0 - visible));
|
||||
}
|
||||
return hidden;
|
||||
}` : ''}
|
||||
varying vec3 vBookReceiverWorldPosition;
|
||||
varying vec3 vBookReceiverWorldNormal;
|
||||
@@ -764,8 +771,10 @@ function configureBookShadowReceiver(material, strength) {
|
||||
`#ifdef USE_MAP
|
||||
vec4 sampledDiffuseColor = texture2D(map, vMapUv);
|
||||
if (bookRevealActive > 0.5) {
|
||||
vec4 revealDiffuseColor = texture2D(bookRevealMap, vMapUv);
|
||||
sampledDiffuseColor = mix(sampledDiffuseColor, revealDiffuseColor, bookRevealMask(vMapUv));
|
||||
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);
|
||||
}
|
||||
diffuseColor *= sampledDiffuseColor;
|
||||
#endif`
|
||||
@@ -1640,44 +1649,58 @@ function uploadPageTextureDirect(side, sourceCanvas) {
|
||||
}
|
||||
|
||||
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
const revealCanvas = pageRevealCanvases[side];
|
||||
const revealTexture = pageRevealTextures[side];
|
||||
if (!revealCanvas || !revealTexture) {
|
||||
uploadPageTextureDirect(side, sourceCanvas);
|
||||
return;
|
||||
}
|
||||
|
||||
drawCanvasPageTexture(revealCanvas, sourceCanvas, side);
|
||||
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (!shader?.uniforms) {
|
||||
uploadPageTextureDirect(side, sourceCanvas);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = revealDetail.bounds || {};
|
||||
const x = THREE.MathUtils.clamp(Number(bounds.x || 0), 0, 1);
|
||||
const y = THREE.MathUtils.clamp(Number(bounds.y || 0), 0, 1);
|
||||
const width = THREE.MathUtils.clamp(Number(bounds.width || 1), 0.001, 1);
|
||||
const height = THREE.MathUtils.clamp(Number(bounds.height || 1), 0.001, 1);
|
||||
shader.uniforms.bookRevealBounds.value.set(
|
||||
x,
|
||||
THREE.MathUtils.clamp(1 - y - height, 0, 1),
|
||||
width,
|
||||
height
|
||||
);
|
||||
shader.uniforms.bookRevealProgress.value = 0;
|
||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||
texture.needsUpdate = true;
|
||||
applyPageRevealWords(shader, revealDetail.wordRects || []);
|
||||
shader.uniforms.bookRevealActive.value = 1;
|
||||
shader.uniforms.bookRevealMap.value = revealTexture;
|
||||
revealTexture.needsUpdate = true;
|
||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
|
||||
pageRevealState[side] = {
|
||||
startedAt: performance.now(),
|
||||
startedAt: revealDetail.startNow ? performance.now() : null,
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
revealCanvas,
|
||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||
};
|
||||
}
|
||||
|
||||
function applyPageRevealWords(shader, words = []) {
|
||||
const rectUniforms = shader.uniforms.bookRevealWordRects.value;
|
||||
const timingUniforms = shader.uniforms.bookRevealWordTimings.value;
|
||||
const source = Array.isArray(words) ? words.slice(0, maxRevealWords) : [];
|
||||
shader.uniforms.bookRevealWordCount.value = source.length;
|
||||
source.forEach((word, index) => {
|
||||
const rect = word.rect || {};
|
||||
const timing = word.timing || {};
|
||||
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);
|
||||
const height = THREE.MathUtils.clamp(Number(rect.height || 0), 0, 1);
|
||||
rectUniforms[index].set(
|
||||
x,
|
||||
THREE.MathUtils.clamp(1 - y - height, 0, 1),
|
||||
Math.max(0.0001, width),
|
||||
Math.max(0.0001, height)
|
||||
);
|
||||
timingUniforms[index].set(
|
||||
Math.max(0, Number(timing.delay || 0)),
|
||||
Math.max(1, Number(timing.duration || 1)),
|
||||
0,
|
||||
0
|
||||
);
|
||||
});
|
||||
for (let index = source.length; index < maxRevealWords; index += 1) {
|
||||
rectUniforms[index].set(0, 0, 0, 0);
|
||||
timingUniforms[index].set(0, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function getPageRevealShader(side) {
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
return material?.userData?.bookRevealShader || null;
|
||||
@@ -1688,10 +1711,34 @@ function clearPageReveal(side) {
|
||||
const shader = getPageRevealShader(side);
|
||||
if (shader?.uniforms?.bookRevealActive) {
|
||||
shader.uniforms.bookRevealActive.value = 0;
|
||||
shader.uniforms.bookRevealProgress.value = 1;
|
||||
shader.uniforms.bookRevealElapsedMs.value = completedRevealElapsedMs;
|
||||
shader.uniforms.bookRevealWordCount.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function startPageRevealForBlock(blockId) {
|
||||
const id = String(blockId ?? '');
|
||||
['left', 'right'].forEach((side) => {
|
||||
const state = pageRevealState[side];
|
||||
if (!state || state.startedAt != null) return;
|
||||
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
||||
state.startedAt = performance.now();
|
||||
const shader = getPageRevealShader(side);
|
||||
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function fastForwardPageReveals(blockIds = []) {
|
||||
const ids = new Set((Array.isArray(blockIds) ? blockIds : []).map(value => String(value)));
|
||||
['left', 'right'].forEach((side) => {
|
||||
const state = pageRevealState[side];
|
||||
if (!state) return;
|
||||
const matches = ids.size === 0 || state.blockIds.some(blockId => ids.has(String(blockId)));
|
||||
if (!matches) return;
|
||||
clearPageReveal(side);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageRevealAnimations(now) {
|
||||
['left', 'right'].forEach((side) => {
|
||||
const state = pageRevealState[side];
|
||||
@@ -1701,14 +1748,14 @@ function updatePageRevealAnimations(now) {
|
||||
clearPageReveal(side);
|
||||
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.bookRevealProgress.value = progress;
|
||||
shader.uniforms.bookRevealElapsedMs.value = Math.max(0, now - state.startedAt);
|
||||
if (progress < 1) return;
|
||||
|
||||
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
drawCanvasPageTexture(canvas, state.revealCanvas, side);
|
||||
texture.needsUpdate = true;
|
||||
clearPageReveal(side);
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||
detail: {
|
||||
|
||||
Reference in New Issue
Block a user