概要
ずっと気になっていたARKit。やっと触ることができたので、ひとまず、空間認識して色々触ったあと、VRのポジトラに流用するのをやってみたのでまとめておきます。
AR自体がポジトラしてモデルなんかを表示できるので、これをVRモードのカメラの位置に転化してあげる、という流れです。
実際に動かしてみた動画です↓
ARKit使って、モバイルVRでポジトラさせてみた。意外とちゃんと動く。ちなみにキャラはオリジナルコンテンツの女神w #中二病VR pic.twitter.com/1W9UZUte8Z
— edom18@VR (@edo_m18) 2017年11月5日
ひとまず、単純に空間を認識して平面などを配置、カメラを移動する、いわゆる「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の機能を使った簡単なモデル配置などはすぐに行うことができます。
このコンポーネント群については、以下の記事を参考にさせていただきました。
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を作成し、それを元にセッションを開始、以後はそのセッションから得られるカメラの位置や回転を、そのままカメラのlocalPosition
とlocalRotation
に適用してやるだけです。
こうすることで、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
クラスのインスタンスを生成し、それぞれ、GameObject
とARPlaneAnchor
への参照をセットにして保持します。
あとはそれを、マネージャクラス自身が持っている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; }
UnityARSessionNativeInterface
にHitTest
メソッドが定義されているので、それを用いてヒットテストを行っています。
もしヒットした平面があった場合はヒット結果が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でもポジトラができるようになります。