0 と 1

物忘れは防げない

Unity ARKit Remoteがうまく動かなくて困った話

f:id:gounsx:20180404155114j:plain

ことの経緯

youtu.be

「毎回ビルドして実機チェックするにはビルドに時間もかかるし大変!」

「そうだプラグインに含まれていたUnity ARKit Remoteを使おう。」

「iPhoneXとMacBook Proのコネクト成功!」

「・・・おかしい。」

「なんだこれは・・・1フレーム目だけCaptureImageが表示されて、それ以降は全く更新されない・・・」

「一応特徴点は検出しているし、謎色空間のCaptureImageらしきものがオーバーレイでうっすら描画されている・・・」

「察するにEditorと実機はConnectされているし一部の情報は毎フレーム送信できているものの、CaptureImage周りの更新ができていない!」

一旦Unity Forumに投げる

Unity Forumsに投げるも救世主現れず。(2018/4/4時点)
Hi, I am in trouble by Unity ARKit Remote

自力で解決することに・・・

エラー発見

f:id:gounsx:20180403163746p:plain

つまり「送信されたデータを受信側で捌ききれていないので送信することができません!」ということ。

なので受信側が毎フレーム送信されたデータを捌き切れるように圧縮処理、展開処理を加えてあげることに。

余談

原因を発見し、これから説明する修正を行った後に気がついたのですが、iPhone7なら修正しなくてもちゃんと動きました。
これはiPhoneXの画面解像度がでかすぎるためでした。 iPhone7と比べると2倍以上の大きさです。 なのでこれから行う修正はiPhoneXを使用してUnity ARKit Remoteを使いたい人向けで、それ以外の端末でしたら修正しなくても問題なく動くと思います。

f:id:gounsx:20180404124240p:plain

修正方法

送信しているデータ

OnPreRenderで毎フレーム描画前にEditor側にデータを送信しています。

  • screenCaptureYMsg : カメラに映る映像(後述)
  • screenCaptureUVMsg : カメラに映る映像(後述)
  • updateCameraFrameMsg : カメラに関するトランスフォームとか、プロジェクション行列の情報

データの容量の削減を行うためにARKitではRGBではなくYUVを使用しています。
YUVフォーマット及び YUV<->RGB変換

とは言っても毎フレーム送信するにはまだ大きすぎるので先程の3つのデータの内、上2つのデータの圧縮を行います。

大まかな流れ

byte[] dataに入っているデータを MemoryStreamを使用して、DeflateStreamによる圧縮をしてから送信するようにします。

Deflateって?な方はまずはここを見るとなんとなくわかると思います。
Deflate (Wikipedia)

UnityRemoteVideo.csiPhoneのカメラに映る映像を取得し、byte配列に格納しているのでここでDeflateを使用し圧縮を行います。

実際にEditor側にデータを送信しているのはConnecetToEditor.cs内で、Guidを指定してPlayerConnectionクラスのSendメソッドで送信を行います。

ARKitRemoteConnection.csで送信されたデータの受信を行っています。
先程のSendメソッドの第一引数で指定したGuidにcallbackを登録するEditorConnectionクラスのRegisterメソッドを使用し、データの受信を行っています。 ここでMessageEventArgs.dataのbyte配列をDeflateを使用して展開し、圧縮前の状態に戻してあげます。

要するに

  1. データ送信側でデータの圧縮を行う
  2. データ受信側でデータの展開を行う

たったこれだけです。送信部分や受信部分のロジックはすでにUnity ARKit Pluginで用意されているので一から書く必要はありません。

圧縮・展開プログラム

 /// <summary>
    /// Compress using deflate.
    /// </summary>
    /// <returns>The byte compress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteCompress(byte[] source)
    {
        using (MemoryStream ms = new MemoryStream())
        using (DeflateStream compressedDStream = new DeflateStream(ms, CompressionMode.Compress, true))
        {
            compressedDStream.Write(source, 0, source.Length);

            compressedDStream.Close();

            byte[] destination = ms.ToArray();

            Debug.Log(source.Length.ToString() + " vs " + ms.Length.ToString());

            return destination;
        }
    }

    /// <summary>
    /// Decompress using deflate.
    /// </summary>
    /// <returns>The byte decompress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteDecompress(byte[] source)
    {
        using (MemoryStream input = new MemoryStream(source))
        using (MemoryStream output = new MemoryStream())
        using (DeflateStream decompressedDstream = new DeflateStream(input, CompressionMode.Decompress))
        {
            decompressedDstream.CopyTo(output);

            byte[] destination = output.ToArray();

            Debug.Log("Decompress Size : " + output.Length);

            return destination;
        }
    }

その他の修正箇所

UnityRemoteVideo.cs

public void OnPreRender()
        {
            ~省略~

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureYMsgId, YByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureYMsgId, ByteConverter.ConvertByteCompress(YByteArrayForFrame(1 - currentFrameIndex)));

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureUVMsgId, UVByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureUVMsgId, ByteConverter.ConvertByteCompress(UVByteArrayForFrame(1 - currentFrameIndex)));
            
        }

ARKitRemoteConnection.cs

void ReceiveRemoteScreenYTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;

            //remoteScreenYTex.LoadRawTextureData(mea.data);
            remoteScreenYTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenYTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetYTexure(remoteScreenYTex);
            }

        }

        void ReceiveRemoteScreenUVTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;
 
            //remoteScreenUVTex.LoadRawTextureData(mea.data);
            remoteScreenUVTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenUVTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetUVTexure(remoteScreenUVTex);
            }

        }

注意!

DeflateStreamクラスのCopyToメソッドを使うに当たってUnityで使用している.NET Framework3.5では使うことができなかったので .NET Framework4.6を使用しています。
Unity2017から利用できるのですが、Experimental(実験的)と記載されています。

ただ、このままビルドして端末にインストールするとエラーが出て動かないので
UnityARKitRemoteシーンのビルドは.NET Framework3.5、ビルド後に .NET Framework4.6に変更してEditorを実行するようにしてください。

これをする理由はCopyToメソッドを使いたいだけなのでデータの展開の際に同等の処理を実行できればなんでも良いので、面倒でしたら書き換えてしまって.NET Framework3.5で動くようにしてしまったほうが楽かもしれません。

結果

youtu.be

f:id:gounsx:20180404144521p:plain

圧縮後のデータサイズが赤文字です。

1フレームに送るデータ量を約1/3程度まで小さくすることができました。

表からも分かるようにiPhoneXではiPhone7に比べて2倍近いデータを送っていました。

これは推測ですが、Unityの中の人はiPhoneXでUnity ARKit Remoteのデバッグを行っていないのかもしれません・・・

紹介動画ではiPadで動作確認をしていました。

www.youtube.com

修正に入る前に念の為

  • Wifiをオフにして確実に優先で接続
  • サードパーティのケーブルではなく純正のケーブルを使用
  • 2014年のMacBook Proのスペックを疑い、2015年モデルでも確認

を行ったのですがどれも同じ結果でした・・・

何はともあれ、データを圧縮して送ることで無事動きました^^

これで実機にビルドしなくても、Editorに接続した実機上で動作確認することができるようになりました。

開発のイテレーションを速く回すためにはこうした環境作りも大切ですね。

ScriptableObjectを使用したデータ管理

ScriptableObjecetを継承したデータクラスを実装

CharacterDataSet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterDataSet : ScriptableObject
{
    public List<Param> DataList = new List<Param>();
}

[System.Serializable]
public class Param
{
    public string Name;
    public Sprite Image;
    public GameObject Model;
}

保存用フォルダ作成

Resources/ScriptableObjecet

ScriptableObjectをアセットとして保存するためのクラスを実装

CharacterDataGenerator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class CharacterDataGenerator
{
    [MenuItem("Assets/Create/CharacterData")]
    static void CreateCheracterData()
    {
        CharacterDataSet data = ScriptableObject.CreateInstance<CharacterDataSet>();
        string path = AssetDatabase.GenerateUniqueAssetPath("Assets/Resources/ScriptableObject/CharacterDataSet.asset");
        AssetDatabase.CreateAsset(data, path);
        AssetDatabase.Refresh();
    }
}

UnityEditorスクリプトなのでEditorフォルダ以下に移動させる必要があります。

読み込み方法

CharacterManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CheracterManager : MonoBehaviour
{
    private CharacterDataSet _characterDataSet;

    private void Start()
    {
        _characterDataSet = Resources.Load<CharacterDataSet>("ScriptableObject/CharacterDataSet");
        Debug.Log(_characterDataSet.DataList.Count);
    }
}

Perspective Divide

ワールド座標から正規化デバイス座標への変換 (NDC)

NDC (Normalized Device Coordinates)

クリップ座標をwで割ると正規化デバイス座標になります。
視錐台の内容をクリップ空間の中に収め、スクリーン描画で使用します。

3Dから2Dへの座標変換の流れ

ローカル空間
ワールド座標変換
ワールド空間
ビュー座標変換
カメラ(ビュー)空間
プロジェクション変換
クリップ空間
NDC
正規化デバイス空間
スクリーン座標変換
スクリーン空間

注意

正規化によるクリップ空間(Z値を収める範囲)はプラットフォームによって異なります。

-1 <= x <= 1
-1 <= y <= 1
-1 <= z <= 1 (OpenGL)
0 <= z <= 1 (DirectX)

Unityは、ワールド空間では左手座標系、ビュー空間以降の変換ではOpenGLに準拠しています。

参考

座標変換
正規化デバイス座標
Essentials of Interactive Computer Graphics: Concepts and Implementation K. Sung, P. Shirley, S. Baer Chapter 14 Chapter 14: The Camera.