これちのPost-it

技術ネタをペラペラと

ARKit を Unity エディタ上で動作確認出来る、ARKitRemoteについて調査した

ARKitRemoteとは

Unity エディタ上で ARKit アプリの動作確認を行うための、iOS 端末上で動作するアプリです。

背景

本来、Unityで開発しているアプリを iOS 端末上で動作確認するためには、いちいち Unity のアプリを Xcode 用にビルドし、それを Xcode 上でビルドして iOS 端末にインストールする必要があります。
それはめんどくさいです。Unity エディタ上だけで動作確認が出来れば非常に便利ですよね。
しかし、ARKit の特性上 iOS 端末からのカメラ映像やセンサーの値が必要です。
それらを iOS 端末から Unity エディタに送信して、Unity 側でよしなに受け取った値をもとに動作をエミュレートさせよう、というのが ARKitRemote シーンの役割です。

動作するアプリ

なので、まずは大きく2つのアプリが動作することになります。

  1. iOS 上で動作する、カメラ映像などを Unity エディタに送信するアプリ
    • ARKitRemoteが動作
    • UnityARKitPlugin -> ARKitRemote -> ARKitRemote.Unityのシーンそのもの
    • 以降リモートアプリと呼ぶ
  2. Unity エディタ上で動作するアプリ
    • 本来作成したいアプリ
    • 以降Myアプリと呼ぶ

動作環境

  • Unity 2017.2.0f3
  • iPhone6s plus (iOS11.2)
  • Xcode 9.2

使い方

まずUnityRemoteKitの使い方は以下のサイトが参考になるので、読んで使ってみてください。
lilea.net
本記事では、ARKitRemote(と、Myアプリに何が必要か)について調査したいと思います。

ARKitRemote シーン

まず中のシーンを開いてみますと以下のようになっており、Main CameraとDirectional LightしかHierarchyにないことが分かると思います。
f:id:korechi:20171205175415p:plain
そして、この Main Camera にアタッチされている、ConnectToEditor.csUnityRemoteVideo.csが ARKitRemote シーンのみに存在していることが他サンプルシーンと比較すると分かります。
(UnityARVideo.cs は他の ARKit サンプルシーンでも使われているスクリプトです)
そのため、上記の2スクリプトを見ていきます。

ConnectToEditor.cs

大まかに言うとこのクラスは、リモートデバイスでの変化(フレームの更新やAnchorの更新)のタイミングのたびに情報を Myアプリ(Editor上で本来動作してる)に送信するクラスです。

PlayerConnection playerConnection;

void Start()
{
        Debug.Log("STARTING ConnectToEditor");
        editorID = -1;
        playerConnection = PlayerConnection.instance;
        playerConnection.RegisterConnection(EditorConnected);
        playerConnection.RegisterDisconnection(EditorDisconnected);
        playerConnection.Register(ConnectionMessageIds.fromEditorARKitSessionMsgId, HandleEditorMessage);
        m_session = null;
}

Start() 時に PlayerConnection インスタンスを生成しています。
これはエディタ、プレイヤー間の接続を扱うクラスです。 この接続はプロファイラとプレイヤーを接続することで確立されます。
インスタンス生成後、接続開始時・終了時のコールバックを設定し、大事なのはその後の処理。
playerConnection.Register(ConnectionMessageIds.fromEditorARKitSessionMsgId, HandleEditorMessage);の第1引数にはConnectionMessageIds.fromEditorARKitSessionMsgIdというのが含まれており、これはARKitRemote.unityと同じ階層にあるConnectionMessageIds.csにIDが設定されているのが確認できます。
このIDは、当然Myアプリ側から同じ値をリモートアプリに接続要求する際に送信されます。(詳しくは、ARKitRemoteConnection.cs参照)
第2引数には、コールバック関数が指定されるため、メッセージが届くたびに HandleEditorMessage () が呼ばれます。

void HandleEditorMessage(MessageEventArgs mea)
{
        serializableFromEditorMessage sfem = mea.data.Deserialize<serializableFromEditorMessage>();
        if (sfem != null && sfem.subMessageId == SubMessageIds.editorInitARKit) {
            InitializeARKit ( sfem.arkitConfigMsg );
        }
}

HandleEditorMessage 内では、メッセージをデシリアライズし、初期化関数が呼ばれます。

UnityARSessionNativeInterface m_session;

void InitializeARKit(serializableARKitInit sai)
{
        #if !UNITY_EDITOR

        //get the config and runoption from editor and use them to initialize arkit on device
        Application.targetFrameRate = 60;
        m_session = UnityARSessionNativeInterface.GetARSessionNativeInterface();
        ARKitWorldTrackingSessionConfiguration config = sai.config;
        UnityARSessionRunOption runOptions = sai.runOption;
        m_session.RunWithConfigAndOptions(config, runOptions);

        UnityARSessionNativeInterface.ARFrameUpdatedEvent += ARFrameUpdated;
        UnityARSessionNativeInterface.ARAnchorAddedEvent += ARAnchorAdded;
        UnityARSessionNativeInterface.ARAnchorUpdatedEvent += ARAnchorUpdated;
        UnityARSessionNativeInterface.ARAnchorRemovedEvent += ARAnchorRemoved;

        #endif
}

前半ではARKitに関する初期化を行い、後半ではフレームの更新や平面の検知・更新・削除の際のイベントに関数を追加しています。UnityARSessionNativeInterfaceクラスの中身の一部は以下のようになっています。

// UnityARSessionNativeInterface.cs

public class UnityARSessionNativeInterface {
//      public delegate void ARFrameUpdate(UnityARMatrix4x4 cameraPos, UnityARMatrix4x4 projection);
//        public static event ARFrameUpdate ARFrameUpdatedEvent;
    
    // Plane Anchors
    public delegate void ARFrameUpdate(UnityARCamera camera);
    public static event ARFrameUpdate ARFrameUpdatedEvent;

ARKitでは水平面を自動で認識しそこにARAnchorを設置してくれるようになっています。
Anchors 以外のイベント(例えばARFaceAnchorAdded)もとれるみたいですが、今回は使いません。(FaceID関係かな?)
あとは、イベントが発生したタイミングで MyアプリにplayerConnection.Send()を使って送信しています。

public void ARFrameUpdated(UnityARCamera camera)
{
    serializableUnityARCamera serARCamera = camera;
    SendToEditor(ConnectionMessageIds.updateCameraFrameMsgId, serARCamera);
}

public void ARAnchorAdded(ARPlaneAnchor planeAnchor)
{
    serializableUnityARPlaneAnchor serPlaneAnchor = planeAnchor;
    SendToEditor (ConnectionMessageIds.addPlaneAnchorMsgeId, serPlaneAnchor);
}

public void SendToEditor(System.Guid msgId, object serializableObject)
{
    byte[] arrayToSend = serializableObject.SerializeToByteArray ();
    SendToEditor (msgId, arrayToSend);
}

public void SendToEditor(System.Guid msgId, byte[] data)
{
    if (playerConnection.isConnected)
    {
        playerConnection.Send(msgId, data);
    }
}

送っている情報は、ARFrameUpdated()では serializableUnityARCamera のインスタンス、ARAnchorAdded()(と書いていないがARAnchorUpdated()、ARAnchorRemoved())ではserializableUnityARPlaneAnchor のインスタンスが送られる。何が送られているのかちょっと覗いてみると

// ARKitRemote -> SerializableObjects.cs

    [Serializable]  
    public class serializableUnityARCamera
    {
        public serializableUnityARMatrix4x4 worldTransform;
        public serializableUnityARMatrix4x4 projectionMatrix;
        public ARTrackingState trackingState;
        public ARTrackingStateReason trackingReason;
        public UnityVideoParams videoParams;
        public serializableUnityARLightData lightData;
        public serializablePointCloud pointCloud;
        public serializableUnityARMatrix4x4 displayTransform;
 [Serializable]  
    public class serializableUnityARPlaneAnchor
    {
        public serializableUnityARMatrix4x4 worldTransform;
        public SerializableVector4 center;
        public SerializableVector4 extent;
        public ARPlaneAnchorAlignment planeAlignment;
        public byte[] identifierStr;

serializableUnityARMatrix4x4というのはMatrix4x4クラスのようなものです。
全部の変数の型を調べてはいませんが、名前からなんとなく送られているデータを察することはできます。
pointCloud 変数には特徴点が含まれているのでしょうね

UnityRemoteVideo.cs

    public ConnectToEditor connectToEditor;
    private UnityARSessionNativeInterface m_Session;

    public void OnPreRender()
    {
        ARTextureHandles handles = m_Session.GetARVideoTextureHandles();
        if (handles.textureY == System.IntPtr.Zero || handles.textureCbCr == System.IntPtr.Zero)
        {
            return;
        }

        if (!bTexturesInitialized)
            return;
        
        currentFrameIndex = (currentFrameIndex + 1) % 2;

        Resolution currentResolution = Screen.currentResolution;

        m_Session.SetCapturePixelData (true, PinByteArray(ref m_pinnedYArray,YByteArrayForFrame(currentFrameIndex)), PinByteArray(ref m_pinnedUVArray,UVByteArrayForFrame(currentFrameIndex)));

        connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureYMsgId, YByteArrayForFrame(1-currentFrameIndex));
        connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureUVMsgId, UVByteArrayForFrame(1-currentFrameIndex));         
    }

UnityRemoteVideo.csクラス内では、OnPreRender() が呼ばれており、これはカメラがシーンのレンダリングを開始する前に呼び出されます。関数の実行順はここを参考。
最終的に、connectToEditor.SendToEditor() でbyte[] を送信しています。
詳しくないですが、ここで送信されている Y やら UV は、色空間を表すものらしいので、おそらくこれがビデオ映像の一部ではないかと思われます。

Unity エディタ上で動作するアプリ

ふぅ長くなりましたね。次は、Myアプリ上での処理について見ていきましょう。
今回使用するのはUnityARKitPlugin -> ARKitRemote -> EditorTestScene.unityです。
まずは、このシーン内で他の ARKit サンプルシーンと何が違うか見てみましょう。
f:id:korechi:20171205195956p:plain
ARKitRemoteConnection というプレハブと、HitCubeにEditorHitTest.csがアタッチされているのが分かると思います。それぞれ見ていきましょう。

ARKitRemoteConnection.cs

ARKitRemoteConnection プレハブは、ARKitRemoteConnection.cs がアタッチされているだけのオブジェクトです。
まずは、Start() の中の一部を見てみましょう。

// Start(): 
    editorConnection = EditorConnection.instance;
    editorConnection.Initialize ();
    editorConnection.RegisterConnection (PlayerConnected);
    editorConnection.RegisterDisconnection (PlayerDisconnected);

    editorConnection.Register (ConnectionMessageIds.updateCameraFrameMsgId, UpdateCameraFrame);
    editorConnection.Register (ConnectionMessageIds.addPlaneAnchorMsgeId, AddPlaneAnchor);
    editorConnection.Register (ConnectionMessageIds.updatePlaneAnchorMsgeId, UpdatePlaneAnchor);
    editorConnection.Register (ConnectionMessageIds.removePlaneAnchorMsgeId, RemovePlaneAnchor);
    editorConnection.Register (ConnectionMessageIds.screenCaptureYMsgId, ReceiveRemoteScreenYTex);
    editorConnection.Register (ConnectionMessageIds.screenCaptureUVMsgId, ReceiveRemoteScreenUVTex);

EditorConnection のインスタンスを取得し、コールバック関数を複数設定しています。
EditorConnection はEditor側からプレイヤー側に接続要求をするクラスです。
AddPlaneAnchor() を見てみましょう。

void AddPlaneAnchor(MessageEventArgs mea)
{
    serializableUnityARPlaneAnchor serPlaneAnchor = mea.data.Deserialize<serializableUnityARPlaneAnchor> ();

    ARPlaneAnchor arPlaneAnchor = serPlaneAnchor;
    UnityARSessionNativeInterface.RunAddAnchorCallbacks (arPlaneAnchor);
}

メッセージをデシリアライズし、それをUnityARSessionNativeInterfaceのコールバック関数に登録しています。
こうすることで、カメラフレームの更新があるたびにそれがそのままUnityARSessionNativeInterfaceに送られます

void ReceiveRemoteScreenYTex(MessageEventArgs mea)
{
    if (!bTexturesInitialized)
        return;
    remoteScreenYTex.LoadRawTextureData(mea.data);
    remoteScreenYTex.Apply ();
    UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
    if (arVideo) {
        arVideo.SetYTexure(remoteScreenYTex);
    }

}

こっちはテクスチャの方で、最後のarVideo.SetYTexure(remoteScreenYTex);いかにもリモートアプリから送られてきたテクスチャを Editor 上に表示させているっぽいですね。(詳しくは分かりませんがおそらくそう)
と、まぁ大まかに、
- Editor側からプレイヤー側に接続要求 - リモートアプリから受信した情報をUnityARSessionNativeInterfaceに送る

でいいのかな?(ちょっと自信ない)

EditorHitTest.cs

こっちは非常に単純です。
Editor上で疑似タップ操作を行えるように、Input.GetMouseButtonDown を Update 関数の中で呼んでいるだけです。

#if UNITY_EDITOR   //we will only use this script on the editor side, though there is nothing that would prevent it from working on device
void Update () {
    if (Input.GetMouseButtonDown (0)) {
        Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
        RaycastHit hit;

        //we'll try to hit one of the plane collider gameobjects that were generated by the plugin
        //effectively similar to calling HitTest with ARHitTestResultType.ARHitTestResultTypeExistingPlaneUsingExtent
        if (Physics.Raycast (ray, out hit, maxRayDistance, collisionLayerMask)) {
            //we're going to get the position from the contact point
            m_HitTransform.position = hit.point;
            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));

            //and the rotation from the transform of the plane collider
            m_HitTransform.rotation = hit.transform.rotation;

つまり、この関数がないと Editor 上でタップができなくなるので、ただ眺めるだけのアプリになります。笑

自分のアプリをEditor上で動作確認するためには

  1. ARKitRemoteConnection プレハブをヒエラルキーに配置
    • これだけでも見た目は確認可能
  2. EditorHitTest.cs のような Input.GetMouseButtonDown (0) が書かれたスクリプトをどこかに記述する