一、角度插值(Angle Interpolation)
1.1 核心问题
在游戏中,物体从当前方向快速转向目标方向时,若直接使用线性插值会出现速度不均匀,且无法正确处理超过 180° 的反向旋转。我们需要沿最短旋转弧线、以恒定角速度或加速度完成平滑过渡。
1.2 数学原理
$$\theta = \arccos\Bigl(\frac{\mathbf{u}\cdot\mathbf{v}}{|\mathbf{u}|\ |\mathbf{v}|}\Bigr), \quad\text{旋转轴 } \mathbf{a} = \frac{\mathbf{u}\times\mathbf{v}}{|\mathbf{u}\times\mathbf{v}|},\quad\Delta\theta = \omega\ \Delta t$$
构造步进四元数:
$$q_{\Delta} = \bigl(\cos\frac{\Delta\theta}{2},\ \mathbf{a}\ \sin\frac{\Delta\theta}{2}\bigr)$$
更新方向:
$$\mathbf{u}' = q_{\Delta}\ \mathbf{u}\ q_{\Delta}^{-1}$$
1.3 Unity 内置方法
float maxDeg = turnSpeed * Time.deltaTime;
Vector3 newDir = Vector3.RotateTowards(
currentDir, targetDir,
maxRadiansDelta: maxDeg * Mathf.Deg2Rad,
maxMagnitudeDelta: 0f
);
transform.rotation = Quaternion.LookRotation(newDir);
1.4 手写伪代码
// fromDir, toDir 已归一化;turnSpeed 单位:deg/s
float dot = Mathf.Clamp(Vector3.Dot(fromDir, toDir), -1f, 1f);
float theta = Mathf.Acos(dot); // [0, π]
float maxDelta = turnSpeed * Time.deltaTime * Mathf.Deg2Rad;
float t = Mathf.Min(1f, maxDelta / theta);
Vector3 axis = Vector3.Cross(fromDir, toDir).normalized;
float half = theta * t * 0.5f;
float sinH = Mathf.Sin(half);
Quaternion qStep = new Quaternion(
axis.x * sinH, axis.y * sinH, axis.z * sinH,
Mathf.Cos(half)
);
Vector3 resultDir = (qStep * new Vector4(fromDir, 0)).normalized;
二、方向曲线(Direction Curves)
2.1 场景需求
抛物线、贝塞尔轨迹、自然摆动效果等,都需要在 3D 空间内让方向或位置沿曲线平滑运动。
2.2 二次/三次贝塞尔曲线
$$B_2(t) = (1-t)^2P_0 + 2t(1-t)P_1 + t^2P_2,\quad t\in[0,1]$$
$$B_3(t) = (1-t)^3P_0 + 3t(1-t)^2P_1 + 3t^2(1-t)P_2 + t^3P_3$$
2.3 三次贝塞尔伪代码
Vector3 Bezier3(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Vector3 point = uuu * p0; // (1-t)^3 * P0
point += 3 * uu * t * p1; // 3(1-t)^2 * t * P1
point += 3 * u * tt * p2; // 3(1-t) * t^2 * P2
point += ttt * p3; // t^3 * P3
return point;
}
// 用法示例
void Update() {
float t = Mathf.Clamp01((Time.time - startTime) / duration);
transform.position = Bezier3(A, B, C, D, t);
}
2.4 曲线方向(切线)计算
$$B_3'(t)= 3(1-t)^2(P_1 - P_0)\ +\ 6(1-t)t(P_2 - P_1)\ +\ 3t^2(P_3 - P_2)$$
归一化后即可作为方向向量:
Vector3 tangent = Bezier3Tangent(A, B, C, D, t).normalized;
三、最短路径(球面插值 Slerp)
3.1 问题描述
在单位球面上插值时,需保持向量长度不变,沿大圆(Great Circle)轨迹运动。
3.2 SLERP 公式
$$\mathrm{Slerp}(u,v,t)=\frac{\sin\bigl((1 - t)\ \theta\bigr)}{\sin\theta}\ u\ +\ \frac{\sin\bigl(t,\theta\bigr)}{\sin\theta}\ v,\quad\theta = \arccos\bigl(u \cdot v\bigr)$$
3.3 Unity 接口
Vector3 result = Vector3.Slerp(fromDir, toDir, t);
3.4 手写伪代码
Vector3 Slerp(Vector3 u, Vector3 v, float t) {
float dot = Mathf.Clamp(Vector3.Dot(u, v), -1f, 1f);
float theta = Mathf.Acos(dot);
float sinTheta = Mathf.Sin(theta);
if (sinTheta < 1e-5f)
return Vector3.Lerp(u, v, t); // 退化到 Lerp
float a = Mathf.Sin((1 - t) * theta) / sinTheta;
float b = Mathf.Sin(t * theta) / sinTheta;
return a * u + b * v;
}
四、自定义向量结构(Custom Vec3)
4.1 动机
在 ECS 或顶点重构等场景,对 Vector3 的封装开销敏感时,可自定义轻量结构并内联计算。
4.2 结构定义
public struct Vec3 {
public float x, y, z;
public Vec3(float x, float y, float z) {
this.x = x; this.y = y; this.z = z;
}
// 运算符重载
public static Vec3 operator +(Vec3 a, Vec3 b)
=> new Vec3(a.x + b.x, a.y + b.y, a.z + b.z);
public static Vec3 operator -(Vec3 a, Vec3 b)
=> new Vec3(a.x - b.x, a.y - b.y, a.z - b.z);
public static Vec3 operator *(Vec3 v, float s)
=> new Vec3(v.x * s, v.y * s, v.z * s);
public static Vec3 operator /(Vec3 v, float s)
=> new Vec3(v.x / s, v.y / s, v.z / s);
public float SqrMagnitude() => x*x + y*y + z*z;
public float Magnitude() => Mathf.Sqrt(SqrMagnitude());
public Vec3 Normalized() {
float inv = 1f / Magnitude();
return this * inv;
}
}
4.3 性能对比
| 操作 | Unity Vector3 |
自定义 Vec3 |
|---|---|---|
| 构造 | 方法调用 | 直接赋值 |
normalized |
多次函数/属性访问 | 单次 Sqrt 与算术运算 |
| 数组存取 | 额外封装 | 结构对齐更易 SIMD 优化 |
Tip:在 Burst/ECS 环境下,自定义
Vec3结合float3能拿到最高性能。
五、总结
- 角度插值:Quaternion 或
RotateTowards可保持旋转弧线最短、速度可控。 - 方向曲线:贝塞尔曲线生成自然轨迹,切线公式可驱动方向。
- 最短路径:SLERP 保持向量长度恒定,沿大圆运动。
- 自定义向量:轻量结构、内联计算,适用于极限性能场景。