环境:Unity2018.3 语言:C#
本文主要参考《3D数学基础:图形与游戏开发》。
幂:
首先我们来看q^1/2,这是说明我们只需要q的一半位移,旋转是由θ来表示的,所以需要将w转换为θ(arccos(θ))后就能对其进行幂运算了。
平滑插值Slerp:
从q1到q2之间的差值为dq=inv(q1)q2,那么如果只需要t份dq,则表示为dq^t=(inv(q1)q2)^t。
最终的公式为:slerp(q0, q1, t) = q0 (inv(q1)q2)^t。
因为四元数可以说成是按照某个轴旋转一个角度,即[cos(θ/2) sin(θ/2)n],角度为θ,需要对其进行平滑插值,就是对θ进行插值即可。
这边我用Unity中的Quaternion类来减少一些类似叉乘的方法,专注于Slerp的实现。
首先我们来看一下Unity直接使用Quaternion进行插值的效果。
public class TestSlerp : MonoBehaviour
{
public Vector3 TargetRot;
public Quaternion targetRot;
private Quaternion startRot;
private float dTime = 0f;
void OnEnable()
{
dTime = 0f;
startRot = transform.rotation;
targetRot = Quaternion.Euler(TargetRot);
}
void Update()
{
dTime += Time.deltaTime;
if (dTime > 1f)
{
dTime = 1f;
enabled = false;
}
// 进行插值
transform.rotation = Quaternion.Slerp(startRot, targetRot, dTime);
}
}
跟Mathf.Lerp类似的使用。
好,现在我们根据上面的公式来实现一下Slerp:
public class TestSlerp2 : MonoBehaviour
{
public Vector3 TargetRot;
public Quaternion targetRot;
private Quaternion startRot;
private float dTime = 0f;
void OnEnable()
{
dTime = 0f;
startRot = transform.rotation;
targetRot = Quaternion.Euler(TargetRot);
}
void Update()
{
dTime += Time.deltaTime;
if (dTime > 1f)
{
dTime = 1f;
enabled = false;
}
// 进行插值
transform.rotation = MySlerp(startRot, targetRot, dTime);
}
Quaternion MySlerp(Quaternion a, Quaternion b, float t)
{
var q = (Quaternion.Inverse(a) * b);
var result = a * Power(q, t);
return result;
}
Quaternion Power(Quaternion q, float t)
{
float x = q.x;
float y = q.y;
float z = q.z;
float w = q.w;
// 防止除零
if (Mathf.Abs(w) < 0.9999f)
{
// 提取半角alpha = theta/2
float alpha = Mathf.Acos(w);
// 计算新的alpha
float newAlpha = alpha * t;
// 转换成w
w = Mathf.Cos(newAlpha);
// 消去sin(alpha)
float mult = Mathf.Sin(newAlpha) / Mathf.Sin(alpha);
x *= mult;
y *= mult;
z *= mult;
}
return new Quaternion(x, y, z, w);
}
}
以上是我直接照搬公式实现的插值方法,实际上书中的方法针对性得优化了一些性能:
public Quaternion SlerpBook(Quaternion a, Quaternion b, float t)
{
// 使用点乘计算四元数的夹角
float cosOmega = Quaternion.Dot(a, b);
// 如果点乘为负,则反转一个四元数取得最短的弧
if (cosOmega < 0f)
{
a.x = -a.x;
a.y = -a.y;
a.z = -a.z;
a.w = -a.w;
}
float k0, k1;
if (cosOmega > 0.9999f)
{
// 非常接近 进行线性插值
k0 = 1.0f - t;
k1 = t;
}
else
{
//使用sin^2(z)+cos^2(z)=1计算sin值
float sinOmega = Mathf.Sqrt(1.0f - cosOmega * cosOmega);
// 通过sin和cos计算角度
float omega = Mathf.Atan2(sinOmega, cosOmega);
// 计算分母的倒数
float oneOverSinOmega = 1.0f / sinOmega;
// 计算插值变量
k0 = Mathf.Sin((1.0f - t) * omega) * oneOverSinOmega;
k1 = Mathf.Sin(t * omega) * oneOverSinOmega;
}
// 最后插值
return new Quaternion(
a.x * k0 + b.x * k1,
a.y * k0 + b.y * k1,
a.z * k0 + b.z * k1,
a.w * k0 + b.w * k1);
}
今天研究了一天的四元数,肯定说不上完全理解吧,但是用法和特性都有了大体上的掌握。感触最深的是,其实很多东西以前学过,但是一直不用就完全忘记了,数学也是一门语言,需要时常锻炼对其的感觉。