Lazy-load page textures with LRU cache and double-sided materials

Replace eager texture generation with a reference-counted pool
that loads only the visible window and evicts unused pages, and
swap front/back mesh pairs for single meshes using a two-sided
material to halve draw calls. The timeline now drives its texture
window from the camera's visible world range each frame, and the
binder tracks a current-page window. Flip animations also finalize
immediately so rapid flips chain smoothly.
This commit is contained in:
2026-09-19 02:44:15 +08:00
parent 2a173833d0
commit eb165d03c5
4 changed files with 303 additions and 61 deletions
+123 -14
View File
@@ -7,7 +7,7 @@ export const PAGE_W = 1.6; // 世界单位:页宽
export const PAGE_H = 2.24; // 世界单位:页高
export const TEX_W = 768;
export const TEX_H = 1075;
// 输出纹理与绘制分辨率一致,保证文字清晰
// 输出纹理与绘制分辨率一致,保证文字清晰(显存靠懒加载 + LRU 控制,不牺牲画质)
export const TEX_OUT_W = 768;
export const TEX_OUT_H = 1075;
export const HOLE_U = [0.1, 0.26, 0.42, 0.58, 0.74, 0.9]; // 活页孔(自顶部归一化,6孔等距)
@@ -323,19 +323,128 @@ function downscale(src) {
return c;
}
// 生成页面的两个纹理
const texCache = new Map();
export function getPageTextures(entry) {
if (texCache.has(entry.id)) return texCache.get(entry.id);
const front = new THREE.CanvasTexture(downscale(renderFront(entry)));
front.colorSpace = THREE.SRGBColorSpace;
front.anisotropy = 8;
// —— 页面纹理池:懒生成 + 引用计数 + LRU 释放 ——
// 启动时只为“当前可见窗口”生成真实纹理,其余页面用共享占位纹理;
// 浏览中按需生成,离开窗口的纹理自动释放,控制显存占用。
const back = new THREE.CanvasTexture(downscale(renderBack(entry)));
back.colorSpace = THREE.SRGBColorSpace;
back.anisotropy = 8;
const MAX_CACHED_PAGES = 40; // 最多缓存 40 页(80 张)真实纹理
const texCache = new Map(); // id -> { entry, front, back, refs, lastUsed }
let placeholder = null;
const pair = { front, back };
texCache.set(entry.id, pair);
return pair;
function makeTexture(canvas) {
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 8; // 保持倾斜视角下的清晰度
return tex;
}
// 空白活页纸占位纹理(全局共享一份)
function renderPlaceholder() {
const w = TEX_W, h = TEX_H;
const { canvas, ctx } = makePaper(w, h, { seed: 42 });
drawBorder(ctx, w, h);
drawHoles(ctx, w, h);
return canvas;
}
export function getPlaceholderTextures() {
if (!placeholder) {
const front = makeTexture(downscale(renderPlaceholder()));
const back = makeTexture(downscale(renderPlaceholder()));
placeholder = { front, back };
}
return placeholder;
}
function generatePair(entry) {
const front = makeTexture(downscale(renderFront(entry)));
const back = makeTexture(downscale(renderBack(entry)));
return { front, back };
}
function evictIfNeeded() {
if (texCache.size <= MAX_CACHED_PAGES) return;
const candidates = [...texCache.values()]
.filter((r) => r.refs === 0)
.sort((a, b) => a.lastUsed - b.lastUsed);
while (texCache.size > MAX_CACHED_PAGES && candidates.length) {
const r = candidates.shift();
r.front.dispose();
r.back.dispose();
texCache.delete(r.entry.id);
}
}
export function acquirePageTextures(entry) {
let rec = texCache.get(entry.id);
if (!rec) {
rec = { entry, front: null, back: null, refs: 0, lastUsed: 0 };
texCache.set(entry.id, rec);
}
rec.refs++;
rec.lastUsed = performance.now();
if (!rec.front) {
const pair = generatePair(entry);
rec.front = pair.front;
rec.back = pair.back;
evictIfNeeded();
}
return { front: rec.front, back: rec.back };
}
export function releasePageTextures(entry) {
const rec = texCache.get(entry.id);
if (!rec) return;
rec.refs = Math.max(0, rec.refs - 1);
if (rec.refs === 0) rec.lastUsed = performance.now();
}
// 双面材质:同一 mesh 用 gl_FrontFacing 在正/反面显示不同纹理,
// 替代原先 front/back 两个 mesh,把每页 draw call 减半。
export function makeTwoSidedMaterial(front, back, {
roughness = 0.88,
metalness = 0.0,
transparent = false,
alphaTest = 0,
} = {}) {
const mat = new THREE.MeshStandardMaterial({
map: front, // 仅用于激活 USE_MAP / UV;实际采样由注入代码接管
roughness,
metalness,
side: THREE.DoubleSide,
transparent,
alphaTest,
});
mat.userData.pageMaps = { front, back };
let compiled = null;
mat.onBeforeCompile = (shader) => {
compiled = shader;
shader.uniforms.frontMap = { value: mat.userData.pageMaps.front };
shader.uniforms.backMap = { value: mat.userData.pageMaps.back };
shader.fragmentShader = shader.fragmentShader
.replace(
'#include <common>',
'#include <common>\nuniform sampler2D frontMap;\nuniform sampler2D backMap;',
)
.replace(
'#include <map_fragment>',
`#ifdef USE_MAP
vec4 _sideColor = gl_FrontFacing ? texture2D( frontMap, vMapUv ) : texture2D( backMap, vMapUv );
diffuseColor *= _sideColor;
#endif`,
);
};
mat.setPageMaps = (f, b) => {
mat.userData.pageMaps = { front: f, back: b };
mat.map = f;
if (compiled) {
compiled.uniforms.frontMap.value = f;
compiled.uniforms.backMap.value = b;
}
};
return mat;
}