ARKitのサンプルにタッチイベントを追加しよう
インタラクティブなアプリケーションを作る場合にはユーザーの操作を処理する必要が出てきます。
通常のアプリではボタンなどのUI部品それぞれにイベントハンドラを仕込むことができるので、何がユーザーによって押されたかを調べる必要はありません。
しかし、ARKitの場合オブジェクトそれぞれにイベントハンドラを仕込むことはできないため、hitTestを使ってどのオブジェクトが押されたのかを調べる必要があります。
しかし、ARKitの場合オブジェクトそれぞれにイベントハンドラを仕込むことはできないため、hitTestを使ってどのオブジェクトが押されたのかを調べる必要があります。
hitTestとは
hitTestとは、ある特定の座標から直線を伸ばしたときに、その延長線上にオブジェクトがあるかを確認する方法です。
ユーザーがタッチした座標でhitTestを行うことで何が選択されたのかを知ることができます。
そして、ARKitの場合hitTestで調べられるオブジェクトは主に2つあります。
ユーザーがタッチした座標で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で判定した方が、ユーザーの期待値に沿うことになります。
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 件のコメント:
コメントを投稿