unity四元数学习总结--LookRotation

最近工作内容主要是四元数方面,所以在此对unity的四元数做一个总结,也防止以后自己遗忘。

在计算机图形学中,旋转的表示主要包括矩阵、四元数、欧拉角,当然还有轴角对的方式。这几种方式各有优劣,并且相互之间可以互相转化。

矩阵形式主要是计算比较方便,直接使用矩阵的乘法就可以完成旋转操作,通常情况下使用其他形式描述旋转也是转换为矩阵的形式进行计算,矩阵的缺点是需要采用九个数字记录旋转状态,占用内存过多。同时矩阵形式也不够直观。需要注意的是矩阵运算即矩阵的乘法是不可逆的,因为旋转也是不可逆的,unity世界坐标下的旋转次序是Z-X-Y;

欧拉角形式的优缺点正好和矩阵形式互补,欧拉角的优点是非常直观,直接采用三个角度的旋转角度记录旋转数据,并且占用内存空间较少,缺点是如果需要做旋转,是不可以直接使用欧拉角的运算的,一般需要转换为矩阵形式进行运算。同时由于欧拉角的旋转次序的问题,欧拉角还有一个万向节锁的问题。

四元数相对来说实现了欧拉角和矩阵的折中,四元数需要用四个元素表示旋转,同时四元数可以直接进行乘法操作,不过四元数相对来说也是不够直观,四元数可以理解为轴角对,绕某个轴旋转一定的角度,但是四个元素和旋转轴和旋转角度的关系相对复杂一些,假设旋转轴为(X,Y,Z),旋转角度为A,则对应四元数为(cos(A/2),X*SIN(A/2),Y*SIN(A/2),Z*SIN(A/2)) =(W,X,Y,Z) 。四元数还有一个好处就是可以实现旋转插值从而实现平滑旋转。

主要实现了两个函数:

(1)LookRotation 根据朝向获得四元数,使得物体面向特定的方向

(2)FromToRotation 将物体从一个特定的朝向变为另一个朝向的中间四元数

同时实现了配套的四元数与欧拉角之间的相互转换。

 

(1)LookRotation(dir)

此函数实现了根据朝向dir= (X,Y,Z)获取四元数,即从(0,0,1)转向(X,Y,Z)需要的四元数,注意旋转次序为ZXY,并且还有一个需要注意的点为此函数获取的四元数转化为欧拉角对应的Z角为0,所以在最终转化得来的欧拉角中Z为0

经过计算,假设欧拉角为(X,Y,Z),朝向为(x,y,z),朝向和欧拉角之间的换算公式为

 

X = acos(sqrt((x^2+z^2)/(x^2+y^2+z^2)))

Y= atan2(x/z)

Z= 0

需要注意角度和弧度之间的换算以及一些边界条件,同时朝向为0,0,0时unity返回的四元数为1,0,0,0,欧拉角为0,0,0。

上述公式的得来可以简单的思考为,Unity的旋转次序为Z-X-Y,由于朝向的初始位置为(0,0,1),因此对于方向向量来说Z的旋转是无所谓的,因此欧拉角Z为零,即欧拉角的Z分量不影响最后的朝向;

a、Y分量公式详解

对于Y分量,可以理解为LookRotation的逆过程即从(X,Y,Z)转至(0,0,1)时,首先绕Y轴旋转,之后绕X轴旋转,因此绕Y轴旋转需要使方向向量转至YOZ平面,即旋转之后X分量需要为0,因此tan(y) = (x/z),同时由于arcTan返回的角度范围为(-90,,90),还需要做一些边界换算,换算用程序描述如下

// x/z
            if (x == 0)
            {
                euler.y = 0;
            }
            else if (z == 0)
            {
                euler.y = x > 0 ? pi/2 : -pi/2;
            }
            else
            {
                euler.y = Atan(x / z);//返回值的范围为-pi/2~pi/2
            }
            //角度边界换算
            if (z < 0) euler.y += pi;
            else if (euler.y < 0) euler.y += pi + pi;
            euler.y = RadToDeg(euler.y);

由于公式中涉及到除以零的问题,因此最先的判断就是判断除0问题,由于朝向本身还有几何意义,因此对于0/0这种问题,即x和z均为0的时候,欧拉角中y为0或者180,因为只要X=0,方向向量位于YOZ平面,只有在旋转0或者180的时候方向向量才能位于YOZ平面,之后通过绕X轴的旋转才能使方向向量转至(0,0,1)。当X不为0Z为0时,方向向量位于XOZ平面,此时需要旋转的角度为-90或者90。

至于方向向量绕Y轴的旋转转至YOZ平面,是转至YOZ的上半平面(Z>0),还是下半平面(Z<0),经过试验,方向向量绕Y轴旋转至上半平面,因此所得X的范围是(0,90)或者(270,360)。对于角度换算,只是简单的atan,不做赘述。

b、X分量详解

对于X分量,根据上述对Y分量的介绍,可以理解,方向向量绕Y轴旋转之后Y分量不变,同时该方向向量的模值不变,因此该方向向量与Z轴的角度x满足sin(x) = (Y/SQRT(X*X+Y*Y+Z*Z))。用程序描述为:

 

 //x^2 + z^2
            float x2Pz2 = x * x + z * z;
            //x^2 + y^2+ z^2
            float x2Py2Pz2 = x2Pz2 + y * y;
            euler.x = Acos((x2Pz2 / x2Py2Pz2).Sqrt());//范围0~1,对应反三角范围为0~90
            euler.x = RadToDeg(euler.x);
            //边界换算
            if (y > 0) euler.x = 360 - euler.x;

 

主要还是注意一些边界问题,同时由于旋转方向的问题,当Y>0时,旋转所得角度大于270,需要用360减去所得角度。

(2)LookRotation四元数版本

四元数版本其实就是将上述过程中的欧拉角转换为四元数而已,由于朝向转欧拉角再转四元数中间会有一些损耗,所以做了公式化简使得能够直接从朝向即方向向量获得四元数。所得公式较为简单,主要是要求得X、Y的半角的三角函数,再利用欧拉角转四元数的公式进行转换即可

unity四元数学习总结--LookRotation_第1张图片

所用 公式如图所示,因此用代码描述为:

             //x
            float x2pz2 = dir.x * dir.x + dir.z * dir.z;
            float cosX = (x2pz2 / (x2pz2 + dir.y * dir.y)).Sqrt();
            //if (dir.y > 0) cosX = -cosX;
            float cosXDiv2 = ((cosX + 1) / 2).Sqrt();
            if (dir.y > 0) cosXDiv2 = -cosXDiv2;
            float sinXDiv2 = (1 - cosXDiv2 * cosXDiv2).Sqrt();

            //Y
            float cosY;
            if (dir.x == 0)
            {
                cosY = dir.z <0 ? -1 : 1;
            }
            else if (dir.z == 0)
            {
                cosY = 0;
            }
            else
            {
                float xDz = dir.x / dir.z;
                cosY = dir.z > 1 ? 1 / (1 + xDz * xDz).Sqrt() : -1 / (1 + xDz * xDz).Sqrt();//可能会有溢出问题
            }
            float cosYDiv2 = ((cosY + 1) / (1 + 1)).Sqrt();
            if (dir.x < 0) cosYDiv2 = -cosYDiv2;
            float sinYDiv2 = ((1 - cosY) / (1 + 1)).Sqrt();



            q1.w = cosXDiv2 * cosYDiv2;
            q1.x = sinXDiv2 * cosYDiv2;
            q1.y = cosXDiv2 * sinYDiv2;
            q1.z = -sinXDiv2 * sinYDiv2;

主要还是注意角度求半角之后所得三角函数的正负以及一些边界问题,如果有溢出的话还需要检测下溢出问题。 

(3)四元数与欧拉角之间的相互换算

通过LookRotation所得的欧拉角由于特殊,即Z分量为0,因此在做四元数与欧拉角之间的转换的时候相对较为简单。

欧拉角转四元数的公式如下所示

q1.w = cosXDiv2 * cosYDiv2;
q1.x = sinXDiv2 * cosYDiv2;
q1.y = cosXDiv2 * sinYDiv2;
q1.z = -sinXDiv2 * sinYDiv2;

四元数转欧拉角的公式为:

            //euler.x/2 = atan(x/w) or atan(-z/y)
            if (q1.w != 0)
            {
                //euler.x/2 = atan(x/w)
                euler.x = Atan2(q1.x, q1.w);
            }
            else if (q1.y != 0)
            {
                //euler.x/2 = atan(-z/y)
                euler.x = Atan2(-q1.z, q1.y);
            }
            else
            {
                //TO DO w和y同时为0
                euler.x = Atan2(q1.x, q1.w);
            }
            euler.x += euler.x;
            euler.x *= radToDeg;

            //euler.y  = atan(y/w) or atan(-z/x)
            if (q1.w != 0)
            {
                euler.y = Atan2(q1.y, q1.w);
            }
            else if (q1.x != 0)
            {
                euler.y = Atan2(-q1.z, q1.x);
            }
            else
            {
                //TO DO w和x同时为0
                euler.y = Atan2(q1.y, q1.w);
            }
            euler.y += euler.y;
            euler.y *= radToDeg;

            //范围从-180 180 映射到 0 360
            euler.x = From180To360(euler.x);
            euler.y = From180To360(euler.y);

主要还是对于欧拉角来说,

euler.x/2 = atan(x/w) or atan(-z/y)

euler.y  = atan(y/w) or atan(-z/x)

主要需要处理下除以0和边界问题,即反三角函数所得角度的范围可能和需要的角度范围是不同的

你可能感兴趣的:(unity学习,工作思考)