# 固體對象
現在你懂得了在3D空間的一些圖層布局的基礎,我們來試著創建一個固態的3D對象(實際上是一個技術上所謂的*空洞*對象,但它以固態呈現)。我們用六個獨立的視圖來構建一個立方體的各個面。
在這個例子中,我們用Interface Builder來構建立方體的面(圖5.19),我們當然可以用代碼來寫,但是用Interface Builder的好處是可以方便的在每一個面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當把它折成一個立方體之后也不會改變這個性質。

圖5.19 用Interface Builder對立方體的六個面進行布局
這些面視圖并沒有放置在主視圖當中,而是松散地排列在根nib文件里面。我們并不關心在這個容器中如何擺放它們的位置,因為后續將會用圖層的`transform`對它們進行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們容易看清楚它們的內容,如果把它們一個疊著一個都塞進主視圖,將會變得很難看。
我們把一個有顏色的`UILabel`放置在視圖內部,是為了清楚的辨別它們之間的關系,并且`UIButton`被放置在第三個面視圖里面,后面會做簡單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結果見圖5.20
清單5.9 創建一個立方體
~~~
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
@end
~~~

圖5.20 正面朝上的立方體
從這個角度看立方體并不是很明顯;看起來只是一個方塊,為了更好地欣賞它,我們將更換一個*不同的視角*。
旋轉這個立方體將會顯得很笨重,因為我們要單獨對每個面做旋轉。另一個簡單的方案是通過調整容器視圖的`sublayerTransform`去旋轉*照相機*。
添加如下幾行去旋轉`containerView`圖層的`perspective`變換矩陣:
~~~
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
~~~
這就對相機(或者相對相機的整個場景,你也可以這么認為)繞Y軸旋轉45度,并且繞X軸旋轉45度。現在從另一個角度去觀察立方體,就能看出它的真實面貌(圖5.21)。

圖5.21 從一個邊角觀察的立方體
## 光亮和陰影
現在它看起來更像是一個立方體沒錯了,但是對每個面之間的連接還是很難分辨。Core Animation可以用3D顯示圖層,但是它對*光線*并沒有概念。如果想讓立方體看起來更加真實,需要自己做一個陰影效果。你可以通過改變每個面的背景顏色或者直接用帶光亮效果的圖片來調整。
如果需要*動態*地創建光線效果,你可以根據每個視圖的方向應用不同的alpha值做出半透明的陰影圖層,但為了計算陰影圖層的不透明度,你需要得到每個面的*正太向量*(垂直于表面的向量),然后根據一個想象的光源計算出兩個向量*叉乘*結果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
清單5.10實現了這樣一個結果,我們用GLKit框架來做向量的計算(你需要引入GLKit庫來運行代碼),每個面的`CATransform3D`都被轉換成`GLKMatrix4`,然后通過`GLKMatrix4GetMatrix3`函數得出一個3×3的*旋轉矩陣*。這個旋轉矩陣指定了圖層的方向,然后可以用它來得到正太向量的值。
結果如圖5.22所示,試著調整`LIGHT_DIRECTION`和`AMBIENT_LIGHT`的值來切換光線效果
清單5.10 對立方體的表面應用動態的光線效果
~~~
#import "ViewController.h"
#import
#import
#define LIGHT_DIRECTION 0, 1, -0.5
#define AMBIENT_LIGHT 0.5
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)applyLightingToFace:(CALayer *)face
{
//add lighting layer
CALayer *layer = [CALayer layer];
layer.frame = face.bounds;
[face addSublayer:layer];
//convert the face transform to matrix
//(GLKMatrix4 has the same structure as CATransform3D)
//譯者注:GLKMatrix4和CATransform3D內存結構一致,但坐標類型有長度區別,所以理論上應該做一次float到CGFloat的轉換,感謝[@zihuyishi](https://github.com/zihuyishi)同學~
CATransform3D transform = face.transform;
GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
//get face normal
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
//get dot product with light direction
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
float dotProduct = GLKVector3DotProduct(light, normal);
//set lighting layer opacity
CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
layer.backgroundColor = color.CGColor;
}
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
//apply lighting
[self applyLightingToFace:face.layer];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
self.containerView.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
@end
~~~

圖5.22 動態計算光線效果之后的立方體
##點擊事件
你應該能注意到現在可以在第三個表面的頂部看見按鈕了,點擊它,什么都沒發生,為什么呢?
這并不是因為iOS在3D場景下正確地處理響應事件,實際上是可以做到的。問題在于*視圖順序*。在第三章中我們簡要提到過,點擊事件的處理由視圖在父視圖中的順序決定的,并不是3D空間中的Z軸順序。當給立方體添加視圖的時候,我們實際上是按照一個順序添加,所以按照視圖/圖層順序來說,4,5,6在3的前面。
即使我們看不見4,5,6的表面(因為被1,2,3遮住了),iOS在事件響應上仍然保持之前的順序。當試圖點擊表面3上的按鈕,表面4,5,6截斷了點擊事件(取決于點擊的位置),這就和普通的2D布局在按鈕上覆蓋物體一樣。
你也許認為把`doubleSided`設置成`NO`可以解決這個問題,因為它不再渲染視圖后面的內容,但實際上并不起作用。因為背對相機而隱藏的視圖仍然會響應點擊事件(這和通過設置`hidden`屬性或者設置`alpha`為0而隱藏的視圖不同,那兩種方式將不會響應事件)。所以即使禁止了雙面渲染仍然不能解決這個問題(雖然由于性能問題,還是需要把它設置成`NO`)。
這里有幾種正確的方案:把除了表面3的其他視圖`userInteractionEnabled`屬性都設置成`NO`來禁止事件傳遞。或者簡單通過代碼把視圖3覆蓋在視圖6上。無論怎樣都可以點擊按鈕了(圖5.23)。

圖5.23 背景視圖不再阻礙按鈕,我們可以點擊它了
- 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 總結