From 39d4c728b0245cea6077567a1ac598dfa0028198 Mon Sep 17 00:00:00 2001 From: fukunee Date: Sat, 19 Sep 2026 04:05:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BF=BB=E9=A1=B5=E9=97=AA?= =?UTF-8?q?=E7=83=81=E4=B8=8E=E6=B8=B2=E6=9F=93=E6=97=B6=E5=BA=8F=EF=BC=8C?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E7=BF=BB=E9=A1=B5=E5=BC=AF=E6=9B=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复翻页弯曲(双几何:顶层高分段/栈内极简),关闭纸张自然弧度 - 翻页开始即预切露出页的几何/亮度/投影,消除完成瞬间跳变 - 纹理懒加载窗口 ±1 扩到 ±2,覆盖向后翻页露出的下一层页 - isSettling 纳入按需渲染判断,修复页面 z 过渡期间不渲染 - 顶层页抬高 TOP_LIFT、翻页 z 起伏 FLIP_LIFT,拉开深度避免穿模 --- src/core/App.js | 1 + src/scene/binder.js | 51 ++++++++++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/core/App.js b/src/core/App.js index 59cece7..a71d087 100644 --- a/src/core/App.js +++ b/src/core/App.js @@ -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 || diff --git a/src/scene/binder.js b/src/scene/binder.js index 2ccc533..54e2ec0 100644 --- a/src/scene/binder.js +++ b/src/scene/binder.js @@ -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);