diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index ae2b53f..0e2d33a 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -25,7 +25,12 @@ const appInitialState = window.WebGLBookInitialState || {}; const tableDebugName = urlParams.get('tableDebug') || 'none'; const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none; const isAppIntegrationMode = appInitialState.appMode === true; -const appRenderPixelRatio = 2; +// Render scale. The book is tilted, so page glyphs are minified along the tilt and need +// supersampling to stay crisp — 2× is the established sharp baseline. This is deliberately +// NOT reduced for performance (that blurs text); 60fps comes from the reflection/shadow +// passes instead, whose cost is independent of this scale. +const renderSupersample = 2; +const appRenderPixelRatio = Math.min((window.devicePixelRatio || 1) * renderSupersample, 2); const labStatus = document.getElementById('lab_status'); if (labStatus && tableDebugMode !== tableDebugModes.none) { labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`; @@ -127,6 +132,12 @@ const bookShadowBiasMatrix = new THREE.Matrix4().set( 0, 0, 0, 1 ); const dynamicBufferRefreshIntervalMs = 1000 / 30; +// Shadows and the table reflection only need full-rate updates while the book geometry is +// moving (a page flip). When the geometry is static — idle, or a text reveal where only the +// page texture mask animates — the soft shadows/reflection refresh far less often, so those +// frames are just the cheap scene render and hold 60fps. Candle flicker is the only thing +// changing them then, which 8Hz captures imperceptibly. +const staticGeometryBufferRefreshIntervalMs = 1000 / 8; const flipDynamicBufferGraceMs = 180; let lastBookShadowRefreshAt = -Infinity; let lastTableReflectionRefreshAt = -Infinity; @@ -4686,11 +4697,13 @@ function animate(now = performance.now()) { ? Math.min(...activeFlips.map(flip => Math.max(0, now - Number(flip.startTime || now)))) : Infinity; const deferDynamicBuffersForFlipStart = activeFlips.length > 0 && newestFlipAge < flipDynamicBufferGraceMs; + const geometryAnimating = activeFlips.length > 0; + const bufferRefreshIntervalMs = geometryAnimating ? dynamicBufferRefreshIntervalMs : staticGeometryBufferRefreshIntervalMs; const shadowRefreshDue = !deferDynamicBuffersForFlipStart && ( - forceDynamicBufferRefresh || now - lastBookShadowRefreshAt >= dynamicBufferRefreshIntervalMs + forceDynamicBufferRefresh || now - lastBookShadowRefreshAt >= bufferRefreshIntervalMs ); const reflectionRefreshDue = !deferDynamicBuffersForFlipStart && ( - forceDynamicBufferRefresh || now - lastTableReflectionRefreshAt >= dynamicBufferRefreshIntervalMs + forceDynamicBufferRefresh || now - lastTableReflectionRefreshAt >= bufferRefreshIntervalMs ); const bothHeavyPassesDue = shadowRefreshDue && reflectionRefreshDue && !forceDynamicBufferRefresh; const refreshShadowsThisFrame = shadowRefreshDue && (