Stabilize WebGL title and timeline texture flow
This commit is contained in:
@@ -39,6 +39,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
'waitForVisualCompletion',
|
||||
'waitForRevealCommit',
|
||||
'requestPageFlip',
|
||||
'prepareFlipPlan',
|
||||
'waitForPageFlipFinished',
|
||||
'prewarmSegmentTextures',
|
||||
'getPageMetaForIndex',
|
||||
@@ -361,6 +362,11 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
const startedAt = Number(segment.revealStartedAt)
|
||||
|| await (segment.revealStartedPromise || Promise.resolve(performance.now()));
|
||||
const duration = this.getRightRevealDurationMs(segment);
|
||||
segment.plannedRightRevealDurationMs = duration;
|
||||
this.recordDiagnostic('wait-right-reveal-planned', {
|
||||
...segment,
|
||||
plannedRightRevealDurationMs: duration
|
||||
});
|
||||
const elapsed = Math.max(0, performance.now() - Number(startedAt || performance.now()));
|
||||
const remaining = Math.max(0, duration - elapsed);
|
||||
const planned = new Promise(resolve => {
|
||||
@@ -413,13 +419,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
|
||||
async requestPageFlip(direction = 1, options = {}) {
|
||||
if (this.isChoiceAwaitingPlayer()) return false;
|
||||
await this.pageCache?.prewarmNavigationWindow?.({
|
||||
currentSpread: this.getVisibleSpreadIndex(),
|
||||
targetSpread: options.targetSpread,
|
||||
endSpread: Math.max(0, Number(this.pagination?.spreads?.length || 1) - 1),
|
||||
getPageMetaForIndex: this.getPageMetaForIndex,
|
||||
recordMiss: false
|
||||
});
|
||||
const flipPlan = await this.prepareFlipPlan(direction, options);
|
||||
await this.assertSegmentReady({
|
||||
blockId: options.blockId ?? null,
|
||||
targetSpreadIndex: options.targetSpread,
|
||||
@@ -432,11 +432,48 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
window.BookLabDebug.requestPageFlip(direction, {
|
||||
force: options.force === true,
|
||||
reason: options.reason || 'timeline',
|
||||
targetSpread: options.targetSpread
|
||||
targetSpread: options.targetSpread,
|
||||
prewarm: flipPlan.prewarm,
|
||||
flipPlan
|
||||
});
|
||||
return wait;
|
||||
}
|
||||
|
||||
async prepareFlipPlan(direction = 1, options = {}) {
|
||||
const currentSpread = this.getVisibleSpreadIndex();
|
||||
const targetSpread = Number.isFinite(Number(options.targetSpread))
|
||||
? Math.max(0, Math.round(Number(options.targetSpread)))
|
||||
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
||||
const prewarm = await this.pageCache?.prewarmNavigationWindow?.({
|
||||
currentSpread,
|
||||
targetSpread,
|
||||
endSpread: Math.max(0, Number(this.pagination?.spreads?.length || 1) - 1),
|
||||
getPageMetaForIndex: this.getPageMetaForIndex,
|
||||
recordMiss: false
|
||||
});
|
||||
const sourceSide = direction > 0 ? 'right' : 'left';
|
||||
const backSide = direction > 0 ? 'left' : 'right';
|
||||
const sourcePageIndex = currentSpread * 2 + (sourceSide === 'right' ? 1 : 0);
|
||||
const backPageIndex = targetSpread * 2 + (backSide === 'right' ? 1 : 0);
|
||||
const plan = {
|
||||
direction,
|
||||
currentSpread,
|
||||
targetSpread,
|
||||
sourceSide,
|
||||
backSide,
|
||||
sourcePageMeta: this.getPageMetaForIndex(sourcePageIndex),
|
||||
backPageMeta: this.getPageMetaForIndex(backPageIndex),
|
||||
prewarm,
|
||||
createdAt: performance.now()
|
||||
};
|
||||
this.markBenchmark('flip-plan-ready', plan);
|
||||
this.recordDiagnostic('flip-plan-ready', {
|
||||
...plan,
|
||||
targetSpreadIndex: targetSpread
|
||||
});
|
||||
return plan;
|
||||
}
|
||||
|
||||
async prewarmSegmentTextures(segment = {}) {
|
||||
if (!this.pageCache || typeof this.pageCache.prewarmNavigationWindow !== 'function') return null;
|
||||
const targetSpread = Math.max(0, Number(segment.targetSpreadIndex || 0));
|
||||
@@ -604,6 +641,9 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
spreadIndex: segment.targetSpreadIndex ?? null,
|
||||
status: segment.status || null,
|
||||
revealSides: Array.isArray(segment.revealSides) ? segment.revealSides : [],
|
||||
plannedRightRevealDurationMs: Number.isFinite(Number(segment.plannedRightRevealDurationMs))
|
||||
? Math.round(Number(segment.plannedRightRevealDurationMs))
|
||||
: undefined,
|
||||
at: Math.round(performance.now())
|
||||
});
|
||||
while (this.timelineDiagnostics.length > 200) this.timelineDiagnostics.shift();
|
||||
|
||||
@@ -7,10 +7,11 @@ import { BaseModule } from './base-module.js';
|
||||
class BookTextureRendererModule extends BaseModule {
|
||||
constructor() {
|
||||
super('book-texture-renderer', 'Book Texture Renderer');
|
||||
this.dependencies = ['book-page-format', 'book-pagination', 'localization', 'webgl-page-cache'];
|
||||
this.dependencies = ['book-page-format', 'book-pagination', 'localization', 'game-config', 'webgl-page-cache'];
|
||||
this.pageFormat = null;
|
||||
this.pagination = null;
|
||||
this.localization = null;
|
||||
this.gameConfig = null;
|
||||
this.pageCache = null;
|
||||
this.metrics = null;
|
||||
this.canvases = {
|
||||
@@ -103,6 +104,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.pageFormat = this.getModule('book-page-format');
|
||||
this.pagination = this.getModule('book-pagination');
|
||||
this.localization = this.getModule('localization');
|
||||
this.gameConfig = this.getModule('game-config');
|
||||
this.pageCache = this.getModule('webgl-page-cache');
|
||||
window.BookTextureRendererDebug = {
|
||||
pipelineTimings: this.pipelineTimings
|
||||
@@ -119,6 +121,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
||||
const visibility = event.detail?.visibility || 'current';
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
const timelineOwnsPlayback = window.BookPlaybackTimeline?.ownsPageFlipCommit === true;
|
||||
if (document.documentElement.dataset.webglPageFlipActive === 'true' && this.activeAnimations.size === 0) {
|
||||
this.markPipelineTiming('spreadUpdate:skip-during-flip', {
|
||||
spreadIndex,
|
||||
@@ -129,8 +132,19 @@ class BookTextureRendererModule extends BaseModule {
|
||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||
this.markPendingReveal(latestBlockId);
|
||||
const id = String(latestBlockId);
|
||||
if (timelineOwnsPlayback && visibility !== 'future-ready') {
|
||||
this.markPipelineTiming('spreadUpdate:skip-timeline-owned-reveal', {
|
||||
spreadIndex,
|
||||
latestBlockId: id,
|
||||
visibility
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (visibility === 'future-ready' && !this.activeAnimations.has(id)) {
|
||||
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right']);
|
||||
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right'], {
|
||||
phase: 'prepare',
|
||||
publishEvent: !timelineOwnsPlayback
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.activeAnimations.has(id)) {
|
||||
@@ -145,6 +159,15 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timelineOwnsPlayback && visibility !== 'future-ready' && latestBlockId) {
|
||||
this.markPipelineTiming('spreadUpdate:skip-timeline-owned-commit', {
|
||||
spreadIndex,
|
||||
latestBlockId,
|
||||
latestRenderedBlockId,
|
||||
visibility
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.drawSpread(this.currentSpread);
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:fast-forward', this.fastForwardAnimations);
|
||||
@@ -230,7 +253,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const hasReveal = this.revealPublishBlockIds && this.revealPublishBlockIds.size > 0;
|
||||
const phase = this.getDrawPhase(options);
|
||||
const drawSignature = this.getDrawSignature(this.currentSpread, sidesToDraw);
|
||||
if (phase !== 'prepare' && !hasReveal && drawSignature === this.lastDrawSignature) {
|
||||
if (options.force !== true && phase !== 'prepare' && !hasReveal && drawSignature === this.lastDrawSignature) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastDrawSkipLoggedAt > 1000) {
|
||||
this.lastDrawSkipLoggedAt = now;
|
||||
@@ -326,11 +349,17 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const ctx = this.contexts[side];
|
||||
if (!ctx || !this.metrics) return;
|
||||
const content = this.getPageContent(side);
|
||||
const titleText = document.getElementById('game_title')?.textContent?.trim() || '';
|
||||
const authorText = document.getElementById('game_author')?.textContent?.trim() || '';
|
||||
const subtitleText = document.getElementById('game_subtitle')?.textContent?.trim() || '';
|
||||
const metadata = this.gameConfig?.getMetadata?.() || {};
|
||||
const titleText = document.getElementById('game_title')?.textContent?.trim() || metadata.title || '';
|
||||
const authorText = document.getElementById('game_author')?.textContent?.trim()
|
||||
|| (metadata.author ? this.localization?.t?.('title.byAuthor', { author: metadata.author }) : '')
|
||||
|| '';
|
||||
const subtitleText = document.getElementById('game_subtitle')?.textContent?.trim() || metadata.subtitle || '';
|
||||
const ornamentText = document.querySelector('#start_prompt .separator, #start_prompt .ornament, #start_prompt [class*="separator"]')?.textContent?.trim() || '';
|
||||
const legalText = document.getElementById('game_legal_text')?.textContent?.trim() || '';
|
||||
const legalText = document.getElementById('game_legal_text')?.textContent?.trim() || [
|
||||
metadata.version ? this.localization?.t?.('title.version', { version: metadata.version }) : '',
|
||||
metadata.copyright || ''
|
||||
].filter(Boolean).join(' | ');
|
||||
const centerX = content.x + content.width * 0.5;
|
||||
const font = this.metrics.typography.fontFamily;
|
||||
|
||||
@@ -658,7 +687,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
blockId: region.blockId,
|
||||
lineIndex: region.lineIndex,
|
||||
rect: region.rect,
|
||||
timing: region.timing
|
||||
timing: region.timing,
|
||||
timingArea: region.timingArea || region.area || 0
|
||||
})),
|
||||
bounds: {
|
||||
x: bounds.x / this.metrics.width,
|
||||
@@ -707,7 +737,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}
|
||||
|
||||
assignRevealTiming(blockRegions = [], animation = {}) {
|
||||
const totalDuration = Math.max(
|
||||
const requestedTotalDuration = Math.max(
|
||||
Number(animation.totalDuration || 0),
|
||||
...((Array.isArray(animation.wordTimings) ? animation.wordTimings : []).map(timing => Number(timing.delay || 0) + Number(timing.duration || 0)))
|
||||
);
|
||||
@@ -723,32 +753,22 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const textRegions = sortedRegions.filter(region => !(region.fixedDurationMs > 0));
|
||||
const fixedRegions = sortedRegions.filter(region => region.fixedDurationMs > 0);
|
||||
let fallbackDelay = 0;
|
||||
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
|
||||
const wordTimings = Array.isArray(animation.wordTimings) ? animation.wordTimings : [];
|
||||
const canUseLineWordSpans = wordTimings.length > 0
|
||||
&& textRegions.every(region => Number.isFinite(Number(region.blockWordStart)) && Number(region.blockWordCount) > 0);
|
||||
|
||||
if (canUseLineWordSpans) {
|
||||
textRegions.forEach((region) => {
|
||||
const timing = this.getLineTimingFromWords(region, wordTimings);
|
||||
timedRegions.push({
|
||||
...region,
|
||||
timing
|
||||
});
|
||||
fallbackDelay = Math.max(fallbackDelay, timing.delay + timing.duration);
|
||||
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.timingArea || region.area), 0);
|
||||
const lineHeight = Math.max(1, Number(this.metrics?.typographyLineHeightPx || 1));
|
||||
const estimatedTextWidth = totalArea / lineHeight;
|
||||
const totalDuration = requestedTotalDuration > 1
|
||||
? requestedTotalDuration
|
||||
: Math.max(800, estimatedTextWidth * 16);
|
||||
textRegions.forEach((region) => {
|
||||
const duration = totalArea > 0
|
||||
? Math.max(1, totalDuration * (Math.max(1, region.timingArea || region.area) / totalArea))
|
||||
: Math.max(1, totalDuration / Math.max(1, textRegions.length));
|
||||
timedRegions.push({
|
||||
...region,
|
||||
timing: { delay: fallbackDelay, duration }
|
||||
});
|
||||
} else {
|
||||
textRegions.forEach((region) => {
|
||||
const duration = totalArea > 0
|
||||
? Math.max(1, totalDuration * (Math.max(1, region.area) / totalArea))
|
||||
: Math.max(1, totalDuration / Math.max(1, textRegions.length));
|
||||
timedRegions.push({
|
||||
...region,
|
||||
timing: { delay: fallbackDelay, duration }
|
||||
});
|
||||
fallbackDelay += duration;
|
||||
});
|
||||
}
|
||||
fallbackDelay += duration;
|
||||
});
|
||||
|
||||
fixedRegions.forEach((region) => {
|
||||
timedRegions.push({
|
||||
@@ -813,6 +833,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const bottom = Math.min(this.metrics.height, y + height + padding);
|
||||
const rectWidth = Math.max(1, right - left);
|
||||
const rectHeight = Math.max(1, bottom - top);
|
||||
const timingWidth = Math.max(1, Number(lineRecord.timingWidthPx || width || rectWidth));
|
||||
const timingHeight = Math.max(1, Number(lineRecord.timingHeightPx || height || rectHeight));
|
||||
return {
|
||||
side,
|
||||
spreadIndex: Math.max(0, Number((spreadIndex ?? Math.floor(Number(lineRecord.pageIndex || 0) / 2)) || 0)),
|
||||
@@ -822,6 +844,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
blockWordCount: Number(lineRecord.lineWordCount ?? 0),
|
||||
fixedDurationMs,
|
||||
area: rectWidth * rectHeight,
|
||||
timingArea: timingWidth * timingHeight,
|
||||
pixelRect: { x: left, y: top, right, bottom },
|
||||
rect: {
|
||||
x: left / this.metrics.width,
|
||||
@@ -975,7 +998,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
phase,
|
||||
publishEvent: options.publishEvent !== false
|
||||
});
|
||||
if (phase !== 'prepare') this.preloadAdditionalRevealSpreads(id, spread);
|
||||
this.preloadAdditionalRevealSpreads(id, spread);
|
||||
if (phase === 'prepare' && published) {
|
||||
this.pageCache?.rememberPreparedRevealPlan?.(id, {
|
||||
...published,
|
||||
|
||||
+88
-23
@@ -40,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
|
||||
|
||||
const generatedTextureCanvases = {};
|
||||
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
const reflectionPixelRatio = 1;
|
||||
const reflectionPixelRatio = 0.72;
|
||||
const pageTextureWidth = 3072;
|
||||
const reflectionTargetSize = new THREE.Vector2();
|
||||
const pageRaycaster = new THREE.Raycaster();
|
||||
@@ -80,13 +80,13 @@ let tableDustTexture = null;
|
||||
let tableGreaseTexture = null;
|
||||
const tableTopY = -0.02;
|
||||
const bookTableContactClearance = 0.002;
|
||||
const tableReflectionBaseWidth = 2048;
|
||||
const tableReflectionBaseHeight = 1152;
|
||||
const tableReflectionBaseWidth = 1536;
|
||||
const tableReflectionBaseHeight = 864;
|
||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
||||
colorSpace: THREE.SRGBColorSpace,
|
||||
depthBuffer: true,
|
||||
stencilBuffer: false,
|
||||
samples: renderer.capabilities.isWebGL2 ? 8 : 0
|
||||
samples: renderer.capabilities.isWebGL2 ? 4 : 0
|
||||
});
|
||||
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
||||
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
|
||||
@@ -105,7 +105,7 @@ const reflectionUp = new THREE.Vector3();
|
||||
const candleShadowSources = [];
|
||||
const candleWorldPosition = new THREE.Vector3();
|
||||
const flameWorldPosition = new THREE.Vector3();
|
||||
const bookShadowMapSize = 1536;
|
||||
const bookShadowMapSize = 1024;
|
||||
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
||||
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
||||
colorSpace: THREE.NoColorSpace,
|
||||
@@ -126,6 +126,10 @@ const bookShadowBiasMatrix = new THREE.Matrix4().set(
|
||||
0, 0, 0.5, 0.5,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
const dynamicBufferRefreshIntervalMs = 1000 / 30;
|
||||
const flipDynamicBufferGraceMs = 180;
|
||||
let lastBookShadowRefreshAt = -Infinity;
|
||||
let lastTableReflectionRefreshAt = -Infinity;
|
||||
const bookShadowDepthMaterial = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.RGBADepthPacking
|
||||
});
|
||||
@@ -158,6 +162,7 @@ configureScenePostprocessing();
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const targetFrameDurationMs = 1000 / 60;
|
||||
const minRenderFrameIntervalMs = targetFrameDurationMs * 0.5;
|
||||
let lastRenderFrameAt = 0;
|
||||
let fpsDisplay = null;
|
||||
let fpsWindowStartedAt = performance.now();
|
||||
@@ -626,7 +631,8 @@ window.BookLabDebug = {
|
||||
preparedPageTextureCount: textureStoreState.preparedTextureCount || 0,
|
||||
singlePageTextureStore: true,
|
||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||
mirrorRefreshesEveryFrame: true,
|
||||
mirrorRefreshesAtFps: Math.round(1000 / dynamicBufferRefreshIntervalMs),
|
||||
mirrorDefersDuringFlipStartMs: flipDynamicBufferGraceMs,
|
||||
mirrorRefreshesWhenStaticDirty: true,
|
||||
lastFlipTexturePreflight
|
||||
};
|
||||
@@ -668,6 +674,30 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const incomingSpreadIndex = Math.max(0, Number(detail.spreadIndex || 0));
|
||||
const latestBlockId = Math.max(0, Number(detail.latestBlockId || 0));
|
||||
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
||||
if (
|
||||
window.BookPlaybackTimeline?.ownsPageFlipCommit === true
|
||||
&& detail.visibility !== 'future-ready'
|
||||
&& latestBlockId > 0
|
||||
) {
|
||||
markPageTextureTiming('spreadUpdate:timeline-owned-state-only', {
|
||||
incomingSpreadIndex,
|
||||
visibleSpreadIndex: bookPaginationState.spreadIndex,
|
||||
latestBlockId,
|
||||
latestRenderedBlockId
|
||||
});
|
||||
bookPaginationState = {
|
||||
...bookPaginationState,
|
||||
spreadCount: Math.max(1, Number(detail.spreadCount || bookPaginationState.spreadCount || 1)),
|
||||
writtenPageLimit: Math.max(
|
||||
Math.max(0, Number(bookPaginationState.writtenPageLimit || 0)),
|
||||
Math.max(0, Number(detail.writtenPageLimit || 0))
|
||||
)
|
||||
};
|
||||
growBookIfWritableLimitReached();
|
||||
syncBookControls();
|
||||
if (pendingRightPageFlip) tryStartPendingRightPageFlip('spread-updated');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
latestBlockId > latestRenderedBlockId
|
||||
&& detail.visibility !== 'future-ready'
|
||||
@@ -2464,6 +2494,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
visualElapsedMs: activeStartedAt ? Math.max(0, performance.now() - activeStartedAt) : 0,
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
blockIds: revealBlockIds,
|
||||
pageMeta: revealDetail.pageMeta ? { ...revealDetail.pageMeta } : null,
|
||||
baseTexture,
|
||||
pageFlipAfterReveal: revealDetail.pageFlipAfterReveal === true,
|
||||
fastForwarding: false,
|
||||
@@ -2602,6 +2633,14 @@ function syncFlipRevealShaderFromSource(sourceSide, targetMaterial = materials.f
|
||||
return true;
|
||||
}
|
||||
|
||||
function revealStateMatchesPage(side, pageMeta = null) {
|
||||
const statePageIndex = Number(pageRevealState[side]?.pageMeta?.pageIndex);
|
||||
const expectedPageIndex = Number(pageMeta?.pageIndex);
|
||||
return Number.isFinite(statePageIndex)
|
||||
&& Number.isFinite(expectedPageIndex)
|
||||
&& Math.max(0, Math.round(statePageIndex)) === Math.max(0, Math.round(expectedPageIndex));
|
||||
}
|
||||
|
||||
function getRevealDebugState() {
|
||||
return ['left', 'right'].reduce((state, side) => {
|
||||
const shader = getPageRevealShader(side);
|
||||
@@ -2622,7 +2661,7 @@ function getRevealDebugState() {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function clearPageReveal(side, reason = 'clear') {
|
||||
function clearPageReveal(side, reason = 'clear', options = {}) {
|
||||
const previousState = pageRevealState[side];
|
||||
pageRevealClearLog.push({
|
||||
side,
|
||||
@@ -2646,7 +2685,7 @@ function clearPageReveal(side, reason = 'clear') {
|
||||
shader.uniforms.bookRevealRegionCount.value = 0;
|
||||
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = 0;
|
||||
}
|
||||
previousState?.baseTexture?.dispose?.();
|
||||
if (options.preserveBaseTexture !== true) previousState?.baseTexture?.dispose?.();
|
||||
}
|
||||
|
||||
function startPageRevealForBlock(blockId) {
|
||||
@@ -2885,7 +2924,7 @@ async function startPageFlip(direction, options = {}) {
|
||||
if (activeFlips.length || !currentProceduralBookModel) return false;
|
||||
if (!options.force && !canPageFlip(direction)) return false;
|
||||
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||
const prewarm = await prewarmFlipTextures(direction, targetSpread);
|
||||
const prewarm = options.prewarm || options.flipPlan?.prewarm || await prewarmFlipTextures(direction, targetSpread);
|
||||
return startPageFlipPrepared(direction, {
|
||||
...options,
|
||||
targetSpread,
|
||||
@@ -2922,7 +2961,7 @@ function startPageFlipPrepared(direction, options = {}) {
|
||||
async function startFastPageFlip(direction, options = {}) {
|
||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||
const prewarm = await prewarmFlipTextures(direction, targetSpread);
|
||||
const prewarm = options.prewarm || options.flipPlan?.prewarm || await prewarmFlipTextures(direction, targetSpread);
|
||||
return startFastPageFlipPrepared(direction, {
|
||||
...options,
|
||||
targetSpread,
|
||||
@@ -3040,14 +3079,16 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
}
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.userData.sourceRevealSide = pageRevealState[sourceSide] ? sourceSide : null;
|
||||
materials.flipPageBackSurface.userData.sourceRevealSide = null;
|
||||
materials.flipPageSurface.userData.sourceRevealSide = revealStateMatchesPage(sourceSide, sourcePageMeta) ? sourceSide : null;
|
||||
materials.flipPageBackSurface.userData.sourceRevealSide = revealStateMatchesPage(targetBackSide, targetBackPageMeta) ? targetBackSide : null;
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
materials.flipPageBackSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
materials.flipPageSurface.needsUpdate = true;
|
||||
materials.flipPageBackSurface.needsUpdate = true;
|
||||
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
|
||||
syncFlipRevealShaderFromSource(targetBackSide, materials.flipPageBackSurface);
|
||||
flip.sourceTexture = sourceTexture;
|
||||
flip.sourcePageMeta = sourcePageMeta ? { ...sourcePageMeta } : null;
|
||||
flip.backTexture = backTexture || getBlankPageTexture();
|
||||
@@ -3070,14 +3111,14 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
clearPageReveal('right', 'page-flip-start');
|
||||
clearPageReveal('right', 'page-flip-start', { preserveBaseTexture: sourceSide === 'right' });
|
||||
materials.rightPage.map = blankTexture;
|
||||
materials.rightPage.needsUpdate = true;
|
||||
}
|
||||
} else if (flip.direction < 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.leftPage.map !== blankTexture) {
|
||||
clearPageReveal('left', 'page-flip-start');
|
||||
clearPageReveal('left', 'page-flip-start', { preserveBaseTexture: sourceSide === 'left' });
|
||||
materials.leftPage.map = blankTexture;
|
||||
materials.leftPage.needsUpdate = true;
|
||||
}
|
||||
@@ -3086,7 +3127,6 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
...lastFlipTexturePreflight,
|
||||
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
|
||||
});
|
||||
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3094,7 +3134,7 @@ function resolveCurrentFlipSourceTexture(side) {
|
||||
const pageMeta = currentPageMeta?.[side] || null;
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
if (pageRevealState[side]) return material?.map || null;
|
||||
if (revealStateMatchesPage(side, pageMeta)) return material?.map || null;
|
||||
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
if (resident) return resident;
|
||||
return material?.map || null;
|
||||
@@ -3222,7 +3262,8 @@ function applyResidentSpreadTextures(spreadIndex, reason = 'resident-spread') {
|
||||
return;
|
||||
}
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
clearPageReveal(side, reason);
|
||||
const activeRevealForPage = revealStateMatchesPage(side, pageMeta);
|
||||
if (!activeRevealForPage) clearPageReveal(side, reason);
|
||||
if (material.map !== texture) {
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
@@ -3385,8 +3426,8 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
||||
const targetSide = -sourceSide;
|
||||
const topPageSide = direction > 0 ? targetSide : sourceSide;
|
||||
const bottomPageSide = direction > 0 ? sourceSide : targetSide;
|
||||
const topMaterialIndex = direction > 0 ? 1 : 0;
|
||||
const bottomMaterialIndex = direction > 0 ? 0 : 1;
|
||||
const topMaterialIndex = 0;
|
||||
const bottomMaterialIndex = 1;
|
||||
const push = (point, yOffset, uv) => {
|
||||
const index = positions.length / 3;
|
||||
positions.push(point.x, point.y + yOffset, point.z);
|
||||
@@ -4553,7 +4594,7 @@ function renderMirrorDebugView() {
|
||||
|
||||
function animate(now = performance.now()) {
|
||||
const elapsedSinceLastFrame = lastRenderFrameAt ? now - lastRenderFrameAt : targetFrameDurationMs;
|
||||
if (lastRenderFrameAt && elapsedSinceLastFrame < targetFrameDurationMs) {
|
||||
if (lastRenderFrameAt && elapsedSinceLastFrame < minRenderFrameIntervalMs) {
|
||||
requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
@@ -4603,12 +4644,36 @@ function animate(now = performance.now()) {
|
||||
updateCandleShadowUniforms();
|
||||
lastFrameTiming.update = performance.now() - updateStartedAt;
|
||||
renderedFrameCount += 1;
|
||||
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
|
||||
const shadowStartedAt = performance.now();
|
||||
updateBookShadowMaps();
|
||||
const forceDynamicBufferRefresh = staticSceneBuffersDirty && activeFlips.length === 0;
|
||||
const newestFlipAge = activeFlips.length
|
||||
? Math.min(...activeFlips.map(flip => Math.max(0, now - Number(flip.startTime || now))))
|
||||
: Infinity;
|
||||
const deferDynamicBuffersForFlipStart = activeFlips.length > 0 && newestFlipAge < flipDynamicBufferGraceMs;
|
||||
const shadowRefreshDue = !deferDynamicBuffersForFlipStart && (
|
||||
forceDynamicBufferRefresh || now - lastBookShadowRefreshAt >= dynamicBufferRefreshIntervalMs
|
||||
);
|
||||
const reflectionRefreshDue = !deferDynamicBuffersForFlipStart && (
|
||||
forceDynamicBufferRefresh || now - lastTableReflectionRefreshAt >= dynamicBufferRefreshIntervalMs
|
||||
);
|
||||
const bothHeavyPassesDue = shadowRefreshDue && reflectionRefreshDue && !forceDynamicBufferRefresh;
|
||||
const refreshShadowsThisFrame = shadowRefreshDue && (
|
||||
!bothHeavyPassesDue || lastBookShadowRefreshAt <= lastTableReflectionRefreshAt
|
||||
);
|
||||
const refreshReflectionThisFrame = reflectionRefreshDue && (
|
||||
!bothHeavyPassesDue || !refreshShadowsThisFrame
|
||||
);
|
||||
if (refreshShadowsThisFrame) {
|
||||
updateBookShadowMaps();
|
||||
lastBookShadowRefreshAt = now;
|
||||
}
|
||||
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
|
||||
const reflectionStartedAt = performance.now();
|
||||
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
|
||||
updateTableReflection();
|
||||
if (refreshReflectionThisFrame) {
|
||||
updateTableReflection();
|
||||
lastTableReflectionRefreshAt = now;
|
||||
}
|
||||
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
|
||||
const renderStartedAt = performance.now();
|
||||
if (tableDebugMode === tableDebugModes.mirror) {
|
||||
|
||||
@@ -11,7 +11,7 @@ const DEFAULT_PAGE_RESERVE = 50;
|
||||
class WebGLBookSceneModule extends BaseModule {
|
||||
constructor() {
|
||||
super('webgl-book-scene', 'WebGL Book Scene');
|
||||
this.dependencies = ['persistence-manager', 'localization', 'game-config', 'book-texture-renderer'];
|
||||
this.dependencies = ['persistence-manager', 'localization', 'game-config', 'book-pagination', 'book-texture-renderer'];
|
||||
this.persistenceManager = null;
|
||||
this.localization = null;
|
||||
this.gameConfig = null;
|
||||
@@ -365,7 +365,13 @@ class WebGLBookSceneModule extends BaseModule {
|
||||
this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(cacheBuster)}`);
|
||||
await this.labImportPromise;
|
||||
this.reportProgress(94, 'Uploading initial book page textures');
|
||||
window.BookTextureRenderer?.publishSpread?.();
|
||||
const pagination = this.getModule('book-pagination');
|
||||
const initialSpread = pagination?.getCurrentSpread?.();
|
||||
if (initialSpread && typeof window.BookTextureRenderer?.drawSpread === 'function') {
|
||||
window.BookTextureRenderer.drawSpread(initialSpread, ['left', 'right'], { force: true });
|
||||
} else {
|
||||
window.BookTextureRenderer?.publishSpread?.();
|
||||
}
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
this.reportProgress(96, 'Binding WebGL page controls');
|
||||
this.installTextureEventBridge();
|
||||
|
||||
Reference in New Issue
Block a user