Add WebGL FPS cap and texture word reveal
This commit is contained in:
+70
-13
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user