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);
};
</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>
</html>
+66 -7
View File
@@ -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,
+9 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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;
/**
+43 -5
View File
@@ -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);
}
+127 -54
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 { 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 <opaque_fragment>`
);
@@ -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;