Restore WebGL book quality settings
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user