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:
+123
-14
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user