也许你都知道四元数这么个东西,也许你还知道万向锁。但是对于弄懂它们还是不那么容易的——起码对于我就是如此了。今天是丢三落四日,我就自己来捡捡吧。——ZwqXin.com
事先声明,原理神马的,其实我也不是很懂,本文仅作抛砖引玉之用。想无误认识万向锁和四元数的朋友们请绕道回来。
本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/arithmetic/gimballock-and-quaternion.html
其实一言蔽之,因为我们对三维对象的旋转操作会出现gimballock的问题,所以求助于quaternion。但是问题的本质应该是:是具体什么操作出现问题了?是具体怎样的一个问题?四元数又有什么改进或好处了?从传统的旋转指令出发吧。
glPushMatrix();
glRotatef(m_vRotation.z,0,0,1);
glRotatef(m_vRotation.y,0,1,0);
glRotatef(m_vRotation.x,1,0,0);
DrawSomething();
glPopMatrix();
glRotate是我们熟悉得不能再熟悉的函数之一了,它看起来就是接受一个转动角度和转动轴,然后给予物件一个绕该轴的逆时针旋转。上面我们给出了绕三个轴的相应旋转的指令,按照你的3D图形学常识,Draw的物件先绕X轴转一个角度,再绕Y轴转一个角度,再绕Z轴转一个角度。现在我们把各个旋转角置0,得到物件的无旋转状态(图1):
GimbalLock万向节锁与四元数旋转图1
接下来让我们把m_vRotation.y置90,让模型绕Y轴逆转个90度,用欧拉角术语来说,就是Yaw+90。一切看上去那么自然(图2):
GimbalLock万向节锁与四元数旋转图2
接下来我们重新尝试,把m_vRotation.y置90,也把m_vRotation.z置90,让模型绕Y轴和Z轴都逆转90(Yaw+90,Roll+90),得出这样的结果(图3):
GimbalLock万向节锁与四元数旋转图3
再重新尝试,把m_vRotation.y置90,把m_vRotation.x置-90,让模型绕Y轴转90,绕X轴都顺转90(Yaw+90,Pitch-90),得出这样的结果(图4):
GimbalLock万向节锁与四元数旋转图4
居然是一样的结果。你想清楚了吗?
提醒一下,这里要考虑的是上面后两步中,都绕Y轴转了90度,但是一个是另绕X轴转一个是另绕Z轴转,但是居然结果都是同样的结果:在我们看来是都绕Z轴转了(注意这里转角的正负不是考虑的重点,即使都是正90度,它们同样是绕的Z轴旋转)。
想清楚没有?这就是所谓GimbalLock的影响哦。因为我们现在只能让物件绕Y轴转,绕Z轴转,无法绕X轴自由地转了,这模型没掉了一个选择自由度。
或许你事先在网上看了些资料,里面说“欧拉角”是导致GimbalLock的元凶——是的,我们的确在有意无意地用了欧拉角去表示旋转,就像上面括号里的文字一样。但是你确定这确实是欧拉这位数学大师给你出的难题?如果是的话,你弄清楚这个难题到何种程度?
附 欧拉角表示法:
以旅途中的平驶飞机为对象,
Yaw - 偏航角,飞机绕垂直机身的中轴左右旋转的角度;
Pitch- 俯仰角,飞机绕平行机翼方向的中轴上下摇摆的角度;
Roll - 横滚角,飞机绕行驶方向平行的中轴顺/逆时针旋动的角度。
OpenGL是不认识欧拉角的。即使是glRotate,那也就是输入任意角度和任意转轴,然后生成一个旋转矩阵。无错,请从矩阵角度再考虑一下:究竟在矩阵变换的途中出了什么状况?
如果你觉得自己对这块不很熟的话,在我颇久之前写的两篇文章[乱弹OpenGL中的矩阵变换(上)]/ [乱弹OpenGL中的矩阵变换(下)]中,也许能给予你一些许的启发吧。仔细想过后,再来看看我下面的说法有没有问题(毕竟我在此自己记录的同时也是向各位请教)?
按代码执行顺序来看,在进入模型变换后,通常给予一个单位矩阵,然后glTransalate/glRotate之类函数生成对应的矩阵,右乘到之前的这个矩阵上,一直下去直到遇到需要绘制的数据(定义在模型空间),即模型坐标系下的每一个点的坐标作为竖向量右乘到之前连串右乘操作后的结果矩阵上。这是执行顺序。在逻辑顺序上看,模型坐标系下的点坐标是被一个一个的变换矩阵左乘直至最后屏幕坐标系下的坐标的。上面最开始给出的代码同样是这么个情况:三个glRotatef生成的旋转矩阵依次左乘到模型坐标点上。——问题就出在这句话上,你是否正确理解了这句话的意思?其实这里有个歧义,只是我们通常不会被引导到歧义上,但是想清楚这个歧义反而对你的理解更有帮助:是每个旋转矩阵都左乘一次模型坐标点,还是连续地左乘至之前左乘的结果(第一个矩阵是左乘到最初的模型坐标点)上?显然是后者。想到这的同时,你脑中大概会出现这幅景象:
Mr = Mrz * Mry * Mrx * (VertexCoord)
——> Mr1 = Mrx * (VertexCoord)①; Mr2 = Mry * Mr1 ②; Mr = Mrz * Mr2 ③。
这个景象是很重要的。分解了矩阵操作为三步后,分析一下:第一步,绕X轴旋转的旋转矩阵与模型空间下的点坐标相乘,而结果呢?这个 Mr1是在哪个空间(坐标系)下的?它在一个模型变换后,但在其他模型变换之前,介于模型坐标系和世界坐标系下,是一个临时性的(temp)空间;第二步,我们的第二个旋转矩阵所操作的目标点是定义在这个空间坐标系下的,而不再是模型坐标系!同样,这一步把点转换到另一个temp空间,第三步则是把旋转效果应用到这个temp空间坐标系下表示的点坐标。
事实上,你调换一下Mrz Mry Mrx 的位置,这两个temp空间也就不一样(只要不是相同的变换矩阵,MryVertexCoord 和 MrxVertexCoord的结果就不一样)。这与相邻的glTranslate位移矩阵可以互换位置效果不变(因为最终它们只是加合到模型变换矩阵的右侧)不一样,它们的顺序决定了它们的效果。从另一个角度考虑,因为这样的矩阵不满足乘法交换律,所以即使同维数,结果都会不同。再换一个角度,顺序不变,但给予两个角度分别应用于不同的旋转矩阵,所得的结果有可能是一致的。
现在再回头看开头那个问题:在图3中,是先让物体绕自身的模型坐标系的Y轴逆转了90度,到了temp1坐标系,再绕temp1坐标系的Z轴转90度;在图4中,是先绕模型坐标系的X轴转了90度,到了一个temp2坐标系,再temp2坐标系的Y轴逆转90度。给予的旋转完全不同,只是恰巧到达同一种结果而已。这与我们的期望不同:我们希望给予不同方向的旋转,到达的结果应该不一样。(注意,因为我们都是参照世界坐标系定义旋转轴的,所以这里各空间的XYZ轴都是与世界坐标的XYZ轴重合的,只是模型在每个空间的“朝向”不一样。)
结论是,我们的旋转顺序,才是导致了GimbalLock的原因。或者就如这篇文章,Quaternion Powers所说的,第一次旋转没有改掉第二次旋转所用的转轴——我们还是根据最直观的世界坐标系下的轴来定义旋转,而我们实质通常是期望物体在自身模型坐标系下做旋转,就似飞机中的飞行员希望飞机总是按自己为参照物来作各向的旋转运动一样。当然可以在每次旋转后自己调整旋转坐标轴来纠正:譬如出问题的图4,我们在绕X轴转了90度之后,希望模型仍然针对自己的模型坐标系的Y轴来转90度的话,就置换一下参考系:现在模型在模型坐标系下的Y轴与世界坐标系下的Z轴负向一致,所以在X轴的glRotate指令上方起作用的glRotate指令,所传入的旋转轴(如前所述,它定义于世界坐标系)须是glRotatef(-90, 0, 0, 1),如图5所示:
GimbalLock万向节锁与四元数旋转
(图1基础上,绕自身坐标轴的X轴旋转90度)
GimbalLock万向节锁与四元数旋转图5
(上图基础上,绕自身坐标轴的Y轴旋转90度)
关于模型编辑工具的GimbalLock,这里有个视频,我觉得讲得蛮形象的:
真的是欧拉角导致了GimbalLock吗?当我们说让模型偏航(Yaw)XX度,俯仰YY度,侧滚ZZ度,在(0,360)范围内会有这么三个值让我们的模型无法去到那个位置?呵呵,怎么可能~怎么想得出来这三个出世的值!是的,三维空间下任何一个复杂的旋转都应该能被分解到绕三个正交轴的旋转上。那么,真的是旋转的次序导致了GimbalLock吗?根据上面的讨论,你完全可以通过矫正旋转轴,让模型旋转到任意角度。那么,究竟是什么?是什么导致了GimbalLock这种直觉下难以想象却又客观存在的现象?
是我们的观念。
无论是欧拉角导致GimbalLock,旋转次序导致GimbalLock,甚至你说矩阵相乘导致GimbalLock也好,其实都是表面的诱因。真正出了问题的,是我们的思维惯性,我们的观念。GimbalLock并不意味着客观三维世界里存在一个旋转的死角,它意味着我们思维的一个盲点。我们很少考虑到“偏航45度,俯仰54度”与“俯仰45度,偏航45度”的顺序有什么区别,很少说明这个偏航、俯仰是参照什么而言。我们看到与期望相悖的结果,发现机器转不了了,于是GimbalLock出现了。我们怪罪于它,殊不知它只是我们心中的其中一个魔鬼,而已。
这就是为什么有的人说只要订立合适的规则,就能避免GimbalLock。其中一种是限制转角,如同我们经常限制摄像机一样;一种是纠正转轴,如同上文一样;一种是控制旋转顺序,如同上述视频中,还有还有……有没有不需要这种事后订立的限定规则就能成事的方法呢?考虑本文最开始的代码清单,我们被三个glRotate的调用次序问题引向了GimbalLock,虽然当我们观念转变后能通过其他方法摆脱(譬如常说的AxisAngle这种表示法,每次旋转的转轴都明确地标明),但这是很麻烦的事,尤其转轴不再那么“正规”的情形。这时候可能会想到,有没办法一次搞掂三次的旋转呢——生成一个集合各个旋转的旋转矩阵,这样就没有次序的干扰啦。你要知道,当OpenGL代码执行到绘制部分时,其实就只是把坐标左乘一个集合矩阵罢了——次序反映在矩阵相乘的顺序(再提醒自己一次,矩阵乘法不满足统一的交换律),所以至少在旋转这里我们要摆脱矩阵相乘,才能得到无须。从观念层面摆脱"旋转顺序"的,也许要到观念之外寻觅吧。
四元数就很好。因为它不是三维世界的东西。
老实说,四元数这个东西已经完全超越了我的数感范畴了。最初是大三时候看的一本《游戏编程精粹》里接触的,只觉得强大、神奇、诡异。其实很多网络的一些人写的demo里都有这东西,还有更重要的,游戏引擎基本使用四元数来处理旋转,OGRE是熟悉的典范。
一个四元数的形式是(x,y,z,w),定义在一个封闭的四维空间。什么四维?三维世界+时间?霍金的时间简史看傻阁下了吧,请绕地自转一周回来。数学上的四维空间是基本无法用日常经验去模拟的,数学系的同学你们快自豪起来吧……什么四维空间的向量+角度,说什么我也无法就这样具化出一个四元数。你当然可以把它单纯看作数学上的四个数字,数字间和每组数字间满足一些奇怪的关系式,定义一个名字叫四元数。恩,这样感觉好点- -。
至少要明白它在三维图形学中的一个重要用途:表示一个旋转。我们的模型从一个地方到另一个地方,其实就是一个单纯的旋转,只是我们要求方便表达才分解到三个轴,但是分解后却又(由于各种限制)无法使它们同时旋转,导致GimbalLock。与其用AxisAngle去表达这个单纯旋转,Quaternion(四元数)更方便,如此而已。它的方便之处在于,它有一套成熟的转换系统,可以从无序欧拉角(即欧拉角的表示均相对于模型坐标系)转换到它,也可以由它转换到无序的欧拉角;再者,可以与AxisAngle互转;更重要的,它可以与旋转矩阵互转。不是说欧拉角和轴角就不能有类似能力,只是四元数比起它们,转换系统更成熟(更不需动太多脑筋、不需死太多脑细胞)。
我们最喜欢的旋转表示法是欧拉表示法,显卡最喜欢的旋转表示法是矩阵表示法。既然两者之间出现了那么多“沟通问题”,就需要一个很好的协调者,四元数表示法,作为转换的桥梁。三个轴的欧拉角可转成一个四元数,四元数再转成矩阵。四元数不是三维空间的东西,所以立场很好的,你们不要黑它。四元数可以表示三维的东西,这是它能与双方沟通的资本;沟通成本通常是有点大的(我指转换进转换出所须的计算量),但是因为它工作很到位,所以我们3D编程人员也应该舍得请他。请他来我们这里担任协调部主任也要先花点精力说明一下状况(我指,呃,写一个Quternion类……),这样之后我们就鲜有脑袋跳线(遭遇GimbalLock)之忧了。如果你还嫌花费大宁愿麻烦点自己动手的话,那你知道四元数表示的旋转能够作比较完美的插值计算(SLERP,Spherical Linear Interpolation)时,你还有什么好犹豫的?
好了,具体的四元数的特性,和转换公式什么的,请网上去找找吧(上文提及的一篇文章Quaternion Powers里也有一些),实在太多了(我是说精明的生意人)。这里提及几点:
我们都用单位四元数(1 = x2 + y2 + z2 + w2)来表示一个旋转或旋转位置;
别望文生义,这里x,y,z,w跟三维下的齐次坐标没啥关系;
为什么SLERP平滑插值这么重要?因为一个任意向量要转到另一个位置,除了一个坐标点的插值还有一个角度的插值,它们不是同质的,所以各自线性插值的结果是不稳定的,看上去转速不恒定。
或许之后还有补充,这里,就先到这里吧。丢三落四之日,也结束了。