From 1b593c8c7ba30e7c7361fb02dad6fd2e18529180 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sun, 7 Jun 2026 11:13:05 +0200 Subject: [PATCH] Restore WebGL book quality settings --- public/index.html | 2 +- public/js/book-page-format-module.js | 73 ++++++++- public/js/book-texture-renderer-module.js | 10 +- public/js/loader.js | 2 +- public/js/procedural-book-model.js | 48 +++++- public/js/webgl-book-lab.js | 181 +++++++++++++++------- 6 files changed, 247 insertions(+), 69 deletions(-) diff --git a/public/index.html b/public/index.html index 6c63a81..940c17e 100644 --- a/public/index.html +++ b/public/index.html @@ -280,6 +280,6 @@ console.log(message); }; - + diff --git a/public/js/book-page-format-module.js b/public/js/book-page-format-module.js index 019f58e..34591fd 100644 --- a/public/js/book-page-format-module.js +++ b/public/js/book-page-format-module.js @@ -3,6 +3,7 @@ * Defines the canonical page geometry used by the WebGL book renderer. */ import { BaseModule } from './base-module.js'; +import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-physical-stack-quality'; class BookPageFormatModule extends BaseModule { constructor() { @@ -17,8 +18,13 @@ class BookPageFormatModule extends BaseModule { margins: Object.freeze({ topIn: 0.46, bottomIn: 0.58, - innerIn: 0.56, - outerIn: 0.44 + innerBaseIn: 0.375, + innerMinIn: 0.44, + innerMaxIn: 0.68, + innerThicknessFactor: 0.25, + outerBaseIn: 0.44, + outerThicknessFactor: 0.04, + outerMaxIn: 0.5 }), typography: Object.freeze({ fontFamily: '"EB Garamond", "EB Garamond 12", serif', @@ -28,16 +34,27 @@ class BookPageFormatModule extends BaseModule { dropCapLines: 2 }) }); + this.pageCount = snapProceduralPageCount(window.WebGLBookInitialState?.pageCount ?? 300); this.bindMethods([ 'getFormat', 'getAspectRatio', 'getTextureMetrics', + 'setPageCount', + 'getPageCount', + 'getDynamicMargins', 'inchesToTexture' ]); } async initialize() { + this.addEventListener(document, 'webgl-book:page-count-changed', (event) => { + this.setPageCount(event.detail?.pageCount); + }); + this.addEventListener(document, 'preference-updated', (event) => { + const detail = event.detail || {}; + if (detail.category === 'webgl' && detail.key === 'bookPageCount') this.setPageCount(detail.value); + }); this.reportProgress(100, 'Book page format ready'); return true; } @@ -54,14 +71,49 @@ class BookPageFormatModule extends BaseModule { return (Number(valueIn) / this.format.trim.heightIn) * textureHeight; } - getTextureMetrics(textureWidth = 1280) { + setPageCount(value) { + const nextPageCount = snapProceduralPageCount(value ?? this.pageCount); + if (nextPageCount === this.pageCount) return this.pageCount; + this.pageCount = nextPageCount; + return this.pageCount; + } + + getPageCount() { + return this.pageCount; + } + + getDynamicMargins(pageCount = this.pageCount) { + const marginConfig = this.format.margins; + const thickness = calculateProceduralBookThickness(pageCount); + const innerIn = Math.min( + marginConfig.innerMaxIn, + Math.max( + marginConfig.innerMinIn, + marginConfig.innerBaseIn + thickness.textBlockThicknessIn * marginConfig.innerThicknessFactor + ) + ); + const outerIn = Math.min( + marginConfig.outerMaxIn, + marginConfig.outerBaseIn + thickness.textBlockThicknessIn * marginConfig.outerThicknessFactor + ); + return { + topIn: 0.46, + bottomIn: 0.58, + innerIn, + outerIn, + thickness + }; + } + + getTextureMetrics(textureWidth = 1280, pageCount = this.pageCount) { const width = Math.max(1, Math.round(Number(textureWidth) || 1280)); const height = Math.round(width / this.getAspectRatio()); + const dynamicMargins = this.getDynamicMargins(pageCount); const margins = { - top: this.inchesToTexture(this.format.margins.topIn, height), - bottom: this.inchesToTexture(this.format.margins.bottomIn, height), - inner: this.inchesToTexture(this.format.margins.innerIn, height), - outer: this.inchesToTexture(this.format.margins.outerIn, height) + top: this.inchesToTexture(dynamicMargins.topIn, height), + bottom: this.inchesToTexture(dynamicMargins.bottomIn, height), + inner: this.inchesToTexture(dynamicMargins.innerIn, height), + outer: this.inchesToTexture(dynamicMargins.outerIn, height) }; const content = { x: margins.outer, @@ -89,6 +141,13 @@ class BookPageFormatModule extends BaseModule { margins, content, contentBySide, + marginsIn: { + top: dynamicMargins.topIn, + bottom: dynamicMargins.bottomIn, + inner: dynamicMargins.innerIn, + outer: dynamicMargins.outerIn + }, + thickness: dynamicMargins.thickness, linesPerPage, bodyFontSizePx, typographyLineHeightPx, diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index 60dbaf3..b748288 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -53,6 +53,7 @@ class BookTextureRendererModule extends BaseModule { 'publishSpread', 'getPageCanvas', 'getHitMap', + 'handlePageCountChanged', 'handleSceneReady' ]); } @@ -64,6 +65,7 @@ class BookTextureRendererModule extends BaseModule { this.reportProgress(20, 'Preparing page texture canvases'); this.createPageCanvases(); this.drawEmptySpread(); + this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged); this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady); this.addEventListener(document, 'book-pagination:spread-updated', (event) => { const latestBlockId = event.detail?.latestBlockId; @@ -84,7 +86,7 @@ class BookTextureRendererModule extends BaseModule { return true; } - createPageCanvases(textureWidth = 1280) { + createPageCanvases(textureWidth = 3072) { this.metrics = this.pageFormat.getTextureMetrics(textureWidth); ['left', 'right'].forEach((side) => { const canvas = document.createElement('canvas'); @@ -408,6 +410,12 @@ class BookTextureRendererModule extends BaseModule { return this.hitMaps[side] || []; } + handlePageCountChanged(event) { + this.pageFormat?.setPageCount?.(event.detail?.pageCount); + this.createPageCanvases(); + this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.()); + } + handleSceneReady() { this.publishSpread(); } diff --git a/public/js/loader.js b/public/js/loader.js index 1cbd73b..5d28210 100644 --- a/public/js/loader.js +++ b/public/js/loader.js @@ -24,7 +24,7 @@ const ModuleState = { ERROR: 'ERROR' }; -const MODULE_CACHE_BUSTER = '20260607-webgl-page-uv-endpoints'; +const MODULE_CACHE_BUSTER = '20260607-webgl-physical-stack-quality'; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER; /** diff --git a/public/js/procedural-book-model.js b/public/js/procedural-book-model.js index e3bd10e..d25f66a 100644 --- a/public/js/procedural-book-model.js +++ b/public/js/procedural-book-model.js @@ -4,6 +4,10 @@ export const PROCEDURAL_BOOK = { PAGE_COUNT_MIN: 40, PAGE_COUNT_MAX: 500, PAGE_COUNT_STEP: 10, + TRIM_WIDTH_IN: 4.25, + TRIM_HEIGHT_IN: 6.87, + PAPER_CALIPER_MM: 0.097, + PAGES_PER_BUNDLE: 10, PAGE_LINE_SEGMENTS: 48, PAGE_DEPTH: 2.24, PAGE_WIDTH: 2.24 * (4.25 / 6.87), @@ -16,11 +20,16 @@ export const PROCEDURAL_BOOK = { raisedHingeY: 0.056, paperContactOffset: 0.0012, singlePageCoverGap: 0.006, - bundleSpacing: 0.014 + bundleSpacing: 0.0062 } }; PROCEDURAL_BOOK.PAGE_SPLINE_LENGTH = PROCEDURAL_BOOK.PAGE_WIDTH; PROCEDURAL_BOOK.COVER_OVERHANG = (PROCEDURAL_BOOK.COVER_DEPTH - PROCEDURAL_BOOK.PAGE_DEPTH) * 0.5; +PROCEDURAL_BOOK.MODEL_UNITS_PER_INCH = PROCEDURAL_BOOK.PAGE_DEPTH / PROCEDURAL_BOOK.TRIM_HEIGHT_IN; +PROCEDURAL_BOOK.PAPER_CALIPER_IN = PROCEDURAL_BOOK.PAPER_CALIPER_MM / 25.4; +PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL = PROCEDURAL_BOOK.PAPER_CALIPER_IN * PROCEDURAL_BOOK.MODEL_UNITS_PER_INCH; +PROCEDURAL_BOOK.BUNDLE_SHEET_COUNT = PROCEDURAL_BOOK.PAGES_PER_BUNDLE / 2; +PROCEDURAL_BOOK.BUNDLE_THICKNESS_MODEL = PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL * PROCEDURAL_BOOK.BUNDLE_SHEET_COUNT; export function snapProceduralPageCount(value) { const parsed = Number.parseFloat(value); @@ -32,6 +41,33 @@ export function snapProceduralPageCount(value) { ); } +export function calculateProceduralBookThickness(pageCountValue) { + const pageCount = snapProceduralPageCount(pageCountValue); + const sheetCount = Math.max(1, pageCount / 2); + const bundleCount = Math.max(4, Math.round(pageCount / PROCEDURAL_BOOK.PAGES_PER_BUNDLE)); + const sheetThicknessIn = PROCEDURAL_BOOK.PAPER_CALIPER_IN; + const sheetThicknessModel = PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL; + const bundleSheetCount = PROCEDURAL_BOOK.BUNDLE_SHEET_COUNT; + const bundleThicknessIn = sheetThicknessIn * bundleSheetCount; + const bundleThicknessModel = sheetThicknessModel * bundleSheetCount; + const textBlockThicknessIn = sheetThicknessIn * sheetCount; + const textBlockThicknessModel = sheetThicknessModel * sheetCount; + return { + pageCount, + sheetCount, + bundleCount, + sheetThicknessIn, + sheetThicknessModel, + bundleSheetCount, + bundleThicknessIn, + bundleThicknessModel, + tenPageStackThicknessIn: bundleThicknessIn, + tenPageStackThicknessModel: bundleThicknessModel, + textBlockThicknessIn, + textBlockThicknessModel + }; +} + export function createProceduralBookModel(options = {}) { const context = createBookContext(options); const group = new THREE.Group(); @@ -104,7 +140,8 @@ function calculateBookModel(context) { const pageWidth = PROCEDURAL_BOOK.PAGE_WIDTH; const pageDepth = PROCEDURAL_BOOK.PAGE_DEPTH; const coverDepth = PROCEDURAL_BOOK.COVER_DEPTH; - const bundleCount = Math.max(4, Math.round(context.pageCount / 10)); + const thickness = calculateProceduralBookThickness(context.pageCount); + const bundleCount = thickness.bundleCount; const spineWidth = calculateSpineWidth(bundleCount); const leftCount = calculateLeftBundleCount(context, bundleCount); const spineHalf = spineArcHalf(spineWidth); @@ -125,6 +162,7 @@ function calculateBookModel(context) { coverOuterX, bundleSpacing, leftCount, + thickness, lines }; } @@ -872,9 +910,9 @@ function pointAtMeasuredPathDistance(support, distance) { function calculateSpineWidth(bundleCount) { const minimumWidth = 0.006; if (bundleCount <= 1) return minimumWidth; - const targetArcLength = (bundleCount - 1) * PROCEDURAL_BOOK.PROFILE.bundleSpacing + PROCEDURAL_BOOK.OPEN_SEAM_GAP; + const targetArcLength = (bundleCount - 1) * PROCEDURAL_BOOK.BUNDLE_THICKNESS_MODEL + PROCEDURAL_BOOK.OPEN_SEAM_GAP; let low = minimumWidth; - let high = Math.max(minimumWidth, bundleCount * PROCEDURAL_BOOK.PROFILE.bundleSpacing * 1.4); + let high = Math.max(minimumWidth, bundleCount * PROCEDURAL_BOOK.BUNDLE_THICKNESS_MODEL * 1.4); while (measureSpineArcLength(high) < targetArcLength) high *= 1.25; for (let i = 0; i < 24; i += 1) { const mid = (low + high) * 0.5; @@ -887,7 +925,7 @@ function calculateSpineWidth(bundleCount) { function calculateBundleSpacing(bundleCount, spineWidth, leftCount) { const rightCount = bundleCount - leftCount; const stackIntervals = Math.max(0, leftCount - 1) + Math.max(0, rightCount - 1); - if (stackIntervals <= 0) return PROCEDURAL_BOOK.PROFILE.bundleSpacing; + if (stackIntervals <= 0) return PROCEDURAL_BOOK.BUNDLE_THICKNESS_MODEL; return Math.max(0.001, (measureSpineArcLength(spineWidth) - PROCEDURAL_BOOK.OPEN_SEAM_GAP) / stackIntervals); } diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index 33cf745..dc32451 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -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-page-uv-endpoints'; +import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-physical-stack-quality'; const canvas = document.getElementById('scene'); canvas.style.cursor = 'grab'; @@ -25,7 +25,7 @@ const appInitialState = window.WebGLBookInitialState || {}; const tableDebugName = urlParams.get('tableDebug') || 'none'; const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none; const isAppIntegrationMode = appInitialState.appMode === true; -const appRenderPixelRatio = isAppIntegrationMode ? 1 : Math.min(window.devicePixelRatio || 1, 2); +const appRenderPixelRatio = 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}`; @@ -40,8 +40,8 @@ renderer.shadowMap.type = THREE.VSMShadowMap; const generatedTextureCanvases = {}; const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy(); -const reflectionPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2); -const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200; +const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2); +const pageTextureWidth = 3072; const reflectionTargetSize = new THREE.Vector2(); const pageRaycaster = new THREE.Raycaster(); const pointerNdc = new THREE.Vector2(); @@ -53,6 +53,8 @@ let sceneSmaaPass = null; let sceneOutputPass = null; const aoExcludedObjects = new Set(); let renderedFrameCount = 0; +let staticSceneBuffersDirty = true; +let lastStaticCameraSignature = ''; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x080604); @@ -65,13 +67,13 @@ let tableDustTexture = null; let tableGreaseTexture = null; const tableTopY = -0.02; const bookTableContactClearance = 0.002; -const tableReflectionBaseWidth = isAppIntegrationMode ? 640 : 4096; -const tableReflectionBaseHeight = isAppIntegrationMode ? 360 : 2304; +const tableReflectionBaseWidth = 4096; +const tableReflectionBaseHeight = 2304; const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, { colorSpace: THREE.SRGBColorSpace, depthBuffer: true, stencilBuffer: false, - samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0 + samples: renderer.capabilities.isWebGL2 ? 8 : 0 }); tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace; tableReflectionTarget.texture.minFilter = THREE.LinearFilter; @@ -90,7 +92,7 @@ const reflectionUp = new THREE.Vector3(); const candleShadowSources = []; const candleWorldPosition = new THREE.Vector3(); const flameWorldPosition = new THREE.Vector3(); -const bookShadowMapSize = isAppIntegrationMode ? 256 : 1536; +const bookShadowMapSize = 1536; const bookShadowTargets = Array.from({ length: 3 }, () => { const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, { colorSpace: THREE.NoColorSpace, @@ -169,7 +171,7 @@ const fastFlipOverlap = 5; let activeFlips = []; let pendingPageFlips = 0; -const paperColor = new THREE.Color(0xf3dfad); +const paperColor = new THREE.Color(0xf1ead2); const inkColor = '#1a1009'; const leftCanvas = createPageCanvas('left'); @@ -179,9 +181,9 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas); [leftTexture, rightTexture].forEach((texture) => { texture.colorSpace = THREE.SRGBColorSpace; texture.anisotropy = maxTextureAnisotropy; - texture.minFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearMipmapLinearFilter; texture.magFilter = THREE.LinearFilter; - texture.generateMipmaps = false; + texture.generateMipmaps = true; }); const leatherTextures = createLeatherTextures(); const spineClothTextures = createSpineClothTextures(); @@ -234,44 +236,44 @@ const materials = { side: THREE.DoubleSide }), pageBlock: new THREE.MeshStandardMaterial({ - color: 0xfffbef, + color: 0xf4eed8, map: paperTextures.color, normalMap: paperTextures.normal, - normalScale: new THREE.Vector2(0.032, 0.032), + normalScale: new THREE.Vector2(0.014, 0.014), roughnessMap: paperTextures.roughness, roughness: 0.88, metalness: 0, envMapIntensity: 0.06 }), pageEdge: new THREE.MeshStandardMaterial({ - color: 0xfff4cf, + color: 0xf0e5c7, map: paperTextures.edge, normalMap: paperTextures.normal, - normalScale: new THREE.Vector2(0.024, 0.024), + normalScale: new THREE.Vector2(0.012, 0.012), roughnessMap: paperTextures.roughness, roughness: 0.94, metalness: 0, envMapIntensity: 0.05 }), pageSurface: new THREE.MeshStandardMaterial({ - color: 0xfffbf0, + color: 0xf5efd9, map: paperTextures.color, normalMap: paperTextures.normal, - normalScale: new THREE.Vector2(0.03, 0.03), + normalScale: new THREE.Vector2(0.012, 0.012), roughnessMap: paperTextures.roughness, roughness: 0.9, metalness: 0, emissive: 0x14110b, - emissiveIntensity: 0.025, + emissiveIntensity: 0.012, envMapIntensity: 0.035, side: THREE.DoubleSide }), flipPageSurface: new THREE.MeshStandardMaterial({ - color: 0xfffcf2, + color: 0xf5efd9, roughness: 0.92, metalness: 0, emissive: 0x100d08, - emissiveIntensity: 0.018, + emissiveIntensity: 0.01, envMapIntensity: 0.02, side: THREE.DoubleSide }), @@ -279,24 +281,24 @@ const materials = { color: 0xffffff, map: leftTexture, normalMap: paperTextures.normal, - normalScale: new THREE.Vector2(0.025, 0.025), + normalScale: new THREE.Vector2(0.01, 0.01), roughnessMap: paperTextures.roughness, roughness: 0.86, metalness: 0, emissive: 0x11100c, - emissiveIntensity: 0.035, + emissiveIntensity: 0.012, side: THREE.DoubleSide }), rightPage: new THREE.MeshStandardMaterial({ color: 0xffffff, map: rightTexture, normalMap: paperTextures.normal, - normalScale: new THREE.Vector2(0.025, 0.025), + normalScale: new THREE.Vector2(0.01, 0.01), roughnessMap: paperTextures.roughness, roughness: 0.86, metalness: 0, emissive: 0x11100c, - emissiveIntensity: 0.035, + emissiveIntensity: 0.012, side: THREE.DoubleSide }), spineCloth: new THREE.MeshStandardMaterial({ @@ -333,18 +335,19 @@ configureBookShadowReceiver(materials.leather, 0.52); configureBookShadowReceiver(materials.hingeLeather, 0.36); configureBookShadowReceiver(materials.spineBaseLeather, 0.34); configureBookShadowReceiver(materials.coverEdge, 0.28); -configureBookShadowReceiver(materials.pageBlock, 0.46); -configureBookShadowReceiver(materials.pageEdge, 0.34); -configureBookShadowReceiver(materials.pageSurface, 0.34); -configureBookShadowReceiver(materials.flipPageSurface, 0.32); -configureBookShadowReceiver(materials.leftPage, 0.38); -configureBookShadowReceiver(materials.rightPage, 0.38); +configureBookShadowReceiver(materials.pageBlock, 0.3); +configureBookShadowReceiver(materials.pageEdge, 0.24); +configureBookShadowReceiver(materials.pageSurface, 0.2); +configureBookShadowReceiver(materials.flipPageSurface, 0.2); +configureBookShadowReceiver(materials.leftPage, 0.18); +configureBookShadowReceiver(materials.rightPage, 0.18); configureBookShadowReceiver(materials.spineCloth, 0.48); configureBookShadowReceiver(materials.headband, 0.62); buildTable(); buildLighting(); buildBook(); +notifyBookPageCountChanged(); loadAiRoomReflection(); window.BookLabDebug = { textures: generatedTextureCanvases, @@ -629,11 +632,11 @@ function configureBookShadowReceiver(material, strength) { float sideFill = grazingSide * sideReach; float tableFill = tableReach * (0.16 + underside * 0.22) * (1.0 - upFacing * 0.58); float pageFill = smoothstep(0.02, 0.2, tableDistance) * (1.0 - smoothstep(0.24, 0.72, tableDistance)); - vec3 tableWarmth = vec3(0.058, 0.039, 0.026) * tableFill; - vec3 roomWarmth = vec3(0.04, 0.034, 0.028) * sideFill; - vec3 pageWarmth = vec3(0.045, 0.041, 0.034) * pageFill * grazingSide * (1.0 - upFacing * 0.42); + vec3 tableWarmth = vec3(0.042, 0.034, 0.028) * tableFill; + vec3 roomWarmth = vec3(0.032, 0.032, 0.03) * sideFill; + vec3 pageWarmth = vec3(0.032, 0.032, 0.029) * pageFill * grazingSide * (1.0 - upFacing * 0.42); vec3 indirect = tableWarmth + roomWarmth + pageWarmth; - return albedo * indirect * mix(1.0, 0.72, shadow); + return albedo * indirect * mix(1.0, 0.86, shadow); } float spineClothThread(float coordinate, float frequency, float sharpness) { @@ -663,8 +666,8 @@ function configureBookShadowReceiver(material, strength) { sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718); float cloud = sin((uv.x * 17.0 + uv.y * 11.0) * 6.28318530718) * sin((uv.x * 29.0 - uv.y * 23.0) * 6.28318530718); - float fiber = clamp(fleck * 0.018 + cloud * 0.022, -0.04, 0.05); - vec3 paperTint = mix(vec3(0.96, 0.945, 0.89), vec3(1.08, 1.055, 0.98), clamp(0.62 + fiber, 0.0, 1.0)); + float fiber = clamp(fleck * 0.008 + cloud * 0.012, -0.02, 0.026); + vec3 paperTint = mix(vec3(0.94, 0.925, 0.875), vec3(1.025, 1.015, 0.97), clamp(0.56 + fiber, 0.0, 1.0)); return baseLight * paperTint; } @@ -681,7 +684,7 @@ function configureBookShadowReceiver(material, strength) { ${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''} ${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''} float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength; - outgoingLight *= mix(vec3(1.0), ${isHeadband ? 'vec3(0.16, 0.095, 0.055)' : 'vec3(0.38, 0.29, 0.2)'}, bookReceiverShadow); + outgoingLight *= mix(vec3(1.0), ${isHeadband ? 'vec3(0.16, 0.095, 0.055)' : isHardcoverPaper ? 'vec3(0.68, 0.62, 0.52)' : 'vec3(0.38, 0.29, 0.2)'}, bookReceiverShadow); outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb); #include ` ); @@ -715,7 +718,13 @@ function configureScenePostprocessing() { sceneAoPass.output = SSAOPass.OUTPUT.Default; } const renderAoPass = sceneAoPass.render.bind(sceneAoPass); + sceneAoPass.userData = sceneAoPass.userData || {}; + sceneAoPass.userData.cachedRenderPass = renderAoPass; sceneAoPass.render = (...args) => { + if (!staticSceneBuffersDirty && activeFlips.length === 0) { + renderCachedAoPass(sceneAoPass, ...args); + return; + } aoExcludedObjects.forEach((object) => { object.userData.wasVisibleForAo = object.visible; object.visible = false; @@ -1360,6 +1369,7 @@ function configureTableShader(material) { } function buildBook() { + markStaticSceneBuffersDirty(); clearActiveFlips(); book.traverse((object) => { if (object.isMesh) aoExcludedObjects.delete(object); @@ -1404,17 +1414,52 @@ function buildBook() { }); currentProceduralBookModel = proceduralBook.model; book.add(proceduralBook.group); + document.documentElement.dataset.webglBookThickness = JSON.stringify(currentProceduralBookModel.thickness); +} + +function renderCachedAoPass(pass, rendererInstance, writeBuffer, readBuffer) { + if (!pass.copyMaterial || !pass.renderPass || !pass.blurRenderTarget) { + pass.userData?.cachedRenderPass?.(rendererInstance, writeBuffer, readBuffer); + return; + } + if (pass.output === SSAOPass.OUTPUT?.Default) { + pass.copyMaterial.uniforms.tDiffuse.value = readBuffer.texture; + pass.copyMaterial.blending = THREE.NoBlending; + pass.renderPass(rendererInstance, pass.copyMaterial, pass.renderToScreen ? null : writeBuffer); + pass.copyMaterial.uniforms.tDiffuse.value = pass.blurRenderTarget.texture; + pass.copyMaterial.blending = THREE.CustomBlending; + pass.renderPass(rendererInstance, pass.copyMaterial, pass.renderToScreen ? null : writeBuffer); + return; + } + const texture = pass.output === SSAOPass.OUTPUT?.SSAO + ? pass.ssaoRenderTarget?.texture + : pass.output === SSAOPass.OUTPUT?.Blur + ? pass.blurRenderTarget.texture + : pass.output === SSAOPass.OUTPUT?.Normal + ? pass.normalRenderTarget?.texture + : null; + if (!texture) { + pass.userData?.cachedRenderPass?.(rendererInstance, writeBuffer, readBuffer); + return; + } + pass.copyMaterial.uniforms.tDiffuse.value = texture; + pass.copyMaterial.blending = THREE.NoBlending; + pass.renderPass(rendererInstance, pass.copyMaterial, pass.renderToScreen ? null : writeBuffer); +} + +function markStaticSceneBuffersDirty() { + staticSceneBuffersDirty = true; } function configureHardcoverPaperMaterial(material, { useEdgeMap = false } = {}) { material.userData.isHardcoverPaper = true; if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color; material.normalMap = paperTextures.normal; - material.normalScale = material.normalScale ?? new THREE.Vector2(0.024, 0.024); + material.normalScale = material.normalScale ?? new THREE.Vector2(useEdgeMap ? 0.012 : 0.01, useEdgeMap ? 0.012 : 0.01); material.roughnessMap = paperTextures.roughness; - material.roughness = Math.max(material.roughness ?? 0.86, useEdgeMap ? 0.92 : 0.86); + material.roughness = Math.max(material.roughness ?? 0.9, useEdgeMap ? 0.94 : 0.9); material.metalness = 0; - material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.05, 0.06); + material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.025, 0.035); material.needsUpdate = true; } @@ -1432,10 +1477,20 @@ function setBookPageCount(value) { if (!Number.isFinite(nextPageCount)) return; bookPageCount = nextPageCount; buildBook(); + notifyBookPageCountChanged(); syncBookControls(); window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount); } +function notifyBookPageCountChanged() { + document.dispatchEvent(new CustomEvent('webgl-book:page-count-changed', { + detail: { + pageCount: bookPageCount, + model: currentProceduralBookModel + } + })); +} + function stepReadingProgress(pageDelta) { setReadingProgress(readingProgress + pageDelta / Math.max(1, bookPageCount)); } @@ -2414,32 +2469,32 @@ function createHardcoverPaperTextures() { const cloudA = Math.sin((nx * 19 + ny * 11) * 6.28318530718); const cloudB = Math.sin((nx * 31 - ny * 27) * 6.28318530718); const fleck = Math.max(0, 0.5 - Math.abs(pulpA * pulpB)); - return cloudA * cloudB * 0.026 - fleck * 0.035; + return cloudA * cloudB * 0.014 - fleck * 0.018; }; for (let y = 0; y < size; y += 1) { for (let x = 0; x < size; x += 1) { const index = (y * size + x) * 4; const fiber = fiberAt(x, y); - const warmth = 0.97 + 0.018 * Math.sin(x * 0.017 + y * 0.003) + 0.012 * Math.sin(y * 0.041); - const shade = THREE.MathUtils.clamp(0.975 + fiber, 0.88, 1.0); - colorImage.data[index] = Math.round(255 * shade * warmth); - colorImage.data[index + 1] = Math.round(251 * shade * warmth); - colorImage.data[index + 2] = Math.round(235 * shade); + const warmth = 0.985 + 0.008 * Math.sin(x * 0.017 + y * 0.003) + 0.006 * Math.sin(y * 0.041); + const shade = THREE.MathUtils.clamp(0.985 + fiber, 0.92, 1.0); + colorImage.data[index] = Math.round(246 * shade * warmth); + colorImage.data[index + 1] = Math.round(239 * shade * warmth); + colorImage.data[index + 2] = Math.round(216 * shade); colorImage.data[index + 3] = 255; const linePhase = (y + Math.sin(x * 0.021) * 4) % 34; const line = linePhase < 1.2 ? 0.72 : linePhase < 2.1 ? 0.82 : 1; - edgeImage.data[index] = Math.round(255 * shade * line); - edgeImage.data[index + 1] = Math.round(244 * shade * line); - edgeImage.data[index + 2] = Math.round(207 * shade * line); + edgeImage.data[index] = Math.round(240 * shade * line); + edgeImage.data[index + 1] = Math.round(230 * shade * line); + edgeImage.data[index + 2] = Math.round(194 * shade * line); edgeImage.data[index + 3] = 255; const hLeft = fiberAt((x - 1 + size) % size, y); const hRight = fiberAt((x + 1) % size, y); const hDown = fiberAt(x, (y - 1 + size) % size); const hUp = fiberAt(x, (y + 1) % size); - const normal = new THREE.Vector3((hLeft - hRight) * 3.2, (hDown - hUp) * 3.2, 1).normalize(); + const normal = new THREE.Vector3((hLeft - hRight) * 1.45, (hDown - hUp) * 1.45, 1).normalize(); normalImage.data[index] = Math.round((normal.x * 0.5 + 0.5) * 255); normalImage.data[index + 1] = Math.round((normal.y * 0.5 + 0.5) * 255); normalImage.data[index + 2] = Math.round((normal.z * 0.5 + 0.5) * 255); @@ -2567,6 +2622,7 @@ function tintAmbientFromCanvas(canvas) { } function resize() { + markStaticSceneBuffersDirty(); const width = Math.max(1, window.innerWidth); const height = Math.max(1, window.innerHeight); renderer.setSize(width, height, false); @@ -2687,6 +2743,19 @@ function updateCameraRig(deltaSeconds) { cameraRig.target.z + Math.cos(cameraRig.yaw) * horizontalRadius ); camera.lookAt(cameraRig.target); + const signature = [ + camera.position.x, + camera.position.y, + camera.position.z, + cameraRig.target.x, + cameraRig.target.y, + cameraRig.target.z, + camera.aspect + ].map((value) => value.toFixed(4)).join('|'); + if (signature !== lastStaticCameraSignature) { + lastStaticCameraSignature = signature; + markStaticSceneBuffersDirty(); + } } function updateCandleShadowUniforms() { @@ -2884,16 +2953,17 @@ function animate(now = performance.now()) { waxShader.uniforms.waxLightPower.value = THREE.MathUtils.clamp(pulse * object.userData.baseIntensity * 0.42, 0.35, 1.6); } }); + const hadActiveFlips = activeFlips.length > 0; updateActiveFlips(performance.now()); + if (hadActiveFlips) markStaticSceneBuffersDirty(); updateCandleShadowUniforms(); renderedFrameCount += 1; const shadowStartedAt = performance.now(); - if (!isAppIntegrationMode || renderedFrameCount % 6 === 1 || activeFlips.length > 0) { - updateBookShadowMaps(); - } + updateBookShadowMaps(); lastFrameTiming.shadows = performance.now() - shadowStartedAt; const reflectionStartedAt = performance.now(); - if (!isAppIntegrationMode || renderedFrameCount % 4 === 1 || cameraRig.navigationActive || activeFlips.length > 0) { + const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0; + if (refreshStaticSceneBuffers) { updateTableReflection(); } lastFrameTiming.reflection = performance.now() - reflectionStartedAt; @@ -2909,6 +2979,9 @@ function animate(now = performance.now()) { } lastFrameTiming.render = performance.now() - renderStartedAt; lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render; + if (refreshStaticSceneBuffers && activeFlips.length === 0) { + staticSceneBuffersDirty = false; + } window.BookLabDebug.renderedFrames += 1; window.BookLabDebug.ready = true; fpsWindowFrames += 1;