5.2 3D变换
3D变换
CG的前缀告诉我们,CGAffineTransform
类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform
仅仅对2D变换有效。
在第三章中,我们提到了zPosition
属性,可以用来让图层靠近或者远离相机(用户视角),transform
属性(CATransform3D
类型)可以真正做到这点,即让图层在3D空间内移动或者旋转。
和CGAffineTransform
类似,CATransform3D
也是一个矩阵,但是和2x3的矩阵不同,CATransform3D
是一个可以在3维空间内做变换的4x4的矩阵(图5.6)。
图5.7 X,Y,Z轴,以及围绕它们旋转的方向
由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。
举个例子:清单5.4的代码使用了CATransform3DMakeRotation
对视图内的图层绕Y轴做了45度角的旋转,我们可以把视图向右倾斜,这样会看得更清晰。
结果见图5.8,但并不像我们期待的那样。
清单5.4 绕Y轴旋转图层
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees along the Y axis
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
}
@end
图5.9 CATransform3D
的m34
元素,用来做透视
m34
的默认值是0,我们可以通过设置m34
为-1.0 / d
来应用透视效果,d
代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。
因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的防止的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小后者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果,对视图应用透视的代码见清单5.5,结果见图5.10。
清单5.5 对变换应用透视效果
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a new transform
CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;
}
@end
图5.11 灭点
Core Animation定义了这个点位于变换图层的anchorPoint
(通常位于图层中心,但也有例外,见第三章)。这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint
的位置。
当改变一个图层的position
,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34
来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position
),这样所有的3D图层都共享一个灭点。
sublayerTransform
属性
如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position
,如果用一个函数封装这些操作的确会更加方便,但仍然有限制(例如,你不能在Interface Builder中摆放视图),这里有一个更好的方法。
CALayer
有一个属性叫做sublayerTransform
。它也是CATransform3D
类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。
相较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用position
和frame
来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。
我们来用一个demo举例说明。这里用Interface Builder并排放置两个视图(图5.12),然后通过设置它们容器视图的透视变换,我们可以保证它们有相同的透视和灭点,代码见清单5.6,结果见图5.13。
图5.13 通过相同的透视效果分别对视图做变换
背面
我们既然可以在3D场景下旋转图层,那么也可以从背面去观察它。如果我们在清单5.4中把角度修改为M_PI
(180度)而不是当前的M_PI_4
(45度),那么将会把图层完全旋转一个半圈,于是完全背对了相机视角。
那么从背部看图层是什么样的呢,见图5.14
图5.15 反方向变换的嵌套图层
注意做了-45度旋转的内部图层是怎样抵消旋转45度的图层,从而恢复正常状态的。
如果内部图层相对外部图层做了相反的变换(这里是绕Z轴的旋转),那么按照逻辑这两个变换将被相互抵消。
验证一下,相应代码见清单5.7,结果见5.16
清单5.7 绕Z轴做相反的旋转变换
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *outerView;
@property (nonatomic, weak) IBOutlet UIView *innerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the outer layer 45 degrees
CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees
CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
self.innerView.layer.transform = inner;
}
@end
图5.17 绕Y轴做相反旋转的预期结果。
但其实这并不是我们所看到的,相反,我们看到的结果如图5.18所示。发什么了什么呢?内部的图层仍然向左侧旋转,并且发生了扭曲,但按道理说它应该保持正面朝上,并且显示正常的方块。
这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在同一个3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。