Chapter 11: Beginning User Interaction

大綱

接下來要繼續上一章的功能的接續。已經可以偵測到一個長方形平面,並在上面置放一個虛擬平面。

  • 目前這個平面的方向是有問題

  • 從平面偵測變成可以偵測QR Code

  • 增加一些使用者互動

  • 虛擬平面變成image再變成carousel

  • 播放影片

摘要

The world coordinate system

worldTransform property

  • 記錄了有關於hit test在world coordinate system的結果,包含了position和orientation。

The camera world alignment

Detecting a QR code

只要把VNDetectRectanglesRequest換成VNDetectBarcodesRequest, VNRectangleObservation換成VNBarcodeObservation就可以。

     DispatchQueue.global(qos: .background).async {
       do {
-        let request = VNDetectRectanglesRequest { (request, error) in
+        let request = VNDetectBarcodesRequest { (request, error) in
           // Access the first result in the array,
           // after converting to an array
           // of VNRectangleObservation
-          guard let results = request.results?.compactMap({ $0 as? VNRectangleObservation }), let result = results.first else {
+          guard let results = request.results?.compactMap({ $0 as? VNBarcodeObservation }), let result = results.first else {
             print ("[Vision] VNRequest produced no result")
             return
           }

Showing an image

在虛擬平面上秀出一張圖片。

A UIView can be assigned to a material

    func setBillboardImage(image: UIImage) {
        let material = SCNMaterial()
        material.isDoubleSided = true

        DispatchQueue.main.async {
            let imageView = UIImageView(image: image)
            // UIView 可以被指定為一種 material
            // 當然也可以直接將image 指定一種 material
            // material.diffuse.contents = image
            material.diffuse.contents = imageView

            self.billboard?.billboardNode?.geometry?.materials = [material]
        }
    }

Showing an image carousel

既然可以用UIImageView當作content,當然可以用更複雜的UIView來當作content,設計一個比較複雜的UIView carousel layout。

-    func setBillboardImage(image: UIImage) {
+    func setBillboardImage(_ images: [UIImage]) {
         let material = SCNMaterial()
         material.isDoubleSided = true

         DispatchQueue.main.async {
-            let imageView = UIImageView(image: image)
+            let billboardViewController = BillboardViewController(
+            nibName: "BillboardViewController", bundle: nil)
+            billboardViewController.images = images
+            self.billboard?.viewController = billboardViewController
             // UIView 可以被指定為一種 material
             // 當然也可以直接將image 指定一種 material
             // material.diffuse.contents = image
-            material.diffuse.contents = imageView
-
+            material.diffuse.contents = billboardViewController.view
             self.billboard?.billboardNode?.geometry?.materials = [material]
         }

Playing a video

在UIView carousel layout中有個play button, 當play button被觸發時會開始播放影片。

billboardViewController.delegate = self

////////////////////////////////

extension AdViewController: BillboardViewDelegate {

    func billboardViewDidSelectPlayVideo(_ view: BillboardView) {
        createVideo()
    }
}

////////////////////////////////

func createVideo() {
        guard let billboard = self.billboard else { return }

        let rotation = SCNMatrix4MakeRotation(Float.pi / 2.0, 0.0, 0.0, 1.0)
        let rotaionCentor = billboard.plane.center * matrix_float4x4(rotation)

        // 建立一個新anchor其中心跟billboard相同
        let anchor = ARAnchor(transform: rotaionCentor)
        sceneView.session.add(anchor: anchor)
        // 記錄video anchor
        self.billboard?.videoAnchor = anchor
    }

////////////////////////////////


// 在renderer(_:nodeFor:) 中的switch case新增一個case
case (let videoAnchor) where videoAnchor == billboard.videoAnchor:
      node = addVideoPlayerNode()

////////////////////////////////

func addVideoPlayerNode() -> SCNNode? {
        guard let billboard = self.billboard else { return nil }

        // 等下會用到一些變數
        let billboardSize = CGSize(width: billboard.plane.width, height: billboard.plane.height / 2)
        let frameSize = CGSize(width: 1024, height: 512)
        let videoUrl = URL(string:"https://www.rmp-streaming.com/media/bbb-360p.mp4")!

        // 建立player和SpriteKit video node
        let player = AVPlayer(url: videoUrl)
        let videoPlayerNode = SKVideoNode(avPlayer: player)
        videoPlayerNode.size = frameSize
        videoPlayerNode.position = CGPoint(x: frameSize.width / 2, y: frameSize.height / 2)
        // it’s an undocumented “feature” which causes a SpriteKit node to be rendered on a SceneKit node rotated by 90 degrees counterclockwise
        videoPlayerNode.zRotation = CGFloat.pi

        // 建立SpriteKit scene並加入video node
        let spritekitScene = SKScene(size: frameSize)
        // 此時再SceneView加上一個spriteKit video node
        spritekitScene.addChild(videoPlayerNode)

        // 建立plane並加入node中
        let plane = SCNPlane(width: billboardSize.width, height: billboardSize.height)
        plane.firstMaterial?.isDoubleSided = true
        // 跟billboard做法類似,將某個scence直接當作diffuse的content使用
        plane.firstMaterial?.diffuse.contents = spritekitScene
        let node = SCNNode(geometry: plane)

        self.billboard?.videoNode = node

        self.billboard?.billboardNode?.isHidden = true
        videoPlayerNode.play()

        return node
    }

Removing the video player

若已經在播放影片,在點擊螢幕一次,可以停止播放並換成billboard。

 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if billboard?.hasVideoNode == true {
        billboard?.billboardNode?.isHidden = false
        removeVideo()
        return
    }

////////////////////////////////

    func removeVideo() {
        if let videoAnchor = billboard?.videoAnchor {
            sceneView.session.remove(anchor: videoAnchor)
            billboard?.videoNode?.removeFromParentNode()

            billboard?.videoAnchor = nil
            billboard?.videoNode = nil
        }
    }

Last updated

Was this helpful?