Core Animation 三 : CALayer的仿射變換,3D變換,透視效果

什么是仿射變換
CALayer的變換和Core Graphics變換沒什么區(qū)別,本篇主要講一些關于3D變換的內(nèi)容.

3D變換的平移和縮放與平面的變換完全一樣,只是增加一個坐標軸的參數(shù),但是3D的旋轉就不一樣了,重點來看這個

CAGradientLayer *gl = [CAGradientLayer layer];
    gl.startPoint = CGPointMake(0, 0);
    gl.endPoint = CGPointMake(1, 1);
    gl.colors = @[(__bridge id)UIColor.redColor.CGColor,(__bridge id)UIColor.orangeColor.CGColor,(__bridge id)UIColor.yellowColor.CGColor];
    gl.locations = @[@0.2,@.6,@1.0];
    gl.type = kCAGradientLayerAxial;
    gl.frame = (CGRect){75,100,ScreenWidth-150, ScreenWidth-150};
    [self.view.layer addSublayer:gl];
初始狀態(tài)

3D視角下Z軸是垂直于屏幕的,不管是2d變換還是3d變換,其實z軸是一直存在的,
想象一下,2d變換的旋轉,就是Z軸位中心進行旋轉,并且是以自身的中心為旋轉中心,即layer的中心是三維坐標軸的(0,0,0),與anchorPoint錨點無關,不管錨點在哪,原點都在layer的中心.
也就是說,可以認為是圍繞一個(0,0,1)的單位向量旋轉的,當然也可以是(0,0,-1)

gl.affineTransform = CGAffineTransformRotate(gl.affineTransform, M_PI_4);
2d旋轉

所以CGAffineTransformRotate這個方法是以z軸為中心旋轉,如果不是的話,就需要增加參數(shù)了

/* Rotate 't' by 'angle' radians about the vector '(x, y, z)' and return
 * the result. If the vector has zero length the behavior is undefined:
 * t' = rotation(angle, x, y, z) * t. 

CA_EXTERN CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle,
    CGFloat x, CGFloat y, CGFloat z)
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
*/
gl.transform = CATransform3DRotate(gl.transform, -M_PI_4, 1, 0, 0);
gl.contents = (id)[UIImage imageNamed:@"avatar"].CGImage; 

所以CATransform3DRotate除了角度之外還有3個參數(shù),并且不能都為0,因為需要一個向量作為軸,上面這個就是以(1,0,0)為軸,實質就是x軸.


image.png

為了看起來方便,加了個contents,結果看起來只是變的扁了,這是因為變換本身并沒有透視效果,沒有近大遠小,當圍繞x軸旋轉時,遠離我們的一端應該看起來更小,靠近的一端應該看起來更大;

struct CATransform3D
{
  CGFloat m11, m12, m13, m14;
  CGFloat m21, m22, m23, m24;
  CGFloat m31, m32, m33, m34;
  CGFloat m41, m42, m43, m44;
};
/* The identity transform: [1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]. */

這是CATransform3D的結構和單位向量;再提一下,這個和前面的什么是仿射變換,里面略有區(qū)別,里面的齊次坐標把點定義為單列矩陣(或者說是列向量),而Apple把點定義為單行矩陣(行向量);
想象一下帶有透視的旋轉,它的投影應該是一個梯形,也就是每個點的x坐標是要發(fā)生變化的,這里直接說結論,最簡單的方法是在旋轉變換前,先修改m34的值

    CATransform3D t = gl.transform;
    t.m34 = .0015f;
    t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
    //t = (CATransform3D){t.m11,t.m12,t.m13,t.m14,t.m21,t.m22,t.m23,-0.001,t.m31,t.m32,t.m33,0.001,t.m41,t.m42,t.m43,t.m44};
    gl.transform = t;

設置了m34之后再旋轉,現(xiàn)在就有了透視效果


修改m34之后再旋轉

可以打印出變換矩陣看一看,和單位矩陣對比,發(fā)現(xiàn)有哪些元素發(fā)生了變化


沒有經(jīng)過變換的單位矩陣

旋轉矩陣

修改m34之后再旋轉的矩陣

但是這里m34的值還是有問題,后面說明

    那么為什么m34和m24會影響到變換的結果,通過矩陣乘法能看出來明明xyz的值與最右邊一列沒有關系

這又得重新說到齊次坐標,幫助理解齊次坐標
在二維變換中,齊次坐標右下角的元素經(jīng)常默認是1,而點的矩陣是(x,y,1),但是實際上它可以是任意值w,而點的矩陣變成了(x/w,y/w,1);這么一來,w直接影響到整個視圖的比例,如果設置m44是2,則相當于做了一次縮小,寬高都變成原來的1/2,在iOS中,平面的仿射變換CGAffineTransform定義為6個值,也就是忽略了齊次坐標增加的一列,這一列在iOS中默認為(0,0,1),但是CATransform3D是16個值,m44是可以不為1的;
還沒完,除了m44可以不是1之外,x和y也不是看起來的x和y,因為在3d變換中,最終的顯示效果都是光柵化的結果,也就是在xy平面的投影,當m34發(fā)生變化時,變換后的m44就不是原來的m44了,所以變換后的w也不再是原來的w(如果一開始m44是1的話,w也是1),x和y也就發(fā)生了變化.
修改m34 -> 點(x,y,1)變換 -> m44變成w -> m44改成1 -> 變換后的點(x/w,y/w,1)

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

根據(jù)上面的理解,所以即便改成y軸旋轉也是修改m34,繞z軸旋轉就是平面變換了,m34不會影響結果


繞y軸旋轉
t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);

x軸旋轉之后再y軸旋轉


image.png
  • 關于滅點
    滅點就是透視效果的視野消失點,在iOS中,它就是layer的錨點anchorPoint


    image.png

    所以修改錨點會對透視效果產(chǎn)生影響,想要平移圖形應該使用變換而不是修改錨點和position
    下面兩張圖,再沒有變換的時候,都是居中的,同樣的變換,因為anchorPoint不同效果也不同


    anchorPoint是(.5,.5)

    anchorPoint是(0,0)
  • sublayerTransform
    這個屬性是將子視圖的變換同步起來,它通常是使用在統(tǒng)一子layer的滅點
    例如,一個大layer A上有一個小的layer a,A的錨點是(0,0),a的錨點是自己的中心
    當a設置了m34然后做3d變換之后是這樣的,圖1


    圖1

    如果設置了A的sublayerTransform,然后a不再設置m34,直接做變換,是這樣的,圖2

    CATransform3D t = CATransform3DIdentity;
    A.m34 = .0015f;
    A.sublayerTransform = t;
圖2

這樣,子layer的滅點就統(tǒng)一成了父layer的滅點,如果有很多個子layer的話,所有的子layer都統(tǒng)一了透視.

此時打印a的anchorPoint,發(fā)現(xiàn)還是0.500000,0.500000,完全沒變,也就是說,設置了sublayerTransform之后,子layer的anchorPoint不再影響滅點,就可以隨意的使用anchorPoint和position或者frame來布局.

這個屬性非常強大,后面的例子會繼續(xù)說明.

  • layer的背面
    如果一個layer做3d變換,如果轉到背面去了,會發(fā)生翻轉


    image.png

    如果設置doubleSided = NO;就什么都看不到了,甚至gpu都不會去執(zhí)行繪制.

下面通過一個demo來說明上面的內(nèi)容

  • 目標是繪制一個正方體,每個面都區(qū)分開來,實現(xiàn)透視效果,滑動屏幕可以使正方體轉動
    想象一下,用6個視圖來構建正方體,想要正方體轉動,每個面都有不同的變換,及其繁瑣,這里就需要sublayerTransform出場了,6個面處在同一視覺系統(tǒng)內(nèi),有同一個滅點,正方體的立體效果就實現(xiàn)了.
    同時,當進行變換的時候,修改sublayerTransform,六個面都會發(fā)生變化;

對于這個正方體,如果想在它轉動的時候不會看起來一會兒遠一會兒近,就應該把正方體的中心放在坐標原點(0,0,0)
正方體六個面大小都是(200,200),它的大小本身不會變化,但是當設置了m34之后,z值就會影響它在視覺上的大小.
首先第一個面,離我們最近,在z軸正方向設置為100;第六個面離我們最遠,z是-100,看看效果

CATransform3D subt = CATransform3DIdentity;
subt.m34 = .0015f;
self.bottomLayer.sublayerTransform = subt;
CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
image.png

結果發(fā)現(xiàn)反而1小,6大,這里就是之前說的,想要達到合適的透視效果,m34其實應該是負值


m34改成負值

這樣就對了,不過還有一個問題,第六個面并不是簡單的往后移動100單位就行了,它其實應該是翻轉180度,假如layer1有不透明度,我們看到的應該是layer6的背面,再假如如果layer6把doubleSided設置為NO,那我們應該看不到layer6.

t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);

所以這里應該翻轉layer6


翻轉

layer6關閉doubleSided就看不到了

說明了這些問題之后,直接上代碼就可以了

@property (nonatomic, strong) CALayer *bottomLayer;
@property (nonatomic, strong) CALayer *layer1;
@property (nonatomic, strong) CALayer *layer2;
@property (nonatomic, strong) CALayer *layer3;
@property (nonatomic, strong) CALayer *layer4;
@property (nonatomic, strong) CALayer *layer5;
@property (nonatomic, strong) CALayer *layer6;
@property (nonatomic, assign) CGPoint touchPoint;


- (void)viewDidLoad {
    [super viewDidLoad];
   
    self.bottomLayer = [CALayer layer];
    self.bottomLayer.frame = CGRectMake(0, 100, ScreenWidth, ScreenWidth);
    self.bottomLayer.backgroundColor = [[UIColor hex:@"cccccc"] colorWithAlphaComponent:.7].CGColor;
    [self.view.layer addSublayer:self.bottomLayer];
    
    CATransform3D subt = CATransform3DIdentity;
    subt.m34 = -.0015f;
    self.bottomLayer.sublayerTransform = subt;
    
    CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
    t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
    self.layer6 = [self createLayer:6 color:[UIColor hex:@"A0522D"] transform:t6];
    
    CATransform3D t5 = CATransform3DMakeTranslation(-100, 0, 0);
    t5 = CATransform3DRotate(t5, -M_PI_2, 0, 1, 0);
    self.layer5 = [self createLayer:5 color:[UIColor hex:@"DAA520"] transform:t5];

    CATransform3D t4 = CATransform3DMakeTranslation(0, 100, 0);
    t4 = CATransform3DRotate(t4, -M_PI_2, 1, 0, 0);
    self.layer4 = [self createLayer:4 color:[UIColor hex:@"228B22"] transform:t4];

    CATransform3D t3 = CATransform3DMakeTranslation(0, -100, 0);
    t3 = CATransform3DRotate(t3, M_PI_2, 1, 0, 0);
    self.layer3 = [self createLayer:3 color:[UIColor hex:@"5F9EA0"] transform:t3];

    CATransform3D t2 = CATransform3DMakeTranslation(100, 0, 0);
    t2 = CATransform3DRotate(t2, M_PI_2, 0, 1, 0);
    self.layer2 = [self createLayer:2 color:[UIColor hex:@"4682B4"] transform:t2];
    
    CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
    self.layer1 = [self createLayer:1 color:[UIColor hex:@"708090"] transform:t1];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.touchPoint = CGPointZero;
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = [touches anyObject];
    CGPoint currentPoint = [touch locationInView:self.view];
    if(CGPointEqualToPoint(self.touchPoint, CGPointZero)){
        self.touchPoint = currentPoint;
    }
    CGFloat deltax = currentPoint.x - self.touchPoint.x;
    CGFloat deltay = currentPoint.y - self.touchPoint.y;
    CGFloat delta = sqrt(pow((self.touchPoint.y - currentPoint.y), 2) + pow((self.touchPoint.x - currentPoint.x), 2));
    self.touchPoint = currentPoint;
    CATransform3D subt = self.bottomLayer.sublayerTransform;
    subt = CATransform3DRotate(subt, M_PI/360.0*delta, -deltay, deltax, 0);
    self.bottomLayer.sublayerTransform = subt;
}

- (CALayer *)createLayer:(NSInteger)index color:(UIColor *)color transform:(CATransform3D)transform{
    CALayer *layer = [CALayer layer];
    CGFloat wh = 200;
    CGFloat xy = (ScreenWidth - 200)/2;
    CGRect frame = CGRectMake(xy, xy, wh, wh);
    layer.frame = frame;
    layer.backgroundColor = [color colorWithAlphaComponent:.3].CGColor;
    [self.bottomLayer addSublayer:layer];
//    layer.doubleSided = NO;
    layer.transform = transform;
    
    CATextLayer *tl = [CATextLayer layer];
    tl.contentsScale = UIScreen.mainScreen.scale;
    tl.alignmentMode = kCAAlignmentCenter;
    tl.doubleSided = NO;
    UIFont *f = [UIFont systemFontOfSize:100 weight:UIFontWeightBold];
    NSMutableAttributedString *att = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithFormat:@"%ld",index]];
    NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
    [att addAttributes:@{NSFontAttributeName:f,NSForegroundColorAttributeName:UIColor.whiteColor,NSParagraphStyleAttributeName:paragraph} range:NSMakeRange(0, att.length)];
    tl.string = att;
    tl.position = CGPointMake(wh/2, wh/2);
    tl.bounds = CGRectMake(0, 0, f.lineHeight, f.lineHeight);
    [layer addSublayer:tl];
    return layer;
}

解釋下touchesMoved里的內(nèi)容
手指滑動時,bottomLayer圍繞currentPoint和self.touchPoint組成的向量轉動;delta是手指滑動的距離
6個面通過自身的變換,在空間上形成正方體,正方體的轉動通過bottomLayer的sublayerTransform來實現(xiàn).
最終效果如下

doubleSided YES
doubleSided NO
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市把将,隨后出現(xiàn)的幾起案子躲因,更是在濱河造成了極大的恐慌珠插,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儡陨,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機瑟蜈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渣窜,“玉大人铺根,你說我怎么就攤上這事∏撬蓿” “怎么了位迂?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長详瑞。 經(jīng)常有香客問我掂林,道長,這世上最難降的妖魔是什么坝橡? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任泻帮,我火速辦了婚禮,結果婚禮上计寇,老公的妹妹穿的比我還像新娘锣杂。我一直安慰自己,他們只是感情好番宁,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布元莫。 她就那樣靜靜地躺著,像睡著了一般蝶押。 火紅的嫁衣襯著肌膚如雪踱蠢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天播聪,我揣著相機與錄音朽基,去河邊找鬼。 笑死离陶,一個胖子當著我的面吹牛稼虎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播招刨,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼霎俩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起打却,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤杉适,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后柳击,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猿推,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年捌肴,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹬叭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡状知,死狀恐怖秽五,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饥悴,我是刑警寧澤坦喘,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站西设,受9級特大地震影響瓣铣,放射性物質發(fā)生泄漏。R本人自食惡果不足惜济榨,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一坯沪、第九天 我趴在偏房一處隱蔽的房頂上張望绿映。 院中可真熱鬧擒滑,春花似錦、人聲如沸叉弦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淹冰。三九已至库车,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間樱拴,已是汗流浹背柠衍。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晶乔,地道東北人珍坊。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像正罢,于是被迫代替她去往敵國和親阵漏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容