修复翻页闪烁与渲染时序,恢复翻页弯曲

- 恢复翻页弯曲(双几何:顶层高分段/栈内极简),关闭纸张自然弧度
- 翻页开始即预切露出页的几何/亮度/投影,消除完成瞬间跳变
- 纹理懒加载窗口 ±1 扩到 ±2,覆盖向后翻页露出的下一层页
- isSettling 纳入按需渲染判断,修复页面 z 过渡期间不渲染
- 顶层页抬高 TOP_LIFT、翻页 z 起伏 FLIP_LIFT,拉开深度避免穿模
This commit is contained in:
2026-09-19 04:05:24 +08:00
parent e10dfed93d
commit 39d4c728b0
2 changed files with 33 additions and 19 deletions
+1
View File
@@ -495,6 +495,7 @@ export class App {
// 是否有进行中的动画(决定本帧是否需要重绘)
const animating =
this.binder.flip !== null ||
this.binder.isSettling() ||
this.timeline.hasActiveFlip() ||
this.cameraTween !== null ||
this.zoomTarget !== null ||
+32 -19
View File
@@ -5,7 +5,9 @@ import { clamp, easeOutCubic } from '../utils/easing.js';
const RING_R = 0.085;
const RING_TUBE = 0.0115;
const STACK_GAP = 0.002;
const STACK_GAP = 0.004;
const TOP_LIFT = 0.02; // 顶层左右两页高出栈内的距离
const FLIP_LIFT = 0.028; // 翻页过程中的最大抬高(略高于顶层静止高度)
const PAGE_GAP = 0.02; // 页内缘离书脊的小空隙
function makeLeatherTexture({ emblem = false } = {}) {
@@ -371,18 +373,9 @@ export class Binder {
}
_makePageGeometry(segX, segY) {
// 纯平面页(无自然弧度):页内缘离书脊留出小空隙,左右页不紧贴
const geo = new THREE.PlaneGeometry(PAGE_W, PAGE_H, segX, segY);
// 页内缘离书脊留出小空隙,左右页不紧贴
geo.translate(PAGE_W / 2 + PAGE_GAP, 0, 0);
// 纸张自然弧度:自由边略向正面翘起
const pa = geo.attributes.position;
for (let k = 0; k < pa.count; k++) {
const x = pa.getX(k), y = pa.getY(k);
const u = Math.min(1, Math.max(0, (x - PAGE_GAP) / PAGE_W));
const yNorm = (y + PAGE_H / 2) / PAGE_H;
pa.setZ(k, pa.getZ(k) + 0.05 * Math.sin((Math.PI * u) / 2) * (0.8 + 0.2 * yNorm));
}
pa.needsUpdate = true;
return geo;
}
@@ -435,8 +428,10 @@ export class Binder {
const angle = j < this.current ? -Math.PI : 0;
p.angle = angle;
p.group.rotation.y = angle;
const onTop = (j === this.current || j === this.current - 1);
const depth = j < this.current ? (this.current - 1 - j) : (j - this.current);
p.targetZ = -depth * STACK_GAP;
// 顶层页抬高,与栈内页拉开深度,避免低精度深度缓冲下的穿模(z-fighting)
p.targetZ = onTop ? TOP_LIFT : -depth * STACK_GAP;
if (instant) {
p.z = p.targetZ;
p.group.position.z = p.z;
@@ -447,7 +442,6 @@ export class Binder {
const dim = 1 - Math.min(depth * 0.06, 0.22);
p.material.color.setScalar(dim);
// 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 pass
const onTop = (j === this.current || j === this.current - 1);
p.mesh.castShadow = onTop;
// 顶层页用高分段几何(翻页弯曲),栈内页用极简几何(省顶点)
if ((p.mesh.geometry === p.hiGeo) !== onTop) {
@@ -458,11 +452,11 @@ export class Binder {
this._ensureTextures();
}
// 需要真实纹理的窗口:当前页 ±1(左右可见页 + 翻页缓冲)
// 需要真实纹理的窗口:当前页 ±2(左右可见页 + 翻页时会露出的下一层页 + 缓冲)
_pageWindow() {
const len = this.pages.length;
const win = new Set();
for (let d = -1; d <= 1; d++) {
for (let d = -2; d <= 2; d++) {
const i = this.current + d;
if (i >= 0 && i < len) win.add(i);
}
@@ -527,6 +521,11 @@ export class Binder {
canFlipForward() { return !this.flip; }
canFlipBackward() { return !this.flip && this.current > 0; }
// 页面 z 是否仍在向目标位置过渡(用于按需渲染:过渡期间需要持续重绘)
isSettling() {
return this.pages.some((p) => Math.abs(p.z - p.targetZ) > 0.0001);
}
_finalizeFlip() {
// 立即把正在翻的页收到目标位置,好让下一页马上开始翻
if (!this.flip) return;
@@ -534,8 +533,8 @@ export class Binder {
const p = this.pages[f.index];
p.angle = f.to;
p.group.rotation.y = f.to;
p.z = 0;
p.group.position.z = 0;
p.z = TOP_LIFT;
p.group.position.z = TOP_LIFT;
if (f.to === -Math.PI) {
this.current = (this.current + 1) % this.pages.length;
} else {
@@ -549,6 +548,13 @@ export class Binder {
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
if (!this.canFlipForward()) return;
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38 };
// 提前把即将露出的下一层右页切到顶层状态(高分段几何/亮度/投影),避免翻页完成瞬间跳变
const reveal = this.pages[(this.current + 1) % this.pages.length];
if (reveal) {
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
reveal.mesh.castShadow = true;
reveal.material.color.setScalar(1.0);
}
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
}
@@ -557,6 +563,13 @@ export class Binder {
if (this.flip) this._finalizeFlip();
if (!this.canFlipBackward()) return;
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38 };
// 提前把即将露出的下一层左页切到顶层状态,避免翻页完成瞬间跳变
const reveal = this.pages[this.current - 2];
if (reveal) {
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
reveal.mesh.castShadow = true;
reveal.material.color.setScalar(1.0);
}
// 优先更新底部导航与信息卡
if (this.onChange) this.onChange(this.current - 1);
}
@@ -572,8 +585,8 @@ export class Binder {
const p = this.pages[f.index];
p.angle = angle;
p.group.rotation.y = angle;
// 翻页时 z 随进度同步起伏(中途 0.006,起止为 0),不瞬跳
p.z = 0.006 * Math.sin(Math.PI * k);
// 翻页时 z 随进度同步起伏:起止停在顶层高度,中途抬得更高,不归零不瞬跳
p.z = TOP_LIFT + (FLIP_LIFT - TOP_LIFT) * Math.sin(Math.PI * k);
p.group.position.z = p.z;
this._updateCurvature(f.index);