From e10dfed93d9dd5df07caf41d18755b13ae313d64 Mon Sep 17 00:00:00 2001 From: fukunee Date: Sat, 19 Sep 2026 03:38:50 +0800 Subject: [PATCH] =?UTF-8?q?binder=20=E5=87=A0=E4=BD=95=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=EF=BC=9A=E6=A0=88=E5=86=85=E9=A1=B5=E7=94=A8=E6=9E=81=E7=AE=80?= =?UTF-8?q?=E5=87=A0=E4=BD=95=E3=80=81=E5=B0=81=E9=9D=A2/=E7=8E=AF/?= =?UTF-8?q?=E9=93=86=E9=92=89=E9=99=8D=E4=BD=8E=E5=88=86=E6=AE=B5=EF=BC=88?= =?UTF-8?q?=E4=B8=89=E8=A7=92=E5=BD=A2=20357K=E2=86=9224K=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scene/binder.js | 68 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/scene/binder.js b/src/scene/binder.js index a9babb6..2ccc533 100644 --- a/src/scene/binder.js +++ b/src/scene/binder.js @@ -85,7 +85,7 @@ function makeSuedeTexture() { } // 圆角矩形轮廓(直线段 + 圆弧段都用 lineTo 细分,正反面/皮边共享同一轮廓) -function roundedRectShape(w, h, r, edgeSeg = 40, arcSeg = 12) { +function roundedRectShape(w, h, r, edgeSeg = 16, arcSeg = 10) { const hw = w / 2, hh = h / 2; const s = new THREE.Shape(); s.moveTo(-hw + r, hh); @@ -112,7 +112,7 @@ function roundedRectShape(w, h, r, edgeSeg = 40, arcSeg = 12) { // 软皮曲面:ShapeGeometry 三角化(圆角正确、无缺口)+ 书脊处弧度弯曲 function makeLeatherSurface(w, h, crXY, curve, flip) { // 规则网格(含内部顶点,曲面平滑无对角褶痕)+ 角区收缩到圆弧 - const segX = 128, segY = 96; + const segX = 64, segY = 48; const hw = w / 2, hh = h / 2; const bx = hw - crXY, by = hh - crXY; const sigma = 0.5; @@ -337,7 +337,7 @@ export class Binder { plate.receiveShadow = true; // 铆钉:把薄片钉在牛皮上(覆盖新的铁环范围 ±0.896) - const rivetGeo = new THREE.SphereGeometry(0.013, 12, 10); + const rivetGeo = new THREE.SphereGeometry(0.013, 8, 6); const rivets = []; for (const ry of [-0.78, -0.39, 0, 0.39, 0.78]) { const rivet = new THREE.Mesh(rivetGeo, mat); @@ -357,7 +357,7 @@ export class Binder { color: 0xd4d7dc, metalness: 0.95, roughness: 0.2, }); // 更平滑的活页环 - const geo = new THREE.TorusGeometry(RING_R, RING_TUBE, 18, 56); + const geo = new THREE.TorusGeometry(RING_R, RING_TUBE, 12, 24); this.rings = []; for (const u of HOLE_U) { const y = (0.5 - u) * PAGE_H; @@ -370,30 +370,38 @@ 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; + } + _buildPages() { const segX = 96, segY = 8; ENTRIES.forEach((entry, i) => { - 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; - const base = Float32Array.from(geo.attributes.position.array); + // 高分段几何:翻页/顶层页(弯曲需要足够分段) + const hiGeo = this._makePageGeometry(segX, segY); + const hiBase = Float32Array.from(hiGeo.attributes.position.array); + // 极简几何:栈内页(只露边缘,无需弯曲,2 个三角形) + const loGeo = this._makePageGeometry(1, 1); // 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择) // 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call) const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, { alphaTest: 0.5, }); - const mesh = new THREE.Mesh(geo, mat); + const mesh = new THREE.Mesh(loGeo, mat); // 初始为栈内页,用极简几何 mesh.castShadow = false; // 由 _restack 依据是否为顶层页控制 const g = new THREE.Group(); @@ -403,8 +411,9 @@ export class Binder { this.pages.push({ group: g, - geometry: geo, - base, + hiGeo, + loGeo, + hiBase, material: mat, mesh, isPlaceholder: true, @@ -438,7 +447,12 @@ export class Binder { const dim = 1 - Math.min(depth * 0.06, 0.22); p.material.color.setScalar(dim); // 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 pass - p.mesh.castShadow = (j === this.current || j === this.current - 1); + const onTop = (j === this.current || j === this.current - 1); + p.mesh.castShadow = onTop; + // 顶层页用高分段几何(翻页弯曲),栈内页用极简几何(省顶点) + if ((p.mesh.geometry === p.hiGeo) !== onTop) { + p.mesh.geometry = onTop ? p.hiGeo : p.loGeo; + } }); this._updateCurvature(); this._ensureTextures(); @@ -481,14 +495,15 @@ export class Binder { // 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整 const pages = index === null ? this.pages : [this.pages[index]]; pages.forEach((p) => { - const pos = p.geometry.attributes.position; + // 弯曲只作用于高分段几何(顶层/翻页页);栈内极简几何不弯曲 + const pos = p.hiGeo.attributes.position; const arr = pos.array; const theta = Math.abs(p.angle); // 0..π const c = Math.sin(theta); // 0 静止,1 翻页中途 const A = 0.58 * c; // 抬升幅度(整页弧) for (let i = 0; i < arr.length; i += 3) { - const x = p.base[i]; - const y = p.base[i + 1]; + const x = p.hiBase[i]; + const y = p.hiBase[i + 1]; const s = Math.max(0, x - PAGE_GAP); // 距页内缘 0..W(防浮点误差产生负值→NaN) const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边 const yNorm = (y + PAGE_H / 2) / PAGE_H; // 0 底边 .. 1 顶边 @@ -496,7 +511,7 @@ export class Binder { // 主抬升 + 中段“腹”的次级谐波,曲线更饱满、更柔软 const belly = 0.85 * Math.sin((Math.PI * u) / 2) + 0.15 * Math.sin(Math.PI * u); const z = A * twist * belly; - arr[i + 2] = p.base[i + 2] + z; + arr[i + 2] = p.hiBase[i + 2] + z; } pos.needsUpdate = true; }); @@ -583,7 +598,8 @@ export class Binder { dispose() { this.pages.forEach((p) => { - p.geometry.dispose(); + p.hiGeo.dispose(); + p.loGeo.dispose(); p.material.dispose(); }); }