#3D變換
CG的前綴告訴我們,`CGAffineTransform`類型屬于Core Graphics框架,Core Graphics實際上是一個嚴格意義上的2D繪圖API,并且`CGAffineTransform`僅僅對2D變換有效。
在第三章中,我們提到了`zPosition`屬性,可以用來讓圖層靠近或者遠離相機(用戶視角),`transform`屬性(`CATransform3D`類型)可以真正做到這點,即讓圖層在3D空間內移動或者旋轉。
和`CGAffineTransform`類似,`CATransform3D`也是一個矩陣,但是和2x3的矩陣不同,`CATransform3D`是一個可以在3維空間內做變換的4x4的矩陣(圖5.6)。

圖5.6 對一個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軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標準結構,在Mac OS,Y軸朝上為正方向),Z軸和這兩個軸分別垂直,指向視角外為正方向(圖5.7)。

圖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.8 繞y軸旋轉45度的視圖
看起來圖層并沒有被旋轉,而是僅僅在水平方向上的一個壓縮,是哪里出了問題呢?
其實完全沒錯,視圖看起來更窄實際上是因為我們在用一個斜向的視角看它,而不是*透視*。
## 透視投影
在真實世界中,當物體遠離我們的時候,由于視角的原因看起來會變小,理論上說遠離我們的視圖的邊要比靠近視角的邊跟短,但實際上并沒有發生,而我們當前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當前我們并不需要。
為了做一些修正,我們需要引入*投影變換*(又稱作*z變換*)來對除了旋轉之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設置透視變換的函數,因此我們需要手動修改矩陣值,幸運的是,很簡單:
`CATransform3D`的透視效果通過一個矩陣中一個很簡單的元素來控制:`m34`。`m34`(圖5.9)用于按比例縮放X和Y的值來計算到底要離視角多遠。

圖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.10 應用透視效果之后再次對圖層做旋轉
## 滅點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。
在現實中,這個點通常是視圖的中心(圖5.11),于是為了在應用中創建擬真效果的透視,這個點應該聚在屏幕中點,或者至少是包含所有3D對象的視圖中點。

圖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.12 在一個視圖容器內并排放置兩個視圖
清單5.6 應用`sublayerTransform`
~~~
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//apply perspective transform to container
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.13 通過相同的透視效果分別對視圖做變換
## 背面
我們既然可以在3D場景下旋轉圖層,那么也可以從*背面*去觀察它。如果我們在清單5.4中把角度修改為`M_PI`(180度)而不是當前的`M_PI_4`(45度),那么將會把圖層完全旋轉一個半圈,于是完全背對了相機視角。
那么從背部看圖層是什么樣的呢,見圖5.14

圖5.14 視圖的背面,一個鏡像對稱的圖片
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個鏡像圖片。
但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,那用戶看到這些內容的鏡像圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什么浪費GPU來繪制它們呢?
`CALayer`有一個叫做`doubleSided`的屬性來控制圖層的背面是否要被繪制。這是一個`BOOL`類型,默認為`YES`,如果設置為`NO`,那么當圖層正面從相機視角消失的時候,它將不會被繪制。
## 扁平化圖層
如果對包含已經做過變換的圖層的圖層做反方向的變換將會發什么什么呢?是不是有點困惑?見圖5.15

圖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.16 旋轉后的視圖
運行結果和我們預期的一致。現在在3D情況下再試一次。修改代碼,讓內外兩個視圖繞Y軸旋轉而不是Z軸,再加上透視效果,以便我們觀察。注意不能用`sublayerTransform`屬性,因為內部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉變換
~~~
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the outer layer 45 degrees
CATransform3D outer = CATransform3DIdentity;
outer.m34 = -1.0 / 500.0;
outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees
CATransform3D inner = CATransform3DIdentity;
inner.m34 = -1.0 / 500.0;
inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
self.innerView.layer.transform = inner;
}
~~~
預期的效果應該如圖5.17所示。

圖5.17 繞Y軸做相反旋轉的預期結果。
但其實這并不是我們所看到的,相反,我們看到的結果如圖5.18所示。發什么了什么呢?內部的圖層仍然向左側旋轉,并且發生了扭曲,但按道理說它應該保持正面朝上,并且顯示正常的方塊。
這是由于盡管Core Animation圖層存在于3D空間之內,但它們并不都存在*同一個*3D空間。每個圖層的3D場景其實是扁平化的,當你從正面觀察一個圖層,看到的實際上由子圖層創建的想象出來的3D場景,但當你傾斜這個圖層,你會發現實際上這個3D場景僅僅是被繪制在圖層的表面。

圖5.18 繞Y軸做相反旋轉的真實結果
類似的,當你在玩一個3D游戲,實際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發生變化;圖層也是同樣的道理。
這使得用Core Animation創建非常復雜的3D場景變得十分困難。你不能夠使用圖層樹去創建一個3D結構的層級關系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因為每個的父視圖都把它的子視圖扁平化了。
至少當你用正常的`CALayer`的時候是這樣,`CALayer`有一個叫做`CATransformLayer`的子類來解決這個問題。具體在第六章“特殊的圖層”中將會具體討論。
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結