2019年3月10日日曜日

ARKitのサンプルにタッチイベントを追加しよう


ARKitのサンプルにタッチイベントを追加しよう


インタラクティブなアプリケーションを作る場合にはユーザーの操作を処理する必要が出てきます。

通常のアプリではボタンなどのUI部品それぞれにイベントハンドラを仕込むことができるので、何がユーザーによって押されたかを調べる必要はありません。

しかし、ARKitの場合オブジェクトそれぞれにイベントハンドラを仕込むことはできないため、hitTestを使ってどのオブジェクトが押されたのかを調べる必要があります。


hitTestとは





hitTestとは、ある特定の座標から直線を伸ばしたときに、その延長線上にオブジェクトがあるかを確認する方法です。

ユーザーがタッチした座標でhitTestを行うことで何が選択されたのかを知ることができます。

そして、ARKitの場合hitTestで調べられるオブジェクトは主に2つあります。

  • 平面検知や画像認識によって置かれたARAnchor
  • AR空間上に置かれた3Dオブジェクト(例:サンプルアプリの飛行機)


今回は後者を扱いますが、次回以降で検知した平面をhitTestする方法を紹介したいと思います。


修正方針


今回は次のようにアプリを修正してみたいと思います。


  • 画面のタップイベントを拾う
  • タップ座標を取得する
  • 座標情報を元にhitTestを行う
  • 飛行機がタップされたのであれば10cm前進させる


画面のタップイベントを拾う


画面のタップイベントを拾うためにはメインのViewControllerがGestureRecognizerのdelegateになる必要があります。

クラス定義に下記のDelegateを追加しましょう。

UIGestureRecognizerDelegate

また、画面(sceneView)がタップされたことを検知したいので、TapGestureRecognizerを生成し、イベント処理者をViewControllerに設定し、sceneViewに登録をしておきます。

let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
tapRecognizer.delegate = self
sceneView.addGestureRecognizer(tapRecognizer)
UITapGestureRecognizerのtarget引数がイベント処理者を指定し、actionが呼ばれるメソッドを指定します。

そのため、呼ばれるメソッドをViewControllerにtapという名前で用意しておきます。

@objc func tap(_ tapRecognizer: UITapGestureRecognizer) {
}

タップ座標を取得する


画面をタップした時にtap()が呼ばれるようになったので、tap()の中に次のようにタップした座標を取得するメソッドを追加します。

let touchPoint = tapRecognizer.location(in: self.sceneView)

座標情報を基にhitTestを行う


次のようにhitTestを呼び出し、touchPointを渡してやります。

let results = self.sceneView.hitTest(touchPoint, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.all.rawValue])

option引数にパラメータを仕込むことで、細かな制御ができますが、今回はシンプルにhitTestの延長線上の全てのオブジェクトを検知対象とするようにします。



飛行機がタップされたのであれば10cm前進させる


飛行機がタップされたことは検知ノードの名前を調べることで判断することができます。

また、このときに注意して欲しいのがhitTestの当たり判定は延長線上にある全てのSCNNodeを引っ張ってくるため、必然的に当たり判定は3Dデータのメッシュを表すノードが判定され易くなります。

サンプルの飛行機データはshipというノード配下の子ノードにshipMeshというデータがあり、これがメッシュデータを表すノードになります。


shipノードの当たり判定がない訳ではありませんが、非常に小さいためユーザーにタップされたことを検知するには子ノードのshipMeshで判定した方が、ユーザーの期待値に沿うことになります。

if let result = results.first {
   guard let hitNodeName = result.node.name else { return }
   guard hitNodeName == "shipMesh" else { return }
            
   if let ship = result.node.parent {
      let actMove = SCNAction.move(by: SCNVector3(0, 0, 0.1), duration: 0.2)
      ship.runAction(actMove)
   }
}


実際の動作





修正後のソースコード


import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate, UIGestureRecognizerDelegate {

    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        
        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true
        
        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        
        if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.opacity = 0
        }
        
        // Setup Omni Light
        let light = SCNLight()
        light.type = .omni

        let LightNode = SCNNode()
        LightNode.light = light
        LightNode.name = "light"
        LightNode.position = SCNVector3(-1,2,-1)
        scene.rootNode.addChildNode(LightNode)
        
        // Set the scene to the view
        sceneView.scene = scene
        
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
        tapRecognizer.delegate = self
        sceneView.addGestureRecognizer(tapRecognizer)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        
        // Run the view's session
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Pause the view's session
        sceneView.session.pause()
    }

    @objc func tap(_ tapRecognizer: UITapGestureRecognizer) {
        let touchPoint = tapRecognizer.location(in: self.sceneView)
        
        let results = self.sceneView.hitTest(touchPoint, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.all.rawValue])
        
        if let result = results.first {
            guard let hitNodeName = result.node.name else { return }
            guard hitNodeName == "shipMesh" else { return }
            
            if let ship = result.node.parent {
                let actMove = SCNAction.move(by: SCNVector3(0, 0, 0.1), duration: 0.2)
                ship.runAction(actMove)
            }
        }
    }
    
    // MARK: - ARSCNViewDelegate
    
/*
    // Override to create and configure nodes for anchors added to the view's session.
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let node = SCNNode()
     
        return node
    }
*/
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        
        if let ship = sceneView.scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.removeFromParentNode()
            node.addChildNode(ship)
            
            let configuration = ARWorldTrackingConfiguration()
            sceneView.session.pause()
            sceneView.session.run(configuration)
            
            let completion = {
                let rotationAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 1))
                ship.runAction(rotationAction)
            }
            
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 1.5
            SCNTransaction.completionBlock = completion
            ship.opacity = 1
            SCNTransaction.commit()
        }
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        guard let lightEstimate = self.sceneView.session.currentFrame?.lightEstimate else { return }
        let ambientIntensity = lightEstimate.ambientIntensity
        let ambientColorTemperature = lightEstimate.ambientColorTemperature
        
        if let lightNode = self.sceneView.scene.rootNode.childNode(withName: "light", recursively: true) {
            guard let light = lightNode.light else { return }
            light.intensity = ambientIntensity
            light.temperature = ambientColorTemperature
        }
    }
    
    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user
        
    }
    
    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay
        
    }
    
    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required
        
    }
}

0 件のコメント:

コメントを投稿