binder 几何降级:栈内页用极简几何、封面/环/铆钉降低分段(三角形 357K→24K)

This commit is contained in:
2026-09-19 03:38:50 +08:00
parent 6204b17db3
commit e10dfed93d
+42 -26
View File
@@ -85,7 +85,7 @@ function makeSuedeTexture() {
} }
// 圆角矩形轮廓(直线段 + 圆弧段都用 lineTo 细分,正反面/皮边共享同一轮廓) // 圆角矩形轮廓(直线段 + 圆弧段都用 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 hw = w / 2, hh = h / 2;
const s = new THREE.Shape(); const s = new THREE.Shape();
s.moveTo(-hw + r, hh); s.moveTo(-hw + r, hh);
@@ -112,7 +112,7 @@ function roundedRectShape(w, h, r, edgeSeg = 40, arcSeg = 12) {
// 软皮曲面:ShapeGeometry 三角化(圆角正确、无缺口)+ 书脊处弧度弯曲 // 软皮曲面:ShapeGeometry 三角化(圆角正确、无缺口)+ 书脊处弧度弯曲
function makeLeatherSurface(w, h, crXY, curve, flip) { 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 hw = w / 2, hh = h / 2;
const bx = hw - crXY, by = hh - crXY; const bx = hw - crXY, by = hh - crXY;
const sigma = 0.5; const sigma = 0.5;
@@ -337,7 +337,7 @@ export class Binder {
plate.receiveShadow = true; plate.receiveShadow = true;
// 铆钉:把薄片钉在牛皮上(覆盖新的铁环范围 ±0.896) // 铆钉:把薄片钉在牛皮上(覆盖新的铁环范围 ±0.896)
const rivetGeo = new THREE.SphereGeometry(0.013, 12, 10); const rivetGeo = new THREE.SphereGeometry(0.013, 8, 6);
const rivets = []; const rivets = [];
for (const ry of [-0.78, -0.39, 0, 0.39, 0.78]) { for (const ry of [-0.78, -0.39, 0, 0.39, 0.78]) {
const rivet = new THREE.Mesh(rivetGeo, mat); const rivet = new THREE.Mesh(rivetGeo, mat);
@@ -357,7 +357,7 @@ export class Binder {
color: 0xd4d7dc, metalness: 0.95, roughness: 0.2, 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 = []; this.rings = [];
for (const u of HOLE_U) { for (const u of HOLE_U) {
const y = (0.5 - u) * PAGE_H; 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() { _buildPages() {
const segX = 96, segY = 8; const segX = 96, segY = 8;
ENTRIES.forEach((entry, i) => { ENTRIES.forEach((entry, i) => {
const geo = new THREE.PlaneGeometry(PAGE_W, PAGE_H, segX, segY); // 高分段几何:翻页/顶层页(弯曲需要足够分段)
// 页内缘离书脊留出小空隙,左右页不紧贴 const hiGeo = this._makePageGeometry(segX, segY);
geo.translate(PAGE_W / 2 + PAGE_GAP, 0, 0); const hiBase = Float32Array.from(hiGeo.attributes.position.array);
// 纸张自然弧度:自由边略向正面翘起 // 极简几何:栈内页(只露边缘,无需弯曲,2 个三角形)
const pa = geo.attributes.position; const loGeo = this._makePageGeometry(1, 1);
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);
// 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择) // 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择)
// 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call) // 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call)
const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, { const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, {
alphaTest: 0.5, alphaTest: 0.5,
}); });
const mesh = new THREE.Mesh(geo, mat); const mesh = new THREE.Mesh(loGeo, mat); // 初始为栈内页,用极简几何
mesh.castShadow = false; // 由 _restack 依据是否为顶层页控制 mesh.castShadow = false; // 由 _restack 依据是否为顶层页控制
const g = new THREE.Group(); const g = new THREE.Group();
@@ -403,8 +411,9 @@ export class Binder {
this.pages.push({ this.pages.push({
group: g, group: g,
geometry: geo, hiGeo,
base, loGeo,
hiBase,
material: mat, material: mat,
mesh, mesh,
isPlaceholder: true, isPlaceholder: true,
@@ -438,7 +447,12 @@ export class Binder {
const dim = 1 - Math.min(depth * 0.06, 0.22); const dim = 1 - Math.min(depth * 0.06, 0.22);
p.material.color.setScalar(dim); p.material.color.setScalar(dim);
// 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 pass // 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 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._updateCurvature();
this._ensureTextures(); this._ensureTextures();
@@ -481,14 +495,15 @@ export class Binder {
// 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整 // 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整
const pages = index === null ? this.pages : [this.pages[index]]; const pages = index === null ? this.pages : [this.pages[index]];
pages.forEach((p) => { pages.forEach((p) => {
const pos = p.geometry.attributes.position; // 弯曲只作用于高分段几何(顶层/翻页页);栈内极简几何不弯曲
const pos = p.hiGeo.attributes.position;
const arr = pos.array; const arr = pos.array;
const theta = Math.abs(p.angle); // 0..π const theta = Math.abs(p.angle); // 0..π
const c = Math.sin(theta); // 0 静止,1 翻页中途 const c = Math.sin(theta); // 0 静止,1 翻页中途
const A = 0.58 * c; // 抬升幅度(整页弧) const A = 0.58 * c; // 抬升幅度(整页弧)
for (let i = 0; i < arr.length; i += 3) { for (let i = 0; i < arr.length; i += 3) {
const x = p.base[i]; const x = p.hiBase[i];
const y = p.base[i + 1]; const y = p.hiBase[i + 1];
const s = Math.max(0, x - PAGE_GAP); // 距页内缘 0..W(防浮点误差产生负值→NaN) const s = Math.max(0, x - PAGE_GAP); // 距页内缘 0..W(防浮点误差产生负值→NaN)
const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边 const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边
const yNorm = (y + PAGE_H / 2) / PAGE_H; // 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 belly = 0.85 * Math.sin((Math.PI * u) / 2) + 0.15 * Math.sin(Math.PI * u);
const z = A * twist * belly; const z = A * twist * belly;
arr[i + 2] = p.base[i + 2] + z; arr[i + 2] = p.hiBase[i + 2] + z;
} }
pos.needsUpdate = true; pos.needsUpdate = true;
}); });
@@ -583,7 +598,8 @@ export class Binder {
dispose() { dispose() {
this.pages.forEach((p) => { this.pages.forEach((p) => {
p.geometry.dispose(); p.hiGeo.dispose();
p.loGeo.dispose();
p.material.dispose(); p.material.dispose();
}); });
} }