高级动画学习心得笔记(五)变换

2017-06-13  本文已影响336人  默默_David

5.1 仿射变换

5.1.1 仿射变换基础

UIView的transform属性是一个CGAffineTransform类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform是一个可以和二维空间向量(例如CGPoint)做乘法的3X3的矩阵

用矩阵表示的CGAffineTransform和CGPoint

如图所示,通过矩形的相乘运算,得到一个新的CGPoint类型的结果。图中的灰色元素是为了矩形做乘法而添加的辅助标志值,因为他并不改变结果,所以得到的新值中不会保存它,它的用处就是做矩形相乘运算。

当对图层应用变换矩阵,图层矩形内的每一个点都被响应地做变换,从而形成一个新的四边形的形状。CGAffineTransform中的”仿射”的意思是无论变换举证用什么值,图层中平行的两条线在变换之后仍然保持平行,CGAffineTransform可以做出任意符合上述标注的变换。

由矩阵的乘法规则,可得:

x’ = a*x + c*y + tx;

y’ = b*x + d*y + ty;

(1) 让a、d=1 其余均为0,得到 x’ = x , y’ = y。这就是CGAffineTransformIdentity(未发生变换,单位矩阵)

(2) 平移: 让a、d=1,b、c=0,得到x’ = x + tx,y’ = y + ty,可以知道tx代表视图沿x轴方向的位移,ty代表视图沿y轴方向的位移,代码如下:

CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

所以平移矩阵为

平移矩阵

(3) 旋转的公式比较复杂,涉及到平面向量的旋转,这里是讲解:平面向量旋转

2D旋转的公式如下:

x’ = x*cos(a) - y*sina(a);

y’ = x*sina(a) - y*cos(a);

可以反推导出旋转的矩阵:

旋转矩阵

代码如下:

CGAffineTransformMakeRotation(CGFloat angle);

参数只有一个角度,而不是旋转公式里面的cosa,sina,原因就是不管正弦还是余弦,变化的量只是角度a,所以这里的变换函数就提供一个角度了,调用之后系统在函数里就把角度angle转化成相对应的正弦余弦值,再把计算后的矩阵(也就是CGAffineTransform结构体)返回给你了.

angle参数为弧度值,而不是角度值,弧度用数学常量PI的倍数表示,一个PI表示180°。C的数学函数库中提供了弧度的一些简便的换算,如果对换算不太清楚的话,可以用如下的宏做换算:

//将弧度值转化为角度值,参数x为弧度值

#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

//将角度值转化为弧度制,参数x为角度值

#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)

(4)放缩:让除了a、d外,其它参数都等于0,则得到放缩公式:

x’ = a*x;

y’ = d*y;

所以缩放矩阵为:

缩放矩阵

代码如下:

CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)

其它用法:

//检查是否有做过仿射效果

CGAffineTransformIsIdentity(transform)

//检查2个仿射效果是否相同

CGAffineTransformEqualToTransform(transform1,transform2)

//仿射效果反转(反效果,比如原来扩大,就变成缩小)

CGAffineTransformInvert(transform)

总结

CGAffineTransform本质就是一个结构体,这个结构体代表一个3*3的矩阵.由于矩阵第三列始终是固定的(0,0,1),所以这个结构体只有6个元素.

将一个代表2D变换的矩阵CGAffineTransform设置给view.transform,系统就会在内部让[x,y,1]和这个矩阵进行乘法运算,最终得到变换后的座标x’,y’,从而实现2D变换.

2D图像变换也称2D仿射变换,主要就是放大,缩小,平移,旋转,这几种变换的参数只涉及到矩阵的1,2列的数据,第三列始终都是0,0,1.

5.1.2 ios与安卓的transform图形变换矩阵之间的快速转换方法

首先,为了实现快速的转换,我们必须了解两者之间的异同。 iOS: IOS的Transform matrix 形式是这样的:

a    b    0

c    d    0

tx  ty  1

其中,

a :缩放X    b :错切Y    0

c :错切X    d :缩放Y    0

tx:位移X    ty:位移Y    0

当进行位图片操作的时候,假设元像素点坐标为(x,y,1) 则:

|a    b    0|

(x,y,1)*|c    d    0| =(ax + by +tx,bx + dy + ty,1)

|tx  ty  1|

当进行变换时,根据具体需求,分别改动对应矩阵的对应值,就可以实现想要的效果了,这些实现都被IOS封装到了Transform类方法之中了

Android Android的Transform matrix 形式是这样的:

a    c    tx

b    d    ty

0    0    1

其中,

a :缩放X    c :错切X    tx:位移X

b :错切Y    d :缩放Y    ty:位移Y

0          0            1

当进行位图片操作的时候,假设元像素点坐标为(x,y,1) 则:

|a    c    tx|  |x|

|b    d    ty|* |y|=(ax + by +tx,bx + dy + ty,1)

|0    0    1 |  |1|

由此不难看出,IOS和Android的Transformation Matrix 是互为转置矩阵关系。所以我们有算法:

for( int k = 0; k < 9; k++){

int n = (k % 3) * 3 + k / 3;

matrix[k] = matrix[k] + matrix[n];

matrix[n] = matrix[k] - matrix[n];

matrix[k] = matrix[k] - matrix[n];

}

来实现两者之间的转换。 使用时,将此算法嵌入到程序中即可。

5.1.3 图层的仿射变换

CALayer同样也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransform。CALayer对应于UIView的transform属性叫做affineTransform。

5.1.4 混合变换

Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个多次的变换的话,这就会非常有用。

多次变换要使用这些函数:

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)

CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)

CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操纵一个变换的时候,初始生成一个什么也不做的变换很重要,也就是创建一个CGAffineTransform类型的控制,矩阵中称作单位矩阵,Core Graphics同样也提供了一个方便的常量: CGAffineTransformIdentity

注意:在混合变换中,多次的变换如果其中的顺序改变,可能会改变最终的结果,也就是说,上一个变换的结果将会影响以后的变换,所以,在混合变换中,对顺序的控制是很重要的

5.2 3D变换

5.2.1 3D变换

CGAffineTransform类型属于Core Graphics框架,它是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效。

CALayer有一个zPosition属性,它的transform属性可以做到让图层靠近或者原理相机,即让图层在3D控件内移动或者旋转。

对一个3D像素点做CATransform3D矩阵变换

和CGAffineTransform矩阵类似,Core Animation提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,和Core Graphics的函数类似,但是3D的平移和旋转多出了一个z参数,并且旋转函数除了angle之外多出了x,y,z三个参数,分别决定了每个坐标轴方向上的旋转。


CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)

CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)

CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

我们对X轴和Y轴比较熟悉了,分别以右和下为正方向(在Mac OS上,Y轴向上为正方向),Z轴和这两个轴分别垂直,指向视图外为正方形。

X,Y,Z轴,以及围绕它们旋转的方向

由图所见,绕Z轴旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。

如果我们通过代码使得图层绕着X轴或者Y轴旋转,看起来图层并没有被旋转,而是仅仅在垂直/水平方向上产生了压缩,其实这是没错的,视图看起来更窄实际上是因为我们在用一个斜向的视角看它,而不是透视。

5.2.2 透视投影

真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边更短,但实际上并不一定,而我们当前的视角是等距离的,也就是在3D变换中仍然保持平行,和之前提到的仿射变换类似。

在等距投影中,远处的物体和近处的物体保持同样的缩放比例,这种投影也有它自己的用处,但当前我们并不需要。

为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,幸运的是,很简单:

CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34。m34用于按比例缩放X和Y的值来计算到底要离视角多远。

CATransform3D的m34元素,用来做透视

m34的默认值是0,我们可以通过设置m34为-1.0/d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。

因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它放置的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小或者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果。

5.2.3 灭点

当在透视角度绘图的时候,远离视角的物体会变小边缘,当远离到一个极限距离,他们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。

在现实中,这个点通常是视图的中心,于是为了在应用中穿件拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象的视图中点。

灭点

5.2.4 sublayerTransform属性

如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position,如果用一个函数封装这些操作的确会更加方便,但仍然有限制。

CALayer有一个属性叫做sublayerTransform,它也是CATransform3D类型,但和对一个图层的变换不同,它能影响所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。

相比较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用position和frame来设置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。

CATransform3D perspective = CATransform3DIdentity;

perspective.m34 = - 1.0 / 500.0;

self.containerView.layer.sublayerTransform = perspective;

//rotate layerView1 by 45 degrees along the Y axis

CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);

self.layerView1.layer.transform = transform1;

//rotate layerView2 by 45 degrees along the Y axis

CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);

self.layerView2.layer.transform = transform2;

通过相同的透视效果分别对视图做变换

5.2.5 背面

我们既然可以在3D场景下旋转图层,那么也可以从背面去观察它。如果我们把相对y轴的旋转角度设置为M_PI(π,180°),那么将会把图层完全旋转一个半圈,于是图层完全背对了相机视角。

视图的背面,一个镜像对称的图片

如图所示,图层是双面绘制的,反面显示的是正面的一个镜像图片。

CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制,这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机角度消失的时候,它将不会被绘制。(如果在不需要看到背面的时候,我们可以设置这个属性为NO,那么就不用浪费GPU来绘制它)

5.2.6 扁平化图层

Core Animation创建非常复杂的3D场景是非常困难的。你不能够使用图层树去创建一个3D结构的层级关系—在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父视图都把它的子视图扁平化了。

至少当你用正常的CALayer的时候是这样,CALayer有一个叫做CATransformLayer的子类来解决这个问题。

5.3 固体对象

5.3.1 固体对象

现在你懂得了在3D空间的一些图层布局的基础,我们可以试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)。我们用六个独立的视图来构建一个立方体的各个面。

Demo   

旋转这个立方体将会显得很笨重,因为我们要单独对每个面做旋转,另一个简单的方案是通过调整容器视图的sublayerTransform去旋转照相机。

perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);

perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

从一个边角观察的立方体
上一篇下一篇

猜你喜欢

热点阅读