Add WebGL FPS cap and texture word reveal

This commit is contained in:
2026-06-06 15:37:44 +02:00
parent bc736513d4
commit 431e305df9
5 changed files with 219 additions and 18 deletions
+70 -13
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=20260606-webgl-overlay-page-layout';
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-fps-texture-animation';
const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab';
@@ -25,12 +25,13 @@ const appInitialState = window.WebGLBookInitialState || {};
const tableDebugName = urlParams.get('tableDebug') || 'none';
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
const isAppIntegrationMode = appInitialState.appMode === true;
const appRenderPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2);
const labStatus = document.getElementById('lab_status');
if (labStatus && tableDebugMode !== tableDebugModes.none) {
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
}
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setPixelRatio(appRenderPixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
@@ -39,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
const generatedTextureCanvases = {};
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const reflectionPixelRatio = isAppIntegrationMode ? 0.28 : Math.min(window.devicePixelRatio || 1, 2);
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
const reflectionTargetSize = new THREE.Vector2();
const pageRaycaster = new THREE.Raycaster();
@@ -64,13 +65,13 @@ let tableDustTexture = null;
let tableGreaseTexture = null;
const tableTopY = -0.02;
const bookTableContactClearance = 0.002;
const tableReflectionBaseWidth = isAppIntegrationMode ? 1280 : 4096;
const tableReflectionBaseHeight = isAppIntegrationMode ? 720 : 2304;
const tableReflectionBaseWidth = isAppIntegrationMode ? 480 : 4096;
const tableReflectionBaseHeight = isAppIntegrationMode ? 270 : 2304;
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
colorSpace: THREE.SRGBColorSpace,
depthBuffer: true,
stencilBuffer: false,
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 2 : 8) : 0
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0
});
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
@@ -89,7 +90,7 @@ const reflectionUp = new THREE.Vector3();
const candleShadowSources = [];
const candleWorldPosition = new THREE.Vector3();
const flameWorldPosition = new THREE.Vector3();
const bookShadowMapSize = isAppIntegrationMode ? 512 : 1536;
const bookShadowMapSize = isAppIntegrationMode ? 128 : 1536;
const bookShadowTargets = Array.from({ length: 3 }, () => {
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
colorSpace: THREE.NoColorSpace,
@@ -141,6 +142,12 @@ updateCameraRig(0);
configureScenePostprocessing();
const clock = new THREE.Clock();
const targetFrameDurationMs = 1000 / 30;
let lastRenderFrameAt = 0;
let fpsDisplay = null;
let fpsWindowStartedAt = performance.now();
let fpsWindowFrames = 0;
const lastFrameTiming = {};
const book = new THREE.Group();
scene.add(book);
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
@@ -421,8 +428,33 @@ installBookControls();
installCameraControls();
resize();
document.dispatchEvent(new CustomEvent('webgl-book:scene-ready'));
ensureFpsDisplay();
animate();
function ensureFpsDisplay() {
if (fpsDisplay) return fpsDisplay;
fpsDisplay = document.createElement('div');
fpsDisplay.id = 'webgl_fps_display';
Object.assign(fpsDisplay.style, {
position: 'fixed',
top: '0.65rem',
right: '0.75rem',
zIndex: '80',
minWidth: '4.2rem',
padding: '0.22rem 0.42rem',
border: '1px solid rgba(246, 231, 201, 0.28)',
background: 'rgba(10, 7, 4, 0.62)',
color: 'rgba(255, 238, 202, 0.94)',
font: '12px ui-monospace, SFMono-Regular, Consolas, monospace',
lineHeight: '1.2',
textAlign: 'right',
pointerEvents: 'none'
});
fpsDisplay.textContent = '0 fps';
document.body.appendChild(fpsDisplay);
return fpsDisplay;
}
function buildTable() {
const tableTexture = new THREE.TextureLoader().load('/assets/webgl/wood_table_diff_1k.jpg');
tableTexture.colorSpace = THREE.SRGBColorSpace;
@@ -661,14 +693,14 @@ function configureScenePostprocessing() {
colorSpace: THREE.SRGBColorSpace,
depthBuffer: true,
stencilBuffer: false,
samples: renderer.capabilities.isWebGL2 ? 8 : 0
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0
});
sceneComposerTarget.texture.colorSpace = THREE.SRGBColorSpace;
sceneComposerTarget.texture.minFilter = THREE.LinearFilter;
sceneComposerTarget.texture.magFilter = THREE.LinearFilter;
composer = new EffectComposer(renderer, sceneComposerTarget);
composer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
composer.setPixelRatio(appRenderPixelRatio);
sceneRenderPass = new RenderPass(scene, camera);
composer.addPass(sceneRenderPass);
@@ -2543,7 +2575,7 @@ function resize() {
camera.aspect = width / height;
camera.updateProjectionMatrix();
const desiredReflectionScale = reflectionPixelRatio * 1.5;
const reflectionScale = Math.max(1, Math.min(
const reflectionScale = Math.max(isAppIntegrationMode ? 0.35 : 1, Math.min(
desiredReflectionScale,
4096 / width,
2304 / height
@@ -2812,9 +2844,17 @@ function renderMirrorDebugView() {
});
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
function animate(now = performance.now()) {
const elapsedSinceLastFrame = lastRenderFrameAt ? now - lastRenderFrameAt : targetFrameDurationMs;
if (lastRenderFrameAt && elapsedSinceLastFrame < targetFrameDurationMs) {
setTimeout(animate, Math.max(1, targetFrameDurationMs - elapsedSinceLastFrame));
return;
}
const frameElapsedMs = lastRenderFrameAt ? elapsedSinceLastFrame : targetFrameDurationMs;
lastRenderFrameAt = now;
setTimeout(animate, targetFrameDurationMs);
const delta = Math.min(0.1, frameElapsedMs / 1000);
clock.getDelta();
const t = clock.elapsedTime;
updateCameraRig(delta);
scene.traverse((object) => {
@@ -2847,12 +2887,17 @@ function animate() {
updateActiveFlips(performance.now());
updateCandleShadowUniforms();
renderedFrameCount += 1;
const shadowStartedAt = performance.now();
if (!isAppIntegrationMode || renderedFrameCount % 6 === 1 || activeFlips.length > 0) {
updateBookShadowMaps();
}
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
const reflectionStartedAt = performance.now();
if (!isAppIntegrationMode || renderedFrameCount % 4 === 1 || cameraRig.navigationActive || activeFlips.length > 0) {
updateTableReflection();
}
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
const renderStartedAt = performance.now();
if (tableDebugMode === tableDebugModes.mirror) {
renderer.setRenderTarget(null);
renderer.clear();
@@ -2862,6 +2907,18 @@ function animate() {
} else {
renderer.render(scene, camera);
}
lastFrameTiming.render = performance.now() - renderStartedAt;
lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render;
window.BookLabDebug.renderedFrames += 1;
window.BookLabDebug.ready = true;
fpsWindowFrames += 1;
if (now - fpsWindowStartedAt >= 500) {
const fps = Math.round((fpsWindowFrames * 1000) / Math.max(1, now - fpsWindowStartedAt));
ensureFpsDisplay().textContent = `${fps} fps`;
document.documentElement.dataset.webglFps = String(fps);
fpsWindowFrames = 0;
fpsWindowStartedAt = now;
}
document.documentElement.dataset.webglRenderedFrames = String(window.BookLabDebug.renderedFrames);
document.documentElement.dataset.webglFrameTiming = JSON.stringify(lastFrameTiming);
}