Add shader page reveal checkpoint
This commit is contained in:
+161
-7
@@ -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-paper-loader-fix';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-shader-reveal';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -188,15 +188,31 @@ const inkColor = '#1a1009';
|
||||
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);
|
||||
[leftTexture, rightTexture].forEach((texture) => {
|
||||
const leftRevealTexture = new THREE.CanvasTexture(leftRevealCanvas);
|
||||
const rightRevealTexture = new THREE.CanvasTexture(rightRevealCanvas);
|
||||
[leftTexture, rightTexture, leftRevealTexture, rightRevealTexture].forEach((texture) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.anisotropy = maxTextureAnisotropy;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = true;
|
||||
});
|
||||
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');
|
||||
@@ -340,6 +356,14 @@ const materials = {
|
||||
envMapIntensity: 0
|
||||
})
|
||||
};
|
||||
materials.leftPage.userData.bookPageReveal = {
|
||||
side: 'left',
|
||||
texture: leftRevealTexture
|
||||
};
|
||||
materials.rightPage.userData.bookPageReveal = {
|
||||
side: 'right',
|
||||
texture: rightRevealTexture
|
||||
};
|
||||
materials.spineCloth.userData.isSpineCloth = true;
|
||||
materials.headband.userData.isHeadband = true;
|
||||
configureHardcoverPaperMaterial(materials.pageBlock);
|
||||
@@ -535,13 +559,22 @@ function configureBookShadowReceiver(material, strength) {
|
||||
const isSpineCloth = material.userData?.isSpineCloth === true;
|
||||
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
||||
const isHeadband = material.userData?.isHeadband === true;
|
||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||
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.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||
shader.uniforms.bookShadowMapTexelSize = { value: new THREE.Vector2(1 / bookShadowMapSize, 1 / bookShadowMapSize) };
|
||||
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.bookRevealSoftness = { value: 0.035 };
|
||||
material.userData.bookRevealShader = shader;
|
||||
}
|
||||
|
||||
shader.vertexShader = shader.vertexShader
|
||||
.replace(
|
||||
@@ -575,6 +608,19 @@ 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;
|
||||
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);
|
||||
}` : ''}
|
||||
varying vec3 vBookReceiverWorldPosition;
|
||||
varying vec3 vBookReceiverWorldNormal;
|
||||
${isSpineCloth || isHardcoverPaper || isHeadband ? 'varying vec2 vBookSurfaceUv;' : ''}
|
||||
@@ -712,6 +758,19 @@ function configureBookShadowReceiver(material, strength) {
|
||||
outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb);
|
||||
#include <opaque_fragment>`
|
||||
);
|
||||
if (pageReveal) {
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
'#include <map_fragment>',
|
||||
`#ifdef USE_MAP
|
||||
vec4 sampledDiffuseColor = texture2D(map, vMapUv);
|
||||
if (bookRevealActive > 0.5) {
|
||||
vec4 revealDiffuseColor = texture2D(bookRevealMap, vMapUv);
|
||||
sampledDiffuseColor = mix(sampledDiffuseColor, revealDiffuseColor, bookRevealMask(vMapUv));
|
||||
}
|
||||
diffuseColor *= sampledDiffuseColor;
|
||||
#endif`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1551,12 +1610,18 @@ function syncBookControls() {
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
if (detail.left) {
|
||||
drawCanvasPageTexture(leftCanvas, detail.left, 'left');
|
||||
leftTexture.needsUpdate = true;
|
||||
if (detail.reveal?.left) {
|
||||
beginPageReveal('left', detail.left, detail.reveal.left);
|
||||
} else {
|
||||
uploadPageTextureDirect('left', detail.left);
|
||||
}
|
||||
}
|
||||
if (detail.right) {
|
||||
drawCanvasPageTexture(rightCanvas, detail.right, 'right');
|
||||
rightTexture.needsUpdate = true;
|
||||
if (detail.reveal?.right) {
|
||||
beginPageReveal('right', detail.right, detail.reveal.right);
|
||||
} else {
|
||||
uploadPageTextureDirect('right', detail.right);
|
||||
}
|
||||
}
|
||||
markStaticSceneBuffersDirty();
|
||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||
@@ -1566,6 +1631,94 @@ function handlePageCanvases(event) {
|
||||
});
|
||||
}
|
||||
|
||||
function uploadPageTextureDirect(side, sourceCanvas) {
|
||||
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
clearPageReveal(side);
|
||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
|
||||
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 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;
|
||||
shader.uniforms.bookRevealActive.value = 1;
|
||||
shader.uniforms.bookRevealMap.value = revealTexture;
|
||||
revealTexture.needsUpdate = true;
|
||||
|
||||
pageRevealState[side] = {
|
||||
startedAt: performance.now(),
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
revealCanvas,
|
||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||
};
|
||||
}
|
||||
|
||||
function getPageRevealShader(side) {
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
return material?.userData?.bookRevealShader || null;
|
||||
}
|
||||
|
||||
function clearPageReveal(side) {
|
||||
pageRevealState[side] = null;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (shader?.uniforms?.bookRevealActive) {
|
||||
shader.uniforms.bookRevealActive.value = 0;
|
||||
shader.uniforms.bookRevealProgress.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePageRevealAnimations(now) {
|
||||
['left', 'right'].forEach((side) => {
|
||||
const state = pageRevealState[side];
|
||||
if (!state) return;
|
||||
const shader = getPageRevealShader(side);
|
||||
if (!shader?.uniforms) {
|
||||
clearPageReveal(side);
|
||||
return;
|
||||
}
|
||||
const progress = THREE.MathUtils.clamp((now - state.startedAt) / state.durationMs, 0, 1);
|
||||
shader.uniforms.bookRevealProgress.value = progress;
|
||||
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: {
|
||||
side,
|
||||
blockIds: state.blockIds
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#f2ead0';
|
||||
@@ -2998,6 +3151,7 @@ function animate(now = performance.now()) {
|
||||
const hadActiveFlips = activeFlips.length > 0;
|
||||
updateActiveFlips(performance.now());
|
||||
if (hadActiveFlips) markStaticSceneBuffersDirty();
|
||||
updatePageRevealAnimations(now);
|
||||
updateCandleShadowUniforms();
|
||||
renderedFrameCount += 1;
|
||||
const shadowStartedAt = performance.now();
|
||||
|
||||
Reference in New Issue
Block a user