Stage WebGL scene loading

This commit is contained in:
2026-06-07 12:08:13 +02:00
parent 1b593c8c7b
commit de81a7c5c5
7 changed files with 73 additions and 16 deletions
+1 -1
View File
@@ -280,6 +280,6 @@
console.log(message); console.log(message);
}; };
</script> </script>
<script type="module" src="/js/loader.js?v=20260607-webgl-physical-stack-quality"></script> <script type="module" src="/js/loader.js?v=20260607-webgl-loader-quality-fix"></script>
</body> </body>
</html> </html>
+9 -2
View File
@@ -3,7 +3,9 @@
* Defines the canonical page geometry used by the WebGL book renderer. * Defines the canonical page geometry used by the WebGL book renderer.
*/ */
import { BaseModule } from './base-module.js'; 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 { class BookPageFormatModule extends BaseModule {
constructor() { constructor() {
@@ -39,6 +41,7 @@ class BookPageFormatModule extends BaseModule {
this.bindMethods([ this.bindMethods([
'getFormat', 'getFormat',
'getAspectRatio', 'getAspectRatio',
'getTextureWidth',
'getTextureMetrics', 'getTextureMetrics',
'setPageCount', 'setPageCount',
'getPageCount', 'getPageCount',
@@ -67,6 +70,10 @@ class BookPageFormatModule extends BaseModule {
return this.format.trim.widthIn / this.format.trim.heightIn; return this.format.trim.widthIn / this.format.trim.heightIn;
} }
getTextureWidth() {
return BOOK_TEXTURE_WIDTH;
}
inchesToTexture(valueIn, textureHeight) { inchesToTexture(valueIn, textureHeight) {
return (Number(valueIn) / this.format.trim.heightIn) * 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 width = Math.max(1, Math.round(Number(textureWidth) || 1280));
const height = Math.round(width / this.getAspectRatio()); const height = Math.round(width / this.getAspectRatio());
const dynamicMargins = this.getDynamicMargins(pageCount); const dynamicMargins = this.getDynamicMargins(pageCount);
+9 -1
View File
@@ -36,6 +36,7 @@ class BookPaginationModule extends BaseModule {
'getSpread', 'getSpread',
'getCurrentSpread', 'getCurrentSpread',
'setCurrentSpread', 'setCurrentSpread',
'handlePageCountChanged',
'publish' 'publish'
]); ]);
} }
@@ -44,9 +45,10 @@ class BookPaginationModule extends BaseModule {
this.pageFormat = this.getModule('book-page-format'); this.pageFormat = this.getModule('book-page-format');
this.paragraphLayout = this.getModule('paragraph-layout'); this.paragraphLayout = this.getModule('paragraph-layout');
this.storyHistory = this.getModule('story-history'); 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.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, 'story:history-updated', this.refreshFromHistory);
this.addEventListener(document, 'book-pagination:set-spread', (event) => { this.addEventListener(document, 'book-pagination:set-spread', (event) => {
this.setCurrentSpread(event.detail?.spreadIndex); this.setCurrentSpread(event.detail?.spreadIndex);
@@ -55,6 +57,12 @@ class BookPaginationModule extends BaseModule {
return true; return true;
} }
handlePageCountChanged(event) {
this.pageFormat?.setPageCount?.(event.detail?.pageCount);
this.metrics = this.pageFormat.getTextureMetrics(this.pageFormat.getTextureWidth?.());
this.refreshFromHistory();
}
async refreshFromHistory(event = null) { async refreshFromHistory(event = null) {
const token = ++this.refreshToken; const token = ++this.refreshToken;
const detail = event?.detail || {}; const detail = event?.detail || {};
+1 -1
View File
@@ -86,7 +86,7 @@ class BookTextureRendererModule extends BaseModule {
return true; return true;
} }
createPageCanvases(textureWidth = 3072) { createPageCanvases(textureWidth = this.pageFormat?.getTextureWidth?.() || 3072) {
this.metrics = this.pageFormat.getTextureMetrics(textureWidth); this.metrics = this.pageFormat.getTextureMetrics(textureWidth);
['left', 'right'].forEach((side) => { ['left', 'right'].forEach((side) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
+1 -1
View File
@@ -24,7 +24,7 @@ const ModuleState = {
ERROR: 'ERROR' 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; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
/** /**
+40 -3
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 { 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 { 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 { 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'); const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab'; canvas.style.cursor = 'grab';
@@ -25,7 +25,7 @@ const appInitialState = window.WebGLBookInitialState || {};
const tableDebugName = urlParams.get('tableDebug') || 'none'; const tableDebugName = urlParams.get('tableDebug') || 'none';
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none; const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
const isAppIntegrationMode = appInitialState.appMode === true; const isAppIntegrationMode = appInitialState.appMode === true;
const appRenderPixelRatio = Math.min(window.devicePixelRatio || 1, 2); const appRenderPixelRatio = 2;
const labStatus = document.getElementById('lab_status'); const labStatus = document.getElementById('lab_status');
if (labStatus && tableDebugMode !== tableDebugModes.none) { if (labStatus && tableDebugMode !== tableDebugModes.none) {
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`; labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
@@ -40,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
const generatedTextureCanvases = {}; const generatedTextureCanvases = {};
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy(); const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2); const reflectionPixelRatio = 2;
const pageTextureWidth = 3072; const pageTextureWidth = 3072;
const reflectionTargetSize = new THREE.Vector2(); const reflectionTargetSize = new THREE.Vector2();
const pageRaycaster = new THREE.Raycaster(); const pageRaycaster = new THREE.Raycaster();
@@ -56,6 +56,17 @@ let renderedFrameCount = 0;
let staticSceneBuffersDirty = true; let staticSceneBuffersDirty = true;
let lastStaticCameraSignature = ''; 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(); const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080604); scene.background = new THREE.Color(0x080604);
scene.fog = new THREE.FogExp2(0x080604, 0.035); scene.fog = new THREE.FogExp2(0x080604, 0.035);
@@ -174,6 +185,7 @@ let pendingPageFlips = 0;
const paperColor = new THREE.Color(0xf1ead2); const paperColor = new THREE.Color(0xf1ead2);
const inkColor = '#1a1009'; const inkColor = '#1a1009';
await reportLabStep(48, 'Preparing high-resolution page textures');
const leftCanvas = createPageCanvas('left'); const leftCanvas = createPageCanvas('left');
const rightCanvas = createPageCanvas('right'); const rightCanvas = createPageCanvas('right');
const leftTexture = new THREE.CanvasTexture(leftCanvas); const leftTexture = new THREE.CanvasTexture(leftCanvas);
@@ -185,10 +197,15 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas);
texture.magFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = true; texture.generateMipmaps = true;
}); });
await reportLabStep(52, 'Generating leather texture set');
const leatherTextures = createLeatherTextures(); const leatherTextures = createLeatherTextures();
await reportLabStep(56, 'Generating spine cloth texture set');
const spineClothTextures = createSpineClothTextures(); const spineClothTextures = createSpineClothTextures();
await reportLabStep(60, 'Generating headband texture set');
const headbandTextures = createHeadbandTextures(); const headbandTextures = createHeadbandTextures();
await reportLabStep(64, 'Generating paper texture set');
const paperTextures = createHardcoverPaperTextures(); const paperTextures = createHardcoverPaperTextures();
await reportLabStep(68, 'Creating WebGL book materials');
const materials = { const materials = {
leather: new THREE.MeshStandardMaterial({ leather: new THREE.MeshStandardMaterial({
@@ -344,11 +361,18 @@ configureBookShadowReceiver(materials.rightPage, 0.18);
configureBookShadowReceiver(materials.spineCloth, 0.48); configureBookShadowReceiver(materials.spineCloth, 0.48);
configureBookShadowReceiver(materials.headband, 0.62); configureBookShadowReceiver(materials.headband, 0.62);
await reportLabStep(70, 'Building reflective table');
buildTable(); buildTable();
await reportLabStep(74, 'Building candle lighting');
buildLighting(); buildLighting();
await reportLabStep(78, 'Building physical book stack');
buildBook(); buildBook();
notifyBookPageCountChanged(); notifyBookPageCountChanged();
await reportLabStep(82, 'Loading room reflection texture');
loadAiRoomReflection(); loadAiRoomReflection();
await reportLabStep(86, 'Preparing static shadow and mirror maps');
primeSceneForLoader();
await reportLabStep(90, 'Compiled WebGL scene passes');
window.BookLabDebug = { window.BookLabDebug = {
textures: generatedTextureCanvases, textures: generatedTextureCanvases,
ready: false, ready: false,
@@ -1534,6 +1558,7 @@ function handlePageCanvases(event) {
drawCanvasPageTexture(rightCanvas, detail.right, 'right'); drawCanvasPageTexture(rightCanvas, detail.right, 'right');
rightTexture.needsUpdate = true; rightTexture.needsUpdate = true;
} }
markStaticSceneBuffersDirty();
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({ document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
width: leftCanvas.width, width: leftCanvas.width,
height: leftCanvas.height, height: leftCanvas.height,
@@ -2586,6 +2611,7 @@ function loadAiRoomReflection() {
if (tableShader) { if (tableShader) {
tableShader.uniforms.roomReflectionMap.value = texture; tableShader.uniforms.roomReflectionMap.value = texture;
} }
markStaticSceneBuffersDirty();
const image = texture.image; const image = texture.image;
if (!image) return; if (!image) return;
@@ -2596,11 +2622,22 @@ function loadAiRoomReflection() {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
generatedTextureCanvases.aiRoomReflection = canvas; generatedTextureCanvases.aiRoomReflection = canvas;
tintAmbientFromCanvas(canvas); tintAmbientFromCanvas(canvas);
markStaticSceneBuffersDirty();
}, undefined, () => { }, undefined, () => {
tintAmbientFromCanvas(generatedTextureCanvases.roomReflection); tintAmbientFromCanvas(generatedTextureCanvases.roomReflection);
markStaticSceneBuffersDirty();
}); });
} }
function primeSceneForLoader() {
updateCameraRig(0);
updateCandleShadowUniforms();
updateBookShadowMaps();
updateTableReflection();
renderer.compile(scene, camera);
staticSceneBuffersDirty = false;
}
function tintAmbientFromCanvas(canvas) { function tintAmbientFromCanvas(canvas) {
if (!canvas || !candleBounceLight) return; if (!canvas || !candleBounceLight) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
+12 -7
View File
@@ -10,7 +10,7 @@ const DEFAULT_BOOK_PROGRESS = 0.5;
class WebGLBookSceneModule extends BaseModule { class WebGLBookSceneModule extends BaseModule {
constructor() { constructor() {
super('webgl-book-scene', 'WebGL Book Scene'); super('webgl-book-scene', 'WebGL Book Scene');
this.dependencies = ['persistence-manager', 'localization']; this.dependencies = ['persistence-manager', 'localization', 'book-texture-renderer'];
this.persistenceManager = null; this.persistenceManager = null;
this.localization = null; this.localization = null;
this.mode = '2d'; this.mode = '2d';
@@ -73,6 +73,8 @@ class WebGLBookSceneModule extends BaseModule {
this.reportProgress(35, 'Creating WebGL host'); this.reportProgress(35, 'Creating WebGL host');
this.ensureShell(); this.ensureShell();
this.installPreferenceBridge(); this.installPreferenceBridge();
this.reportProgress(45, 'Loading WebGL scene modules');
await this.initializeScene();
this.reportProgress(100, 'WebGL book host ready'); this.reportProgress(100, 'WebGL book host ready');
return true; return true;
@@ -173,7 +175,10 @@ class WebGLBookSceneModule extends BaseModule {
window.WebGLBookInitialState = { window.WebGLBookInitialState = {
appMode: true, appMode: true,
pageCount, 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(); const cacheBuster = window.MODULE_CACHE_BUSTER || Date.now();
this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(cacheBuster)}`); this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(cacheBuster)}`);
await this.labImportPromise; 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.installTextureEventBridge();
this.triggerTextureRefresh(); this.triggerTextureRefresh();
return this.labImportPromise; return this.labImportPromise;
@@ -459,11 +468,7 @@ class WebGLBookSceneModule extends BaseModule {
if (this.mode === '3d') { if (this.mode === '3d') {
this.createLabHost(); this.createLabHost();
this.installPreferenceBridge(); this.installPreferenceBridge();
this.initializeScene() if (this.labImportPromise) this.triggerTextureRefresh();
.then(() => this.triggerTextureRefresh())
.catch((error) => {
console.error('WebGLBookScene: Failed to initialize procedural scene', error);
});
} }
const title = document.getElementById('game_title')?.textContent?.trim(); const title = document.getElementById('game_title')?.textContent?.trim();
const label = document.getElementById('lab_title'); const label = document.getElementById('lab_title');