首页 >Swift

如何使用 CATransform3D 处理 3D 影像、制做互动立体旋转的效果 ?

2017-05-18 10:34 编辑: sasukeo 分类:Swift 来源:APPCODA(台湾)

今天让我们讨论一下在iOS App的立体图像处理,而要讲这题目就不可不讲解 –CATransform3D。CATransform3D 是一个用来处理 3D 影像,像是旋转、缩放、平移等 3D 影像的控制。CATransform3D 是采用三维坐标系统,x 轴向下为正,y 向右为正,z 轴则是垂直于屏幕向外为正。

你可以这样理解CATransform3D ,它本身就是一个 4×4 的矩阵:

Matrix.png

在这边,我们不需要了解矩阵中每一个数字是什么意思,因为在 CATransform3D 中已经有方法可以处理大部分的功能:

? CATransform3DTranslate

? CATransform3DRotate

? CATransform3DInvert

? CATransform3DScale

? CATransform3DAffine

如果只说道理,相信很难理解CATransform3D的运作,最好还是实作一个简单的范例程序以阐释当中的原理。在这篇,我要教大家如何做出一个可以被转动的骰子,下图就是完成品:

 Dice.gif

开始建立范例App

如常开启Xcode并使用Single View Application建立一个新项目。在Main.storyboard,先在View Controller 做一个简单的 View,将背景颜色转为蓝色(或其他颜色也可以)。

rotatedice-storyboard-1024x657.png

在 ViewController 加入IBOutlet,并将之连接至新建立的View:

@IBOutlet weak var blueView: UIView!

如何将View变得立体

现在要加入一个方法让 View (即blueView) 以 y 轴为中心旋转 45度,CATransform3D 的方法都是输入一个矩阵去改变内容。以 CATransform3DRotate 为例,第一个参数是定义好的矩阵,之后再用角度与 x、y、z 所形成的向量做旋转:

func viewTransform() {
    var transform = CATransform3DIdentity
    let angle = CGFloat(45)
    blueView.layer.transform = CATransform3DRotate(transform, angle, 0, 1, 0)
}

之后,再在viewDidLoad()方法呼叫viewTransform():

override func viewDidLoad() {
    super.viewDidLoad()
    
    viewTransform()
}

现在可先试试执行程序,你会发觉那个View实际上旋转之后并没有 3D 的感觉,只是变瘦的 View 。

 View2.png

在真实世界中,当物体远离我们时,由于视角的关系会看起来变小,理论上来说,比较远的边会比近的边要短,但是实际上对系统来说他们是一样长的,所以我们要做一些修正。透过设置 CATransform3D 的 m34 为 -1.0 / d 来让影像有远近的 3D 效果,d 代表了想象中视角与屏幕的距离,这个距离只需要大概估算,不需要很精准的计算。

解说:m34 用于按比例缩放 x 和 y 的值来呈现视角的远近。

现在修改viewTransform()并加入一行程序代码以设置m34:

func viewTransform() {
    var transform = CATransform3DIdentity
    let angle = CGFloat(45)
    transform.m34  = -1 / 500
    blueView.layer.transform = CATransform3DRotate(transform, angle, 0, 1, 0)
}

完成后,再试试执行App看看效果如何。现在那个应该有一种立体的感觉:

 View3.png

手势控制

接下来我们要改为手势控制这个 View 的角度,在ViewController类别先加一个angle变量:

angle = CGPoint.init(x: 0, y: 0)

之后,修改viewTransform方法:

func viewTransform(sender: UIPanGestureRecognizer) {
    
    let point = sender.translation(in: blueView)
    let angleX = angle.x + (point.x/30)
    let angleY = angle.y - (point.y/30)
    
    var transform = CATransform3DIdentity
    transform.m34 = -1 / 500
    transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
    transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
    blueView.layer.transform = transform
    
    if sender.state == .ended {
        angle.x = angleX
        angle.y = angleY
    }
}

另外,因为我们会用UIPanGestureRecognizer来识别手势,viewDidLoad也作出相应的修改:

override func viewDidLoad() {
    super.viewDidLoad()
   
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
    blueView.addGestureRecognizer(panGesture)
}

在这边用 CATransform3DRotate 的方法对 X 轴与 Y 轴做转动,以手势在 View 上每次移动的相对坐标为基准(即是sender.translation(in: blueView)),直接去更改整个 View 的翻转角度。

又再试一试执行App,尝试用鼠标转动View,你会发现非常失控!

dice-rotation-crazy.gif

因为判定旋转角度的依据,就是手势在这个 View 上面移动的位置,而 View 在转动的时候坐标会随着旋转而不停的变动,以至于手势无法准确的控制,所以要拿一个不会动的对象当作基准,并且不能因为转动而改变可控的面积,解决办法如下(改动的程序代码以黄色标示):

override func viewDidLoad() {
    super.viewDidLoad()
    
    let subView = UIView.init(frame: blueView.bounds)
    subView.backgroundColor = UIColor.blue
    blueView.addSubview(subView)
        blueView.backgroundColor = UIColor.clear
    
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
    blueView.addGestureRecognizer(panGesture)
}
 
func viewTransform(sender: UIPanGestureRecognizer) {
    
    let point = sender.translation(in: blueView)
    let angleX = angle.x + (point.x/30)
    let angleY = angle.y - (point.y/30)
    
    var transform = CATransform3DIdentity
    transform.m34 = -1 / 500
    transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
    transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
    blueView.layer.sublayerTransform = transform
    
    if sender.state == .ended {
        angle.x = angleX
        angle.y = angleY
    }
}

View 需要用手势移动当作依据,所以不直接对这个 View 做旋转,而是旋转 View 里面的 sublayer,layer 里面的有个方法可以实作这个功能 sublayerTransform ,并把内容以 subView 的方式加入,然后把 blueView 的 backgroundColor 拿掉,这样就能很正常的转动了。

FirstRotate.gif

将普通的View变成骰子

接下来,就是把这个 blueView 换成骰子。然而骰子并不是平面,是一个立体的对象,那要如何在平面上做出一个立体的对象?在这边要利用 CATransform3DTranslate 与 CATransform3DRotate 来做出立体对象的效果,首先做骰子的 1、2、3 点。

现在先下载骰子的图像,并将所有的图加进Xcode项目。我们会先把 View 修改成 1 点,顺便修改 View 的名称。先将原本的blueView从storyboard 删除,之后在ViewController建立diceView:

let diceView = UIView()

将以下viewDidLoad以及viewTransform方法更新至如下,同时加入一个新方法addDice:

override func viewDidLoad() {
    super.viewDidLoad()
 
    addDice()
    
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
    diceView.addGestureRecognizer(panGesture)
}
 
func addDice() {
    
    let viewFrame = UIScreen.main.bounds
 
    diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
    
    //1
    let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
    dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    
    diceView.addSubview(dice1)
    
    view.addSubview(diceView)
}
 
func viewTransform(sender: UIPanGestureRecognizer) {
    
    let point = sender.translation(in: diceView)
    let angleX = angle.x + (point.x/30)
    let angleY = angle.y - (point.y/30)
    
    var transform = CATransform3DIdentity
    transform.m34 = -1 / 500
    transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
    transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
    diceView.layer.sublayerTransform = transform
    
    if sender.state == .ended {
        angle.x = angleX
        angle.y = angleY
    }
}

我们在addDice建立相等于blue view的view并同时加载1点的图像,如你试执行程序,应该会得到以下的效果:

OneRotate.gif

加入骰子第 2、3 点,并分别垂直于 1 点:

func addDice() {
    
    let viewFrame = UIScreen.main.bounds
    
    var diceTransform = CATransform3DIdentity
    
    diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
    
    //1
    let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
    dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    
    //2
    let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
    dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
    dice2.layer.transform = diceTransform
    
    //3
    let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
    dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
    dice3.layer.transform = diceTransform
    
    diceView.addSubview(dice1)
    diceView.addSubview(dice2)
    diceView.addSubview(dice3)
    
    view.addSubview(diceView)
}

又再试一试效果,看来不错!但还是不太像一颗骰子。

 3SideOrigin.gif

要解决这问题,我们就要平移每一个 imageView。CATransform3DRotate 不是只有转了影像,而是整个坐标系统,所以只需要每个面的 z 轴都增加(减少) 50 就能够做一半的正方体:

func addDice() {
    
    let viewFrame = UIScreen.main.bounds
    
    var diceTransform = CATransform3DIdentity
    
    diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
    
    //1
    let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
    dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice1.layer.transform = diceTransform
    
    //2
    let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
    dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice2.layer.transform = diceTransform
    
    //3
    let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
    dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice3.layer.transform = diceTransform
    
    diceView.addSubview(dice1)
    diceView.addSubview(dice2)
    diceView.addSubview(dice3)
    
    view.addSubview(diceView)
}

现在再试试执行程序,应该有了三个面!

3Side.gif

剩下的就是把三个对应的面加进去,就能完成一颗骰子:

func addDice() {
    
    let viewFrame = UIScreen.main.bounds
    
    var diceTransform = CATransform3DIdentity
    
    diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
    
    //1
    let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
    dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice1.layer.transform = diceTransform
    
    //6
    let dice6 = UIImageView.init(image: UIImage(named: "dice6"))
    dice6.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DTranslate(CATransform3DIdentity, 0, 0, -50)
    dice6.layer.transform = diceTransform
    
    //2
    let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
    dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice2.layer.transform = diceTransform
    
    //5
    let dice5 = UIImageView.init(image: UIImage(named: "dice5"))
    dice5.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 0, 1, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice5.layer.transform = diceTransform
    
    //3
    let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
    dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice3.layer.transform = diceTransform
    
    //4
    let dice4 = UIImageView.init(image: UIImage(named: "dice4"))
    dice4.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
    diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 1, 0, 0)
    diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
    dice4.layer.transform = diceTransform
    
    diceView.addSubview(dice1)
    diceView.addSubview(dice2)
    diceView.addSubview(dice3)
    diceView.addSubview(dice4)
    diceView.addSubview(dice5)
    diceView.addSubview(dice6)
    
    view.addSubview(diceView)
}

在这边要注意的是 1 跟 6 都没有旋转,所以要一正一负。好了!终于完成,现在执行程就会有一颗会旋转骰子。

 Dice (1).gif

希望你透过这个实作对 CATransform3D 有所了解,如有问题,请留言给我。另外,你可以在这里下载完整的范例项目

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:如何在Swift项目中使用 Javascript编写一个将Markdown转为HTML的编辑器
下一篇:Swift 开发中,为什么要远离 Heap?
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部