From de81a7c5c505a33cb56f1f5046d23e90e87ca935 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sun, 7 Jun 2026 12:08:13 +0200 Subject: [PATCH] Stage WebGL scene loading --- public/index.html | 2 +- public/js/book-page-format-module.js | 11 ++++-- public/js/book-pagination-module.js | 10 +++++- public/js/book-texture-renderer-module.js | 2 +- public/js/loader.js | 2 +- public/js/webgl-book-lab.js | 43 +++++++++++++++++++++-- public/js/webgl-book-scene-module.js | 19 ++++++---- 7 files changed, 73 insertions(+), 16 deletions(-) diff --git a/public/index.html b/public/index.html index 940c17e..0aec3c1 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 34591fd..8f12ab2 100644 --- a/public/js/book-page-format-module.js +++ b/public/js/book-page-format-module.js @@ -3,7 +3,9 @@ * 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'; +import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-loader-quality-fix'; + +export const BOOK_TEXTURE_WIDTH = 3072; class BookPageFormatModule extends BaseModule { constructor() { @@ -39,6 +41,7 @@ class BookPageFormatModule extends BaseModule { this.bindMethods([ 'getFormat', 'getAspectRatio', + 'getTextureWidth', 'getTextureMetrics', 'setPageCount', 'getPageCount', @@ -67,6 +70,10 @@ class BookPageFormatModule extends BaseModule { return this.format.trim.widthIn / this.format.trim.heightIn; } + getTextureWidth() { + return BOOK_TEXTURE_WIDTH; + } + inchesToTexture(valueIn, textureHeight) { return (Number(valueIn) / this.format.trim.heightIn) * textureHeight; } @@ -105,7 +112,7 @@ class BookPageFormatModule extends BaseModule { }; } - getTextureMetrics(textureWidth = 1280, pageCount = this.pageCount) { + getTextureMetrics(textureWidth = BOOK_TEXTURE_WIDTH, 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); diff --git a/public/js/book-pagination-module.js b/public/js/book-pagination-module.js index 7baa1b3..e06d3f6 100644 --- a/public/js/book-pagination-module.js +++ b/public/js/book-pagination-module.js @@ -36,6 +36,7 @@ class BookPaginationModule extends BaseModule { 'getSpread', 'getCurrentSpread', 'setCurrentSpread', + 'handlePageCountChanged', 'publish' ]); } @@ -44,9 +45,10 @@ class BookPaginationModule extends BaseModule { this.pageFormat = this.getModule('book-page-format'); this.paragraphLayout = this.getModule('paragraph-layout'); this.storyHistory = this.getModule('story-history'); - this.metrics = this.pageFormat.getTextureMetrics(1280); + this.metrics = this.pageFormat.getTextureMetrics(this.pageFormat.getTextureWidth?.()); this.reportProgress(35, 'Preparing book pagination metrics'); + this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged); this.addEventListener(document, 'story:history-updated', this.refreshFromHistory); this.addEventListener(document, 'book-pagination:set-spread', (event) => { this.setCurrentSpread(event.detail?.spreadIndex); @@ -55,6 +57,12 @@ class BookPaginationModule extends BaseModule { return true; } + handlePageCountChanged(event) { + this.pageFormat?.setPageCount?.(event.detail?.pageCount); + this.metrics = this.pageFormat.getTextureMetrics(this.pageFormat.getTextureWidth?.()); + this.refreshFromHistory(); + } + async refreshFromHistory(event = null) { const token = ++this.refreshToken; const detail = event?.detail || {}; diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index b748288..3941b69 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -86,7 +86,7 @@ class BookTextureRendererModule extends BaseModule { return true; } - createPageCanvases(textureWidth = 3072) { + createPageCanvases(textureWidth = this.pageFormat?.getTextureWidth?.() || 3072) { this.metrics = this.pageFormat.getTextureMetrics(textureWidth); ['left', 'right'].forEach((side) => { const canvas = document.createElement('canvas'); diff --git a/public/js/loader.js b/public/js/loader.js index 5d28210..9c03a69 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-physical-stack-quality'; +const MODULE_CACHE_BUSTER = '20260607-webgl-loader-quality-fix'; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER; /** diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index dc32451..1be53cc 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-physical-stack-quality'; +import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-loader-quality-fix'; 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 = Math.min(window.devicePixelRatio || 1, 2); +const appRenderPixelRatio = 2; const labStatus = document.getElementById('lab_status'); if (labStatus && tableDebugMode !== tableDebugModes.none) { labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`; @@ -40,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 = 2; const pageTextureWidth = 3072; const reflectionTargetSize = new THREE.Vector2(); const pageRaycaster = new THREE.Raycaster(); @@ -56,6 +56,17 @@ let renderedFrameCount = 0; let staticSceneBuffersDirty = true; let lastStaticCameraSignature = ''; +function reportLabProgress(percent, message) { + if (typeof appInitialState.reportProgress === 'function') { + appInitialState.reportProgress(percent, message); + } +} + +async function reportLabStep(percent, message) { + reportLabProgress(percent, message); + await new Promise(resolve => requestAnimationFrame(resolve)); +} + const scene = new THREE.Scene(); scene.background = new THREE.Color(0x080604); scene.fog = new THREE.FogExp2(0x080604, 0.035); @@ -174,6 +185,7 @@ let pendingPageFlips = 0; const paperColor = new THREE.Color(0xf1ead2); const inkColor = '#1a1009'; +await reportLabStep(48, 'Preparing high-resolution page textures'); const leftCanvas = createPageCanvas('left'); const rightCanvas = createPageCanvas('right'); const leftTexture = new THREE.CanvasTexture(leftCanvas); @@ -185,10 +197,15 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas); texture.magFilter = THREE.LinearFilter; texture.generateMipmaps = true; }); +await reportLabStep(52, 'Generating leather texture set'); const leatherTextures = createLeatherTextures(); +await reportLabStep(56, 'Generating spine cloth texture set'); const spineClothTextures = createSpineClothTextures(); +await reportLabStep(60, 'Generating headband texture set'); const headbandTextures = createHeadbandTextures(); +await reportLabStep(64, 'Generating paper texture set'); const paperTextures = createHardcoverPaperTextures(); +await reportLabStep(68, 'Creating WebGL book materials'); const materials = { leather: new THREE.MeshStandardMaterial({ @@ -344,11 +361,18 @@ configureBookShadowReceiver(materials.rightPage, 0.18); configureBookShadowReceiver(materials.spineCloth, 0.48); configureBookShadowReceiver(materials.headband, 0.62); +await reportLabStep(70, 'Building reflective table'); buildTable(); +await reportLabStep(74, 'Building candle lighting'); buildLighting(); +await reportLabStep(78, 'Building physical book stack'); buildBook(); notifyBookPageCountChanged(); +await reportLabStep(82, 'Loading room reflection texture'); loadAiRoomReflection(); +await reportLabStep(86, 'Preparing static shadow and mirror maps'); +primeSceneForLoader(); +await reportLabStep(90, 'Compiled WebGL scene passes'); window.BookLabDebug = { textures: generatedTextureCanvases, ready: false, @@ -1534,6 +1558,7 @@ function handlePageCanvases(event) { drawCanvasPageTexture(rightCanvas, detail.right, 'right'); rightTexture.needsUpdate = true; } + markStaticSceneBuffersDirty(); document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({ width: leftCanvas.width, height: leftCanvas.height, @@ -2586,6 +2611,7 @@ function loadAiRoomReflection() { if (tableShader) { tableShader.uniforms.roomReflectionMap.value = texture; } + markStaticSceneBuffersDirty(); const image = texture.image; if (!image) return; @@ -2596,11 +2622,22 @@ function loadAiRoomReflection() { ctx.drawImage(image, 0, 0, canvas.width, canvas.height); generatedTextureCanvases.aiRoomReflection = canvas; tintAmbientFromCanvas(canvas); + markStaticSceneBuffersDirty(); }, undefined, () => { tintAmbientFromCanvas(generatedTextureCanvases.roomReflection); + markStaticSceneBuffersDirty(); }); } +function primeSceneForLoader() { + updateCameraRig(0); + updateCandleShadowUniforms(); + updateBookShadowMaps(); + updateTableReflection(); + renderer.compile(scene, camera); + staticSceneBuffersDirty = false; +} + function tintAmbientFromCanvas(canvas) { if (!canvas || !candleBounceLight) return; const ctx = canvas.getContext('2d'); diff --git a/public/js/webgl-book-scene-module.js b/public/js/webgl-book-scene-module.js index 64081e9..7d2ae37 100644 --- a/public/js/webgl-book-scene-module.js +++ b/public/js/webgl-book-scene-module.js @@ -10,7 +10,7 @@ const DEFAULT_BOOK_PROGRESS = 0.5; class WebGLBookSceneModule extends BaseModule { constructor() { super('webgl-book-scene', 'WebGL Book Scene'); - this.dependencies = ['persistence-manager', 'localization']; + this.dependencies = ['persistence-manager', 'localization', 'book-texture-renderer']; this.persistenceManager = null; this.localization = null; this.mode = '2d'; @@ -73,6 +73,8 @@ class WebGLBookSceneModule extends BaseModule { this.reportProgress(35, 'Creating WebGL host'); this.ensureShell(); this.installPreferenceBridge(); + this.reportProgress(45, 'Loading WebGL scene modules'); + await this.initializeScene(); this.reportProgress(100, 'WebGL book host ready'); return true; @@ -173,7 +175,10 @@ class WebGLBookSceneModule extends BaseModule { window.WebGLBookInitialState = { appMode: true, pageCount, - progress + progress, + reportProgress: (percent, message) => { + this.reportProgress(percent, message); + } }; } @@ -293,6 +298,10 @@ class WebGLBookSceneModule extends BaseModule { const cacheBuster = window.MODULE_CACHE_BUSTER || Date.now(); this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(cacheBuster)}`); await this.labImportPromise; + this.reportProgress(94, 'Uploading initial book page textures'); + window.BookTextureRenderer?.publishSpread?.(); + await new Promise(resolve => requestAnimationFrame(resolve)); + this.reportProgress(96, 'Binding WebGL page controls'); this.installTextureEventBridge(); this.triggerTextureRefresh(); return this.labImportPromise; @@ -459,11 +468,7 @@ class WebGLBookSceneModule extends BaseModule { if (this.mode === '3d') { this.createLabHost(); this.installPreferenceBridge(); - this.initializeScene() - .then(() => this.triggerTextureRefresh()) - .catch((error) => { - console.error('WebGLBookScene: Failed to initialize procedural scene', error); - }); + if (this.labImportPromise) this.triggerTextureRefresh(); } const title = document.getElementById('game_title')?.textContent?.trim(); const label = document.getElementById('lab_title');