美文网首页ARKit教程
ARKit教程16_第十二章:高级用户交互

ARKit教程16_第十二章:高级用户交互

作者: 张芳涛 | 来源:发表于2019-08-28 10:41 被阅读0次

    前言

    ARKit感兴趣的同学,可以订阅ARKit教程专题
    源代码地址在这里

    正文

    在之前的章节,我们学习了:

    • 检测一个矩形。
    • 检测一个QR码。
    • 在检测到的矩形和QR码上面添加一个平面。
    • 在平面上面显示内容。

    在这一章,我们将会学到:

    • 通过使用故事板而不是独立的视图控制器来改善用户交互。
    • 切换全屏模式。
    • 检测预定义的图像。

    我们会采用上一章的代码继续做开发。不过我们需要移除掉一些无用的代码。

    首先,我们把BillboardViewController.xib移除掉,然后再把BillboardView.swift.移除掉。

    打开BillboardViewController.swift并删除对刚刚删除的BillboardView类的引用。此外,删除BillboardViewController类中的其余代码,但保留其定义。你最终会得到一个像这样的空类:

    class BillboardViewController: UIViewController { 
    }
    

    接下来,删除实现UICollectionViewDelegateFlowLayout协议的扩展。

    打开ViewController.swift并找到setBillboardImages(_ :)。找到后,删除整个方法。

    此外,我们可以删除BillboardViewDelegate协议的扩展名。找到以下代码并将其删除:

    extension ViewController: BillboardViewDelegate { 
        func billboardViewDidSelectPlayVideo( 
            _ view: BillboardView) {
            createVideo()
        }
    }
    

    最后,找到addBillboardNode()。删除以下代码行:

    let images = [ 
        "logo_1", "logo_2", "logo_3", "logo_4", "logo_5" 
    ].map { UIImage(named: $0)! }
    setBillboardImages(images)
    

    之后我们需要添加一些新的文件。我们首先创建一个Billboard.storyboard

    请注意,它由导航控制器和具有三个不同自定义单元的集合视图控制器组成。

    以上三个cell用于显示:

    • 视频播放器
    • 图片
    • 网页视图

    为此,我们需要创建三个cell

    • ImageCell:用于显示一张图片。

      import UIKit
      
      class ImageCell: UICollectionViewCell {
          @IBOutlet weak var imageView: UIImageView!
      
          func show(image: UIImage) {
                imageView.image = image
          }
      }
      
    • WebBrowserCell: 用于展示一个网页视图。

      import UIKit
      
      class WebBrowserCell: UICollectionViewCell {
          @IBOutlet weak var webBrowser: UIWebView!
      
        func go(to urlString: String) {
            guard let url = URL(string: urlString) else { return }
            let request = URLRequest(url: url)
            webBrowser.loadRequest(request)
          }
      }
      
    • VideoCell:用于播放视频。

      import UIKit
      import SpriteKit
      import AVFoundation
      import ARKit
      
      class VideoCell: UICollectionViewCell {
          @IBOutlet weak var playButton: UIButton!
          @IBOutlet weak var playerContainer: UIView!
      
        func configure(videoUrl: String, sceneView: ARSCNView, billboard: BillboardContainer) {
        }
           @IBAction func play() {
           }
       }
      

    创建基于故事板的广告牌

    新广告牌使用故事板,因此它与之前的实现有很大不同。打开ViewController.swift并滚动到renderer(_:nodeFor:)

    在上一次迭代中,我们在里面创建了BillboardViewController
    addBillboardNode()。由于实现现在更复杂,而不是再次将代码添加到该方法,最好使用新方法。

    let billboardNode = addBillboardNode()之后添加如下代码:

    createBillboardController()
    

    在上一次迭代中,我们在BillboardViewController里面创建了
    addBillboardNode()方法。由于实现现在更复杂,而不是再次将代码添加到该方法,最好使用新方法。

    func createBillboardController() { 
        // 1 
        DispatchQueue.main.async {
    
        // 2 
        let navController = UIStoryboard(name: "Billboard", bundle: nil) .instantiateInitialViewController() as! UINavigationController
    
        // 3 
        let billboardViewController = navController.visibleViewController as! BillboardViewController
    
        // 4 
        billboardViewController.sceneView = self.sceneView billboardViewController.billboard = self.billboard
    
        // 5 
        billboardViewController.willMove( toParentViewController: self) self.addChildViewController(billboardViewController) self.view.addSubview(billboardViewController.view)
    
        // 6 
         self.show(viewController: billboardViewController)
        }
    }
    

    上面的代码作用如下:

    • 1: 切换到主线程,以便我们可以处理用户界面更新。
    • 2: 创建Billboard故事板的初始视图控制器的实例,它是一个导航控制器。这里的强制转换是一个很好的调试工具:如果应用程序崩溃,则意味着存在开发错误 - 初始视图控制器很可能不是预期的导航控制器。
    • 3: 导航控制器的根视图控制器是BillboardViewController。再次强制转换非常方便,以防我们在故事板中更改某些内容并忘记更新代码。
    • 4: 广告牌将使用场景视图和广告牌容器,因此最好在这里设置它们;但是,我们必须添加两个相应的属性。
    • 5: 要在显示新视图控制器之前准备它,我们需要:
        a:告诉新视图控制器它将移动到ViewController
        b: 将新视图控制器添加到ViewController
        c: 将新视图控制器的视图添加到ViewController的视图中作为子视图。
    • 6: 新视图控制器已准备好显示,因此我们将其传递给show(viewController:)

    这样就完成了广告牌的创建;但是,show方法尚未实现。在刚刚创建的方法之后添加如下代码:

    private func show(viewController: BillboardViewController) { 
        let material = SCNMaterial() material.isDoubleSided = true 
        material.cullMode = .front
        material.diffuse.contents = viewController.view
        billboard?.viewController = viewController 
        billboard?.billboardNode?.geometry?.materials = [material]
    }
    

    这与我们在上一章的setBillboardImages()中所做的类似。

    打开BillboardViewController,添加如下代码:

    var sceneView: ARSCNView?
    
    var billboard: BillboardContainer?
    

    构建并运行应用程序。注意一旦检测到QR码,应用程序就会崩溃。看看Xcode控制台会发现一个无法识别的选择器被发送到广告牌视图控制器:

    [RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]: unrecognized selector sent to instance 0x1030e4600
    
    RazeAd[3180:1334506] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]:unrecognized selector sent to instance 0x1030e4600'
    

    发生这种情况是因为故事板需要广告牌视图控制器是UICollectionViewController的子类,当我们打开故事板文件时可能会注意到它。

    这是一个简单的方法:修改BillboardViewController类并使其继承自UICollectionViewController而不是UIViewController

    class BillboardViewController: UICollectionViewController
    

    与广告牌互动

    加载广告牌故事板后,就可以专注于在Billboard ViewController类中进行连接。

    最初的步骤是提供集合视图;这意味着覆盖UICollectionViewDataSource协议定义的一些方法。

    打开BillboardViewController.swift并且在最后添加如下代码:

    // UICollectionViewDataSource 
    extension BillboardViewController { 
    }
    

    我们需要告诉集合视图三件事:每个部分的部分数量,每个部分的项目数以及每个项目要显示的单元格。

    在扩展中添加如下代码:

    override func numberOfSections( 
        in collectionView: UICollectionView) -> Int { 
        return 3 
    }
    

    每个单元格类型都作为单独的部分处理。这在处理图像序列时很有用。

    接下来是在每个case下面添加以下方法:

    override func collectionView(
        _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    guard let currentSection = Section(rawValue: section) else { return 0 }
    switch currentSection { 
    case .images:
    return images.count case .video:
    return 1 case .webBrowser:
    return 1 
        }
    }
    

    只有一个视频和一个Web浏览器,因此对于这些,我们返回1。但是,可能有多个图像,因此我们返回images.count以确定需要多少行。

    最后,我们必须添加返回给定索引路径的单元格的方法:

    override func collectionView(
    
        _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        // 1 
        guard let currentSection =
        Section(rawValue: indexPath.section) else { fatalError("Unexpected collection view section") }
    
        // 2 
        let cellType: Cell switch currentSection { 
        case .images:
        cellType = .cellImage case .video:
        cellType = .cellVideo case .webBrowser:
        cellType = .cellWebBrowser }
    
        // 3 
        let cell = collectionView.dequeueReusableCell( withReuseIdentifier: cellType.rawValue, for: indexPath)
    
        // 4 
        switch cell { 
        case let imageCell as ImageCell:
             let image = UIImage(named: images[indexPath.row])! imageCell.show(image: image)
    
        case let videoCell as VideoCell:
    
        let videoUrl = "https://www.rmp-streaming.com/media/bbb-360p.mp4" if let sceneView = sceneView, let billboard = billboard { videoCell.configure( videoUrl: videoUrl, sceneView: sceneView, billboard: billboard ) } break
    
        case let webBrowserCell as WebBrowserCell: 
        webBrowserCell.go(to: "https://www.raywenderlich.com")
    
        default:
    
        fatalError("Unrecognized cell") }
    
        return cell
    
    }
    

    这是一个很长的,虽然简单的实现:

    • 1:Section枚举用于标识每个部分。如果其原始值无法识别某个部分,则会抛出fatalError
    • 2: 另一个名为Cell的枚举,用于识别细胞。
    • 3: Cell枚举使用String类型的原始值来定义单元标识符。指定索引路径的单元格已出列。
    • 4: 我们可以根据细胞类型使用适当的数据配置细胞。

    我们可能已经注意到图像单元格的代码引用了不存在的图像属性。此属性将包含要在广告牌中显示的图像名称列表。

    billboard属性之后,添加以下内容:

    var images: [String] = [ "logo_1", "logo_2", "logo_3", "logo_4", "logo_5" ]
    

    目前,我们已完成广告牌视图控制器。构建并运行应用程序。

    第一个单元是视频播放器 - 它还没有实现,所以不要指望它能够工作。但是,我们可以向右滑动以显示图像,并且可以访问最后一个单元格中的Web浏览器,该单元格将加载www.raywenderlich.com

    显示视频播放器

    视频单元实现

    打开VideoCell.swift添加如下代码:

    // 1 
    var isPlaying = false 
    // 2 
    var videoNode: SKVideoNode!
    
    var spriteScene: SKScene!
    
    // 3 
    var videoUrl: String!
    
    // 4 
    var player: AVPlayer?
    
    // 5 
    weak var billboard: BillboardContainer? 
    weak var sceneView: ARSCNView?
    

    上面代码作用如下:

    • 1: isPlaying是指示视频当前是否正在播放的标志。
    • 2: 在创建过程中,将ARKit节点存储到videoNode中,将SpriteKit场景存储到spriteScene中。
    • 3: 这包含目标视频的URL
    • 4: 这是负责播放视频的AVPlayer
    • 5: 这些是对广告牌容器和ARKit场景视图的引用。

    其中一些属性可以在configure方法中初始化,该方法当前为空。将此内容添加到其正文:

    self.videoUrl = videoUrl 
    self.billboard = billboard 
    self.sceneView = sceneView
    

    要播放视频,用户将点按“播放”按钮;这个按钮已经连接到空的play()动作方法,我们只需要提供实现。添加以下代码:

    guard let billboard = billboard else { return }
    
    if billboard.isFullScreen {
    
    } else {
    
        // 1 
        createVideoPlayerAnchor() 
        // 2 
        billboard.videoPlayerDelegate?.didStartPlay() 
        // 3
        playButton.isEnabled = false
    }
    

    上面代码作用如下:

    • 1: 创建视频播放器锚点。
    • 2: 通知代表视频开始播放。
    • 3: 禁用播放按钮以防止双击触发相同的操作两次。

    广告牌容器没有isFullScreen属性,但我们可以快速添加它。打开BillboardContainer.swift并添加以下内容:

    var isFullScreen = false
    

    视频播放器代理方法

    在最后一个代码段中,我们通过委托方法调用发送了通知;此委托存储在广告牌容器中,因此我们需要添加协议定义。仍然在BillboardContainer.swift中,在import语句之后添加以下内容:

    protocol VideoPlayerDelegate: class { 
        func didStartPlay() func didEndPlay() 
    }
    

    这两种方法用于在视频开始或停止时通知应用。

    现在,我们需要保留对要通知的目标实体的引用。将此属性添加到BillboardContainer

    weak var videoPlayerDelegate: VideoPlayerDelegate?
    

    要使代理有效,必须将此属性设置为要通知的实例。在这种情况下,实例是ViewController,因为它是必须对视频播放器状态的变化作出反应的实体。

    打开ViewController.swift并找到createBillboard,然后在广告牌实例化后添加如下代码:

    billboard?.videoPlayerDelegate = self
    

    除了设置代理之外,还必须实现它。在文件的末尾,添加以下代码以处理视频播放器状态更改:

    extension ViewController: VideoPlayerDelegate { 
        func didStartPlay() { 
            // 1 
            billboard?.billboardNode?.isHidden = true 
        }
    
        func didEndPlay() { 
            // 2 
            billboard?.billboardNode?.isHidden = false 
        }
    }
    

    上面代码作用如下:

    • 1: 在视频播放器启动时隐藏广告牌。
    • 2: 视频播放器停止时再次显示广告牌。

    回到视频单元格

    VideoCellplay()处理程序中调用的createVideoPlayerAnchor()方法仍未实现。打开VideoCell.swift,在configure()方法之后添加以下内容:

    func createVideoPlayerAnchor() {
        guard let billboard = billboard else { return } 
        guard let sceneView = sceneView else { return }
        // 1 
        let center = billboard.plane.center *matrix_float4x4(SCNMatrix4MakeRotation( Float.pi / 2.0, 0.0, 0.0, 1.0))
        let anchor = ARAnchor(transform: center)
        // 2 
        sceneView.session.add(anchor: anchor)
        // 3 
        billboard.videoAnchor = anchor
    }
    

    上面代码作用如下:

    • 1: 创建一个以广告牌中心为中心的新锚点,并围绕z轴应用通常的90度旋转。
    • 2: 将新锚添加到ARKit场景。
    • 3: 保留对锚点的引用,因为稍后将需要用到它。

    视频播放器创建逻辑更新

    我们需要添加一些代码来将新创建的锚与SceneKit节点相关联。 ARSCNView的委托是ViewController类,因此我们必须在其中创建新节点。但是,为了保持一致性,最好将该任务保留在VideoCell中,最简单的方法是通过委托实现。

    打开ViewController.swift并找到renderer(_:nodeFor)方法。在switch语句中,我们将看到已经存在处理视频节点创建的情况;但是,它使用的是旧实现。

    我们需要删除包含以下方法的实现:

    • addVideoPlayerNode()
    • createVideo()
    • removeVideo()

    找到这三种方法并且删除它们。

    现在,我们在renderer方法中,找到如下代码:

    node = addVideoPlayerNode()
    

    把这一行代码替换为:

    node = billboard.videoNodeHandler?.createNode()
    

    如前所述,我们将使用代理来公开视频单元格方法。打开BillboardContainer.swift,在videoPlayerDelegate属性之前添加:

    weak var videoNodeHandler: VideoNodeHandler?
    

    要关闭循环,我们需要定义VideoNodeHandler协议。在import语句后添加如下代码:

    protocol VideoNodeHandler: class { 
        func createNode() -> SCNNode? 
        func removeNode() 
    }
    

    上面代码作用如下:

    • createNode(): 当广告视图控制器需要视频播放器的新SCNNode时,由广告视图控制器调用。
    • removeNode(): 必须删除节点时调用。

    打开VideController.swift并在touchesBegun(_:with :)中找到有问题的代码行。将其替换为以下内容:

    billboard?.videoNodeHandler?.removeNode()
    

    删除广告牌后,我们需要重置之前添加的videoNodeHandler属性。

    找到removeBillboard()并在最后一行之前插入以下代码行,其中billboard属性重置为nil

    billboard?.videoNodeHandler = nil
    

    运行应用程序。然后扫描QR码,当前的效果是点击视频播放器中的播放按钮......广告牌消失,但不显示视频。

    实现视频节点处理程序

    打开VideoCell.swift。在configure(videoUrl:sceneView:billboard :)结束时,设置属性:

    billboard.videoNodeHandler = self
    

    VideoHandlerProtocol分别定义了两种方法来为视频播放器创建和删除SceneKit节点。

    完成后,构建并运行应用程序。扫描二维码后,点击播放按钮;广告牌消失,并由显示视频的简约视频播放器取代。

    点按屏幕上的任意位置即可关闭视频播放器并返回广告牌。

    全屏

    触发全屏模式

    我们可以通过多种方式使用全屏模式,例如使用按钮或放大/缩小手势。也可以使用双击,这个项目中,我们就采用双击手势。

    打开BillboardViewController.swift并且在images属性之后添加如下代码:

    // 1 
    let doubleTapGesture = UITapGestureRecognizer()
    override func viewDidLoad() { 
        super.viewDidLoad()
        // 2 
        doubleTapGesture.numberOfTapsRequired = 2 doubleTapGesture.addTarget( self, action: #selector(didDoubleTap)) view.addGestureRecognizer(doubleTapGesture)
    }
    // 3 
    @objc func didDoubleTap() { 
        guard let billboard = billboard else { return } 
        if billboard.isFullScreen { 
            restoreFromFullScreen() 
    } else { 
        showFullScreen() 
        } 
    }
    

    如果之前使用过手势,那么这些代码可能看起来很熟悉。我们正在使用点击手势识别器来检测广告牌视图控制器上的双击:

    • 1: 此属性用于保留对手势的引用,以防我们要禁用或以其他方式更改手势。
    • 2: 我们在viewDidLoad()中配置并激活点击手势。 numberOfTapsRequired设置为2**表示我们要捕获双击。
    • 3: 在双击处理程序中,切换全屏模式。这两种情况都是由接下来要实施的各种方法处理的。

    进入全屏

    当广告牌以ARKit模式显示时,其导航控制器 - 以及整个堆栈 - 是ViewController的子节点,因为这是显示ARKit的视图控制器。要进入全屏模式,我们必须将广告牌的视图与其超级视图分离,并将其附加到父视图控制器的视图中。

    创建广告牌视图控制器时,我们将其视图添加到ViewControllershow(viewController :)方法中的SCNMaterial

    private func show(viewController: BillboardViewController) { 
        let material = SCNMaterial() 
        ...
        material.diffuse.contents = viewController.view ...
    }
    

    BillboardViewController的末尾,添加以下代码:

    extension BillboardViewController {
        func showFullScreen() { 
            guard let billboard = billboard else { return } 
            guard billboard.isFullScreen == false else { return }
            // 1 
            guard let mainViewController = parent as? ViewController else { return }
            self.mainViewController = mainViewController mainView = view.superview
            // 2 
            willMove(toParentViewController: nil) 
            view.removeFromSuperview() 
            removeFromParentViewController()
            // 3
            willMove(toParentViewController: mainViewController) 
            mainViewController.view.addSubview(view) 
            mainViewController.addChildViewController(self)
            // 4 
             billboard.isFullScreen = true
        }
    }
    

    上面作用如下:

    • 1; 保持对父视图控制器和超级视图的引用。退出全屏模式时,我们需要它们才能恢复视图。
    • 2: 从各自的父母中删除视图控制器及其视图。
    • 3: 再次将视图控制器添加到其上一个父级,并将其视图添加到父级视图(而不是SCNMaterial)。
    • 4: 设置标记以跟踪全屏模式是打开还是关闭。

    现在,我们需要添加用于跟踪原始父视图控制器和父视图的两个属性。在广告牌属性后添加它们:

    weak var mainViewController: AdViewController? 
    weak var mainView: UIView?
    

    退出全屏

    要退出全屏模式,必须执行相反操作:从ViewController视图中删除视图,并在进入全屏模式之前将其放回视图所具有的任何父类。

    回想一下,我们在mainView属性中保存了这个值。

    打开BillboardViewController.swift文件,在showFullScreen()之后添加此代码:

    func restoreFromFullScreen() {
        guard let billboard = billboard else { return } 
        guard billboard.isFullScreen == true else { return } 
        guard let mainViewController = mainViewController else { return } 
        guard let mainView = mainView else { return }
        // 1 
        willMove(toParentViewController: nil) 
        view.removeFromSuperview() 
        removeFromParentViewController()
        // 2 
        willMove(toParentViewController: mainViewController)
        mainView.addSubview(view) mainViewController.addChildViewController(self)
        // 3 
        billboard.isFullScreen = false 
        self.mainViewController = nil 
        self.mainView = nil
    }
    

    上面的代码作用如下:

    • 1: 从父视图中删除BillboardViewController的视图,在本例中是ViewController的视图。
    • 2:将视图放回到原始视图中,该视图在全屏显示时存储在mainView属性中。
    • 3: 重置全屏图像,以及对主视图控制器和主视图的引用。

    运行应用程序并执行常规的QR检测。然后,双击广告牌以转换到全屏。

    到此为止,视频播放器还不能用。

    修复视频播放器

    打开VideoCell.swift。在play()中,将if billboard.isFullScreen分支留空,插入以下代码:

    if isPlaying == false {
        // 1 
        createVideoPlayerView()
        playButton.setImage( #imageLiteral(resourceName: "arKit-pause"), for: .normal) 
    } else { 
        // 2 
        stopVideo()
        playButton.setImage( #imageLiteral(resourceName: "arKit-play"), for: .normal)
    } 
    // 3 
    isPlaying = !isPlaying
    

    上面的代码作用如下:

    • 1: 如果没有播放视频,请创建视频播放器视图并更改按钮的图标以显示paused图像。

    • 2: 如果当前正在播放视频,请将其停止并将按钮的图标恢复为play图像。

    • 3: 切换isPlaying标志,以跟踪视频播放状态。

    接下来需要做的是创建视频播放器。在createVideoPlayerAnchor()之后添加如下代码:

    func createVideoPlayerView() {
        if player == nil { 
            guard let url = URL(string: videoUrl) else { return } 
            player = AVPlayer(url: url) 
            let layer = AVPlayerLayer(player: player) 
            layer.frame = playerContainer.bounds 
            playerContainer.layer.addSublayer(layer) 
        }
        player?.play()
    }
    

    如果不存在视频播放器我们会创建一个视频播放器并且开始播放。接下来,我们还需要一个停止播放的方法:

    func stopVideo() { 
        player?.pause() 
    }
    

    上面的这个方法会让播放器暂停播放。

    构建并运行应用程序。扫描QR码并双击广告牌进入全屏模式。然后,点击play按钮播放视频;再次点击它可暂停视频。

    检测参考图像

    ARKit 1.5引入了一些新功能:能够检测自定义图像。因此,我们可能希望让应用程序识别一个或多个预定义图像,而不是检测白色矩形或QR码。

    在下一节中,我们将通过使用自定义图像检测替换QR代码检测。但是,要实现此目的,有两个先决条件:

    • Xcode版本大于或等于9.3
    • iOS 系统版本大于或等于11.3

    我们添加一个或多个参考图像。

    选择参考图像

    第一步是让应用知道需要检测哪些图像。在Xcode中,打开Assets.xcassets目录,然后单击位于窗口右下角的+按钮。将显示一个弹出菜单,其中包含一长串选项。找到这两个:New AR Resource GroupNew AR Reference Image

    我们将使用前者创建组,后者将新图像添加到AR资源组。 AR资源组是为ARKit创建的特殊组类型。它在运行时加载,ARKit*使用它来确定它应该检测哪些图像。

    创建一个新的AR资源组并将其命名为RMK-ARKit-triggers。然后,将图像添加到新创建的组:

    • 1: 右键选择logo_3图片
    • 2: 选择Show in Finder
    • 3: 在Xcode打开的Finder窗口中,拖动图像文件并将其放入RMK-ARKit-triggers组。
    • 4: 对logo_4图像重复步骤1-3

    选择组中两个图像之一,然后查看“属性”检查器。除了图像名称,它还包含两个可编辑的参数:大小和度量单位。

    尺寸和度量单位属性用于为现实世界中的图像的物理尺寸提供ARKitARKit使用这些测量值来确定相机的正确视角和距离。

    为宽度和高度输入0.2,并验证Meters是选定的度量单位,如下页所示。如果在A4Letter纸张上最大化打印,这大致是图像的大小。重复其他图像。


    请注意,两个图像现在在其各自的右下角显示警告图标。将鼠标悬停在警告图标上,然后单击以显示更多信息:

    上面警告包含的意思如下:

    • 1: 图像彼此太相似了。
    • 2: 图像颜色直方图不足够,这意味着没有均匀的颜色分布。
    • 3: 图像具有相同颜色的大部分。

    看看你添加的图像,它们并不完全符合第二和第三个要求 - 但不要担心,这些图片能用。

    注册参考图像

    此时,应用程序不知道这些图像的存在 - 你必须告诉它们。这是在ARKit配置期间完成的。

    打开ViewController.swift并导航到viewWillAppear(_ :)。使用以下代码行创建配置:

    // Create a session configuration 
    let configuration = ARWorldTrackingConfiguration() 
    configuration.worldAlignment = .camera
    
    // Run the view's session 
    sceneView.session.run(configuration)
    

    ARWorldTrackingConfiguration继承的ARConfiguration类具有名为detectionImages的属性;我们可以使用此属性来指定希望ARKit识别的一组图像。

    可检测图像捆绑在ARReferenceImage的实例中,该实例添加了一些元数据,即:

    • name
    • physicalSize
      如前所述,在创建AR资源组时,可以分配两者。

    在上面显示的代码段中,在运行会话的最后一行之前,插入此代码以加载和设置参考图像:

    // 1 
    var triggerImages = ARReferenceImage.referenceImages( inGroupNamed: "RMK-ARKit-triggers", bundle: nil) 
    // 2 
    configuration.detectionImages = triggerImages
    

    第一行加载之前添加到RMK-ARKit-triggers AR资源组的图像。第二行将这些图像分配给配置。

    需要注意的是referenceImages(inGroupNamed:bundle :)如何返回Set <ARReferenceImage>而不是数组。

    提供一个hook

    现在,我们需要ARKit在识别参考图像时通知应用程序。识别后,ARKit会自动为会话添加锚点。

    添加锚点时,有三种可能的方法可以通知:

    • ARSessionDelegate委托的session(_:didAdd :)

    • 查看(_:didAdd:for :) ARSKViewDelegate委托。

    • ARSCNViewDelegate委托的rendered(_:didAdd:for:)

      sceneView.session.delegate = self

    向下滚动以找到ARSessionDelegate扩展,并在最后添加:

    // 1 
    func session(_ session: ARSession,
        didAdd anchors: [ARAnchor]) { 
        // 2 
        if let imageAnchor = anchors.compactMap({ $0 as? ARImageAnchor }).first {
            // 3
            self.createBillboard(center: imageAnchor.transform,size: imageAnchor.referenceImage.physicalSize) 
        }
    }
    

    上面代码作用如下:

    • 1: 当锚点添加到会话时,ARKit调用此委托方法;识别参考图像时创建新锚点。
    • 2: 由于该方法接收到一组锚点,因此我们首先按类型过滤它们并仅保留ARImageAnchor的实例;然后你拿第一个。
    • 3: 我们可以使用该锚点创建广告牌。

    打开AdViewController.swift文件,找到createBillboard()的当前实现,并在它之后添加此重载:

    func createBillboard(center: matrix_float4x4, size: CGSize) { 
        // 1 
        let plane = RectangularPlane(center: center, size: size)
    
        // 2 
        let rotation = SCNMatrix4MakeRotation(Float.pi / 2, -1.0, 0.0, 0.0)
    
        // 3 
        let rotatedCenter = plane.center * matrix_float4x4(rotation) 
        let anchor = ARAnchor(transform: rotatedCenter)
    
        billboard = BillboardContainer( billboardAnchor: anchor, plane: plane) 
        billboard?.videoPlayerDelegate = self 
        sceneView.session.add(anchor: anchor)
    
        print("New billboard created")
    }
    

    上面代码作用如下:

    • 1: 我们不必使用4个矩形顶点,而是具有参考图像的中心,因此我们可以使用RectangularPlane初始化程序的正确重载。
    • 2: 旋转矩阵是不同的。而不是围绕z轴旋转90度,我们需要旋转相同的度数,但围绕x轴,以及相反的方向。这是SceneKit节点通常需要的调整。
    • 3: 方法的其余部分与另一个相同。

    注意:如果参考图像的大小不正确,ARKit无论如何都会识别它。但是,观点可能会发生变化。如果图像较大,ARKit可能认为它比它更接近,而如果它更小,它可以看得更远。

    在运行时添加参考图像

    能够检测预定义图像非常棒,但是必须将其与应用程序捆绑在一起可能是一个相当大的限制,因为要更新它,我们必须发布新版本的应用程序。
    ViewController.swift中,向上滚动到viewWillAppear(_ :)并找到创建triggerImages变量的行。在该行之后添加此代码:

    // 1 
    let image = UIImage(named: "logo_2")! 
    // 2
    let referenceImage = ARReferenceImage(image.cgImage!, orientation: .up, physicalWidth: 0.2) 
    // 3 
    triggerImages?.insert(referenceImage)
    

    上面的代码作用如下:

    • 1: 从图像资源中找到的logo_2图像创建UIImage
    • 2: 为它创建一个参考图像,通过0.2米的物理宽度。系统计算物理高度。
    • 3: 将新创建的参考图像添加到加载到triggerImages变量中的组。

    参考图像检测是不同的用例,并不代替QR码检测。使用QR码,我们可以存储附加值的自定义数据。

    上一章 目录 下一章

    相关文章

      网友评论

        本文标题:ARKit教程16_第十二章:高级用户交互

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