四元组
这篇文章内容将比较偏数学,为方便快速复习,这里只列出关键的结论。
四元组简介
四元组类似于一个复数,有实部和虚部,只不过四元组的虚部是个矢量,因此总共有四维,一般来说我们把矢量放在前面,也就是说存在XMFLOAT4里面的时候,前三维是虚部,最后一维是实部。
运算的定义
乘法可以写成矩阵形式
注意因为涉及叉乘,左手系和右手系里的四元组不一样。
乘法和矩阵乘法一样,有结合律没有交换律。
单位四元组(0,0,0,1),纯四元组 。
共轭
共轭的性质
模长的定义就是二阶范数
求逆
逆的性质
单位四元组
可以写成
旋转操作符
复数相乘的时候,模长相乘,辐角相加。乘单位复数相当于作旋转操作。
四元组也类似,假如我们有单位四元组
w
,有
,假如p是一个三维空间中的点或矢量,我们把p看作一个纯四元组,即(v,0),考虑如下计算
其中
相当于对着
这根轴旋转了2θ。
我们可以这样构造q
这样就是绕
轴旋转θ角(左手系)。
旋转操作符转矩阵:
旋转矩阵转旋转操作符:
旋转操作符复合
四元组插值
定义点乘
插值
可以看出这种spherical插值不是线性的,但是对角度而言是匀速的插值,相比之下,(1-t)a+tb这种线性插值再归一化得到的结果是不匀速的旋转,会先加速再减速。
旋转操作符有这么一个性质
即负的q也会得到同样的旋转结果,然而这个过程是不同的
负的q旋转是绕
轴转
度,因此q和-q旋转的方向不同。
我们要怎样判断哪个近呢?我们计算两个四元向量之差的模长就能知道哪个更近了,如果
,那么-b就比b离a更近,我们选-b作为旋转四元组就能走最短路径插值。
插值的代码实现:
// Linear interpolation (for small theta).
public static Quaternion LerpAndNormalize(Quaternion p, Quaternion q, float s)
{
// Normalize to make sure it is a unit quaternion.
return Normalize((1.0f - s)*p + s*q);
}
public static Quaternion Slerp(Quaternion p, Quaternion q, float s)
{
// Recall that q and -q represent the same orientation, but
// interpolating between the two is different: One will take the
// shortest arc and one will take the long arc. To find
// the shortest arc, compare the magnitude of p-q with the
// magnitude p-(-q) = p+q.
if(LengthSq(p-q) > LengthSq(p+q))
q = -q;
float cosPhi = DotP(p, q);
// For very small angles, use linear interpolation.
if(cosPhi > (1.0f - 0.001))
return LerpAndNormalize(p, q, s);
// Find the angle between the two quaternions.
float phi = (float)Math.Acos(cosPhi);
float sinPhi = (float)Math.Sin(phi);
// Interpolate along the arc formed by the intersection of the 4D
// unit sphere and the plane passing through p, q, and the origin of
// the unit sphere.
return ((float)Math.Sin(phi*(1.0-s))/sinPhi)*p +
((float)Math.Sin(phi*s)/sinPhi)*q;
}
DX中已经有了四元组相关的接口给我们调用,下面列出一些常用的
// Returns the quaternion dot product Q1·Q2.
XMVECTOR XMQuaternionDot(XMVECTOR Q1, XMVECTOR Q2);
// Returns the identity quaternion (0, 0, 0, 1).
XMVECTOR XMQuaternionIdentity();
// Returns the conjugate of the quaternion Q.
XMVECTOR XMQuaternionConjugate(XMVECTOR Q);
// Returns the norm of the quaternion Q.
XMVECTOR XMQuaternionLength(XMVECTOR Q);
// Normalizes a quaternion by treating it as a 4D vector.
XMVECTOR XMQuaternionNormalize(XMVECTOR Q);
// Computes the quaternion product Q1Q2.
XMVECTOR XMQuaternionMultiply(XMVECTOR Q1, XMVECTOR Q2);
// Returns a quaternions from axis-angle rotation representation.
XMVECTOR XMQuaternionRotationAxis(XMVECTOR Axis, FLOAT Angle);
// Returns a quaternions from axis-angle rotation representation, where the axis
// vector is normalized—this is faster than XMQuaternionRotationAxis.
XMVECTOR XMQuaternionRotationNormal(XMVECTOR NormalAxis,FLOAT Angle);
// Returns a quaternion from a rotation matrix.
XMVECTOR XMQuaternionRotationMatrix(XMMATRIX M);
// Returns a rotation matrix from a unit quaternion.
XMMATRIX XMMatrixRotationQuaternion(XMVECTOR Quaternion);
// Extracts the axis and angle rotation representation from the quaternion Q.
VOID XMQuaternionToAxisAngle(XMVECTOR *pAxis, FLOAT *pAngle, XMVECTOR Q);
// Returns slerp(Q1, Q2, t)
XMVECTOR XMQuaternionSlerp(XMVECTOR Q0, XMVECTOR Q1, FLOAT t);
旋转动画demo
接下来实现一个简单的动画demo。
我们先定义一下骨骼动画和关键帧
struct Keyframe
{
Keyframe();
~Keyframe();
float TimePos;
DirectX::XMFLOAT3 Translation;
DirectX::XMFLOAT3 Scale;
DirectX::XMFLOAT4 RotationQuat;
};
struct BoneAnimation
{
float GetStartTime()const;
float GetEndTime()const;
void Interpolate(float t, DirectX::XMFLOAT4X4& M)const;
std::vector<Keyframe> Keyframes;
};
实现骨骼动画的时候,我们在两个关键帧之间,scale和trans用线性插值,rotation用四元组的spherical插值。
骨骼动画的插值实现如下,给定时间t,输出世界矩阵M
void BoneAnimation::Interpolate(float t, XMFLOAT4X4& M)const
{
if( t <= Keyframes.front().TimePos )
{
XMVECTOR S = XMLoadFloat3(&Keyframes.front().Scale);
XMVECTOR P = XMLoadFloat3(&Keyframes.front().Translation);
XMVECTOR Q = XMLoadFloat4(&Keyframes.front().RotationQuat);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else if( t >= Keyframes.back().TimePos )
{
XMVECTOR S = XMLoadFloat3(&Keyframes.back().Scale);
XMVECTOR P = XMLoadFloat3(&Keyframes.back().Translation);
XMVECTOR Q = XMLoadFloat4(&Keyframes.back().RotationQuat);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else
{
for(UINT i = 0; i < Keyframes.size()-1; ++i)
{
if( t >= Keyframes[i].TimePos && t <= Keyframes[i+1].TimePos )
{
float lerpPercent = (t - Keyframes[i].TimePos) / (Keyframes[i+1].TimePos - Keyframes[i].TimePos);
XMVECTOR s0 = XMLoadFloat3(&Keyframes[i].Scale);
XMVECTOR s1 = XMLoadFloat3(&Keyframes[i+1].Scale);
XMVECTOR p0 = XMLoadFloat3(&Keyframes[i].Translation);
XMVECTOR p1 = XMLoadFloat3(&Keyframes[i+1].Translation);
XMVECTOR q0 = XMLoadFloat4(&Keyframes[i].RotationQuat);
XMVECTOR q1 = XMLoadFloat4(&Keyframes[i+1].RotationQuat);
XMVECTOR S = XMVectorLerp(s0, s1, lerpPercent);
XMVECTOR P = XMVectorLerp(p0, p1, lerpPercent);
XMVECTOR Q = XMQuaternionSlerp(q0, q1, lerpPercent);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
break;
}
}
}
}
再来看主程序
主程序的构造里面先定义好骨骼动画的几个关键帧。
QuatApp::QuatApp(HINSTANCE hInstance)
: D3DApp(hInstance)
{
DefineSkullAnimation();
}
void QuatApp::DefineSkullAnimation()
{
//
// Define the animation keyframes
//
XMVECTOR q0 = XMQuaternionRotationAxis(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), XMConvertToRadians(30.0f));
XMVECTOR q1 = XMQuaternionRotationAxis(XMVectorSet(1.0f, 1.0f, 2.0f, 0.0f), XMConvertToRadians(45.0f));
XMVECTOR q2 = XMQuaternionRotationAxis(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), XMConvertToRadians(-30.0f));
XMVECTOR q3 = XMQuaternionRotationAxis(XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f), XMConvertToRadians(70.0f));
mSkullAnimation.Keyframes.resize(5);
mSkullAnimation.Keyframes[0].TimePos = 0.0f;
mSkullAnimation.Keyframes[0].Translation = XMFLOAT3(-7.0f, 0.0f, 0.0f);
mSkullAnimation.Keyframes[0].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f);
XMStoreFloat4(&mSkullAnimation.Keyframes[0].RotationQuat, q0);
mSkullAnimation.Keyframes[1].TimePos = 2.0f;
mSkullAnimation.Keyframes[1].Translation = XMFLOAT3(0.0f, 2.0f, 10.0f);
mSkullAnimation.Keyframes[1].Scale = XMFLOAT3(0.5f, 0.5f, 0.5f);
XMStoreFloat4(&mSkullAnimation.Keyframes[1].RotationQuat, q1);
mSkullAnimation.Keyframes[2].TimePos = 4.0f;
mSkullAnimation.Keyframes[2].Translation = XMFLOAT3(7.0f, 0.0f, 0.0f);
mSkullAnimation.Keyframes[2].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f);
XMStoreFloat4(&mSkullAnimation.Keyframes[2].RotationQuat, q2);
mSkullAnimation.Keyframes[3].TimePos = 6.0f;
mSkullAnimation.Keyframes[3].Translation = XMFLOAT3(0.0f, 1.0f, -10.0f);
mSkullAnimation.Keyframes[3].Scale = XMFLOAT3(0.5f, 0.5f, 0.5f);
XMStoreFloat4(&mSkullAnimation.Keyframes[3].RotationQuat, q3);
mSkullAnimation.Keyframes[4].TimePos = 8.0f;
mSkullAnimation.Keyframes[4].Translation = XMFLOAT3(-7.0f, 0.0f, 0.0f);
mSkullAnimation.Keyframes[4].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f);
XMStoreFloat4(&mSkullAnimation.Keyframes[4].RotationQuat, q0);
}
然后在Update里面更新骷髅头的世界矩阵
mAnimTimePos += gt.DeltaTime();
if(mAnimTimePos >= mSkullAnimation.GetEndTime())
{
// Loop animation back to beginning.
mAnimTimePos = 0.0f;
}
mSkullAnimation.Interpolate(mAnimTimePos, mSkullWorld);
mSkullRitem->World = mSkullWorld;
mSkullRitem->NumFramesDirty = gNumFrameResources;
其他渲染的部分和以前一样。
运行结果如下: