美文网首页
如何使用 CATransform3D 处理 3D 影像、制做互动

如何使用 CATransform3D 处理 3D 影像、制做互动

作者: nongjiazhen | 来源:发表于2019-11-25 16:28 被阅读0次

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

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

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

    ? CATransform3DTranslate

    ? CATransform3DRotate

    ? CATransform3DInvert

    ? CATransform3DScale

    ? CATransform3DAffine

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

    开始建立范例App

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

    在 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 。

    在真实世界中,当物体远离我们时,由于视角的关系会看起来变小,理论上来说,比较远的边会比近的边要短,但是实际上对系统来说他们是一样长的,所以我们要做一些修正。透过设置 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看看效果如何。现在那个应该有一种立体的感觉:

    手势控制

    接下来我们要改为手势控制这个 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,你会发现非常失控!

    因为判定旋转角度的依据,就是手势在这个 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 拿掉,这样就能很正常的转动了。

    将普通的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点的图像,如你试执行程序,应该会得到以下的效果:

    加入骰子第 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)
    }
    

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

    要解决这问题,我们就要平移每一个 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)
    }
    

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

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

    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 都没有旋转,所以要一正一负。好了!终于完成,现在执行程就会有一颗会旋转骰子。

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

    原文链接:http://www.cocoachina.com/articles/19305

    相关文章

      网友评论

          本文标题:如何使用 CATransform3D 处理 3D 影像、制做互动

          本文链接:https://www.haomeiwen.com/subject/xtrfsctx.html