2-4.创建自由式相机:使用四元数的全3D旋转
问题
你想要创建一个可以任意旋转的相机,例如飞行游戏。你需要绕三个轴旋转来做到,由于’ 万向节锁h’的限制,也不是不可能,但很难。
方案
由于万向节锁,结合多个轴的几次旋转,会导致不正确的结果。用四元数储存相机的旋转能帮你解决这个问题。
如何运作
当你结合绕不同轴的旋转,万向节锁就会发生。因为第一次的旋转把第二次旋转的轴也改变了,第三次旋转的轴被前两次旋转改变了。很难想象最后会发生什么。
旋转的特性是始终存在一个单一的旋转,它与多次旋转组合的结果相同。因此,这里需要的技巧是定义你的相机只会绕着单轴旋转。这可以是任何轴:不必是X,Y,Z之一。这个轴和旋转角度将储存在一个叫四元数的变量。
问题是你如何计算这个轴?不用。它会在处理过程中自动创建。你只需指定起始轴,每次用户移动鼠标,这个轴将更新。
你把向上向量作为起始轴,旋转0度,所以你看着正前方。接下来,有用户输入,你想相机绕向右的向量旋转。向右向量首先获取当前相机的旋转来转动,而因为这角度是0,所以保持不变。向右向量称为当前旋转轴。相机围着这个轴转,并有点仰视。
接下来用户输入要相机沿其前进轴旋转。这个轴首先沿着当前相机的旋转来旋转,现在不是0而包含绕向右轴的旋转。每个旋转结合,单轴旋转的结果和旋转的角度存储在相机的四元数。这个旋转称为相机的当前旋转。
每次用户输入,相同的程序发生:前/右/上/下向量依据相机当前旋转轴旋转,组合新旋转,当前相机存储新相机旋转到四元数,之后相机绕着新旋转轴旋转。
幸运的是,整个过程变成很简单的代码。刚开始,你要允许只绕一个单一的轴旋转(向右的轴):
float updownRotation = 0.0f;
KeyBoardState keys = Keyboard.GetState();
if(key.IsKeyDown(Keys.Up))
updownRotation = 1.5f;
if(key.IsKeyDown(Keys.Down))
updownRotation = -1.5f;
Quaternion additionalRotation = Quaternion.CreateFromAxisAngle(new Vector3(1,0,0),updownRotation);
cameraRotation = cameraRotation * additionalRotation;
首先,用户输入决定是否上下旋转。接下来,新的四元数被创建来持有这个轴和绕这个轴旋转的数量。最后,新旋转和当前旋转结合,结果存储为新旋转。
注意 四元数这字眼常使程序员害怕。我遇到了许多文章和网站把四元数比作黑暗魔法,是无法理解或抽象化的。但这不完全正确。四元数用来存储绕一个轴的旋转,所以他应该可以储存这个轴和绕着它旋转的角度。一个轴由3个数字定义,角度可以由一个数字定义。所以你要存储这样一个旋转,最简单的就是存储4个数字。猜一下四元数实际上是什么?很简单,4个数字。没什么神奇。尽管背后的数学原理比较神奇。
你想任意旋转相机,你需要可以沿着第二个轴旋转。这个旋转存储在additionalRotation变量,绕第一个轴的旋转乘以绕第二个轴的旋转:
float updownRotation = 0.0f;
float leftrightRotation = 0.0f;
KeyboardState keys = Keyboard.GetState();
if(key.IsKeyDown(Keys.Up))
updownRotation = 0.05f;
if(key.IsKeyDown(Keys.Down))
updownRotation = -0.05f;
if(key.IsKeyDown(Keys.Right))
leftrightRotation = -0.05f;
if(key.IsKeyDown(Keys.Left))
leftrightRotation = 0.05f;
Quaternin additionalRotatin =
Quaternion.CreateFromAxisAngle(new Vector3(1,0,0),updownRotation) *
Quaternion.CreateFromAxisAngle(new Vector3(0,1,0),leftrightRotation);
cameraRotation = cameraRotation * additionalRotation;
注意 四元数乘法像矩阵乘法一样重要。见4-2。你在用四元数乘法时要有这个想法。四元数的规则和矩阵乘法类似,但当两个旋转轴是互相垂直时例外,当你计算additionalRotation变量时。这意味着无论你先绕那个轴,结果都一样。但通常轴没有互相垂直。因此,你写cameraRotation * additionalRotation很重要
注意 矩阵乘法中,*意味着‘先*后’。事情本来可以很容易,在四元数中,*意味着 ‘后*先’。cameraRotation = cameraRotation * additionalRotation 应该是先additionalRotation后cameraRotation,表示轴储存在additionalRotation变量的轴要先绕着cameraRotatin变量中存储的轴旋转。
一旦旋转矩阵被找到,视图矩阵可以被构造:
private void UpdateViewMatrix()
{
Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;
Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
}
移动相机,当用户按下方向键,先旋转移动向量,把变化后的向量加上当前相机位置:
float moveSpeed = 0.05f;
Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation);
cameraPosition += moveSpeed * rotatedVector;
UpdateViewMatrix();
扩展阅读
相机跟随物体
你可以用本章代码很容易创建一个太空游戏,相机定位在航天器的驾驶舱内。但是,如果你想把相机定位在航天器后面,你需要其他代码。现在,位置和旋转对应航天器,而不是相机。相机的位置基于航天器的位置。航天器可以任意旋转。如果你把相机定位在航天器后面,相机会看着航天器。所以你已经知道视图矩阵的目标:航天器的位置。向上向量和航天器相同。航天器转,则相机也转。你要定位相机在航天器后,通常比航天器的向量后一点,比如航天器的位置加上(0,0,1)。这样就可以了。