
从一个很具体的失败开始你在场景里摆了五个点想让相机沿着它们平滑飞过去。第一版直接连直线按段插值。结果相机在每个点都猛地甩一下头像在走折线。这个很好理解——折角处方向是瞬间翻转的。第二版你想起来可以用多项式拟合。五个点四次多项式数学上唯一确定一定穿过所有点。写完一跑曲线在头尾附近开始不受控地往外甩中间还鼓出一个莫名的包。更糟的是你把其中一个点挪动一米整条曲线从头到尾全变了形。这两次失败分别对应了两件事。第一次是连续性不够第二次是龙格现象Runge’s phenomenon——高次多项式插值在控制点之间会剧烈振荡点越多越严重而且完全没有局部性。样条就是对这两个失败的回答。核心思路把难题推到接缝上样条的想法说出来很朴素别用一条高次曲线穿所有点用一堆低次曲线段拼起来然后在接缝处讲清楚条件。这一步转换很关键。原来的难题是如何构造一个高次多项式使它既穿过所有点又不振荡——这个要求本身是矛盾的。转换之后难题变成了相邻两段在接缝处要满足什么关系而这是一个局部的、可以精确控制的问题。代价是你从此必须关心接缝。好处是振荡消失了局部性回来了而且平滑程度可以按需选择。为什么偏偏是三次几乎所有实用样条都用三次多项式。这不是巧合是自由度算出来的。一段三次多项式有 4 个系数也就是 4 个自由度。而一段曲线最自然的约束刚好是 4 个起点位置、终点位置、起点切线、终点切线。不多不少。往下降一级二次只有 3 个自由度没法同时锁住两端的切线。往上升一级四次、五次确实能满足更多约束但振荡的毛病又回来了。三次是那个刚好够用的最低次数。这也是工程里常见的模式取最小的、能满足需求的那个选项。接缝接得有多好连续性分级讨论样条必须先有这套词汇因为平滑这个词太模糊了。C⁰ —— 位置连续。曲线不断开。但在接缝处可以有折角就像前面第一版的失败。C¹ —— 切线连续。方向在接缝处不跳变看起来已经光滑了。但切线的长度可以变而长度对应速度所以物理上还是可能有突兀感。C² —— 曲率连续。加速度也连续。这一级才真正开起来舒服。高速移动的相机、车辆路径、过山车轨道需要这个级别否则体感上会是方向盘被猛拧了一下。还有一组容易被忽略的概念G¹ 和 G²几何连续。它们只要求方向一致不要求大小相同。视觉上你看不出 G¹ 和 C¹ 的区别但如果有东西沿着曲线运动G¹ 的接缝处速度会跳。这个区别在做动画曲线时特别要紧你眼睛看着曲线是光滑的但角色移动起来就是会在某一帧卡一下。问题就出在你以为是 C¹实际只有 G¹。四种常见样条各自的性格Catmull-Rom游戏里的默认选择它解决了一个很实际的问题——Hermite 需要你为每个点指定切线但关卡设计师只想摆点。Catmull-Rom 的做法是切线自动推算miPi1−Pi−12\mathbf{m}_i \frac{\mathbf{P}_{i1} - \mathbf{P}_{i-1}}{2}mi2Pi1−Pi−1也就是每个点的切线方向取它前后两个邻居连线的方向。直观、不需要额外输入。它的三个优点在游戏场景里都很值钱曲线穿过所有控制点是插值不是逼近摆点就是摆路径所见即所得完全局部化改一个点只影响相邻的两段零额外输入给一串点就能跑// t ∈ [0,1]在 p1 和 p2 之间插值// p0 和 p3 只参与切线计算不在这一段上vec3catmull_rom(vec3 p0,vec3 p1,vec3 p2,vec3 p3,floatt){floatt2t*t;floatt3t2*t;return0.5f*((2.0f*p1)(-p0p2)*t(2.0f*p0-5.0f*p14.0f*p2-p3)*t2(-p03.0f*p1-3.0f*p2p3)*t3);}注意边界第一段和最后一段缺少一个邻居。常见做法是把端点复制一份或者让路径首尾相接成环。但标准 Catmull-Rom 有个必须知道的缺陷。如果相邻控制点的间距差异很大曲线会过冲——甩出控制点之外严重时自己绕一个圈。原因是切线长度直接取决于前后点的距离距离悬殊时切线就过长了。解法是改变参数化方式。标准版按每段参数长度相等分配均匀参数化而向心参数化centripetal按点距的平方根分配ti1ti∣Pi1−Pi∣0.5t_{i1} t_i |\mathbf{P}_{i1} - \mathbf{P}_i|^{0.5}ti1ti∣Pi1−Pi∣0.5这个版本有一个被证明的性质不会自交不会过冲。实践里应该把它当作默认而不是等出了问题再去换。Hermite把手柄交给用户Hermite 和 Catmull-Rom 是同一套数学唯一区别是切线由你指定。P(t)h00P0h10m0h01P1h11m1\mathbf{P}(t) h_{00}\mathbf{P}_0 h_{10}\mathbf{m}_0 h_{01}\mathbf{P}_1 h_{11}\mathbf{m}_1P(t)h00P0h10m0h01P1h11m1你在动画编辑器里拖的那些手柄拖的就是m0\mathbf{m}_0m0和m1\mathbf{m}_1m1。反过来看Catmull-Rom 其实就是切线按固定规则自动填充的 Hermite。理解了这层关系两者之间的转换就是显然的。Bézier设计工具的通用语言Bézier 换了一种控制方式控制点不在曲线上除了首尾两个它们像磁铁一样把曲线往自己方向拽。这听起来不如 Catmull-Rom 直观但它有两个很硬的优势。凸包性质曲线必定落在控制点围成的凸包里。换句话说它不会乱跑边界是可预测的。这对设计工具的可控性很重要。可精确切分de Casteljau 算法能在任意参数位置把曲线切成两段 Bézier而且是精确的、无误差的。这是字体渲染、路径求交、曲线细分的基础。三次 Bézier 是 Illustrator、Figma、字体轮廓、CSScubic-bezier()的共同语言。它和 Hermite 可以精确互转切线关系是m03(P1−P0)\mathbf{m}_0 3(\mathbf{P}_1 - \mathbf{P}_0)m03(P1−P0)。B-spline 与 NURBS当你真的需要 C²B-spline 天然是 C²局部性比 Catmull-Rom 还好。代价很直接曲线不穿过控制点。它的扩展 NURBS 加上了权重和非均匀节点能精确表示圆和圆锥曲线——普通多项式样条只能逼近圆这是 NURBS 成为 CAD 和工业建模基础的关键原因。游戏里用得少不是因为不好而是因为曲线不过我摆的点这件事对关卡设计师来说不直观。工具的易用性经常比数学性质更能决定技术选型。必须单独讲的坑参数不等于弧长这是我见过最多人栽进去的地方而且症状很容易被误判。你让t从 0 均匀走到 1取 100 个采样点让相机依次经过。结果相机忽快忽慢——控制点密集的地方慢稀疏的地方快。很多人的第一反应是插值算法有问题或者缓动曲线调得不对。都不是。问题在于t只是一个参数它和沿曲线走过的实际路程没有任何对应关系。数学上弧长是这个积分s(t)∫0t∣P′(u)∣ dus(t) \int_0^t |\mathbf{P}(u)| \, dus(t)∫0t∣P′(u)∣du对三次多项式这个积分没有闭式解。所以没法直接从我想走 5 米反解出t。实用解法预计算弧长表工程上的标准做法是建表加查找// 1. 建表阶段把曲线细分成 N 小段累加每段的近似长度// 得到一张 (t, 累积弧长) 的对应表// 2. 查询阶段给定目标距离 s// 在表里二分查找到相邻两项线性插值出 t// 精度不够时用牛顿迭代再修几步floattarclength_to_t(spline,distance);vec3 posevaluate(spline,t);表的分辨率决定精度通常每段曲线切 16 到 64 份就够用。如果需要更高精度牛顿迭代通常两三步就收敛。做完这一步你才真正拥有了匀速沿路径移动的能力。而且有了弧长之后缓动ease-in/ease-out才能按真实距离来算——否则你是在对一个和距离无关的参数做缓动效果自然不对。这件事值得在项目早期就做掉因为等到后面发现相机运动感觉怪的时候往返排查的成本远高于一开始就建表。还有一个坑单调性假设你在用样条平滑一组血条数值或者一条加载进度曲线。数据本身是单调递增的。标准三次样条可能会在某处插出一个局部下降的小波浪。数学上完全合法视觉上就是个 bug——进度条倒退了一下。这种场合需要单调三次插值Fritsch-Carlson 方法。它的思路是在算完切线之后对切线做一次约束调整保证每一段都不出现反向。原则是如果数据的某个性质单调、非负、有界在业务上必须保持就不能指望通用样条帮你保持得选专门约束过的方法。选择速查场景推荐理由相机路径、巡逻点、样条道路向心 Catmull-Rom过控制点、局部、不过冲动画曲线编辑器Hermite切线暴露成手柄可手调UI 缓动、矢量图形、字体三次 Bézier生态通用、凸包可控、可精确切分高速运动需要 C²自然三次样条 / B-spline曲率连续CAD、需要精确圆弧NURBS唯一能精确表示圆锥曲线数据必须单调单调三次插值显式约束不会反向收束样条插值的全部思想可以压缩成一句话用分段低次多项式替代整体高次多项式把复杂度从曲线整体形状转移到接缝处的连续性条件。这一步转移换来了三样东西不振荡、局部可控、平滑等级可选。而代价也很清楚——你得理解接缝得知道自己要的是 C¹ 还是 C²还得自己处理参数和弧长不是一回事这件事。回到开头那个甩头的相机。真正的修复不是换一个更平滑的算法而是把问题拆开问三遍接缝的连续性够到哪一级参数化会不会过冲运动是按参数还是按弧长驱动的这三个问题答清楚了曲线就不会再怪了。