合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
# 第十七课: 旋转 # 第十七课:旋转 虽然本课有些超出OpenGL的范围,但是解决了一个常见问题:怎样表示旋转? 《第三课:矩阵》中,我们了解到矩阵可以让点绕某个轴旋转。矩阵可以简洁地表示顶点的变换,但使用难度较大:例如,从最终结果中获取旋转轴就很麻烦。 本课将展示两种最常见的表示旋转的方法:欧拉角(Euler angles)和四元数(Quaternion)。最重要的是,本课将详细解释为何要尽量使用四元数。 ![](https://box.kancloud.cn/2015-11-02_5636f30af1552.png) ## 旋转与朝向(orientation) 阅读有关旋转的文献时,你可能会为其中的术语感到困惑。本课中: - “朝向”是状态:该物体的朝向为…… - “旋转”是操作:旋转该物体 也就是说,当实施旋转操作时,就改变了物体的朝向。 两者形式相同,因此容易混淆。闲话少叙,开始进入正题…… ## 欧拉角 欧拉角是表示朝向的最简方法,只需存储绕X、Y、Z轴旋转的角度,非常容易理解。你可以用vec3来存储一个欧拉角: ``` <pre class="calibre16">``` vec3 <span class="token3">EulerAngles</span><span class="token1">(</span> RotationAroundXInRadians<span class="token1">,</span> RotationAroundYInRadians<span class="token1">,</span> RotationAroundZInRadians<span class="token1">)</span><span class="token1">;</span> ``` ``` 这三个旋转是依次施加的,通常的顺序是:Y-Z-X(但并非一定要按照这种顺序)。顺序不同,产生的结果也不同。 一个欧拉角的简单应用就是用于设置角色的朝向。通常,游戏角色不会绕X和Z轴旋转,仅仅绕竖直的Y轴旋转。因此,无需处理三个朝向,只需用一个float型变量表示方向即可。 另外一个使用欧拉角的例子是FPS相机:用一个角度表示头部朝向(绕Y轴),一个角度表示俯仰(绕X轴)。参见`common/controls.cpp`的示例。 不过,面对更加复杂的情况时,欧拉角就显得力不从心了。例如: - 对两个朝向进行插值比较困难。简单地对X、Y、Z角度进行插值得到的结果不太理想。 - 实施多次旋转很复杂且不精确:必须计算出最终的旋转矩阵,然后据此推测书欧拉角。 - “臭名昭著”的“万向节死锁”(Gimbal Lock)问题有时会让旋转“卡死”。其他一些奇异状态还会导致模型方向翻转。 - 不同的角度可产生同样的旋转(例如-180°和180°) - 容易出错——如上所述,一般的旋转顺序是YZX,如果用了非YZX顺序的库,就有麻烦了。 - 某些操作很复杂:如绕指定的轴旋转N角度。 四元数是表示旋转的好工具,可解决上述问题。 ## 四元数 四元数由4个数\[x y z w\]构成,表示了如下的旋转: ``` <pre class="calibre16">``` <span class="token2">// RotationAngle is in radians</span> x <span class="token">=</span> RotationAxis<span class="token1">.</span>x <span class="token">*</span> <span class="token3">sin</span><span class="token1">(</span>RotationAngle <span class="token">/</span> <span class="token6">2</span><span class="token1">)</span> y <span class="token">=</span> RotationAxis<span class="token1">.</span>y <span class="token">*</span> <span class="token3">sin</span><span class="token1">(</span>RotationAngle <span class="token">/</span> <span class="token6">2</span><span class="token1">)</span> z <span class="token">=</span> RotationAxis<span class="token1">.</span>z <span class="token">*</span> <span class="token3">sin</span><span class="token1">(</span>RotationAngle <span class="token">/</span> <span class="token6">2</span><span class="token1">)</span> w <span class="token">=</span> <span class="token3">cos</span><span class="token1">(</span>RotationAngle <span class="token">/</span> <span class="token6">2</span><span class="token1">)</span> ``` ``` `RotationAxis`,顾名思义即旋转轴。`RotationAngle`是旋转的角度。 ![](https://box.kancloud.cn/2015-11-02_5636f30b242c1.png) 因此,四元数实际上存储了一个旋转轴和一个旋转角度。这让旋转的组合变简单了。 ## 解读四元数 四元数的形式当然不如欧拉角直观,不过还是能看懂的:xyz分量大致代表了各个轴上的旋转分量,而w=acos(旋转角度/2)。举个例子,假设你在调试器中看到了这样的值\[ 0.7 0 0 0.7 \]。x=0.7,比y、z的大,因此主要是在绕X轴旋转;而2\*acos(0.7) = 1.59弧度,所以旋转角度应该是90°。 同理,\[0 0 0 1\] (w=1)表示旋转角度 = 2*acos(1) = 0,因此这是一个*单位四元数\*(unit quaternion),表示没有旋转。 ## 基本操作 不必理解四元数的数学原理:这种表示方式太晦涩了,因此我们一般通过一些工具函数进行计算。如果对这些数学原理感兴趣,可以参考[实用工具和链接](http://www.opengl-tutorial.org/miscellaneous/useful-tools-links/)中的数学书籍。 ### 怎样用C++创建四元数? ``` <pre class="calibre16">``` <span class="token2">// Don't forget to #include <glm/gtc/quaternion.hpp> and <glm/gtx/quaternion.hpp></span> <span class="token2">// Creates an identity quaternion (no rotation)</span> quat MyQuaternion<span class="token1">;</span> <span class="token2">// Direct specification of the 4 components</span> <span class="token2">// You almost never use this directly</span> MyQuaternion <span class="token">=</span> <span class="token3">quat</span><span class="token1">(</span>w<span class="token1">,</span>x<span class="token1">,</span>y<span class="token1">,</span>z<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Conversion from Euler angles (in radians) to Quaternion</span> vec3 <span class="token3">EulerAngles</span><span class="token1">(</span><span class="token6">90</span><span class="token1">,</span> <span class="token6">45</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">;</span> MyQuaternion <span class="token">=</span> <span class="token3">quat</span><span class="token1">(</span>EulerAngles<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Conversion from axis-angle</span> <span class="token2">// In GLM the angle must be in degrees here, so convert it.</span> MyQuaternion <span class="token">=</span> gtx<span class="token1">:</span><span class="token1">:</span>quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">angleAxis</span><span class="token1">(</span><span class="token3">degrees</span><span class="token1">(</span>RotationAngle<span class="token1">)</span><span class="token1">,</span> RotationAxis<span class="token1">)</span><span class="token1">;</span> ``` ``` ### 怎样用GLSL创建四元数? 不要在shader中创建四元数。应该把四元数转换为旋转矩阵,用于模型矩阵中。顶点会一如既往地随着MVP矩阵的变化而旋转。 某些情况下,你可能确实需要在shader中使用四元数。例如,GPU骨骼动画。GLSL中没有四元数类型,但是可以将四元数存在vec4变量中,然后在shader中计算。 ### 怎样把四元数转换为矩阵? ``` <pre class="calibre16">``` mat4 RotationMatrix <span class="token">=</span> quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">toMat4</span><span class="token1">(</span>quaternion<span class="token1">)</span><span class="token1">;</span> ``` ``` 这下可以像往常一样建立模型矩阵了: ``` <pre class="calibre16">``` mat4 RotationMatrix <span class="token">=</span> quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">toMat4</span><span class="token1">(</span>quaternion<span class="token1">)</span><span class="token1">;</span> <span class="token1">.</span><span class="token1">.</span><span class="token1">.</span> mat4 ModelMatrix <span class="token">=</span> TranslationMatrix <span class="token">*</span> RotationMatrix <span class="token">*</span> ScaleMatrix<span class="token1">;</span> <span class="token2">// You can now use ModelMatrix to build the MVP matrix</span> ``` ``` ## 那究竟该用哪一个呢? 在欧拉角和四元数之间作选择还真不容易。欧拉角对于美工来说显得很直观,因此如果要做一款3D编辑器,请选用欧拉角。但对程序员来说,四元数却是最方便的。所以在写3D引擎内核时应该选用四元数。 一个普遍的共识是:在程序内部使用四元数,在需要和用户交互的地方就用欧拉角。 这样,在处理各种问题时,你才能得心应手(至少会轻松一点)。如果确有必要(如上文所述的FPS相机,设置角色朝向等情况),不妨就用欧拉角,附加一些转换工作。 ## 其他资源 1. [实用工具和链接](http://www.opengl-tutorial.org/miscellaneous/useful-tools-links/)中的书籍 2. 老是老了点,《游戏编程精粹1》(Game Programming Gems I)有几篇关于四元数的好文章。也许网络上就有这份资料。 1. 一个关于旋转的\[GDC报告\][http://www.essentialmath.com/GDC2012/GDC2012\_JMV\_Rotations.pdf](http://www.essentialmath.com/GDC2012/GDC2012_JMV_Rotations.pdf) 1. The Game Programing Wiki [Quaternion tutorial](http://content.gpwiki.org/index.php/OpenGL:Tutorials:Using_Quaternions_to_represent_rotation) 2. Ogre3D [FAQ on quaternions](http://www.ogre3d.org/tikiwiki/Quaternion+and+Rotation+Primer)。 第二部分大多是针对OGRE的。 3. Ogre3D [Vector3D.h](https://bitbucket.org/sinbad/ogre/src/3cbd67467fab3fef44d1b32bc42ccf4fb1ccfdd0/OgreMain/include/OgreVector3.h?at=default)和[Quaternion.cpp](https://bitbucket.org/sinbad/ogre/src/3cbd67467fab3fef44d1b32bc42ccf4fb1ccfdd0/OgreMain/src/OgreQuaternion.cpp?at=default) ## 速查手册 ## 怎样判断两个四元数是否相同? 向量点积是两向量夹角的余弦值。若该值为1,那么这两个向量同向。判断两个四元数是否相同的方法与之十分相似: ``` <pre class="calibre16">``` float matching <span class="token">=</span> quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">dot</span><span class="token1">(</span>q1<span class="token1">,</span> q2<span class="token1">)</span><span class="token1">;</span> <span class="token4">if</span> <span class="token1">(</span> <span class="token3">abs</span><span class="token1">(</span>matching<span class="token">-</span><span class="token6">1.0</span><span class="token1">)</span> <span class="token"><</span> <span class="token6">0.001</span> <span class="token1">)</span><span class="token1">{</span> <span class="token2">// q1 and q2 are similar</span> <span class="token1">}</span> ``` ``` 由点积的acos值还可以得到q1和q2间的夹角。 ## 怎样旋转一个点? 方法如下: ``` <pre class="calibre16">``` rotated_point <span class="token">=</span> orientation_quaternion <span class="token">*</span> point<span class="token1">;</span> ``` ``` ……但如果想计算模型矩阵,你得先将其转换为矩阵。注意,旋转的中心始终是原点。如果想绕别的点旋转: ``` <pre class="calibre16">``` rotated_point <span class="token">=</span> origin <span class="token">+</span> <span class="token1">(</span>orientation_quaternion <span class="token">*</span> <span class="token1">(</span>point<span class="token">-</span>origin<span class="token1">)</span><span class="token1">)</span><span class="token1">;</span> ``` ``` ## 怎样对两个四元数插值? SLERP意为球面线性插值(Spherical Linear intERPolation)、可以用GLM中的`mix`函数进行SLERP: ``` <pre class="calibre16">``` glm<span class="token1">:</span><span class="token1">:</span>quat interpolatedquat <span class="token">=</span> quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">mix</span><span class="token1">(</span>quat1<span class="token1">,</span> quat2<span class="token1">,</span> <span class="token6">0.5</span>f<span class="token1">)</span><span class="token1">;</span> <span class="token2">// or whatever factor</span> ``` ``` ## 怎样累积两个旋转? 只需将两个四元数相乘即可。顺序和矩阵乘法一致。亦即逆序相乘: ``` <pre class="calibre16">``` quat combined_rotation <span class="token">=</span> second_rotation <span class="token">*</span> first_rotation<span class="token1">;</span> ``` ``` ## 怎样计算两向量之间的旋转? (也就是说,四元数得把v1旋转到v2) 基本思路很简单: - 两向量间的夹角很好找:由点积可知其cos值。 - 旋转轴很好找:两向量的叉乘积。 如下的算法就是依照上述思路实现的,此外还处理了一些特例: ``` <pre class="calibre16">``` quat <span class="token3">RotationBetweenVectors</span><span class="token1">(</span>vec3 start<span class="token1">,</span> vec3 dest<span class="token1">)</span><span class="token1">{</span> start <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span>start<span class="token1">)</span><span class="token1">;</span> dest <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span>dest<span class="token1">)</span><span class="token1">;</span> float cosTheta <span class="token">=</span> <span class="token3">dot</span><span class="token1">(</span>start<span class="token1">,</span> dest<span class="token1">)</span><span class="token1">;</span> vec3 rotationAxis<span class="token1">;</span> <span class="token4">if</span> <span class="token1">(</span>cosTheta <span class="token"><</span> <span class="token">-</span><span class="token6">1</span> <span class="token">+</span> <span class="token6">0.001</span>f<span class="token1">)</span><span class="token1">{</span> <span class="token2">// special case when vectors in opposite directions:</span> <span class="token2">// there is no "ideal" rotation axis</span> <span class="token2">// So guess one; any will do as long as it's perpendicular to start</span> rotationAxis <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">)</span><span class="token1">,</span> start<span class="token1">)</span><span class="token1">;</span> <span class="token4">if</span> <span class="token1">(</span>gtx<span class="token1">:</span><span class="token1">:</span>norm<span class="token1">:</span><span class="token1">:</span><span class="token3">length2</span><span class="token1">(</span>rotationAxis<span class="token1">)</span> <span class="token"><</span> <span class="token6">0.01</span> <span class="token1">)</span> <span class="token2">// bad luck, they were parallel, try again!</span> rotationAxis <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">)</span><span class="token1">,</span> start<span class="token1">)</span><span class="token1">;</span> rotationAxis <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span>rotationAxis<span class="token1">)</span><span class="token1">;</span> <span class="token4">return</span> gtx<span class="token1">:</span><span class="token1">:</span>quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">angleAxis</span><span class="token1">(</span><span class="token6">180.0</span>f<span class="token1">,</span> rotationAxis<span class="token1">)</span><span class="token1">;</span> <span class="token1">}</span> rotationAxis <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span>start<span class="token1">,</span> dest<span class="token1">)</span><span class="token1">;</span> float s <span class="token">=</span> <span class="token3">sqrt</span><span class="token1">(</span> <span class="token1">(</span><span class="token6">1</span><span class="token">+</span>cosTheta<span class="token1">)</span><span class="token">*</span><span class="token6">2</span> <span class="token1">)</span><span class="token1">;</span> float invs <span class="token">=</span> <span class="token6">1</span> <span class="token">/</span> s<span class="token1">;</span> <span class="token4">return</span> <span class="token3">quat</span><span class="token1">(</span> s <span class="token">*</span> <span class="token6">0.5</span>f<span class="token1">,</span> rotationAxis<span class="token1">.</span>x <span class="token">*</span> invs<span class="token1">,</span> rotationAxis<span class="token1">.</span>y <span class="token">*</span> invs<span class="token1">,</span> rotationAxis<span class="token1">.</span>z <span class="token">*</span> invs <span class="token1">)</span><span class="token1">;</span> <span class="token1">}</span> ``` ``` (可在`common/quaternion_utils.cpp`中找到该函数) ## 我需要一个类似gluLookAt的函数。怎样旋转物体使之朝向某点? 调用`RotationBetweenVectors`函数! ``` <pre class="calibre16">``` <span class="token2">// Find the rotation between the front of the object (that we assume towards +Z,</span> <span class="token2">// but this depends on your model) and the desired direction</span> quat rot1 <span class="token">=</span> <span class="token3">RotationBetweenVectors</span><span class="token1">(</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">)</span><span class="token1">,</span> direction<span class="token1">)</span><span class="token1">;</span> ``` ``` 现在,你也许想让物体保持竖直: ``` <pre class="calibre16">``` <span class="token2">// Recompute desiredUp so that it's perpendicular to the direction</span> <span class="token2">// You can skip that part if you really want to force desiredUp</span> vec3 right <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span>direction<span class="token1">,</span> desiredUp<span class="token1">)</span><span class="token1">;</span> desiredUp <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span>right<span class="token1">,</span> direction<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Because of the 1rst rotation, the up is probably completely screwed up.</span> <span class="token2">// Find the rotation between the "up" of the rotated object, and the desired up</span> vec3 newUp <span class="token">=</span> rot1 <span class="token">*</span> <span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.0</span>f<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">)</span><span class="token1">;</span> quat rot2 <span class="token">=</span> <span class="token3">RotationBetweenVectors</span><span class="token1">(</span>newUp<span class="token1">,</span> desiredUp<span class="token1">)</span><span class="token1">;</span> ``` ``` 组合到一起: ``` <pre class="calibre16">``` quat targetOrientation <span class="token">=</span> rot2 <span class="token">*</span> rot1<span class="token1">;</span> <span class="token2">// remember, in reverse order.</span> ``` ``` 注意,“direction”仅仅是方向,并非目标位置!你可以轻松计算出方向:`targetPos – currentPos`。 得到目标朝向后,你很可能想对`startOrientation`和`targetOrientation`进行插值 (可在`common/quaternion_utils.cpp`中找到此函数。) ## 怎样使用LookAt且限制旋转速度? 基本思想是采用SLERP(用`glm::mix`函数),但要控制插值的幅度,避免角度偏大。 ``` <pre class="calibre16">``` float mixFactor <span class="token">=</span> maxAllowedAngle <span class="token">/</span> angleBetweenQuaternions<span class="token1">;</span> quat result <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span>gtc<span class="token1">:</span><span class="token1">:</span>quaternion<span class="token1">:</span><span class="token1">:</span><span class="token3">mix</span><span class="token1">(</span>q1<span class="token1">,</span> q2<span class="token1">,</span> mixFactor<span class="token1">)</span><span class="token1">;</span> ``` ``` 如下是更为复杂的实现。该实现处理了许多特例。注意,出于优化的目的,代码中并未使用`mix`函数。 ``` <pre class="calibre16">``` quat <span class="token3">RotateTowards</span><span class="token1">(</span>quat q1<span class="token1">,</span> quat q2<span class="token1">,</span> float maxAngle<span class="token1">)</span><span class="token1">{</span> <span class="token4">if</span><span class="token1">(</span> maxAngle <span class="token"><</span> <span class="token6">0.001</span>f <span class="token1">)</span><span class="token1">{</span> <span class="token2">// No rotation allowed. Prevent dividing by 0 later.</span> <span class="token4">return</span> q1<span class="token1">;</span> <span class="token1">}</span> float cosTheta <span class="token">=</span> <span class="token3">dot</span><span class="token1">(</span>q1<span class="token1">,</span> q2<span class="token1">)</span><span class="token1">;</span> <span class="token2">// q1 and q2 are already equal.</span> <span class="token2">// Force q2 just to be sure</span> <span class="token4">if</span><span class="token1">(</span>cosTheta <span class="token">></span> <span class="token6">0.9999</span>f<span class="token1">)</span><span class="token1">{</span> <span class="token4">return</span> q2<span class="token1">;</span> <span class="token1">}</span> <span class="token2">// Avoid taking the long path around the sphere</span> <span class="token4">if</span> <span class="token1">(</span>cosTheta <span class="token"><</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">{</span> q1 <span class="token">=</span> q1<span class="token">*</span><span class="token">-</span><span class="token6">1.0</span>f<span class="token1">;</span> cosTheta <span class="token">*</span><span class="token">=</span> <span class="token">-</span><span class="token6">1.0</span>f<span class="token1">;</span> <span class="token1">}</span> float angle <span class="token">=</span> <span class="token3">acos</span><span class="token1">(</span>cosTheta<span class="token1">)</span><span class="token1">;</span> <span class="token2">// If there is only a 2° difference, and we are allowed 5°,</span> <span class="token2">// then we arrived.</span> <span class="token4">if</span> <span class="token1">(</span>angle <span class="token"><</span> maxAngle<span class="token1">)</span><span class="token1">{</span> <span class="token4">return</span> q2<span class="token1">;</span> <span class="token1">}</span> float fT <span class="token">=</span> maxAngle <span class="token">/</span> angle<span class="token1">;</span> angle <span class="token">=</span> maxAngle<span class="token1">;</span> quat res <span class="token">=</span> <span class="token1">(</span><span class="token3">sin</span><span class="token1">(</span><span class="token1">(</span><span class="token6">1.0</span>f <span class="token">-</span> fT<span class="token1">)</span> <span class="token">*</span> angle<span class="token1">)</span> <span class="token">*</span> q1 <span class="token">+</span> <span class="token3">sin</span><span class="token1">(</span>fT <span class="token">*</span> angle<span class="token1">)</span> <span class="token">*</span> q2<span class="token1">)</span> <span class="token">/</span> <span class="token3">sin</span><span class="token1">(</span>angle<span class="token1">)</span><span class="token1">;</span> res <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span>res<span class="token1">)</span><span class="token1">;</span> <span class="token4">return</span> res<span class="token1">;</span> <span class="token1">}</span> ``` ``` 可以这样用`RotateTowards`函数: ``` <pre class="calibre16">``` CurrentOrientation <span class="token">=</span> <span class="token3">RotateTowards</span><span class="token1">(</span>CurrentOrientation<span class="token1">,</span> TargetOrientation<span class="token1">,</span> <span class="token6">3.14</span>f <span class="token">*</span> deltaTime <span class="token1">)</span><span class="token1">;</span> ``` ``` (可在`common/quaternion_utils.cpp`中找到此函数) ## 怎样……