Restore WebGL book quality settings

This commit is contained in:
2026-06-07 11:13:05 +02:00
parent 777e39a650
commit 1b593c8c7b
6 changed files with 247 additions and 69 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=20260516-scroll-window"></script> <script type="module" src="/js/loader.js?v=20260607-webgl-physical-stack-quality"></script>
</body> </body>
</html> </html>
+66 -7
View File
@@ -3,6 +3,7 @@
* 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';
class BookPageFormatModule extends BaseModule { class BookPageFormatModule extends BaseModule {
constructor() { constructor() {
@@ -17,8 +18,13 @@ class BookPageFormatModule extends BaseModule {
margins: Object.freeze({ margins: Object.freeze({
topIn: 0.46, topIn: 0.46,
bottomIn: 0.58, bottomIn: 0.58,
innerIn: 0.56, innerBaseIn: 0.375,
outerIn: 0.44 innerMinIn: 0.44,
innerMaxIn: 0.68,
innerThicknessFactor: 0.25,
outerBaseIn: 0.44,
outerThicknessFactor: 0.04,
outerMaxIn: 0.5
}), }),
typography: Object.freeze({ typography: Object.freeze({
fontFamily: '"EB Garamond", "EB Garamond 12", serif', fontFamily: '"EB Garamond", "EB Garamond 12", serif',
@@ -28,16 +34,27 @@ class BookPageFormatModule extends BaseModule {
dropCapLines: 2 dropCapLines: 2
}) })
}); });
this.pageCount = snapProceduralPageCount(window.WebGLBookInitialState?.pageCount ?? 300);
this.bindMethods([ this.bindMethods([
'getFormat', 'getFormat',
'getAspectRatio', 'getAspectRatio',
'getTextureMetrics', 'getTextureMetrics',
'setPageCount',
'getPageCount',
'getDynamicMargins',
'inchesToTexture' 'inchesToTexture'
]); ]);
} }
async initialize() { 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'); this.reportProgress(100, 'Book page format ready');
return true; return true;
} }
@@ -54,14 +71,49 @@ class BookPageFormatModule extends BaseModule {
return (Number(valueIn) / this.format.trim.heightIn) * textureHeight; 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 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 margins = { const margins = {
top: this.inchesToTexture(this.format.margins.topIn, height), top: this.inchesToTexture(dynamicMargins.topIn, height),
bottom: this.inchesToTexture(this.format.margins.bottomIn, height), bottom: this.inchesToTexture(dynamicMargins.bottomIn, height),
inner: this.inchesToTexture(this.format.margins.innerIn, height), inner: this.inchesToTexture(dynamicMargins.innerIn, height),
outer: this.inchesToTexture(this.format.margins.outerIn, height) outer: this.inchesToTexture(dynamicMargins.outerIn, height)
}; };
const content = { const content = {
x: margins.outer, x: margins.outer,
@@ -89,6 +141,13 @@ class BookPageFormatModule extends BaseModule {
margins, margins,
content, content,
contentBySide, contentBySide,
marginsIn: {
top: dynamicMargins.topIn,
bottom: dynamicMargins.bottomIn,
inner: dynamicMargins.innerIn,
outer: dynamicMargins.outerIn
},
thickness: dynamicMargins.thickness,
linesPerPage, linesPerPage,
bodyFontSizePx, bodyFontSizePx,
typographyLineHeightPx, typographyLineHeightPx,
+9 -1
View File
@@ -53,6 +53,7 @@ class BookTextureRendererModule extends BaseModule {
'publishSpread', 'publishSpread',
'getPageCanvas', 'getPageCanvas',
'getHitMap', 'getHitMap',
'handlePageCountChanged',
'handleSceneReady' 'handleSceneReady'
]); ]);
} }
@@ -64,6 +65,7 @@ class BookTextureRendererModule extends BaseModule {
this.reportProgress(20, 'Preparing page texture canvases'); this.reportProgress(20, 'Preparing page texture canvases');
this.createPageCanvases(); this.createPageCanvases();
this.drawEmptySpread(); this.drawEmptySpread();
this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged);
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady); this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
this.addEventListener(document, 'book-pagination:spread-updated', (event) => { this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
const latestBlockId = event.detail?.latestBlockId; const latestBlockId = event.detail?.latestBlockId;
@@ -84,7 +86,7 @@ class BookTextureRendererModule extends BaseModule {
return true; return true;
} }
createPageCanvases(textureWidth = 1280) { createPageCanvases(textureWidth = 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');
@@ -408,6 +410,12 @@ class BookTextureRendererModule extends BaseModule {
return this.hitMaps[side] || []; return this.hitMaps[side] || [];
} }
handlePageCountChanged(event) {
this.pageFormat?.setPageCount?.(event.detail?.pageCount);
this.createPageCanvases();
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
}
handleSceneReady() { handleSceneReady() {
this.publishSpread(); this.publishSpread();
} }
+1 -1
View File
@@ -24,7 +24,7 @@ const ModuleState = {
ERROR: 'ERROR' 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; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
/** /**
+43 -5
View File
@@ -4,6 +4,10 @@ export const PROCEDURAL_BOOK = {
PAGE_COUNT_MIN: 40, PAGE_COUNT_MIN: 40,
PAGE_COUNT_MAX: 500, PAGE_COUNT_MAX: 500,
PAGE_COUNT_STEP: 10, 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_LINE_SEGMENTS: 48,
PAGE_DEPTH: 2.24, PAGE_DEPTH: 2.24,
PAGE_WIDTH: 2.24 * (4.25 / 6.87), PAGE_WIDTH: 2.24 * (4.25 / 6.87),
@@ -16,11 +20,16 @@ export const PROCEDURAL_BOOK = {
raisedHingeY: 0.056, raisedHingeY: 0.056,
paperContactOffset: 0.0012, paperContactOffset: 0.0012,
singlePageCoverGap: 0.006, singlePageCoverGap: 0.006,
bundleSpacing: 0.014 bundleSpacing: 0.0062
} }
}; };
PROCEDURAL_BOOK.PAGE_SPLINE_LENGTH = PROCEDURAL_BOOK.PAGE_WIDTH; 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.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) { export function snapProceduralPageCount(value) {
const parsed = Number.parseFloat(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 = {}) { export function createProceduralBookModel(options = {}) {
const context = createBookContext(options); const context = createBookContext(options);
const group = new THREE.Group(); const group = new THREE.Group();
@@ -104,7 +140,8 @@ function calculateBookModel(context) {
const pageWidth = PROCEDURAL_BOOK.PAGE_WIDTH; const pageWidth = PROCEDURAL_BOOK.PAGE_WIDTH;
const pageDepth = PROCEDURAL_BOOK.PAGE_DEPTH; const pageDepth = PROCEDURAL_BOOK.PAGE_DEPTH;
const coverDepth = PROCEDURAL_BOOK.COVER_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 spineWidth = calculateSpineWidth(bundleCount);
const leftCount = calculateLeftBundleCount(context, bundleCount); const leftCount = calculateLeftBundleCount(context, bundleCount);
const spineHalf = spineArcHalf(spineWidth); const spineHalf = spineArcHalf(spineWidth);
@@ -125,6 +162,7 @@ function calculateBookModel(context) {
coverOuterX, coverOuterX,
bundleSpacing, bundleSpacing,
leftCount, leftCount,
thickness,
lines lines
}; };
} }
@@ -872,9 +910,9 @@ function pointAtMeasuredPathDistance(support, distance) {
function calculateSpineWidth(bundleCount) { function calculateSpineWidth(bundleCount) {
const minimumWidth = 0.006; const minimumWidth = 0.006;
if (bundleCount <= 1) return minimumWidth; 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 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; while (measureSpineArcLength(high) < targetArcLength) high *= 1.25;
for (let i = 0; i < 24; i += 1) { for (let i = 0; i < 24; i += 1) {
const mid = (low + high) * 0.5; const mid = (low + high) * 0.5;
@@ -887,7 +925,7 @@ function calculateSpineWidth(bundleCount) {
function calculateBundleSpacing(bundleCount, spineWidth, leftCount) { function calculateBundleSpacing(bundleCount, spineWidth, leftCount) {
const rightCount = bundleCount - leftCount; const rightCount = bundleCount - leftCount;
const stackIntervals = Math.max(0, leftCount - 1) + Math.max(0, rightCount - 1); 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); return Math.max(0.001, (measureSpineArcLength(spineWidth) - PROCEDURAL_BOOK.OPEN_SEAM_GAP) / stackIntervals);
} }
+126 -53
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-page-uv-endpoints'; import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-physical-stack-quality';
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 = isAppIntegrationMode ? 1 : Math.min(window.devicePixelRatio || 1, 2); const appRenderPixelRatio = Math.min(window.devicePixelRatio || 1, 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,8 +40,8 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
const generatedTextureCanvases = {}; const generatedTextureCanvases = {};
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy(); const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
const reflectionPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2); const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200; const pageTextureWidth = 3072;
const reflectionTargetSize = new THREE.Vector2(); const reflectionTargetSize = new THREE.Vector2();
const pageRaycaster = new THREE.Raycaster(); const pageRaycaster = new THREE.Raycaster();
const pointerNdc = new THREE.Vector2(); const pointerNdc = new THREE.Vector2();
@@ -53,6 +53,8 @@ let sceneSmaaPass = null;
let sceneOutputPass = null; let sceneOutputPass = null;
const aoExcludedObjects = new Set(); const aoExcludedObjects = new Set();
let renderedFrameCount = 0; let renderedFrameCount = 0;
let staticSceneBuffersDirty = true;
let lastStaticCameraSignature = '';
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080604); scene.background = new THREE.Color(0x080604);
@@ -65,13 +67,13 @@ let tableDustTexture = null;
let tableGreaseTexture = null; let tableGreaseTexture = null;
const tableTopY = -0.02; const tableTopY = -0.02;
const bookTableContactClearance = 0.002; const bookTableContactClearance = 0.002;
const tableReflectionBaseWidth = isAppIntegrationMode ? 640 : 4096; const tableReflectionBaseWidth = 4096;
const tableReflectionBaseHeight = isAppIntegrationMode ? 360 : 2304; const tableReflectionBaseHeight = 2304;
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, { const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
colorSpace: THREE.SRGBColorSpace, colorSpace: THREE.SRGBColorSpace,
depthBuffer: true, depthBuffer: true,
stencilBuffer: false, stencilBuffer: false,
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0 samples: renderer.capabilities.isWebGL2 ? 8 : 0
}); });
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace; tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
tableReflectionTarget.texture.minFilter = THREE.LinearFilter; tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
@@ -90,7 +92,7 @@ const reflectionUp = new THREE.Vector3();
const candleShadowSources = []; const candleShadowSources = [];
const candleWorldPosition = new THREE.Vector3(); const candleWorldPosition = new THREE.Vector3();
const flameWorldPosition = new THREE.Vector3(); const flameWorldPosition = new THREE.Vector3();
const bookShadowMapSize = isAppIntegrationMode ? 256 : 1536; const bookShadowMapSize = 1536;
const bookShadowTargets = Array.from({ length: 3 }, () => { const bookShadowTargets = Array.from({ length: 3 }, () => {
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, { const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
colorSpace: THREE.NoColorSpace, colorSpace: THREE.NoColorSpace,
@@ -169,7 +171,7 @@ const fastFlipOverlap = 5;
let activeFlips = []; let activeFlips = [];
let pendingPageFlips = 0; let pendingPageFlips = 0;
const paperColor = new THREE.Color(0xf3dfad); const paperColor = new THREE.Color(0xf1ead2);
const inkColor = '#1a1009'; const inkColor = '#1a1009';
const leftCanvas = createPageCanvas('left'); const leftCanvas = createPageCanvas('left');
@@ -179,9 +181,9 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas);
[leftTexture, rightTexture].forEach((texture) => { [leftTexture, rightTexture].forEach((texture) => {
texture.colorSpace = THREE.SRGBColorSpace; texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = maxTextureAnisotropy; texture.anisotropy = maxTextureAnisotropy;
texture.minFilter = THREE.LinearFilter; texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false; texture.generateMipmaps = true;
}); });
const leatherTextures = createLeatherTextures(); const leatherTextures = createLeatherTextures();
const spineClothTextures = createSpineClothTextures(); const spineClothTextures = createSpineClothTextures();
@@ -234,44 +236,44 @@ const materials = {
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
pageBlock: new THREE.MeshStandardMaterial({ pageBlock: new THREE.MeshStandardMaterial({
color: 0xfffbef, color: 0xf4eed8,
map: paperTextures.color, map: paperTextures.color,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.032, 0.032), normalScale: new THREE.Vector2(0.014, 0.014),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.88, roughness: 0.88,
metalness: 0, metalness: 0,
envMapIntensity: 0.06 envMapIntensity: 0.06
}), }),
pageEdge: new THREE.MeshStandardMaterial({ pageEdge: new THREE.MeshStandardMaterial({
color: 0xfff4cf, color: 0xf0e5c7,
map: paperTextures.edge, map: paperTextures.edge,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.024, 0.024), normalScale: new THREE.Vector2(0.012, 0.012),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.94, roughness: 0.94,
metalness: 0, metalness: 0,
envMapIntensity: 0.05 envMapIntensity: 0.05
}), }),
pageSurface: new THREE.MeshStandardMaterial({ pageSurface: new THREE.MeshStandardMaterial({
color: 0xfffbf0, color: 0xf5efd9,
map: paperTextures.color, map: paperTextures.color,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.03, 0.03), normalScale: new THREE.Vector2(0.012, 0.012),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.9, roughness: 0.9,
metalness: 0, metalness: 0,
emissive: 0x14110b, emissive: 0x14110b,
emissiveIntensity: 0.025, emissiveIntensity: 0.012,
envMapIntensity: 0.035, envMapIntensity: 0.035,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
flipPageSurface: new THREE.MeshStandardMaterial({ flipPageSurface: new THREE.MeshStandardMaterial({
color: 0xfffcf2, color: 0xf5efd9,
roughness: 0.92, roughness: 0.92,
metalness: 0, metalness: 0,
emissive: 0x100d08, emissive: 0x100d08,
emissiveIntensity: 0.018, emissiveIntensity: 0.01,
envMapIntensity: 0.02, envMapIntensity: 0.02,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
@@ -279,24 +281,24 @@ const materials = {
color: 0xffffff, color: 0xffffff,
map: leftTexture, map: leftTexture,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.025, 0.025), normalScale: new THREE.Vector2(0.01, 0.01),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.86, roughness: 0.86,
metalness: 0, metalness: 0,
emissive: 0x11100c, emissive: 0x11100c,
emissiveIntensity: 0.035, emissiveIntensity: 0.012,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
rightPage: new THREE.MeshStandardMaterial({ rightPage: new THREE.MeshStandardMaterial({
color: 0xffffff, color: 0xffffff,
map: rightTexture, map: rightTexture,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.025, 0.025), normalScale: new THREE.Vector2(0.01, 0.01),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.86, roughness: 0.86,
metalness: 0, metalness: 0,
emissive: 0x11100c, emissive: 0x11100c,
emissiveIntensity: 0.035, emissiveIntensity: 0.012,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
spineCloth: new THREE.MeshStandardMaterial({ spineCloth: new THREE.MeshStandardMaterial({
@@ -333,18 +335,19 @@ configureBookShadowReceiver(materials.leather, 0.52);
configureBookShadowReceiver(materials.hingeLeather, 0.36); configureBookShadowReceiver(materials.hingeLeather, 0.36);
configureBookShadowReceiver(materials.spineBaseLeather, 0.34); configureBookShadowReceiver(materials.spineBaseLeather, 0.34);
configureBookShadowReceiver(materials.coverEdge, 0.28); configureBookShadowReceiver(materials.coverEdge, 0.28);
configureBookShadowReceiver(materials.pageBlock, 0.46); configureBookShadowReceiver(materials.pageBlock, 0.3);
configureBookShadowReceiver(materials.pageEdge, 0.34); configureBookShadowReceiver(materials.pageEdge, 0.24);
configureBookShadowReceiver(materials.pageSurface, 0.34); configureBookShadowReceiver(materials.pageSurface, 0.2);
configureBookShadowReceiver(materials.flipPageSurface, 0.32); configureBookShadowReceiver(materials.flipPageSurface, 0.2);
configureBookShadowReceiver(materials.leftPage, 0.38); configureBookShadowReceiver(materials.leftPage, 0.18);
configureBookShadowReceiver(materials.rightPage, 0.38); 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);
buildTable(); buildTable();
buildLighting(); buildLighting();
buildBook(); buildBook();
notifyBookPageCountChanged();
loadAiRoomReflection(); loadAiRoomReflection();
window.BookLabDebug = { window.BookLabDebug = {
textures: generatedTextureCanvases, textures: generatedTextureCanvases,
@@ -629,11 +632,11 @@ function configureBookShadowReceiver(material, strength) {
float sideFill = grazingSide * sideReach; float sideFill = grazingSide * sideReach;
float tableFill = tableReach * (0.16 + underside * 0.22) * (1.0 - upFacing * 0.58); 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)); 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 tableWarmth = vec3(0.042, 0.034, 0.028) * tableFill;
vec3 roomWarmth = vec3(0.04, 0.034, 0.028) * sideFill; vec3 roomWarmth = vec3(0.032, 0.032, 0.03) * sideFill;
vec3 pageWarmth = vec3(0.045, 0.041, 0.034) * pageFill * grazingSide * (1.0 - upFacing * 0.42); vec3 pageWarmth = vec3(0.032, 0.032, 0.029) * pageFill * grazingSide * (1.0 - upFacing * 0.42);
vec3 indirect = tableWarmth + roomWarmth + pageWarmth; 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) { 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); sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718);
float cloud = sin((uv.x * 17.0 + uv.y * 11.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); sin((uv.x * 29.0 - uv.y * 23.0) * 6.28318530718);
float fiber = clamp(fleck * 0.018 + cloud * 0.022, -0.04, 0.05); float fiber = clamp(fleck * 0.008 + cloud * 0.012, -0.02, 0.026);
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)); 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; return baseLight * paperTint;
} }
@@ -681,7 +684,7 @@ function configureBookShadowReceiver(material, strength) {
${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''} ${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''}
${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''} ${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''}
float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength; 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); outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb);
#include <opaque_fragment>` #include <opaque_fragment>`
); );
@@ -715,7 +718,13 @@ function configureScenePostprocessing() {
sceneAoPass.output = SSAOPass.OUTPUT.Default; sceneAoPass.output = SSAOPass.OUTPUT.Default;
} }
const renderAoPass = sceneAoPass.render.bind(sceneAoPass); const renderAoPass = sceneAoPass.render.bind(sceneAoPass);
sceneAoPass.userData = sceneAoPass.userData || {};
sceneAoPass.userData.cachedRenderPass = renderAoPass;
sceneAoPass.render = (...args) => { sceneAoPass.render = (...args) => {
if (!staticSceneBuffersDirty && activeFlips.length === 0) {
renderCachedAoPass(sceneAoPass, ...args);
return;
}
aoExcludedObjects.forEach((object) => { aoExcludedObjects.forEach((object) => {
object.userData.wasVisibleForAo = object.visible; object.userData.wasVisibleForAo = object.visible;
object.visible = false; object.visible = false;
@@ -1360,6 +1369,7 @@ function configureTableShader(material) {
} }
function buildBook() { function buildBook() {
markStaticSceneBuffersDirty();
clearActiveFlips(); clearActiveFlips();
book.traverse((object) => { book.traverse((object) => {
if (object.isMesh) aoExcludedObjects.delete(object); if (object.isMesh) aoExcludedObjects.delete(object);
@@ -1404,17 +1414,52 @@ function buildBook() {
}); });
currentProceduralBookModel = proceduralBook.model; currentProceduralBookModel = proceduralBook.model;
book.add(proceduralBook.group); 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 } = {}) { function configureHardcoverPaperMaterial(material, { useEdgeMap = false } = {}) {
material.userData.isHardcoverPaper = true; material.userData.isHardcoverPaper = true;
if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color; if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color;
material.normalMap = paperTextures.normal; 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.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.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; material.needsUpdate = true;
} }
@@ -1432,10 +1477,20 @@ function setBookPageCount(value) {
if (!Number.isFinite(nextPageCount)) return; if (!Number.isFinite(nextPageCount)) return;
bookPageCount = nextPageCount; bookPageCount = nextPageCount;
buildBook(); buildBook();
notifyBookPageCountChanged();
syncBookControls(); syncBookControls();
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount); window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
} }
function notifyBookPageCountChanged() {
document.dispatchEvent(new CustomEvent('webgl-book:page-count-changed', {
detail: {
pageCount: bookPageCount,
model: currentProceduralBookModel
}
}));
}
function stepReadingProgress(pageDelta) { function stepReadingProgress(pageDelta) {
setReadingProgress(readingProgress + pageDelta / Math.max(1, bookPageCount)); setReadingProgress(readingProgress + pageDelta / Math.max(1, bookPageCount));
} }
@@ -2414,32 +2469,32 @@ function createHardcoverPaperTextures() {
const cloudA = Math.sin((nx * 19 + ny * 11) * 6.28318530718); const cloudA = Math.sin((nx * 19 + ny * 11) * 6.28318530718);
const cloudB = Math.sin((nx * 31 - ny * 27) * 6.28318530718); const cloudB = Math.sin((nx * 31 - ny * 27) * 6.28318530718);
const fleck = Math.max(0, 0.5 - Math.abs(pulpA * pulpB)); 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 y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) { for (let x = 0; x < size; x += 1) {
const index = (y * size + x) * 4; const index = (y * size + x) * 4;
const fiber = fiberAt(x, y); 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 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.975 + fiber, 0.88, 1.0); const shade = THREE.MathUtils.clamp(0.985 + fiber, 0.92, 1.0);
colorImage.data[index] = Math.round(255 * shade * warmth); colorImage.data[index] = Math.round(246 * shade * warmth);
colorImage.data[index + 1] = Math.round(251 * shade * warmth); colorImage.data[index + 1] = Math.round(239 * shade * warmth);
colorImage.data[index + 2] = Math.round(235 * shade); colorImage.data[index + 2] = Math.round(216 * shade);
colorImage.data[index + 3] = 255; colorImage.data[index + 3] = 255;
const linePhase = (y + Math.sin(x * 0.021) * 4) % 34; const linePhase = (y + Math.sin(x * 0.021) * 4) % 34;
const line = linePhase < 1.2 ? 0.72 : linePhase < 2.1 ? 0.82 : 1; const line = linePhase < 1.2 ? 0.72 : linePhase < 2.1 ? 0.82 : 1;
edgeImage.data[index] = Math.round(255 * shade * line); edgeImage.data[index] = Math.round(240 * shade * line);
edgeImage.data[index + 1] = Math.round(244 * shade * line); edgeImage.data[index + 1] = Math.round(230 * shade * line);
edgeImage.data[index + 2] = Math.round(207 * shade * line); edgeImage.data[index + 2] = Math.round(194 * shade * line);
edgeImage.data[index + 3] = 255; edgeImage.data[index + 3] = 255;
const hLeft = fiberAt((x - 1 + size) % size, y); const hLeft = fiberAt((x - 1 + size) % size, y);
const hRight = fiberAt((x + 1) % size, y); const hRight = fiberAt((x + 1) % size, y);
const hDown = fiberAt(x, (y - 1 + size) % size); const hDown = fiberAt(x, (y - 1 + size) % size);
const hUp = fiberAt(x, (y + 1) % 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] = 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 + 1] = Math.round((normal.y * 0.5 + 0.5) * 255);
normalImage.data[index + 2] = Math.round((normal.z * 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() { function resize() {
markStaticSceneBuffersDirty();
const width = Math.max(1, window.innerWidth); const width = Math.max(1, window.innerWidth);
const height = Math.max(1, window.innerHeight); const height = Math.max(1, window.innerHeight);
renderer.setSize(width, height, false); renderer.setSize(width, height, false);
@@ -2687,6 +2743,19 @@ function updateCameraRig(deltaSeconds) {
cameraRig.target.z + Math.cos(cameraRig.yaw) * horizontalRadius cameraRig.target.z + Math.cos(cameraRig.yaw) * horizontalRadius
); );
camera.lookAt(cameraRig.target); 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() { 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); 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()); updateActiveFlips(performance.now());
if (hadActiveFlips) markStaticSceneBuffersDirty();
updateCandleShadowUniforms(); updateCandleShadowUniforms();
renderedFrameCount += 1; renderedFrameCount += 1;
const shadowStartedAt = performance.now(); const shadowStartedAt = performance.now();
if (!isAppIntegrationMode || renderedFrameCount % 6 === 1 || activeFlips.length > 0) {
updateBookShadowMaps(); updateBookShadowMaps();
}
lastFrameTiming.shadows = performance.now() - shadowStartedAt; lastFrameTiming.shadows = performance.now() - shadowStartedAt;
const reflectionStartedAt = performance.now(); const reflectionStartedAt = performance.now();
if (!isAppIntegrationMode || renderedFrameCount % 4 === 1 || cameraRig.navigationActive || activeFlips.length > 0) { const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
if (refreshStaticSceneBuffers) {
updateTableReflection(); updateTableReflection();
} }
lastFrameTiming.reflection = performance.now() - reflectionStartedAt; lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
@@ -2909,6 +2979,9 @@ function animate(now = performance.now()) {
} }
lastFrameTiming.render = performance.now() - renderStartedAt; lastFrameTiming.render = performance.now() - renderStartedAt;
lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render; lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render;
if (refreshStaticSceneBuffers && activeFlips.length === 0) {
staticSceneBuffersDirty = false;
}
window.BookLabDebug.renderedFrames += 1; window.BookLabDebug.renderedFrames += 1;
window.BookLabDebug.ready = true; window.BookLabDebug.ready = true;
fpsWindowFrames += 1; fpsWindowFrames += 1;