一直都说用欧拉角做旋转会出现万向节锁Bug(Gimble Lock),而用四元数就不会。其实这样的说法是不准确的,当用四元数做旋转,如果使用姿势错误,依然会出现Gimble Lock。
错误的用法
下面是我自己录制的演示视频,当x轴转90度,y轴和z轴就合二为一了:
演示代码:
Transform4x4 trans1 = Translate(Vector3dF(0.0, 0.0, -2.0));
Transform4x4 trans2 = Scale(0.5, 0.5, 0.5);
static float pitch = 0.0, yaw = 0.0, roll = 0.0;
pitch = 90.0f;
yaw += 2.0f;
roll += 3.0f;
QuaternionF rotX = QuaternionF::RotateX(pitch); // x
QuaternionF rotY = QuaternionF::RotateY(yaw); // y
QuaternionF rotZ = QuaternionF::RotateZ(roll); // z
QuaternionF rot = rotZ * rotX * rotY;
Transform4x4 modelTrans = trans1 * trans2 * Transform4x4(rot.toMatrix4x4());
pitch即x轴,pitch到90度时,yaw和roll转的是同一条轴。这和欧拉角的情况没有区别。
原因在于要理解Gimble Lock问题的本质。只要你是利用坐标系3个正交基x、y、z去做转换,当其中一个基旋转了90度时,另外的2个基的其中之一会跟着旋转90度,然后和第三个基合并,那么对这合并的2个基做旋转,肯定都是一个效果。换句话说就是丢失一个自由度。
正确的旋转机制是,不要用3个互相关联的变量来表示物体的旋转,例如pitch、yaw、roll;而是用一个变量表示,例如用唯一一个四元数或旋转矩阵(旋转矩阵也可以由一个四元数导出)来表示物体的朝向,则是可行的。上面的例子虽然也用了四元数,但是错就错在用了三个四元数,所以就出bug了。
下面是正确的四元数转换代码:
const float pitch = 1.0f, yaw = 2.0f, roll = 3.0f;
static QuaternionF orientation = {1.0, 0.0, 0.0, 0.0};
QuaternionF rotX = QuaternionF::RotateX(pitch); // x
QuaternionF rotY = QuaternionF::RotateY(yaw); // y
QuaternionF rotZ = QuaternionF::RotateZ(roll); // z
QuaternionF quatDiff = rotX * rotY * rotZ;
orientation *= quatDiff;
orientation = orientation.Normalize();
Transform4x4 modelTrans = trans1 * trans2 * Transform4x4(orientation.toMatrix4x4());
注意这段代码里依然出现了pitch、yaw、roll。为什么呢?这是因为旋转的数学描述用正交基表示依然是最人性化的。这段代码之所以可行是在于用了一个新的变量orientation,来表示物体的当前朝向。每次迭代更新朝向时,先把pitch、yaw、roll转成Quaternion,然后相乘,从而得到这一帧的朝向改变量quatDiff,最后把quatDiff应用到orientation里,就改变了当前朝向了。
最后要注意的是,orientation每次改变后都要重新规范化(Normalize),否则旋转效果会和预期的不一样。
写作不易,您的支持是我写作的动力!