// 一、四元数连续变换,左右手坐标系关系 /*OGRE中用了标准乘法,但是父节点变换a和子节点变换b?他们之间的连续变换依然是用a*b进行的,每次子节点渲染时候,需要递归到根节点进行变换刚好是从根节点向下连续变换。 因为右手坐标系中顺时针四元数变换为:a-1pa,连续变换为: (ab)-1 p(ab),所以是父节点乘以子节点刚好实现连续四元数旋转变换。 记得右手坐标系顺时针变换,因为相对于左手坐标系的顺时针变换apa-1,因为左右手变换了乘法规则还是不变,a和a-1刚好是互逆的,所以到右手中就变成了a-1pa。 且在左手坐标系中逆时针旋转也是a-1pa,在右手坐标系中逆时针为:apa-1,所以对于四元数变换在标准乘法下在右手坐标系中顺时针下刚好是可以按照先后连续变换的(ab)-1p(ab)。 左手坐标系下顺时针变换如果用标准四元数连续乘法就要变为(ba) p(ba)-1。如果左手坐标系下想要获得按照先后乘法顺序就要改变标准乘法,例如: [w1,v1]*[w2,v2) = (w1*w2 - v1.v2 w1*V2 + w2*V1 + V2xV1) 才能用(ab)-1p(ab)实现连续变换。 */ // 二、四元数变换向量的多种实现 /* OGRE中的四元数变换向量的源码,用nVidia SDK,证明见:http ://www.xuebuyuan.com/2181596.html大概了解但是内部包含的数学模型有待深入,现在先验证之: Vector3 Quaternion::operator* (const Vector3& v) const { // nVidia SDK implementation Vector3 uv, uuv; Vector3 qvec(x, y, z); uv = qvec.crossProduct(v); uuv = qvec.crossProduct(uv); uv *= (2.0f * w); uuv *= 2.0f; return v + uv + uuv; } 经过测试证明和标准四元数变换向量: Vector3 Quaternion::stlChangeVector(const Vector3 &v) const { Quaternion targetQ(0.0f, v.x, v.y, v.z); Quaternion curQ(w, x, y, z); Quaternion curReverse = curQ.GetReverse(); Quaternion resQ1 = StlMultiply(StlMultiply(curQ, targetQ), curReverse); // Quaternion resQ2 = curReverse * targetQ * curQ;// 和reQ1改变乘法规则后改变变换顺序结果也是一样的。 return Vector3(resQ1.x, resQ1.y, resQ1.z); } 是一样的效果。 // 三、四元数的逆和负数的区别 逆是相反或者撤销的变换。 四元数的负数是全部取反是和正数一样的结果方位(负数是绕相同的轴相反的方向变换到相同的方位上,或者再旋转多360变换到相同方位上), 四元数角度不用限制,不同于欧拉角,但是插值时候需要注意,q和-q表示相同的方位,所以保证两个角度之间夹角(点积为正), v0到v1为正数,或者一直正数或者一直负数,反正是单调递增或者单调递减就可以了。 用标准四元数变换向量时候一定要记得,四元数的逆是唯一的,无论是变换轴,还是变换角度,都是(w, -x, -y, -z) : Quaternion Quaternion::GetReverse() { // 逆用,轴不变,旋转角度变为了负数,例如:左手顺时针60度,变为左手-60则:cos-30为正,sin-30为负,轴不变。 //return Quaternion(w, -x, -y, -z); // 逆用,轴发生了改变,旋转角度不变,例如:左手顺时针60度,轴变为左手60则:cos30为正,sin30为正,轴负。 // 还是原来一样,绝对不能够是(-w, x, y, z)这样的求逆这样是错误的 return Quaternion(w, -x, -y, -z); } */ /*四、slerp平滑旋转球面插值核心算法: 在四元数代表的向量中,将一个四元数表示为一个向量v0和v1,然后传入t属于0~1. 应该向量三角加法,和基本线性插值公式: vt = v0 * k0 + v1 * k1; 用向量点积得到两个四元数之间的夹角:cos omega 当前插值为t,那么当前对v0插值角度为t*omega, 对v1为(1-t)*omega 在球面三角形中数形结合思想,因为v0,vt,v1都是单位四元数绝对值都是1,且用基本三角形函数关系得到: k1 = sin(t * omega) / sin(omega); k0 = sin((1-t) * omega) / sin(omega); Quaternion slerp(const Quaternion &q0, const Quaternion &q1, float t){ if (t <= 0.0f) return q0; if (t >= 1.0f) return q1; // 1.点积夹角含义 // 四元数的点积可以计算两个四元数夹角cos theta/2, 该theta/2是两个四元数代表的方位真实角度的一半,因为是用真实角度的一半的 // cos和sin值来表达四元数的,四元数内部使用的角的量上面本来就减半了。 float cosOmega = dotProduct(q0, q1); float q1w = q1.w;//do not change the q1,use &q1 make fast,so pay the space. float q1x = q1.x; float q1y = q1.y; float q1z = q1.z; // 2.四元数的负数,和一个方位可以用两个四元数表示的含义 // 如果两个四元数的半角的差在[0,pi/2]或者[-pi/2,0]以外,也就是四元数表示的旋转方位差在[0,pi]以外,那么反转一个四元数, // 使得半角的差的绝对值在[0,pi/2]内。 // 具体做法是全部反转,也就是四元数的负数,且四元数的负数是再旋转一周2pi的结果(四元数的旋转是加上(2n + 1)pi得到负数,加上2npi有得到非负数), // 也可以解释为绕同一个轴逆向旋转2pi-theta的角度, // 原因是在[0,2pi]内就是cos (theta/2),sin (theta/2)变反了,也就是theta/2本来在第二象限的现在到了第四象限(或者theta/2在第三象限变到第一象限中)。 if (cosOmega < 0.0f){ q1w = -q1w; q1x = -q1x; q1y = -q1y; q1z = -q1z; cosOmega = -cosOmega; } assert(cosOmega < 1.1f); float k0, k1; // 3.普通线性插值 // 如果太小了,那么使用普通的线性插值,也就是Vt =q0 + t*(q1 - q0) = (1 - t)q0 + tq1 // 对比Vt = k0V0 + k1V1得到k0 = 1 - t , k1 = t if (cosOmega > 0.9999f){ k0 = 1.0f - t; k1 = t; } else{ // 4.sinOmega取得正数而不是负数,因为旋转角度Omega是一个两角间的夹角,无论是正向旋转还是逆向旋转四元数的theta/2都在 // [0, pi / 2]内,所以sinOmega取正数就可以了。 float sinOmega = sqrt(1.0f - cosOmega*cosOmega); float oneOverSinOmega = 1.0f / sinOmega; // atan还是区分正负的,omega是返回[-pi,pi]内的值,因为sinOmega是正数cosOmega也是正数,所以omega也会是一个正数 float omega = atan2(sinOmega, cosOmega); // k0,k1都是正数,代表一个旋转的量变,并不修改旋转的方向,其实这样也是很好的 k0 = sin((1.0f - t) * omega) * oneOverSinOmega; k1 = sin(omega * t) * oneOverSinOmega; } Quaternion result; // 5.对四元数进行线性插值结果的计算,返回线性插值的四元数 result.w = k0*q0.w + k1*q1w; result.x = k0*q0.x + k1*q1x; result.y = k0*q0.y + k1*q1y; result.z = k0*q0.z + k1*q1z; return result; } */ /*五、方位旋转之间的相互转化: 必须清楚的前提: 1.是左手坐标系还是右手坐标系,也就是旋转的那个方向为正方向。 2.旋转是从惯性坐标系到物体坐标系(欧拉角体现是hpb),还是物体坐标系到惯性坐标系(欧拉角体现是row pitch yaw)。 3.旋转的是坐标系还是物体,欧拉角旋转的是坐标系且需要用原来坐标系来表达的,每一步都是独立的,所以独立来考虑。 注意细节: 欧拉角需要区分是惯性变换到物体,还是物体变换到惯性,还有变换的是坐标系; 矩阵和四元数变换的都是物体,且都是基于上一个相对的位置变换,不用严格区分是惯性变换到物体,还是物体变换到惯性。 核心转换思想: 欧拉角旋转 变换到 矩阵和四元数,都是将欧拉角每个步骤用矩阵表示或者用四元数表示(记得欧拉角变换的是轴,还有惯物和物惯差别)。 反过来矩阵变换到欧拉角应用了方程思想直接解方程得到注意限制欧拉角和万向锁情况p为pi/2,b为0。 四元数变换到欧拉角,直接转换不容易,需要先四元数变换到矩阵,然后矩阵变换到欧拉角,得到方程关系式,然后注意限制欧拉角和万向锁。 四元数变换到矩阵,用了已知绕任意轴旋转的矩阵(左手或者右手),然后用四元数的定义在绕任意轴旋转矩阵之间通过三角形的倍角函数关系, 引入辅助量1和合并化简多项式,得到mij等于四元数w,x,y,z平方或者混乘的形式来表达。 矩阵转换为四元数,用四元数转换到矩阵的关系解方程,反向用多个矩阵元素和差合并得到四元数中的每个值,然后选择最大的用平方根求取 (虽然四元数的正负表示一个旋转,但是取了一个为正其它的都受影响所以只能取一次)其余的用对角线和差关系求取。 */
// 4)证明四元数的逆,在变换向量时候,(w,-v)和(-w,v)等价的。 // (-w1,v1) x (-w1,-v1) = (w1*w1 + v1*v1, w1*v1 -w1*v1 - v1xv1); // (w1,-v1) x (w1,v1) = (w1*w1 + v1*v1, w1v1 - w1v1 -v1xv1) Quaternion vv1(1.0, 1.0, 2.0,0); Quaternion qq1(0.866f, 0.5, .0, .0); Quaternion qq11(-0.866f, 0.5, .0, .0); Quaternion vvR1 = StlMultiply(StlMultiply(qq1, vv1), qq1.GetReverse()); Quaternion vvR2 = StlMultiply(StlMultiply(qq11, vv1), qq11.GetReverse());
故QuakeIII引擎中传入的四元数为:
Quat MD5Model::buildQuat(float x, float y, float z) const { // compute the 4th component of the quaternion float w = 1.0f - x*x - y*y - z*z; /*http://www.braynzarsoft.net/index.php?p=D3D11MD51 MD5格式定义的四元数转换的格式 Knowing this, we can compute the w component for the quaternion like this: float t = 1.0f - (x * x) - (y * y) - (z * z); if (t < 0.0f) w = 0.0f; else w = -sqrtf(t);*/ // 不是将右手坐标系的逆时针旋转变为,顺时针变换,方便从上往下乘以骨骼节点间的变换的四元数逆, 所以暂时不清楚原由? // 刚好是(-w,v)在变换向量时候等价于(w,-v)也就是负四元数。 w = w < 0.0 ? 0.0f : (float)-sqrt( double(w) ); Quat q(x, y, z, w);// 等价于w= sqrt(w)时候的q(-x, -y, -z, w) q.normalize(); return q; }