概述:主要讲述矩阵变换与图形变化的联系,逐步将矩阵应用到动画编程,重点讲述透视变换 M34;
参考文献: (3D数学基础:图形与游戏开发 -- (美)Fletcher.Dunn)
原书购买地址:请尊重原创
一. 矩阵基础
不做讨论,请参阅[3D数学基础:图形与游戏开发 -- (美)Fletcher.Dunn),视跟人基础选读 Chapter7,Chapter8,Chapter9 即可,重点在矩阵乘法(X);
二.矩阵乘法的几何意义:
矩阵乘法本质:矩阵乘法的本质是线性空间运动的描述! 具体见下图:
对于矩阵乘法,主要是考察一个矩阵对另一个矩阵所起的变换作用。其作用的矩阵看作是动作矩阵,被作用的矩阵可以看作是由行或列向量构成的几何图形。同样,如果一连串的矩阵相乘,就是多次变换的叠加么。而矩阵左乘无非是把一个向量或一组向量(即另一个矩阵)进行伸缩或旋转。乘积的效果就是多个伸缩和旋转的叠加!比如S=ABCDEF会把所有的矩阵线性变化的作用力传递并积累下去,最终得到一个和作用力S。工业上的例子就是机器人的手臂,机械臂上的每个关节就是一个矩阵(比如可以是一个旋转矩阵),机械臂末端的位置或动作是所有关节运动的综合效果。这个综合效果可以用旋转矩阵的乘法得到。
在code 的世界里,可以简单理解为一个视图A(矩阵A) 到另一个视图B(矩阵B)的映射;
三. CALayer的transform扩展解析
在iOS 开发中已经给出了部分API,我们重点关注 M34 的透视效果;
CATransform3DMakeTranslation
CATransform3DMakeScale
CATransform3DMakeRotation
绕坐标轴的旋转场景:
如图:
使用
image.layer.transform = CATransform3DMakeRotation(M_PI/6, 0, 1, 0);
绕Y轴旋转30度后的效果:
可以发现,绕Y轴旋转只是在X轴上进行了缩放,这是因为,在CALayer的显示系统中,默认的相机使用正交投影,正交投影没有远小近大效果,所以在本例中,只能造成相应轴上的缩放。在这种情况下,无论是绕Y轴旋转30度还是-30度都是同样的效果。
透视投影
CALayer默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵。所幸可以通过矩阵连乘自己构造透视投影矩阵。构造透视投影矩阵的代码如下:
CATransform3D CATransform3DMakePerspective(CGPoint center, float disZ){
CATransform3D transToCenter = CATransform3DMakeTranslation(-center.x, -center.y, 0);
CATransform3D transBack = CATransform3DMakeTranslation(center.x, center.y, 0);
CATransform3D scale = CATransform3DIdentity; scale.m34 = -1.0f/disZ;
return CATransform3DConcat(CATransform3DConcat(transToCenter, scale), transBack);
}
CATransform3D CATransform3DPerspect(CATransform3D t, CGPoint center, float disZ){
return CATransform3DConcat(t, CATransform3DMakePerspective(center, disZ));
}
代码中,center指的是相机 的位置,相机的位置是相对于要进行变换的CALayer的来说的,原点是CALayer的anchorPoint在整个CALayer的位置,例如CALayer的大小是(320, 160), anchorPoint值为(0.5, 0.5),此时anchorPoint在整个CALayer中的位置就是(160, 80),正中心的位置。传入透视变换的相机位置为(0, 0),那么相机所在的位置相对于CALayer就是(160, 80)。如果希望相机在左上角,则需要传入(-160, -80)。disZ表示的是相机离z=0平面(也可以理解为屏幕)的距离。
怎么解释这段代码呢?
- 第一步,layer在CATransform3DPerspect方法中首先进行了t变换,要注意的是这时候进行的t变换是以anchorPoint为中心点,默认情况下是在layer的中心位置。
- 第二步,在CATransform3DMakePerspective方法先进行了一个(-center.x, -center.y, 0)的平移,然后进行了透视投影,最后又做了一个(center.x, center.y, 0)平移。关键在于这两个反向的平移。 当center为(0,0)也就是相机中心在CALayer的中心位置,与anchorPoint(0.5, 0.5)重合时,平移的距离为0也就是没有做平移,这时候是直接把已经做过t变换的layer通过scale进行透视投影变换的。
当center不在中心位置时,假设在CALayer的左上角,那么center为(-160, -80)。那center就平移到了(0,0)点,等于把相机点又平移回到了与anchorPoint重合的这一点,但由于平移此时这个重合点成矩形图片的左上角了。那么为什么要平移至使相机点与anchorPoint点重合呢?
这里得明确一点,相机点与layer同时在X-Y平面上做相同的偏移时,因为没有改变z值,在相机点看到的立体效果是相同的,只是相对原点的位置变动了而已。在相机点(-160, -80)看到的立体效果,就等效于在相机点(0,0)看到的把layer平移(160, 80)的立体效果.对一个layer来说,只要没有修改anchorPoint,系统所认为的内部相机点的投影是在anchorPoint这个位置,也就是相机点的(0,0)位置。因此要看到layer在相机点(-160, -80)透视投影的效果,只能先作平移变换,让相机点与layer做相同的平移使相机点移到(0,0), 完成透视投影后再平移回去。
相应的代码为:
CATransform3D rotate = CATransform3DMakeRotation(M_PI/6, 0, 1, 0);
image.layer.transform = CATransform3DPerspect(rotate, CGPointMake(0, 0), 200);
可以发现在进行逆时针旋转30度时,在中心点左侧的图离相机点比较近,呈现出了比原图大的效果,右侧的图离相机点比较远,呈现出了比原图小的效果。对比原图,图的左边界超出了屏幕,而右边界在屏幕之内,这可以通过下面的这个图来解释:
图中PQ为旋转后的图像在X-Z平面上的投影,相机点在O点,从O点看过过,P、Q两点在X轴上的投影为C、D点,C、D两点相对P、Q点在X轴上的原始位置都是靠左的,这也就解释了旋转后的透视投影效果左边界超出了屏幕,右边界在屏幕之内。
上图中A、B两点是O点在无穷远处所看到的P、Q两点在X轴上的投影,也就是我们在第二张图片上看到的效果。这是因为在无穷远处,观察P、Q时已经失去了近大远小的效果,所做的投影是正交投影。
立方体效果
CATransform3D可以使用CATransform3DConcat函数连接起来以构造更复杂的变换, 通过这些方法,可以组合出更多的效果来。下面是个翻转的动画, 使用四张同样大小的图片围成一个框,让这个框动画旋转, 形成一个立方体旋转的效果。
相关实现的代码:
CATransform3D move = CATransform3DMakeTranslation(0, 0, 160);
CATransform3D back = CATransform3DMakeTranslation(0, 0, -160);
CATransform3D rotate0 = CATransform3DMakeRotation(-angle, 0, 1, 0);
CATransform3D rotate1 = CATransform3DMakeRotation(M_PI_2-angle, 0, 1, 0);
CATransform3D rotate2 = CATransform3DMakeRotation(M_PI_2*2-angle, 0, 1, 0);
CATransform3D rotate3 = CATransform3DMakeRotation(M_PI_2*3-angle, 0, 1, 0);
CATransform3D mat0 = CATransform3DConcat(CATransform3DConcat(move, rotate0), back);
CATransform3D mat1 = CATransform3DConcat(CATransform3DConcat(move, rotate1), back);
CATransform3D mat2 = CATransform3DConcat(CATransform3DConcat(move, rotate2), back);
CATransform3D mat3 = CATransform3DConcat(CATransform3DConcat(move, rotate3), back);
image0.layer.transform = CATransform3DPerspect(mat0, CGPointZero, 500);
image1.layer.transform = CATransform3DPerspect(mat1, CGPointZero, 500);
image2.layer.transform = CATransform3DPerspect(mat2, CGPointZero, 500);
image3.layer.transform = CATransform3DPerspect(mat3, CGPointZero, 500);
解析:要形成一个立方体旋转的效果,首先需要构造出一个立方体出来,怎么构造呢?在这个例子里构造的立方体是前后左右四个面的,如果把屏幕当做立方体的“前”面,它的“左”、“后”、“右”面我们是看不见,但是这三个面可以通过“前”面旋转一个角度得到的:以立方体的中心点为支点,将“前”面分别顺时针旋转90度、180度、270。因为屏幕宽度为320,这个立方体的中心点应在屏幕中心点后方160px的地方。
现在需要解决的一个问题就是:怎么实现以立方体的中心点为支点的旋转。我们知道,在CALayer中layer的旋转是以anchorPoint为支点的,而这个anchorPoint并没有在z轴上的维度,所以修改anchorPoint是不可能的,怎么办呢?答案还是通过平移实现,虽然不能修改anchorPoint,但我们可以改变图片的位置,将图片往z轴正方向(靠近用户的方向)平移160px的距离,这时候图片与anchorPoint的相对位置,就等同于图片在原始位置与立方体中心的相对位置,它们所进行的旋转效果是相同的,只是在z轴上的绝对距离不同。旋转完成后,再平移回去,即可达到绕立方体的中心点旋转的效果。这也是变换矩阵mat0为什么要先进行z轴正方向160px平移,执行rotate0旋转之后又进行z轴负方向160px平移的缘故。
要实现旋转动画,就需要动态改变这个立方体的绕y轴的角度,在这个例子里就是添加了一个动态变化的angle达到这个目的。另外注意此例的旋转是绕y轴旋转的,根据此前一篇文章的判断方法,此时旋转的正方向应该是z轴正方向顶点指向x轴正方向顶点,从用户眼睛看来就是逆时针。如果angle是递增的,那么-angle就是递减的,因此实际看到的旋转动画会是顺时针的
也许分析完又有了疑问,对一个layer做平移,会修改它的anchorPoint和position吗?很显然,对旋转和绽放必须要有一个固定的支点,感觉上平移不需要支点也行,是不是就会修改anchorPoint呢?答案是否定时,简单做一下测试,就知道layer在做平移时,anchorPoint和position都不会改变,frame会变化,说明frame不仅受anchorPoint和position影响,还受translation影响.