引言

在游戏中,物体很少做直线运动。无论是角色巡逻、摄像机轨迹还是 UI 弹出动画,我们都需要曲线来让运动看起来平滑自然。

本文将从最基础的线性插值出发,逐步推导出贝塞尔曲线 (Bézier Curve) 和 Catmull-Rom 样条 (Spline),并给出可直接使用的 Unity C# 实现。

Bezier Curves

1. 线性插值回顾 (Lerp Recap)

Lerp 的完整讲解见标量数学,这里只回顾核心公式以便推导贝塞尔曲线。

\[ P(t) = (1-t)P_0 + tP_1, \quad t \in [0, 1] \]

2. 二阶贝塞尔曲线 (Quadratic Bézier)

如果我们想要弯曲,就需要引入一个控制点 \(P_1\)。 原理是:对 Lerp 进行 Lerp

  1. \(P_0\)\(P_1\) 之间插值得到 \(A\)
  2. \(P_1\)\(P_2\) 之间插值得到 \(B\)
  3. \(A\)\(B\) 之间插值得到最终点 \(P(t)\)

展开后得到公式:

\[ P(t) = (1-t)^2 P_0 + 2(1-t)t P_1 + t^2 P_2 \]

Vector3 QuadraticBezier(Vector3 p0, Vector3 p1, Vector3 p2, float t) {
    t = Mathf.Clamp01(t);
    float u = 1f - t;
    return u * u * p0 + 2f * u * t * p1 + t * t * p2;
}

3. 三阶贝塞尔曲线 (Cubic Bézier)

这是最常用的形式(例如 Photoshop 的钢笔工具、CSS cubic-bezier() 缓动函数)。它有两个控制点 \(P_1, P_2\),给予设计者更大的自由度。

\[ P(t) = (1-t)^3 P_0 + 3(1-t)^2 t P_1 + 3(1-t)t^2 P_2 + t^3 P_3 \]

Vector3 CubicBezier(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
    t = Mathf.Clamp01(t);
    float u = 1f - t;
    float uu = u * u;
    float tt = t * t;
    return uu * u * p0 + 3f * uu * t * p1 + 3f * u * tt * p2 + tt * t * p3;
}

4. De Casteljau 算法

直接展开公式虽然快速,但不够直观。De Casteljau 算法用递归几何构造的方式求曲线上的点,更容易理解,也更容易推广到任意阶。

核心思想:将 \(n\) 个控制点通过一轮 Lerp 降为 \(n-1\) 个点,重复这个过程直到只剩一个点——那就是曲线在 \(t\) 处的值。

以三阶(4 个控制点 \(P_0, P_1, P_2, P_3\))为例:

  1. 第一轮:3 次 Lerp → 得到 \(Q_0, Q_1, Q_2\)
  2. 第二轮:2 次 Lerp → 得到 \(R_0, R_1\)
  3. 第三轮:1 次 Lerp → 得到最终点 \(P(t)\)
// 通用 De Casteljau,适用于任意阶贝塞尔曲线
Vector3 DeCasteljau(Vector3[] points, float t) {
    t = Mathf.Clamp01(t);
    int n = points.Length;
    // 复制一份避免修改原数组
    Vector3[] work = new Vector3[n];
    System.Array.Copy(points, work, n);

    for (int r = 1; r < n; r++) {
        for (int i = 0; i < n - r; i++) {
            work[i] = Vector3.Lerp(work[i], work[i + 1], t);
        }
    }
    return work[0];
}

De Casteljau 算法的另一个优势是数值稳定性——当 \(t\) 接近 0 或 1 时,直接展开公式可能因浮点误差偏离端点,而逐步 Lerp 不会。

5. Catmull-Rom 样条 (Catmull-Rom Spline)

贝塞尔曲线的一个缺点是:曲线不一定经过中间控制点。如果我们希望曲线穿过一系列给定的点(例如 NPC 巡逻路径上的路点),就需要插值样条 (Interpolating Spline)。

Catmull-Rom 样条正是为此而生——它保证曲线经过每一个控制点,且在连接处自动保持 \(C^1\) 连续(切线连续)。

5.1 公式推导

计算 \(P_i\)\(P_{i+1}\) 之间的曲线段时,需要前后各一个邻居点 \(P_{i-1}\)\(P_{i+2}\)。每个段的切线由相邻两点决定:

\[ m_i = \frac{P_{i+1} - P_{i-1}}{2} \]

引入张力参数 (tension) \(\alpha\) 后,切线变为:

\[ m_i = \alpha \cdot (P_{i+1} - P_{i-1}) \]

其中 \(\alpha = 0.5\) 是标准 Catmull-Rom(无张力),\(\alpha\) 越小曲线越”紧”,越大越”松”。

对于参数 \(t \in [0, 1]\)\(P_i\)\(P_{i+1}\) 之间的点为:

\[ P(t) = (2t^3 - 3t^2 + 1) P_i + (t^3 - 2t^2 + t) m_i + (-2t^3 + 3t^2) P_{i+1} + (t^3 - t^2) m_{i+1} \]

这其实是Hermite 基函数 (Hermite Basis Functions) 的形式——给定两端的值和切线,唯一确定一条三次曲线。

5.2 为什么适合巡逻路径

  • 设计者只需在场景中放置路点,无需手动调控制点或切线。
  • 曲线自动穿过所有路点,路径行为所见即所得。
  • 张力参数提供了全局的”松紧”控制。

5.3 C# 实现

/// <summary>
/// 计算 Catmull-Rom 样条上的点。
/// p0, p1, p2, p3 为四个连续控制点,曲线位于 p1 到 p2 之间。
/// alpha 为张力参数,标准值 0.5。
/// </summary>
Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3,
                    float t, float alpha = 0.5f) {
    t = Mathf.Clamp01(t);
    float tt = t * t;
    float ttt = tt * t;

    Vector3 m0 = alpha * (p2 - p0);
    Vector3 m1 = alpha * (p3 - p1);

    float a =  2f * ttt - 3f * tt + 1f;
    float b =       ttt - 2f * tt + t;
    float c = -2f * ttt + 3f * tt;
    float d =       ttt -      tt;

    return a * p1 + b * m0 + c * p2 + d * m1;
}

6. 闭合路径 (Closed Path / Loop)

巡逻路径往往需要循环。要让 Catmull-Rom 样条闭合,只需要让首尾控制点环绕(wrap around):

假设有 \(N\) 个路点 \(P_0, P_1, \ldots, P_{N-1}\),计算每一段时,索引取模即可:

Vector3 EvaluateClosedCatmullRom(Vector3[] points, float tGlobal,
                                  float alpha = 0.5f) {
    int n = points.Length;
    // tGlobal ∈ [0, n) 映射到段索引和局部 t
    tGlobal = Mathf.Repeat(tGlobal, n);
    int seg = Mathf.FloorToInt(tGlobal);
    float tLocal = tGlobal - seg;

    Vector3 p0 = points[((seg - 1) % n + n) % n];
    Vector3 p1 = points[seg % n];
    Vector3 p2 = points[(seg + 1) % n];
    Vector3 p3 = points[(seg + 2) % n];

    return CatmullRom(p0, p1, p2, p3, tLocal, alpha);
}

关键是 ((seg - 1) % n + n) % n——C# 的 % 对负数不取正余数,因此需要加 n 修正。这样最后一段 \(P_{N-1} \to P_0\) 也能平滑过渡,形成无缝循环。

7. 曲线选型指南

不同曲线适用于不同场景,下表可以帮助快速决策:

特性 Bézier (三阶) Catmull-Rom Hermite
曲线是否经过控制点 仅起点/终点 全部经过 仅起点/终点
需要手动设置切线 否(用控制点隐式定义) 否(自动计算)
局部可控性 差——移动一个控制点影响整条曲线 好——只影响相邻两段
适合场景 UI 动画、缓动曲线、美术路径 NPC 巡逻、摄像机轨迹、赛道 需要精确切线控制的物理模拟
闭合路径 需额外拼接 天然支持(索引取模) 需额外拼接
实现复杂度

经验法则: - 如果路径由美术/设计手动调整 → Bézier - 如果路径由一系列路点自动生成 → Catmull-Rom - 如果需要在端点精确控制速度方向 → Hermite

8. 总结

  • Lerp 是直线运动的基石,也是所有高阶曲线的构建块。
  • 贝塞尔曲线通过嵌套 Lerp 实现平滑弯曲;De Casteljau 算法提供了直观且数值稳定的求值方式。
  • Catmull-Rom 样条自动穿过所有控制点,非常适合游戏中的路径系统;张力参数可调节松紧。
  • 闭合路径只需让控制点索引环绕取模
  • 根据实际需求(手动调整 vs 自动生成、是否需要经过控制点)选择合适的曲线类型。

< 上一篇: 光照模型 | 回到目录 | 下一篇: 骨骼动画 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。