e.blog

主にUnity周りのことについてまとめていきます

iOSのARKitを使ってVRのポジトラをやってみた

概要

ずっと気になっていたARKit。やっと触ることができたので、ひとまず、空間認識して色々触ったあと、VRのポジトラに流用するのをやってみたのでまとめておきます。
AR自体がポジトラしてモデルなんかを表示できるので、これをVRモードのカメラの位置に転化してあげる、という流れです。

実際に動かしてみた動画です↓

ひとまず、単純に空間を認識して平面などを配置、カメラを移動する、いわゆる「AR」の実装方法を解説したあと、VRモードへの転用を説明します。
(まぁといっても、ほぼAR空間での処理が実装できれば、あとはカメラの位置同期を別のものに置き換えるだけなので大した問題ではありませんが)

単純にARコンテンツをさっと作るだけなら、そもそもUnityが専用のコンポーネントをすでに用意してくれているので、それを組み合わせるだけですぐにでも空間にモデルなんかを配置することができます。
今回の解説は、VRに転用するにあたって、動作の仕組みなんかを把握したかったので、基本クラスを使いつつ、自前で実装するにはどうするか、という視点でのまとめです。
(とはいえ、ネイティブのARKitとの通信はほとんどUnity側でやってくれてしまうので、あまりむずかしいことはやりませんが)

準備

アセットのインポート

UnityのアセットストアですでにUnity ARKit Pluginが公式に配布されているのでそれをダウンロード、インポートします。

Bitbucketから最新のものが取得できるようなので、新しい機能なりを試したい場合は見てみるといいかも。

インポートが終わったら準備完了です。
ネイティブのARKitとの通信はすべてUnityのコンポーネントが行ってくれるので、それを使って構築していきます。

既存のコンポーネントを利用する

今回は、ネイティブからのデータを使って処理を行うように実装していますが、ARの機能をさっと試したいだけであれば、既存のコンポーネントを組み合わせるだけですぐにARの機能を使うことができるようになっています。

使うコンポーネント

使うコンポーネントUnityEngine.XR.iOS名前空間に定義されています。
余談ですが、Android向けビルドでVR SDKをDaydreamにすると、Unity2017以降だとXRという名前になっていて、ARCoreのチェックボックなんかも出てくるので、今後はVR x ARのポジトラは標準になりそうな予感がしますね。

使うコンポーネントは以下になります。

  • UnityARVideo
  • UnityARCameraNearFar
  • UnityARCameraManager
  • UnityARHitTestExample
  • UnityARSessionNativeInterface

UnityARVideo

iOSバイスのカメラの映像を、CommandBuffer経由で描画するようです。
カメラ自体にAddComponentして使います。
また、インスペクタにはマテリアルを設定するようになっていますが、ARKitアセットに含まれているYUVMaterialを設定してあげればOKです。

なお、カメラの映像を出力しないVRモードであっても、このコンポーネントがないとARカメラの位置トラッキングがおかしくなっていたので、もしかしたらARとしての画像解析がこのクラス経由で行われているのかもしれません。

UnityARCameraNearFar

ARカメラのNearとFarを適切に設定するコンポーネント・・・のようですが、これがないとなにがダメなのかはちょっとまだ分かっていません( ;´Д`)
このコンポーネントも、メインのカメラにAddComponentして使います。

UnityARCameraManager

カメラの動きを制御するコンポーネント。マネージャという名前の通り、これは、カメラにAddComponentするのではなく、空オブジェクトなどに設定して、インスペクタからカメラオブジェクトを登録する形で使います。

内部的な処理としては、後述するUnityARSessionNativeInterfaceクラスから、ARKitの解析データを受け取り、適切にカメラの位置をARで認識した空間に基いて移動する仕組みを提供します。

UnityARHitTestExample

画面をタッチした際に、タッチ先に平面が認識されていたらそこに3Dモデルなどを移動してくれるサンプル用コンポーネント
Hit testのやり方などが記述されているので、タッチに反応するアプリを作る場合などは参考にするとよさそうです。

UnityARSessionNativeInterface

ネイティブのARKitからの情報を受け取る最重要クラス。
基本的に、上記のような機能を自前で実装する場合はこのクラスからの値を適切に使う必要があります。

以上のコンポーネントを連携させるだけで、ARKitの機能を使った簡単なモデル配置などはすぐに行うことができます。

このコンポーネント群については、以下の記事を参考にさせていただきました。

qiita.com

ARKitの機能を使う

さて、上記のコンポーネントを使うことで簡単なモックならすぐ作れてしまうでしょう。
ここからは、それらのコンポーネントが行ってくれている部分を少し紐解きながら、ARを使ったコンテンツを作る上で必要になりそうな部分を個別に解説していきたいと思います。

ARKitで認識した位置をUnityのカメラと同期する

ARコンテンツをAR足らしめているのが、この「カメラの移動」でしょう。
空間を認識し、それに基いてカメラが適切に動いてくれることで、3Dオブジェクトなどが本当にそこにあるかのように見せることができるわけです。

UnityARCameraManagerを参考に、必要な部分だけ抜き出す

さて、先ほども紹介したUnityARCameraManagerには、このカメラの位置を同期する処理が書かれています。
といっても、内部的な処理はほぼUnityARSessionNativeInterfaceがやってくれるので、毎フレーム、現在の姿勢をカメラに適用するだけでOKです。

private void Start()
{
    _session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

    ARKitWorldTrackingSessionConfiguration config = new ARKitWorldTrackingSessionConfiguration();
    config.planeDetection = UnityARPlaneDetection.Horizontal; // 現状は`None`か`Horizontal`しか選べない
    config.alignment = UnityARAlignment.UnityARAlignmentGravity;
    config.getPointCloudData = true;
    config.enableLightEstimation = true;
    _session.RunWithConfig(config);
}

private void Update()
{
    Matrix4x4 matrix = _session.GetCameraPose();
    _arCamera.transform.localPosition = UnityARMatrixOps.GetPosition(matrix);
    _arCamera.transform.localRotation = UnityARMatrixOps.GetRotation(matrix);
    _arCamera.projectionMatrix = _session.GetCameraProjection();
}

大雑把に説明すると、ARKitの動作のConfigを作成し、それを元にセッションを開始、以後はそのセッションから得られるカメラの位置や回転を、そのままカメラのlocalPositionlocalRotationに適用してやるだけです。
こうすることで、ARのカメラとして適切に移動、回転が行われます。

UnityARAnchorManagerを元に、平面の位置のトラッキングを行う部分を抜き出す

オブジェクトを配置して、カメラの移動が行われれば、基本的にはARらしい見た目を表現することは可能です。
次は、ARKitのシステムが認識した平面の情報を使って、実際に空間に平面情報を表示する方法を見てみます。

private Dictionary<string, ARPlaneAnchorGameObject> planeAnchorMap;


private void Start()
{
    planeAnchorMap = new Dictionary<string,ARPlaneAnchorGameObject> ();

    UnityARSessionNativeInterface.ARAnchorAddedEvent += AddAnchor;
    UnityARSessionNativeInterface.ARAnchorUpdatedEvent += UpdateAnchor;
    UnityARSessionNativeInterface.ARAnchorRemovedEvent += RemoveAnchor;
}

上記の3つのイベントが、ARKitのシステムから発行されます。
それぞれ、アンカー(平面)が認識された、更新された、破棄されたタイミングで呼ばれます。

そのイベント内でどんな処理が書かれているのか見てみましょう。

ARAnchorAddedEvent

まずは、平面が認識された際のハンドラ内での処理です。

public void AddAnchor(ARPlaneAnchor arPlaneAnchor)
{
    GameObject go = UnityARUtility.CreatePlaneInScene (arPlaneAnchor);
    go.AddComponent<DontDestroyOnLoad> ();  //this is so these GOs persist across scene loads
    ARPlaneAnchorGameObject arpag = new ARPlaneAnchorGameObject ();
    arpag.planeAnchor = arPlaneAnchor;
    arpag.gameObject = go;
    planeAnchorMap.Add (arPlaneAnchor.identifier, arpag);
}

平面が認識された際の処理は、まず、UnityARUtilityクラスのユーティリティを使って平面オブジェクトを生成します。
そして、ARPlaneAnchorGameObjectクラスのインスタンスを生成し、それぞれ、GameObjectARPlaneAnchorへの参照をセットにして保持します。

あとはそれを、マネージャクラス自身が持っているDictionaryに登録しておきます。
これを登録する理由は、平面の情報は、認識後に連続した状態を持つため(※)更新時に、identifierを元に処理を行う必要があるためです。

※ 連続した情報というのは、どうやらARKitが認識した平面は一意なIDが振られ、その平面の状態がどうなったか、という連続的な計算になるようです。
そのため、検知時のIDをキーにして登録し、更新があった場合に、それを元に平面の位置などを変更してやる必要があるのです。

ARAnchorUpdatedEvent

次に更新処理。
上記でも書きましたが、更新処理は、「平面の連続性」故に、検知時のIDを利用して更新処理を行います。

public void UpdateAnchor(ARPlaneAnchor arPlaneAnchor)
{
    if (planeAnchorMap.ContainsKey (arPlaneAnchor.identifier)) {
        ARPlaneAnchorGameObject arpag = planeAnchorMap [arPlaneAnchor.identifier];
        UnityARUtility.UpdatePlaneWithAnchorTransform (arpag.gameObject, arPlaneAnchor);
        arpag.planeAnchor = arPlaneAnchor;
        planeAnchorMap [arPlaneAnchor.identifier] = arpag;
    }
}

Dictionaryに登録のある平面だった場合に、その状態を更新する処理となります。

ARAnchorRemovedEvent

最後に、平面が破棄されたときの処理。
こちらはたんに、Dictionary内にあったらその情報を削除しているだけですね。

public void RemoveAnchor(ARPlaneAnchor arPlaneAnchor)
{
    if (planeAnchorMap.ContainsKey (arPlaneAnchor.identifier)) {
        ARPlaneAnchorGameObject arpag = planeAnchorMap [arPlaneAnchor.identifier];
        GameObject.Destroy (arpag.gameObject);
        planeAnchorMap.Remove (arPlaneAnchor.identifier);
    }
}

平面に対する処理は以上です。
あとは、ARKit側で検知、更新、破棄が起こるタイミングで平面情報が更新されていきます。

画面をタップした際に、その位置の平面にオブジェクトを移動させる

ARでモデルを表示するだけでもだいぶ楽しい体験ができますが、やはりタップしたりしてインタラクティブなことができるとより楽しくなります。

private void Update()
{
    // 中略。Touchの確認処理
    
    var screenPosition = Camera.main.ScreenToViewportPoint(touch.position);
    ARPoint point = new ARPoint {
        x = screenPosition.x,
        y = screenPosition.y
    };

    // prioritize reults types
    ARHitTestResultType[] resultTypes = {
        ARHitTestResultType.ARHitTestResultTypeExistingPlaneUsingExtent, 
        // if you want to use infinite planes use this:
        //ARHitTestResultType.ARHitTestResultTypeExistingPlane,
        ARHitTestResultType.ARHitTestResultTypeHorizontalPlane, 
        ARHitTestResultType.ARHitTestResultTypeFeaturePoint
    }; 

    foreach (ARHitTestResultType resultType in resultTypes)
    {
        if (HitTestWithResultType (point, resultType))
        {
            return;
        }
    }
}

まずは、Updateメソッド内での処理です。
基本的なタッチ判定処理後だけを抜き出しています。

画面のタッチされた位置をViewport座標に変換したのち、ARpointクラスに値を設定します。
そして、検知したいResultTypeの配列を作り、順次、そのタイプに応じてタッチ位置との判定を行います。
判定は同クラスに設定されたHitTestWithResultTypeで行います。

bool HitTestWithResultType (ARPoint point, ARHitTestResultType resultTypes)
{
    List<ARHitTestResult> hitResults = UnityARSessionNativeInterface.GetARSessionNativeInterface ().HitTest (point, resultTypes);
    if (hitResults.Count > 0) {
        foreach (var hitResult in hitResults) {
            Debug.Log ("Got hit!");
            m_HitTransform.position = UnityARMatrixOps.GetPosition (hitResult.worldTransform);
            m_HitTransform.rotation = UnityARMatrixOps.GetRotation (hitResult.worldTransform);
            Debug.Log (string.Format ("x:{0:0.######} y:{1:0.######} z:{2:0.######}", m_HitTransform.position.x, m_HitTransform.position.y, m_HitTransform.position.z));
            return true;
        }
    }
    return false;
}

UnityARSessionNativeInterfaceHitTestメソッドが定義されているので、それを用いてヒットテストを行っています。
もしヒットした平面があった場合はヒット結果が1以上にあるため、それを元に分岐処理を行い、ヒットしたらその情報を出力しています。

なお、このクラスではタッチ位置にオブジェクトを移動する処理が含まれているので、同時に、設定されたオブジェクトの位置を変更する記述が見られます。

ARカメラを使ってポジトラする

さて最後に。
今回のAR関連の機能を使って、VRでのポジトラをする方法を説明します。
といっても、今までの処理を少し変えるだけなので、実装自体は大したことはしません。

まずはざっとコードを見てもらったほうが早いでしょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.iOS;

public class ARAnchorUpdater : MonoBehaviour
{
    [SerializeField]
    private Transform _target;

    [SerializeField]
    private Camera _arCamera;

    [Header("---- AR Config Options ----")]
    [SerializeField]
    private UnityARAlignment _startAlignment = UnityARAlignment.UnityARAlignmentGravity;

    [SerializeField]
    private UnityARPlaneDetection _planeDetection = UnityARPlaneDetection.Horizontal;

    [SerializeField]
    private bool _getPointCloud = true;

    [SerializeField]
    private bool _enableLightEstimation = true;

    private Dictionary<string, ARPlaneAnchorGameObject> _planeAnchorMap = new Dictionary<string, ARPlaneAnchorGameObject>();

    private UnityARSessionNativeInterface _session;

    private void Start()
    {
        _session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

        Application.targetFrameRate = 60;
        ARKitWorldTrackingSessionConfiguration config = new ARKitWorldTrackingSessionConfiguration();
        config.planeDetection = _planeDetection;
        config.alignment = _startAlignment;
        config.getPointCloudData = _getPointCloud;
        config.enableLightEstimation = _enableLightEstimation;
        _session.RunWithConfig(config);
    }

    private void Update()
    {
        // セッションからカメラの情報をもらう
        Matrix4x4 matrix = _session.GetCameraPose();

        _target.transform.localPosition = UnityARMatrixOps.GetPosition(matrix);

        // VRカメラでジャイロを使って回転するため、ここでは回転を適用しない
        //_target.transform.localRotation = UnityARMatrixOps.GetRotation(matrix);

        // ARカメラのプロジェクションマトリクスを更新
        // TODO: もしかしたらいらないかも?
        _arCamera.projectionMatrix = _session.GetCameraProjection();
    }
}

さて、見てもらうと分かりますが、上で書いたUnityARCameraManagerの中身を少しカスタマイズしただけですね。

違う点は、_targetに、ARカメラの移動を適用するためのオブエジェクトを、インスペクタから設定しているだけです。
Updateメソッド内を見てもらうと、カメラの位置同期の処理が_targetに対して行われているのが分かりますね。

そしてもうひとつ注意点として、「回転は適用しない」ということ。
なぜかというと、_targetに指定しているオブジェクトの子要素に、VRカメラが存在しているためです。
そしてVRカメラはCardboard SDKが、ジャイロを使って自動的に回転処理をしてくれます。

つまり、ARカメラの回転も伝えてしまうと、回転が二重にかかってしまうわけなんですね。
(最初それに気づかず、なんで180°回転しただけなのに一周しちゃうんだろうとプチハマりしてました・・)

なので位置だけを同期してあげればいいわけです。
まさに「ポジトラだけ」ARカメラからもらっている感じですね。

以上で、モバイルVRでもポジトラができるようになります。

その他、参考にした記事

lilea.net

recruit.gmo.jp