2025-11-30 · game_math
【游戏中的数学】游戏中的数学 (13) - 骨骼动画数学
角色是如何动的?详解正向动力学 (FK) 与反向动力学 (IK) 的区别与算法原理。
在游戏中,物体很少做直线运动。无论是角色巡逻、摄像机轨迹还是 UI 弹出动画,我们都需要曲线来让运动看起来平滑自然。
本文将从最基础的线性插值出发,逐步推导出贝塞尔曲线 (Bézier Curve) 和 Catmull-Rom 样条 (Spline),并给出可直接使用的 Unity C# 实现。
Lerp 的完整讲解见标量数学,这里只回顾核心公式以便推导贝塞尔曲线。
\[ P(t) = (1-t)P_0 + tP_1, \quad t \in [0, 1] \]
如果我们想要弯曲,就需要引入一个控制点 \(P_1\)。 原理是:对 Lerp 进行 Lerp。
展开后得到公式:
\[ 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;
}这是最常用的形式(例如 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;
}直接展开公式虽然快速,但不够直观。De Casteljau 算法用递归几何构造的方式求曲线上的点,更容易理解,也更容易推广到任意阶。
核心思想:将 \(n\) 个控制点通过一轮 Lerp 降为 \(n-1\) 个点,重复这个过程直到只剩一个点——那就是曲线在 \(t\) 处的值。
以三阶(4 个控制点 \(P_0, P_1, P_2, P_3\))为例:
// 通用 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 不会。
贝塞尔曲线的一个缺点是:曲线不一定经过中间控制点。如果我们希望曲线穿过一系列给定的点(例如 NPC 巡逻路径上的路点),就需要插值样条 (Interpolating Spline)。
Catmull-Rom 样条正是为此而生——它保证曲线经过每一个控制点,且在连接处自动保持 \(C^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) 的形式——给定两端的值和切线,唯一确定一条三次曲线。
/// <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;
}巡逻路径往往需要循环。要让 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\) 也能平滑过渡,形成无缝循环。
不同曲线适用于不同场景,下表可以帮助快速决策:
| 特性 | Bézier (三阶) | Catmull-Rom | Hermite |
|---|---|---|---|
| 曲线是否经过控制点 | 仅起点/终点 | 全部经过 | 仅起点/终点 |
| 需要手动设置切线 | 否(用控制点隐式定义) | 否(自动计算) | 是 |
| 局部可控性 | 差——移动一个控制点影响整条曲线 | 好——只影响相邻两段 | 好 |
| 适合场景 | UI 动画、缓动曲线、美术路径 | NPC 巡逻、摄像机轨迹、赛道 | 需要精确切线控制的物理模拟 |
| 闭合路径 | 需额外拼接 | 天然支持(索引取模) | 需额外拼接 |
| 实现复杂度 | 低 | 低 | 中 |
经验法则: - 如果路径由美术/设计手动调整 → Bézier - 如果路径由一系列路点自动生成 → Catmull-Rom - 如果需要在端点精确控制速度方向 → Hermite
< 上一篇: 光照模型 | 回到目录 | 下一篇: 骨骼动画 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
角色是如何动的?详解正向动力学 (FK) 与反向动力学 (IK) 的区别与算法原理。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。
在游戏中,物体很少做直线运动。无论是角色巡逻、摄像机轨迹还是 UI 弹出动画,我们都需要曲线来让运动看起来平滑自然。
本文将从最基础的线性插值出发,逐步推导出贝塞尔曲线 (Bézier Curve) 和 Catmull-Rom 样条 (Spline),并给出可直接使用的 Unity C# 实现。
Lerp 的完整讲解见标量数学,这里只回顾核心公式以便推导贝塞尔曲线。
\[ P(t) = (1-t)P_0 + tP_1, \quad t \in [0, 1] \]
如果我们想要弯曲,就需要引入一个控制点 \(P_1\)。 原理是:对 Lerp 进行 Lerp。
展开后得到公式:
\[ 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;
}这是最常用的形式(例如 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;
}直接展开公式虽然快速,但不够直观。De Casteljau 算法用递归几何构造的方式求曲线上的点,更容易理解,也更容易推广到任意阶。
核心思想:将 \(n\) 个控制点通过一轮 Lerp 降为 \(n-1\) 个点,重复这个过程直到只剩一个点——那就是曲线在 \(t\) 处的值。
以三阶(4 个控制点 \(P_0, P_1, P_2, P_3\))为例:
// 通用 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 不会。
贝塞尔曲线的一个缺点是:曲线不一定经过中间控制点。如果我们希望曲线穿过一系列给定的点(例如 NPC 巡逻路径上的路点),就需要插值样条 (Interpolating Spline)。
Catmull-Rom 样条正是为此而生——它保证曲线经过每一个控制点,且在连接处自动保持 \(C^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) 的形式——给定两端的值和切线,唯一确定一条三次曲线。
/// <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;
}巡逻路径往往需要循环。要让 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\) 也能平滑过渡,形成无缝循环。
不同曲线适用于不同场景,下表可以帮助快速决策:
| 特性 | Bézier (三阶) | Catmull-Rom | Hermite |
|---|---|---|---|
| 曲线是否经过控制点 | 仅起点/终点 | 全部经过 | 仅起点/终点 |
| 需要手动设置切线 | 否(用控制点隐式定义) | 否(自动计算) | 是 |
| 局部可控性 | 差——移动一个控制点影响整条曲线 | 好——只影响相邻两段 | 好 |
| 适合场景 | UI 动画、缓动曲线、美术路径 | NPC 巡逻、摄像机轨迹、赛道 | 需要精确切线控制的物理模拟 |
| 闭合路径 | 需额外拼接 | 天然支持(索引取模) | 需额外拼接 |
| 实现复杂度 | 低 | 低 | 中 |
经验法则: - 如果路径由美术/设计手动调整 → Bézier - 如果路径由一系列路点自动生成 → Catmull-Rom - 如果需要在端点精确控制速度方向 → Hermite
< 上一篇: 光照模型 | 回到目录 | 下一篇: 骨骼动画 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
角色是如何动的?详解正向动力学 (FK) 与反向动力学 (IK) 的区别与算法原理。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。