前几天有个做数字展厅的朋友拦住我,问了一句:Unity里想放一本能亲手翻页的书,有没有现成demo可以抄?我当时第一反应是搜插件,结果不是收费就是依赖太老,要么就是打开后根本没想象中轻量。后来我没再绕弯子,直接用Shader顶点偏移的方式自己写了一个——一个建立在新空项目上、可拖拽翻页、支持双面渲染和背光修正的完整demo。这篇文章我会把选型思路、几何原理、Shader代码、C#交互,以及我从踩坑中总结出来的渲染细节全部摊开讲,给那些想快速落地又不想被插件绑架的人一条能跑通的路。
1. 翻一本能翻的书,为什么大家都在选型上反复横跳
1.1 四条路线摆在一起比一比
做"翻页书"最迷惑人的地方在于,它看着是单页,实际牵涉到网格变形、渲染状态切换、交互手感三层问题。不同方案在这三层上的取舍完全不同。
| 实现路线 | 灵活度 | 资源依赖 | 性能开销 | 上手难度 | 适合的场景 |
|---|---|---|---|---|---|
| CPU网格变形 | 高 | 需要高细分网格 | 顶点回读、每帧改Mesh | 低 | 固定翻页动画、离线演示 |
| 骨骼动画 | 中 | 需要美术绑骨骼、调权重 | 低 | 高 | 角色手翻书等复杂联动 |
| Shader顶点偏移 | 高 | 几乎为零 | 低,GPU并行 | 中 | 实时交互、动态翻页、Demo快速验证 |
| 第三方插件 | 低 | 闭源黑盒 | 不可控 | 低 | 工期紧、效果不复杂 |
网格变形是我最早试的:在CPU里按曲线移动顶点,写起来直白,但问题也直白——一旦翻到一半,碰撞体、法线、阴影全部要对齐,而且每帧调用 mesh.vertices = ... 会产生不小的GC和延迟,节点一多帧率就往下掉。
骨骼动画更适合那种"角色伸手把书拿起来翻"的复杂场景,可一本纯粹展示内容、让用户自己拖拽翻页的数字书,让美术去绑骨骼完全是大材小用,调试周期也长。
第三方插件在Demo阶段确实香,但我遇到过两次升级Unity版本后Shader失效的情况,排查起来特别被动。自己写哪怕出问题,至少心里有排查路径。
1.2 我选Shader顶点偏移的真实理由
这个方案最核心的逻辑是:书页网格的顶点坐标不用在CPU侧改,而是把"翻页进度"当参数传进Shader,在顶点阶段把每个顶点的局部坐标重新算一遍。书页还是那张书页,顶点还是那些顶点,但位置被Shader重写了,GPU并行处理,速度快,参数也天然适合交互。
我选它的理由有三个,都很实际:
- 不需要额外美术资源,一个基础网格就能起稿,文字、贴图通过UV映射上去,翻动时纹理跟着表面走。
- 翻页进度
_Progress是从C#脚本实时传入的,可以和鼠标拖拽、触摸滑动、动画曲线直接关联,天然适合"用户可以亲手翻"这个核心需求。 - 后续扩展空间大:加一本多页书,不用改Shader核心,只要给每页传不同的进度值就行,甚至可以连多页书脊厚度一起模拟。
这个方案真正难的不是"让顶点动起来",而是动了之后还别露馅——法线、背面、阴影都得跟着动。后面我会专门拆这一块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 翻页的几何内核:把一张平面卷到圆柱面上
2.1 圆柱面卷曲的公式拆解
翻页的视觉效果,抛开炫技,本质就是把一张平面“窝”在一个虚拟圆柱面上。页面上每个点离书脊的距离,决定了这个点沿圆柱走了多长的弧。
我用的网格放在XZ平面上:x 方向是页宽,x=0 是书脊位置,x=PageWidth 是页边;z 是页面高度方向;y 初始为0。翻页进度 _Progress 在0到1之间变化,已卷曲的弧长:
code复制bendLength = _Progress * _PageWidth
这张页面从书脊开始,在半径为 _BendRadius 的圆柱面上展开。对于距离书脊为 x 的点,分两种情况处理。
第一种是点已经卷到圆柱面上,即 x <= bendLength。设它在圆柱上走过的角度为 a = x / _BendRadius,则它的新位置是:
code复制newX = _BendRadius * sin(a)
newY = _BendRadius * (1 - cos(a))
newZ = z
这条公式的几何含义很直观:圆柱面的截面圆心在书脊正上方 _BendRadius 处,页面从书脊出发贴着圆柱表面向上卷,y 越来越大,x 先向右伸再往回收。
第二种是还没卷到的那部分平面,即 x > bendLength。它作为刚体,跟随已弯曲末端的切线方向旋转,旋转角度是总卷曲角度:
code复制angle = bendLength / _BendRadius
local = x - bendLength
newX = _BendRadius * sin(angle) + local * cos(angle)
newY = _BendRadius * (1 - cos(angle)) + local * sin(angle)
newZ = z
这里能看出末节平面其实就是从弯曲端点延伸出去的切线。翻页进行到一半时,平面部分会保持平整,整张页面呈现“根部卷、尖部平”的自然纸张形态,比单纯整张绕书脊硬转要真实得多。
我建议第一次跑通时不要追求圆柱模型完全精确,先把 _Progress 从0到1匀速变化,观察页尖的运动轨迹是不是一条顺滑的弧线。确认这一点,核心几何就对了百分之八十。
2.2 三个关键参数的调参指南
翻页有没有手感,很大程度靠三个参数喂出来。
| 参数 | 作用 | 值偏小的表现 | 值偏大的表现 | 推荐起点 |
|---|---|---|---|---|
_BendRadius |
卷曲半径 | 页面折叠感强、像撕纸 | 页面像平板绕轴转,失去弯曲 | 0.5~0.9 |
_PageWidth |
页面宽度 | 翻页行程短、翻得急 | 行程长、拖拽感笨重 | 按实际书宽 |
_Progress |
翻页进度 | 回弹位置提示明显 | 直接翻完 | 由交互控制 |
调 _BendRadius 时要特别留意:半径太小,靠近书脊的顶点在角度很大时会出现向内侧穿模;半径太大,整页趋近于刚体旋转,又失去了圆柱卷曲的纸张感。我一般先把半径设为书宽的0.6倍,再按实际渲染效果微调。
还有一点容易忽略:为了让翻页方向正确,要确保网格的 x 正方向指向页边,而不是指向书脊。方向反了,公式里的 sin 和 cos 会让页面往桌面底下钻。
2.3 网格细分:弯得顺不顺就看它
顶点位移方案最反直觉的坑是:如果网格精度不够,圆柱弯曲就成了折纸。默认Unity的Plane是10×10段,直接拿来当书页,翻到一半时页面边缘会出现明显的多边形折角,完全没有纸张的丝滑感。
解决方法是代码生成网格,而不是
