2019年3月3日日曜日

ARKitサンプルの飛行機を平面に置いてみよう



はじめに


ARKitはカメラを通して見える現実世界に仮想の3D空間を重ね合わせるため、座標という概念を理解する必要があります。

座標の概念を図示したものが下記の図です。




ワールド座標


ARKitが空有間を認識してセッションが開始されたときに決まるAR空間の原点座標。

原点座標であるため必ず (x y z) 共に (0 0 0)  となります。

アンカー座標とカメラ座標はこの原点からいくつ離れているかという形で (x y z) の値が決まります。



アンカー座標


ARKitが現実世界の平面や特定の画像を認識した時に決まる、そのものの場所を示す座標。

ワールド座標とカメラ座標は必ず一つですが、アンカー座標は検知したものの分だけ存在します。


カメラ座標


ユーザー(iOSデバイス)の位置を示す座標。

ユーザーが移動することにより、(x y z) の値はそれに合わせて変わリます。


飛行機を検知した平面に置く


標準のサンプルアプリを動かした時に飛行機が画面に表示されましたが、これはワールド座標に飛行機を表示しています。

なので少し後ろに動かないと飛行機が見えません、初めてアプリを動かしたときに「画面に何も表示されないじゃないか!」と思った方もいたかも知れません。(実際に
私がそうでした)

ARKitでは検知した平面に何かを置くことがはじめの一歩です。



デフォルトのサンプルアプリではViewControllerクラスがARSCNViewDelegateを継承しています。

ARSCNViewDelegateではARAnchorに対応する3Dオブジェクトを操作するためのメソッドが定義されており、これらのメソッドに独自の処理を加えることで各アプリでやりたいことを実現します。


修正方針


サンプルアプリで検知した平面に飛行機を置くためには次のようなことを行う必要があります。

  1. アプリ開始時にロードされた飛行機オブジェクトを非表示にする
  2. 平面検知を有効にする
  3. 飛行機オブジェクトをARAnchorの場所へ移動させる
  4. 飛行機オブジェクトを表示する

アプリ開始時にロードされた飛行機オブジェクトを非表示にする

// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!

飛行機オブジェクトのロードはviewDidLoad()内の上記箇所で行っています。

これは飛行機オブジェクトのデータだけをロードしているのではなく、飛行機オブジェクトデータを含むSCNSceneをロードしています。



飛行機データはSCNSceneの中にあるRootNodeの下にぶら下がっており、それを取得するには下記のような処理を行います。

let ship = scene.rootNode.childNode(withName: "ship", recursively: true)

このchildNode()のwithNameにはSCNファイル内のノードの名前を指定します。

実際にXcode上でファイルを開くと分かりますが、Scene graphの構成の中にshipというノードがあり、これが名前となります。




shipという名前のノードを取得できたので、HiddenプロパティをONにして非表示に設定しておきます。

if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) {
    ship.isHidden = true
}

平面検知を有効にする


デフォルトの状態では平面検知が有効になっていないのでviewWillAppear()内のARWorldTrakingConfigurationにplaneDetectionをONにするように設定を行います。
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal]


飛行機オブジェクトをARAnchorの場所へ移動させる


ARAnchorが追加されたタイミングの独自処理は
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor)


内に実装を追加します。

先程と同じようにshipノードを取得しますが、先程のscene変数はviewDidLoad()のローカル変数だったのでこのメソッドの中には存在しません。

そのためViewControllerのメンバ変数であるsceneViewから取得を行います。

また、取得後はワールド座標上のshipノードをアンカー座標上に移動させます。
if let ship = sceneView.scene.rootNode.childNode(withName: "ship", recursively: true) {
    ship.removeFromParentNode()
    node.addChildNode(ship)
}

removeFromeParentNode()は読んで字のごとく現在そのノードがぶら下がっている親ノードから自分を削除します。

そして、addChildNode()でshipノードをARアンカーノードの下に持っていきます。

nodeは一体何かと思われるかもしれませんが、これはメソッドの引数で指定されており、SCNKit内でのARAnchorの場所を示すノードとなります。


飛行機オブジェクトを表示する


これは先程と逆にHiddenをOFFにするだけです。
ship.isHidden = false

(平面検知をOFFにしておく)


これは+αの処理ですが、ARSessionにより平面検知が動いている間は新しい平面が検知されたり、すでに検知した平面情報がアップデートされます。

表示した飛行機をとりあえずそのまま動かないようにするためには一度平面検知をOFFにしておきます。
let configuration = ARWorldTrackingConfiguration()
sceneView.session.pause()
sceneView.session.run(configuration)

最終的な変更後のソースコード


最終的な変更後のソースコードは次のようになります。

class ViewController: UIViewController, ARSCNViewDelegate {

    @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.isHidden = true
        }
        
        // Set the scene to the view
        sceneView.scene = scene
    }
    
    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()
    }

    // 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)
            
            ship.isHidden = false
            
            let configuration = ARWorldTrackingConfiguration()
            sceneView.session.pause()
            sceneView.session.run(configuration)
        }
    }
    
    func renderer(_ renderer: SCNSceneRenderer, willUpdate node: SCNNode, for anchor: ARAnchor) {
    }
    
    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 件のコメント:

コメントを投稿