Add WebGL FPS cap and texture word reveal
This commit is contained in:
@@ -77,9 +77,12 @@ class BookPaginationModule extends BaseModule {
|
|||||||
|
|
||||||
const layout = this.layoutTextBlock(block, type);
|
const layout = this.layoutTextBlock(block, type);
|
||||||
if (!layout?.lines?.length) return;
|
if (!layout?.lines?.length) return;
|
||||||
|
let blockWordCursor = 0;
|
||||||
|
cursorLine += layout.topSpaceLines;
|
||||||
|
|
||||||
layout.lines.forEach((line) => {
|
layout.lines.forEach((line) => {
|
||||||
const geometry = this.getLineGeometry(cursorLine);
|
const geometry = this.getLineGeometry(cursorLine);
|
||||||
|
const lineWordCount = line.nodes.filter(node => node?.type === 'box' && node.value).length;
|
||||||
if (!spreads[geometry.spreadIndex]) {
|
if (!spreads[geometry.spreadIndex]) {
|
||||||
spreads[geometry.spreadIndex] = { index: geometry.spreadIndex, left: [], right: [] };
|
spreads[geometry.spreadIndex] = { index: geometry.spreadIndex, left: [], right: [] };
|
||||||
}
|
}
|
||||||
@@ -93,10 +96,13 @@ class BookPaginationModule extends BaseModule {
|
|||||||
pageLine: geometry.pageLine,
|
pageLine: geometry.pageLine,
|
||||||
fontPx: layout.fontPx,
|
fontPx: layout.fontPx,
|
||||||
lineHeightPx: layout.lineHeightPx,
|
lineHeightPx: layout.lineHeightPx,
|
||||||
fontStyle: layout.fontStyle
|
fontStyle: layout.fontStyle,
|
||||||
|
blockWordStart: blockWordCursor
|
||||||
});
|
});
|
||||||
|
blockWordCursor += lineWordCount;
|
||||||
cursorLine += 1;
|
cursorLine += 1;
|
||||||
});
|
});
|
||||||
|
cursorLine += layout.bottomSpaceLines;
|
||||||
});
|
});
|
||||||
|
|
||||||
return spreads.filter(Boolean);
|
return spreads.filter(Boolean);
|
||||||
@@ -109,6 +115,8 @@ class BookPaginationModule extends BaseModule {
|
|||||||
const typography = this.metrics.typography;
|
const typography = this.metrics.typography;
|
||||||
const role = block.role || block.metadata?.role || (type === 'heading' ? 'chapter-heading' : 'body');
|
const role = block.role || block.metadata?.role || (type === 'heading' ? 'chapter-heading' : 'body');
|
||||||
const isHeading = type === 'heading' || role === 'chapter-heading' || role === 'section-heading';
|
const isHeading = type === 'heading' || role === 'chapter-heading' || role === 'section-heading';
|
||||||
|
const topSpaceLines = role === 'chapter-heading' ? 2 : role === 'section-heading' || block.addTopSpace || block.metadata?.addTopSpace ? 1 : 0;
|
||||||
|
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
||||||
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
||||||
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
||||||
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
||||||
@@ -123,6 +131,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
measures,
|
measures,
|
||||||
fontSize: `${fontPx}px`,
|
fontSize: `${fontPx}px`,
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
|
fontFeatureSettings: '"kern" on, "liga" on, "onum" on, "pnum" on, "dlig" on, "clig" on, "calt" on',
|
||||||
lineHeightPx,
|
lineHeightPx,
|
||||||
lineHeight: lineHeightPx / fontPx
|
lineHeight: lineHeightPx / fontPx
|
||||||
});
|
});
|
||||||
@@ -133,6 +142,8 @@ class BookPaginationModule extends BaseModule {
|
|||||||
fontPx,
|
fontPx,
|
||||||
lineHeightPx,
|
lineHeightPx,
|
||||||
fontStyle: isHeading ? 'italic' : 'normal',
|
fontStyle: isHeading ? 'italic' : 'normal',
|
||||||
|
topSpaceLines,
|
||||||
|
bottomSpaceLines,
|
||||||
lines: this.extractLines(layout, {
|
lines: this.extractLines(layout, {
|
||||||
measures,
|
measures,
|
||||||
lineOffsets,
|
lineOffsets,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
left: [],
|
left: [],
|
||||||
right: []
|
right: []
|
||||||
};
|
};
|
||||||
|
this.currentSpread = null;
|
||||||
|
this.activeAnimations = new Map();
|
||||||
|
this.animationFrameId = null;
|
||||||
|
this.lastAnimationFrameAt = 0;
|
||||||
|
this.targetFrameDurationMs = 1000 / 30;
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
'initialize',
|
'initialize',
|
||||||
@@ -33,6 +38,12 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawPageBase',
|
'drawPageBase',
|
||||||
'drawPageLines',
|
'drawPageLines',
|
||||||
'drawLine',
|
'drawLine',
|
||||||
|
'drawWord',
|
||||||
|
'startRevealAnimation',
|
||||||
|
'fastForwardAnimations',
|
||||||
|
'stopAnimations',
|
||||||
|
'requestAnimationFrame',
|
||||||
|
'tickAnimations',
|
||||||
'publishSpread',
|
'publishSpread',
|
||||||
'getPageCanvas',
|
'getPageCanvas',
|
||||||
'getHitMap',
|
'getHitMap',
|
||||||
@@ -51,6 +62,15 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
|
this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
|
||||||
this.drawSpread(event.detail?.spread || this.pagination?.getCurrentSpread?.());
|
this.drawSpread(event.detail?.spread || this.pagination?.getCurrentSpread?.());
|
||||||
});
|
});
|
||||||
|
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||||
|
this.startRevealAnimation(event.detail || {});
|
||||||
|
});
|
||||||
|
this.addEventListener(document, 'book-texture:fast-forward', this.fastForwardAnimations);
|
||||||
|
this.addEventListener(document, 'ui:command', (event) => {
|
||||||
|
if (event.detail?.type === 'continue') this.fastForwardAnimations();
|
||||||
|
});
|
||||||
|
this.addEventListener(document, 'story:manual-scroll', this.fastForwardAnimations);
|
||||||
|
this.addEventListener(document, 'story:history-restoring', this.stopAnimations);
|
||||||
this.reportProgress(100, 'Book texture renderer ready');
|
this.reportProgress(100, 'Book texture renderer ready');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -73,10 +93,11 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawSpread(spread = null) {
|
drawSpread(spread = null) {
|
||||||
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
this.drawPageBase('left');
|
this.drawPageBase('left');
|
||||||
this.drawPageBase('right');
|
this.drawPageBase('right');
|
||||||
this.drawPageLines('left', spread?.left || []);
|
this.drawPageLines('left', this.currentSpread?.left || []);
|
||||||
this.drawPageLines('right', spread?.right || []);
|
this.drawPageLines('right', this.currentSpread?.right || []);
|
||||||
this.publishSpread();
|
this.publishSpread();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +133,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = 'rgba(31, 19, 10, 0.86)';
|
ctx.fillStyle = 'rgba(31, 19, 10, 0.86)';
|
||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
if ('fontKerning' in ctx) ctx.fontKerning = 'normal';
|
||||||
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'normal';
|
||||||
lines.forEach(line => this.drawLine(ctx, line));
|
lines.forEach(line => this.drawLine(ctx, line));
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
@@ -133,6 +156,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
? Math.max(0, (metrics.content.width - naturalWidth) / 2)
|
? Math.max(0, (metrics.content.width - naturalWidth) / 2)
|
||||||
: Number(line.offset || 0);
|
: Number(line.offset || 0);
|
||||||
let x = metrics.content.x + centerOffset;
|
let x = metrics.content.x + centerOffset;
|
||||||
|
let wordIndex = 0;
|
||||||
|
|
||||||
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
|
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||||
nodes.forEach((node, index) => {
|
nodes.forEach((node, index) => {
|
||||||
@@ -140,8 +164,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
if (node.type === 'box' && node.value) {
|
if (node.type === 'box' && node.value) {
|
||||||
const nextNode = nodes[index + 1];
|
const nextNode = nodes[index + 1];
|
||||||
const value = `${node.value}${nextNode?.type === 'penalty' && nextNode.penalty === 100 ? '-' : ''}`;
|
const value = `${node.value}${nextNode?.type === 'penalty' && nextNode.penalty === 100 ? '-' : ''}`;
|
||||||
ctx.fillText(value, x, baseY);
|
this.drawWord(ctx, value, x, baseY, lineRecord, wordIndex);
|
||||||
x += Number(node.width || ctx.measureText(value).width || 0);
|
x += Number(node.width || ctx.measureText(value).width || 0);
|
||||||
|
wordIndex += 1;
|
||||||
} else if (node.type === 'glue' && node.width !== 0) {
|
} else if (node.type === 'glue' && node.width !== 0) {
|
||||||
let width = Number(node.width || 0);
|
let width = Number(node.width || 0);
|
||||||
if (ratio > 0) width += Number(node.stretch || 0) * ratio;
|
if (ratio > 0) width += Number(node.stretch || 0) * ratio;
|
||||||
@@ -151,6 +176,99 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawWord(ctx, value, x, baseY, lineRecord, localWordIndex) {
|
||||||
|
const animation = this.activeAnimations.get(String(lineRecord.blockId ?? ''));
|
||||||
|
if (!animation) {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillText(value, x, baseY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
||||||
|
const timing = animation.wordTimings[globalWordIndex];
|
||||||
|
if (!timing) {
|
||||||
|
ctx.globalAlpha = animation.completed ? 1 : 0;
|
||||||
|
ctx.fillText(value, x, baseY);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = animation.completed
|
||||||
|
? Number.POSITIVE_INFINITY
|
||||||
|
: performance.now() - animation.startedAt;
|
||||||
|
const duration = Math.max(1, Number(timing.duration || 1));
|
||||||
|
const progress = Math.max(0, Math.min(1, (elapsed - Number(timing.delay || 0)) / duration));
|
||||||
|
if (progress <= 0) return;
|
||||||
|
|
||||||
|
const previousAlpha = ctx.globalAlpha;
|
||||||
|
ctx.globalAlpha = previousAlpha * progress;
|
||||||
|
ctx.fillText(value, x, baseY);
|
||||||
|
ctx.globalAlpha = previousAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
startRevealAnimation(detail = {}) {
|
||||||
|
const blockId = detail.blockId ?? detail.id ?? null;
|
||||||
|
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||||
|
this.activeAnimations.set(String(blockId), {
|
||||||
|
blockId,
|
||||||
|
wordTimings: detail.wordTimings,
|
||||||
|
startedAt: performance.now(),
|
||||||
|
completed: false
|
||||||
|
});
|
||||||
|
this.requestAnimationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
fastForwardAnimations() {
|
||||||
|
let changed = false;
|
||||||
|
this.activeAnimations.forEach((animation) => {
|
||||||
|
if (!animation.completed) {
|
||||||
|
animation.completed = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAnimations() {
|
||||||
|
this.activeAnimations.clear();
|
||||||
|
if (this.animationFrameId) {
|
||||||
|
clearTimeout(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame() {
|
||||||
|
if (this.animationFrameId) return;
|
||||||
|
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
tickAnimations(now) {
|
||||||
|
this.animationFrameId = null;
|
||||||
|
if (now - this.lastAnimationFrameAt < this.targetFrameDurationMs) {
|
||||||
|
this.requestAnimationFrame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastAnimationFrameAt = now;
|
||||||
|
|
||||||
|
let hasActive = false;
|
||||||
|
const currentNow = performance.now();
|
||||||
|
this.activeAnimations.forEach((animation) => {
|
||||||
|
if (animation.completed) return;
|
||||||
|
const lastTiming = animation.wordTimings.at(-1);
|
||||||
|
const total = Number(lastTiming?.delay || 0) + Number(lastTiming?.duration || 0);
|
||||||
|
if (currentNow - animation.startedAt >= total + 50) {
|
||||||
|
animation.completed = true;
|
||||||
|
} else {
|
||||||
|
hasActive = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||||
|
if (hasActive) this.requestAnimationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
publishSpread() {
|
publishSpread() {
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||||
detail: {
|
detail: {
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260606-webgl-overlay-page-layout';
|
const MODULE_CACHE_BUSTER = '20260606-webgl-fps-texture-animation';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -241,6 +241,15 @@ class PlaybackCoordinatorModule extends BaseModule {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
document.dispatchEvent(new CustomEvent('book-texture:reveal-block', {
|
||||||
|
detail: {
|
||||||
|
id: sentence.id,
|
||||||
|
blockId: sentence.blockId ?? sentence.metadata?.blockId ?? null,
|
||||||
|
wordTimings,
|
||||||
|
cueTimings,
|
||||||
|
totalDuration: sentence.animation.totalDuration || 0
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const totalDuration = wordTimings.length > 0
|
const totalDuration = wordTimings.length > 0
|
||||||
@@ -350,6 +359,12 @@ class PlaybackCoordinatorModule extends BaseModule {
|
|||||||
|
|
||||||
console.log('PlaybackCoordinator: Fast forwarding');
|
console.log('PlaybackCoordinator: Fast forwarding');
|
||||||
this.accelerateActiveWordAnimations(this.currentSentence);
|
this.accelerateActiveWordAnimations(this.currentSentence);
|
||||||
|
document.dispatchEvent(new CustomEvent('book-texture:fast-forward', {
|
||||||
|
detail: {
|
||||||
|
id: this.currentSentence?.id,
|
||||||
|
blockId: this.currentSentence?.blockId ?? this.currentSentence?.metadata?.blockId ?? null
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const animQueue = this.getModule('animation-queue');
|
const animQueue = this.getModule('animation-queue');
|
||||||
if (animQueue) {
|
if (animQueue) {
|
||||||
|
|||||||
+70
-13
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-overlay-page-layout';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-fps-texture-animation';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -25,12 +25,13 @@ const appInitialState = window.WebGLBookInitialState || {};
|
|||||||
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
||||||
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
||||||
const isAppIntegrationMode = appInitialState.appMode === true;
|
const isAppIntegrationMode = appInitialState.appMode === true;
|
||||||
|
const appRenderPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const labStatus = document.getElementById('lab_status');
|
const labStatus = document.getElementById('lab_status');
|
||||||
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
||||||
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
||||||
}
|
}
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
renderer.setPixelRatio(appRenderPixelRatio);
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
renderer.toneMappingExposure = 1.12;
|
renderer.toneMappingExposure = 1.12;
|
||||||
@@ -39,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
|
|||||||
|
|
||||||
const generatedTextureCanvases = {};
|
const generatedTextureCanvases = {};
|
||||||
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
const reflectionPixelRatio = isAppIntegrationMode ? 0.28 : Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
|
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
|
||||||
const reflectionTargetSize = new THREE.Vector2();
|
const reflectionTargetSize = new THREE.Vector2();
|
||||||
const pageRaycaster = new THREE.Raycaster();
|
const pageRaycaster = new THREE.Raycaster();
|
||||||
@@ -64,13 +65,13 @@ let tableDustTexture = null;
|
|||||||
let tableGreaseTexture = null;
|
let tableGreaseTexture = null;
|
||||||
const tableTopY = -0.02;
|
const tableTopY = -0.02;
|
||||||
const bookTableContactClearance = 0.002;
|
const bookTableContactClearance = 0.002;
|
||||||
const tableReflectionBaseWidth = isAppIntegrationMode ? 1280 : 4096;
|
const tableReflectionBaseWidth = isAppIntegrationMode ? 480 : 4096;
|
||||||
const tableReflectionBaseHeight = isAppIntegrationMode ? 720 : 2304;
|
const tableReflectionBaseHeight = isAppIntegrationMode ? 270 : 2304;
|
||||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
depthBuffer: true,
|
depthBuffer: true,
|
||||||
stencilBuffer: false,
|
stencilBuffer: false,
|
||||||
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 2 : 8) : 0
|
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0
|
||||||
});
|
});
|
||||||
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
|
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
|
||||||
@@ -89,7 +90,7 @@ const reflectionUp = new THREE.Vector3();
|
|||||||
const candleShadowSources = [];
|
const candleShadowSources = [];
|
||||||
const candleWorldPosition = new THREE.Vector3();
|
const candleWorldPosition = new THREE.Vector3();
|
||||||
const flameWorldPosition = new THREE.Vector3();
|
const flameWorldPosition = new THREE.Vector3();
|
||||||
const bookShadowMapSize = isAppIntegrationMode ? 512 : 1536;
|
const bookShadowMapSize = isAppIntegrationMode ? 128 : 1536;
|
||||||
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
||||||
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
||||||
colorSpace: THREE.NoColorSpace,
|
colorSpace: THREE.NoColorSpace,
|
||||||
@@ -141,6 +142,12 @@ updateCameraRig(0);
|
|||||||
configureScenePostprocessing();
|
configureScenePostprocessing();
|
||||||
|
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
const targetFrameDurationMs = 1000 / 30;
|
||||||
|
let lastRenderFrameAt = 0;
|
||||||
|
let fpsDisplay = null;
|
||||||
|
let fpsWindowStartedAt = performance.now();
|
||||||
|
let fpsWindowFrames = 0;
|
||||||
|
const lastFrameTiming = {};
|
||||||
const book = new THREE.Group();
|
const book = new THREE.Group();
|
||||||
scene.add(book);
|
scene.add(book);
|
||||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
||||||
@@ -421,8 +428,33 @@ installBookControls();
|
|||||||
installCameraControls();
|
installCameraControls();
|
||||||
resize();
|
resize();
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:scene-ready'));
|
document.dispatchEvent(new CustomEvent('webgl-book:scene-ready'));
|
||||||
|
ensureFpsDisplay();
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
|
function ensureFpsDisplay() {
|
||||||
|
if (fpsDisplay) return fpsDisplay;
|
||||||
|
fpsDisplay = document.createElement('div');
|
||||||
|
fpsDisplay.id = 'webgl_fps_display';
|
||||||
|
Object.assign(fpsDisplay.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0.65rem',
|
||||||
|
right: '0.75rem',
|
||||||
|
zIndex: '80',
|
||||||
|
minWidth: '4.2rem',
|
||||||
|
padding: '0.22rem 0.42rem',
|
||||||
|
border: '1px solid rgba(246, 231, 201, 0.28)',
|
||||||
|
background: 'rgba(10, 7, 4, 0.62)',
|
||||||
|
color: 'rgba(255, 238, 202, 0.94)',
|
||||||
|
font: '12px ui-monospace, SFMono-Regular, Consolas, monospace',
|
||||||
|
lineHeight: '1.2',
|
||||||
|
textAlign: 'right',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
});
|
||||||
|
fpsDisplay.textContent = '0 fps';
|
||||||
|
document.body.appendChild(fpsDisplay);
|
||||||
|
return fpsDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
function buildTable() {
|
function buildTable() {
|
||||||
const tableTexture = new THREE.TextureLoader().load('/assets/webgl/wood_table_diff_1k.jpg');
|
const tableTexture = new THREE.TextureLoader().load('/assets/webgl/wood_table_diff_1k.jpg');
|
||||||
tableTexture.colorSpace = THREE.SRGBColorSpace;
|
tableTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
@@ -661,14 +693,14 @@ function configureScenePostprocessing() {
|
|||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
depthBuffer: true,
|
depthBuffer: true,
|
||||||
stencilBuffer: false,
|
stencilBuffer: false,
|
||||||
samples: renderer.capabilities.isWebGL2 ? 8 : 0
|
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 0 : 8) : 0
|
||||||
});
|
});
|
||||||
sceneComposerTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
sceneComposerTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
sceneComposerTarget.texture.minFilter = THREE.LinearFilter;
|
sceneComposerTarget.texture.minFilter = THREE.LinearFilter;
|
||||||
sceneComposerTarget.texture.magFilter = THREE.LinearFilter;
|
sceneComposerTarget.texture.magFilter = THREE.LinearFilter;
|
||||||
|
|
||||||
composer = new EffectComposer(renderer, sceneComposerTarget);
|
composer = new EffectComposer(renderer, sceneComposerTarget);
|
||||||
composer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
composer.setPixelRatio(appRenderPixelRatio);
|
||||||
sceneRenderPass = new RenderPass(scene, camera);
|
sceneRenderPass = new RenderPass(scene, camera);
|
||||||
composer.addPass(sceneRenderPass);
|
composer.addPass(sceneRenderPass);
|
||||||
|
|
||||||
@@ -2543,7 +2575,7 @@ function resize() {
|
|||||||
camera.aspect = width / height;
|
camera.aspect = width / height;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
const desiredReflectionScale = reflectionPixelRatio * 1.5;
|
const desiredReflectionScale = reflectionPixelRatio * 1.5;
|
||||||
const reflectionScale = Math.max(1, Math.min(
|
const reflectionScale = Math.max(isAppIntegrationMode ? 0.35 : 1, Math.min(
|
||||||
desiredReflectionScale,
|
desiredReflectionScale,
|
||||||
4096 / width,
|
4096 / width,
|
||||||
2304 / height
|
2304 / height
|
||||||
@@ -2812,9 +2844,17 @@ function renderMirrorDebugView() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate(now = performance.now()) {
|
||||||
requestAnimationFrame(animate);
|
const elapsedSinceLastFrame = lastRenderFrameAt ? now - lastRenderFrameAt : targetFrameDurationMs;
|
||||||
const delta = clock.getDelta();
|
if (lastRenderFrameAt && elapsedSinceLastFrame < targetFrameDurationMs) {
|
||||||
|
setTimeout(animate, Math.max(1, targetFrameDurationMs - elapsedSinceLastFrame));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const frameElapsedMs = lastRenderFrameAt ? elapsedSinceLastFrame : targetFrameDurationMs;
|
||||||
|
lastRenderFrameAt = now;
|
||||||
|
setTimeout(animate, targetFrameDurationMs);
|
||||||
|
const delta = Math.min(0.1, frameElapsedMs / 1000);
|
||||||
|
clock.getDelta();
|
||||||
const t = clock.elapsedTime;
|
const t = clock.elapsedTime;
|
||||||
updateCameraRig(delta);
|
updateCameraRig(delta);
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
@@ -2847,12 +2887,17 @@ function animate() {
|
|||||||
updateActiveFlips(performance.now());
|
updateActiveFlips(performance.now());
|
||||||
updateCandleShadowUniforms();
|
updateCandleShadowUniforms();
|
||||||
renderedFrameCount += 1;
|
renderedFrameCount += 1;
|
||||||
|
const shadowStartedAt = performance.now();
|
||||||
if (!isAppIntegrationMode || renderedFrameCount % 6 === 1 || activeFlips.length > 0) {
|
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) {
|
if (!isAppIntegrationMode || renderedFrameCount % 4 === 1 || cameraRig.navigationActive || activeFlips.length > 0) {
|
||||||
updateTableReflection();
|
updateTableReflection();
|
||||||
}
|
}
|
||||||
|
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
|
||||||
|
const renderStartedAt = performance.now();
|
||||||
if (tableDebugMode === tableDebugModes.mirror) {
|
if (tableDebugMode === tableDebugModes.mirror) {
|
||||||
renderer.setRenderTarget(null);
|
renderer.setRenderTarget(null);
|
||||||
renderer.clear();
|
renderer.clear();
|
||||||
@@ -2862,6 +2907,18 @@ function animate() {
|
|||||||
} else {
|
} else {
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
lastFrameTiming.render = performance.now() - renderStartedAt;
|
||||||
|
lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render;
|
||||||
window.BookLabDebug.renderedFrames += 1;
|
window.BookLabDebug.renderedFrames += 1;
|
||||||
window.BookLabDebug.ready = true;
|
window.BookLabDebug.ready = true;
|
||||||
|
fpsWindowFrames += 1;
|
||||||
|
if (now - fpsWindowStartedAt >= 500) {
|
||||||
|
const fps = Math.round((fpsWindowFrames * 1000) / Math.max(1, now - fpsWindowStartedAt));
|
||||||
|
ensureFpsDisplay().textContent = `${fps} fps`;
|
||||||
|
document.documentElement.dataset.webglFps = String(fps);
|
||||||
|
fpsWindowFrames = 0;
|
||||||
|
fpsWindowStartedAt = now;
|
||||||
|
}
|
||||||
|
document.documentElement.dataset.webglRenderedFrames = String(window.BookLabDebug.renderedFrames);
|
||||||
|
document.documentElement.dataset.webglFrameTiming = JSON.stringify(lastFrameTiming);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user