与上一篇文章中的二维变换类似,我们可以使用一个 3*3 的矩阵来表示一个三维的线性变换:
并且矩阵 的 , 和 即为 变化后的值, , 和 即为 变化后的值, , 和 即为 变化后的值。
仿射变换的公式为:
同样的,我们可以通过引入齐次坐标来表示仿射变换,公式为:
相比其他变换,旋转变换相对的要复杂一些。首先我们要确定我们的坐标系是左手坐标系还是右手坐标系,这会影响到x,y,z三个轴的相对关系,本文我们使用右手坐标系来进行相关的运算。右手坐标系如下图,右手四指弯曲的方向代表x轴到y轴的方向,大拇指方向代表z轴方向。
我们先来看看在三维空间中绕x,y,z三个轴中的某个轴逆时针旋转 角度的矩阵是如何的
对于如下表达式我们应该如何理解
通过上一篇提到的复合变换,上面式子表达的应该是先绕x轴旋转,然后再绕y轴旋转。假设原先物体的x轴方向为(1,0,0),y轴方向为(0,1,0),z轴方向为(0,0,1),我们先将其绕x轴逆时针旋转 度,即 操作。此时物体自身的y轴(0,1,0)会跟着x轴的旋转变为(0,cos,sin),记作 。那么我们再做 操作时,旋转的y轴是原先的 y=(0,1,0) 还是 =(0,cos,sin) ?这是两种完全不一样的情况,如下图
垂直向上较长的那根绿线为 y ,而较短的绿线即为 ,我们来看看绕它们旋转的情况分别是怎样的(左图为绕y旋转,右图为绕旋转)
答案是:再做 操作时,旋转的y轴就是原先的 y=(0,1,0),即上左图的情况。因为 的旋转矩阵是根据 y=(0,1,0)的情况计算出来了,因此使用该矩阵计算时,无论物体属于一个什么角度,计算出来的结果都是根据 y=(0,1,0)这个轴进行逆时针旋转的结果(注:(0,0,0)为物体的中心点)。
我们可以称x轴方向为(1,0,0),y轴方向为(0,1,0),z轴方向为(0,0,1)的三个轴为世界坐标轴,它们不根据物体的旋转而改变。而会根据物体自身的坐标轴会根据物体的旋转而改变,例如上诉中的,我们可以称之为模型坐标轴。(自己按照unity常用的说法瞎定义了,懂原理就好。)
因此上诉的两个旋转操作都是按照世界坐标轴来进行的,这种我们称之为外旋,表达式 ,我们可以称之为 x-y外旋 操作。那么内旋就是使用模型坐标轴来做旋转操作咯,那么如果我想要实现先绕x轴旋转 度,在绕旋转后的模型坐标轴的y轴()旋转 (即 x-y内旋 操作),那么应该怎么计算呢?
首先,第一步是绕x轴旋转,此时物体还没发生旋转,因此模型坐标的x轴等于世界坐标的x轴,因此我们依旧可以使用 矩阵。接着我们要绕 =(0,cos,sin) 旋转,这里我们需要将其进行拆解(类似于上一篇中二维空间绕空间中任意一点旋转那样,拆解成先把任意点移到原点,然后旋转,然后再把改点移回去),先把 绕世界坐标的x轴旋转 度,使其与世界坐标的y轴重叠,然后使用 矩阵进行旋转 角度,最后再绕世界坐标的x轴旋转,是旋转轴回到(0,cos,sin)方向。
因此 x-y内旋操作即为:
可以发现第一第二步可以抵消,因此x-y内旋操作等价于先绕世界坐标y轴旋转度再绕世界坐标x轴旋转度,即 x-y内旋 等于 y-x外旋。
上诉过程用矩阵来表达的话,我们知道绕世界坐标x轴旋转度即为绕世界坐标x轴旋转度的逆变换,因此其矩阵即为的逆矩阵,为,因此矩阵如下
x-y内旋 = = (因为 等于单位矩阵) = y-z外旋
同理也可得出 x-z内旋等于z-x外旋,z-y内旋等于y-z内旋等等。
知道上面这些原理后,我们再进一步,来理解下面表达式
同样的,上面式子可以称之为 x-y-z外旋 操作,即先绕世界坐标的x轴旋转度,然后在绕世界坐标的y轴旋转度,最后绕世界坐标的z轴旋转度。带入可得:
那么它是否和前面一样 x-y-z外旋 等于 z-y-x内旋 呢?答案是肯定的,我们来看下推导。
z-y-x内旋,重点在于如何把z-y操作后模型坐标x轴移动到世界坐标的x轴上,通过前面我们知道 z-y 内旋等于 y-z 外旋,因此旋转后的模型坐标x轴即执行了 操作,此时的模型坐标x轴,我们标记为。因此若我们要使得变回与世界坐标的x重叠,只需要执行一下相反的操作即可,即把先绕z轴旋转度,然后在绕y轴旋转度,对应矩阵为。
因此z-y-x内旋可以分解为如下几步骤:
连起来可得公式为:
z-y-x内旋 = = () = x-y-z外旋
可得结论:所有外旋操作等于与其操作顺序相反的内旋操作。
我们一共可以得到以下六种排列组合。不同的组合得到值是不一样的,因为 等。
上面的这些旋转方式也就是我们常用的欧拉角旋转。
对于上面这些排列组合,很多会解释为每个轴对应的层级关系,例如x-y-z外旋,转动x后,y和z在之后的计算用的还是世界坐标轴,就好像x的转动和yz没有关系。y和z之间也是一样,转动y后,z还是用世界坐标轴计算,不会因为y的改变而改变。这个就很像所谓的层级关系,即z为最父层,x为最子层,y在中间层,当子层转动的时候,不会影响到父层。反过来也一样,x-y-z外旋等于z-y-x内旋,父层的z转动会影响y和x(内旋用的模型坐标轴)。
欧拉角的维基百科:https://en.wikipedia.org/wiki/Euler_angles
关于万向锁是什么,看这个视频应该就可以明白了:https://v.youku.com/v_show/id_XNzkyOTIyMTI=.html
简单来说上诉六个组合的操作,只要把中间那个操作的旋转角度设为90度的整数倍,就会触发万向锁。
我们可以从内旋的角度很好理解,以z-y-x的内旋为例子,当我们做第一步操作时,即绕模型坐标的z轴旋转度,无论怎么旋转,z轴都不会改变且等于世界坐标的z轴,但是模型坐标的x,y轴会跟着旋转变换。此时我们再做第二步操作,即绕模型坐标的y轴逆时针旋转 度,当 时,我们会发现此时的x轴会和做第一步操作时的z轴(同时也是世界坐标的z轴)处于同一条直线,方向相反。那么当我们再做第三步操作时,即绕x轴旋转,那就等于在绕世界坐标的z轴旋转,就等同于第一步操作,只不过方向相反。这就是万向锁了,即欧拉角有两个角的旋转都在绕同一个轴在旋转。
接着我们来从数学的角度分析一下:假设我们使用x-y-z外旋,对应的矩阵即为 :
此时我们把的值设为90,可得下面矩阵:
即:
最终得到:
即无论我们的和取何值, 永远等于 。
怎么理解 永远等于 呢?当我们做一次绕世界坐标y轴逆时针旋转90度时,此时(1,0,0)即会变为(0,0,1),即 等于 (=90的效果)。而永远等于就说明之后的z轴不会发生改变,什么情况下不会改变呢?那就是绕着世界坐标轴的z轴做旋转,即我们修改和的值会发现,只绕着世界坐标轴的z轴在旋转。
总结:每个内旋组合,将第二个轴的旋转角度设为90度或其整数倍时,就会触发万向锁,此时无论第一个轴或第三个轴的旋转角度为多少,都是在绕第一个轴所对应的世界坐标轴做旋转,从而失去了一个方向(第三个轴)的旋转能力。
前面我们说提到的是欧拉角旋转,若我们想要计算绕某个轴旋转,应该如何计算呢?
我们先从简单的来,假如有个轴,它在xy平面上,并且经过原点,那么绕该轴做逆时针旋转应该如何来解?首先因为这个轴在xy平面且经过原点,因此通过绕z轴旋转某个角度(设 度)即可将该轴转到原y轴的方向上,然后绕该轴的逆时针旋转即是原先绕y轴逆时针旋转,旋转完成后再绕z轴旋转 - 度即可。这点和我们前面讲欧拉角旋转时是一样的,使用公式表达即为:
合并得
上诉矩阵即为三维空间中,绕xy平面上过原点的任意轴逆时针旋转 度的矩阵,其中 为该轴与y轴的夹角。
当然了,我们一般会给定的是一个轴(x,y,z)这样,而不是一个轴与某个向量的夹角,那么假设在xy平面上的某个轴为(x,y,0),那么绕这个轴旋转度的矩阵应该是多少呢?
同样的,我们设(x,y,0)与y轴的夹角为度,那么可得: 与 ,看着很麻烦是不是,那么假设我们的(x,y,0)是单位向量呢?那么,即 ,, 所以我们第一步先把给定的轴的向量转换为单位向量。
接着我们将它们带入到上面的矩阵中可得:
再根据矩阵的加法定则,把 sin , cos,和常量 来个质壁分离,可得:
再根据矩阵和常数的乘法,我们可以把sin 和 cos 提取出来,可得到:
因为前面说过,转为了单位向量,所以: 和 ,进一步的简化为:
可以发现,上面中间的那个矩阵,和最左边的那个矩阵很相似,其实就是单位矩阵减去左边的那个矩阵:
换下位置可以得到:
单位矩阵我们标记为 ,而 其实就是 向量 和其转置 的积(该例子中,z=0):
结论:若我们要绕xy平面上的某个轴 A=(x,y,0) 旋转度(其中A为单位向量),其公式为:
前面我们是把xy平面的轴转到y上,当然我们也可以转到x上,然后使用x的旋转,得到最终结果是一样的。举一反三也可以计算绕xz平面或yz平面的轴旋转,不过也不用举了,下面我们之间来看绕xyz空间中的轴旋转的公式。
前面我们是绕 (x,y,0)旋转,最终我们想要的肯定是绕(x,y,z)的旋转公式,那么绕过原点的一个轴 A=(x,y,z) 旋转 度(其中A为单位向量),其公式应该是什么?
其实原理都是一样的,我们要先把这个轴搞到世界坐标y轴上,然后执行旋转操作,最后把这个轴归位。分如下几步操作:
可以得到如下公式:
根据三角函数可以得到 ,,,,我们设, (m*n =1),将它们带入上面公式,得到:
进一步得到:
因为m*n =1所有再简化为:
全部相乘可得:
把 sin , cos,和常量拆分出来分别可得:
cos 为:
所以
第五项带入m值得到:
第七项等价于第三项。
所以简化后的矩阵为:
结论:若我们要绕xy平面上的某个轴 A=(x,y,z) 旋转度(其中A为单位向量),其公式为:
若z=0,就是我们上面在xy平面上的轴旋转,同理y=0就是在xz平面上的轴旋转,x=0就是yz平面上的轴旋转。
前面我们的轴都是规定是过原点的,若我们想要绕不过原点的某个轴旋转,其实一样的思路,先把这个轴使用平移变换移动到原点处,然后做上面的操作,最后把这个轴移回去即可。因为是平移操作,使用矩阵时要使用齐次坐标的矩阵,推导就不多描述了,懂原理就肯定能算出来!
关于正交矩阵的定义可以查看前面矩阵相关的文章。
我们知道旋转矩阵的值即为(1,0,0),(0,1,0),(0,0,1)三个向量旋转后的值,我们假设旋转后的值分别为 ,,,可得旋转矩阵:
根据正交矩阵的定义可得 ,根据 我们可得 的值如下:
让我们来验证下,如下:
因为单位向量旋转后依旧是单位向量,因此 。同时原本互相垂直的三个轴旋转后依旧互相垂直,因此(向量点乘的定义),其他几个位置的值也同理,因此可得结果为单位矩阵,旋转矩阵为正交矩阵没有问题。
这个性质非常重要,因为旋转矩阵是正交矩阵,所以旋转变换的逆变换对应的矩阵即为旋转矩阵的转置,即 。
我们先来说说上诉两种旋转的缺点:
欧拉角旋转:
绕任意轴旋转:
因此又衍生出了一种旋转,即四元数旋转,它最大的优势就是可以实现平滑的插值,以及不会产生万向锁的问题,缺点就是理解起来比较困难。
接下来我们先来了解一下四元数是什么,以及他的一些运算法则。
一个四元数可以表示为 q = w + xi + yj + zk,其中w,x,y,z为实数,i,j,k三者为复数,即 ,因此四元数的实部为w,虚部为xi,yj和zk。
此外三个虚数还有如下关系:ij=k,ji=-k,jk=i,kj=-1,ki=j,ik=-j。这是不是和我们的坐标系叉乘一样(x轴叉乘y轴等于z轴等等)
除了上面的表达式外,四元数还有一种我们更熟悉的写法,为 q = ((x,y,z),w),其中(x,y,z)代表一个向量,即 或者写成 。
若我们空间中的一个向量(1,2,3),使用四元数表示即为 0+1i+2j+3k 或者是 ((1,2,3),0)。
因此一个向量看作为一个实部为0的四元数,而一个虚部为0的四元数即为一个实数。
类似于向量的模,四元数q的模即为:
设我们有两个四元数,分别为 和 ,那么他们相乘的结果为:
根据该式子我们也可发现 (不满足交换律)因为
根据向量点乘,可得
根据向量叉乘,可得
根据向量的数量积,可得
所有全部结合起来可得:
四元数的各部位相加即可,满足交换律和结合律:
类似于向量的点积,如下:
因此也可知:
四元数q的共轭四元数,常标记为 ,其值为 。共轭四元数即为矩阵的共轭转置。根据四元数的乘法我们可得:
由于 所以
最终可得:
一个非零四元数q的逆,常标记为 ,其值为:
了解了四元数以及四元数的运算后,我们来看看怎么用四元数来表示旋转。前面我们说过变换的本质是函数,使用四元数表达也是一样的,即从一个四元数通过一个函数变成一个新的四元数。而这个用于表达旋转的函数即为:
其中p为我们的输入向量对应的四元数, 为旋转后的向量对应的四元数,而 q 的值为 ,xyz为我们旋转轴的值(需要是单位向量,下面有解释), 为我们的旋转角度。
根据前面所知 ,我们可以求得 ,emmmm,很复杂,但是假如我们的旋转轴是单位向量呢?那么,可得 ,因此 。
例如我们的输入向量为(1,0,0),那么它对应的四元数即为((1,0,0),0),此时我们想将它绕(0,1,0)旋转90度,就可得到如下公式
通过四元数的乘法,我们来计算一下:
可得旋转后的向量为 (0,0,-1) ,结果正确!
备注:相关其他知识待后续补充
注:虽然是说图形学基础,但是个人学习的目的主要还是更深入理解Unity,因此顺便也记录一些其中与Unity相关的知识点。
在Unity中,我们在Inspector界面中设置Transform的Rotation属性,或者在代码里设置 transform.localEulerAngles ,这两种方法都是使用的欧拉角旋转。
此外我们还要注意,Unity使用的是左手坐标系而非前面我们一直所举例用的右手坐标系。
左手坐标系:
前面我们提到欧拉角旋转有6中排列组合,那么Unity使用的是哪种呢?在Transform的Rotate方法中,有如下一段注释:
The implementation of this method applies a rotation of zAngle degrees around the z axis, xAngle degrees around the x axis, and yAngle degrees around the y axis (in that order).
因此我们可以知道,Unity使用的是z-x-y外旋,也就是y-x-z内旋。
如何确定是正确的呢?我们可以创建一个Cube,然后通过按照y-x-z的顺序来调整值,这个是内旋的顺序,即每次旋转都是按照模型坐标来旋转的,因此每次旋转对应的模型旋转轴不会发生改变。如下:
既然是使用欧拉角旋转,那么同样会存着万向锁的问题,因为是z-x-y的顺序,那么我们只需要把x轴旋转90度,就可触发万向锁,如图无论怎么修改y和z,都是在绕模型坐标的x轴或者说是世界坐标的y轴在旋转。
同时我们还要注意,像图中,我们是先设置好x轴的旋转,然后再设置y或者z的旋转,这并不代表unity先旋转了x轴,再旋转其他轴。实际上无论我们怎么先后设置xyz三个轴的旋转值时,Unity始终都是要按照z-x-y的顺序来从初始位置开始旋转。这点也很好理解,因为顺序不一样的话,结果也是不一样的。
Transform中提供了Rotate和RotateAround两种方法用于绕任意轴旋转。
有如下三个参数,第一个参数为旋转轴的向量,第二个参数为旋转角度,第三个参数决定旋转轴的起点(默认为Space.Self即起点为物体的中心点,否则为世界坐标的原点)。
public void Rotate(Vector3 axis, float angle, [DefaultValue("Space.Self")] Space relativeTo)
{
if (relativeTo == Space.Self)
this.RotateAroundInternal(this.transform.TransformDirection(axis), angle * ((float) Math.PI / 180f));
else
this.RotateAroundInternal(axis, angle * ((float) Math.PI / 180f));
}
例如我们想绕过物体中心点的方向为(1,1,1)的轴旋转,使用下面方法即可
transform.Rotate(new Vector3(1, 1, 1), Time.deltaTime * 30);
效果如下:
若想自定义旋转轴的起点,则使用RotateAround方法即可
public void RotateAround(Vector3 point, Vector3 axis, float angle)
第一个参数即为旋转轴的起点,需要注意的是该起点的值为世界坐标的值,而不是相对于物体中心的偏移量。
若想绕起点在世界坐标(1,0,0)的向量(1,1,1)旋转,则代码为:
transform.RotateAround(new Vector3(1,0,0), new Vector3(1, 1, 1), Time.deltaTime * 30);
效果图如下(物体中心在世界坐标(0,0,0)上):