UnityとARkitでVtuberアプリを作ってみた

#はじめに
最近の流行りのVtuberになれるアプリ作ってみました。

このアプリは２つのアプリから成り立っていて
iPhone側でフェイスキャプチャー用が
PC側(Windows10)でLive2Dの制御用です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">動きよくなりましたね <a href="https://t.co/NB5034WkeB">pic.twitter.com/NB5034WkeB</a></p>&mdash; Teal (@teal_rx39) <a href="https://twitter.com/teal_rx39/status/1232162838375845888?ref_src=twsrc%5Etfw">February 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
動画左がiPhoneのアプリで、右側がPCアプリです。
※この動画では下のサンプルコードで取得してる部位以外も使用しています。


##開発環境

Unity2019.2.15f1
Windows10
iPhoneXR 
ios13.3
Mac 10.15.1
Xcode 11.3
ARFoundatinは、Unity公式のGitHubからARFoundationの最新プロジェクトをクローンしてきます。
[ARFoundatinリンク](https://github.com/Unity-Technologies/arfoundation-samples)
Live2DモデルはCubism3.0桃瀬ひよりを使用しました。

*※ARFoundationに関しては自分も勉強途中なので間違っているところもあるかもしれません*
#iPhone側のアプリについて
今回はARKitFaceBlendShapesのシーンをメインで使っていきます。

iPhone側のアプリで参考にするのは**ARKitBlendShapeVisualizer.cs**になります。
今回はこれをもとに改造していきます。

###ARKitBlendShapeVisualizerの簡単な説明
ARFaceコンポーネントを取得していて、これを軸に作成されています。
ARFaceは顔情報の更新が検知できるので、ここのupdatedに更新ごとの処理を登録していく形になります。
###データの取得


```c#ARKitBlendShapeVisualizer.cs

 void UpdateFaceFeatures()
    {
        if (skinnedMeshRenderer == null || !skinnedMeshRenderer.enabled || skinnedMeshRenderer.sharedMesh == null)
        {
            return;
        }  

        using (var blendShapes = m_ARKitFaceSubsystem.GetBlendShapeCoefficients(m_Face.trackableId, Allocator.Temp))
        {
            foreach (var featureCoefficient in blendShapes)
            {
                int mappedBlendShapeIndex;
                if (m_FaceArkitBlendShapeIndexMap.TryGetValue(featureCoefficient.blendShapeLocation, out mappedBlendShapeIndex))
                {
                    if (mappedBlendShapeIndex >= 0)
                    {
                        skinnedMeshRenderer.SetBlendShapeWeight(mappedBlendShapeIndex, featureCoefficient.coefficient * coefficientScale);
                    }
                }
                switch (featureCoefficient.blendShapeLocation)
                {
                    case ARKitBlendShapeLocation.EyeBlinkLeft:
                        motionDate.Leye = featureCoefficient.coefficient;
                        break;
                    case ARKitBlendShapeLocation.EyeBlinkRight:
                        motionDate.Reye = featureCoefficient.coefficient;
                        break;
                    case ARKitBlendShapeLocation.JawOpen:
                        motionDate.Mouth = featureCoefficient.coefficient;
                        break;
                }
            }   
        }
    }
```

基本的に参考にしているscript内のUpdateFaceFeaturesメソッドで実装されています。

NativeArrayの中に構造体で格納されているので必要な部位のデータだけ
取得します。

foreachを使って構造体ごとに取り出していきます。
部位の選択はstruct内の部位情報と自分の欲しい部位が一致しているかで行っています。
**ARKitBlendShapeLocation**(enum)で部位指定できます。

switch文部分が追加したコードです

データ格納用クラスを作成し
格納クラス→JSON→byteと変換しPCに送信していきます
送信タイミングは顔データが更新されると送信としました。

送信方法はUDPで実装しています。
PC側が受信、iPhone側で送信で実装しています

よく使われるUDPの通信です。
すでに多くの資料が公開されているので通信部分は省略します。
(UDP C# 実装で検索)

#ここからはPC側のアプリについてです
データが受信できたら復元しLive2Dに適用していきます
データを復元したタイミングでnullチェックしておくのがオススメです

モデルへのデータ適用はLive2D公式のドキュメントのパラメーター更新と同じ方法です([ドキュメントリンク](https://docs.live2d.com/cubism-sdk-tutorials/about-parameterupdating-of-model/))

###データ適用時の注意点だけ記述します

1. 取得したデータは個人差があるので1.0〜0.0でキレイに取得できることは無いです
なので各部位ごとに平均最大値と平均最小値を算出し
正規化した数値を適用したほうがきれいに見えると思います
2. 目の開きのデータはLive2Dと最大最小が逆で取得されるので数値の変換が必要になります
3. 部位によって最大最小値が異なるのでその対応も必要になります
4. 純粋なARkitの数値が入っているという感じでない部位がある。


#作っている中で躓いたポイントです

###Wi-Fiでの通信ができない
PC側でWi-Fiの設定をプライベートに変更
あとはポートの設定も変えとく(windows10 ポート開放で検索)

iPhone側でWi-Fi設定からプロキシ設定を手動
サーバに使用するIPアドレスを
ポートに使用するポート番号を入力して保存

###上の設定しても通信ができない
多分ルーターとかのセキュリティ問題　(学校の学科専用Wi-Fiでやったらできなかった)

###急に接続が切れた
どうやらiPhoneで残り電池残量等のポップアップが出ると接続が切れるっぽいので再接続

##その他
###NativeArrayの部分を改造するときは開放処理を忘れずに
NativeArrayは指定したAllocatorのタイプごとに開放処理が必要です
今回は、Tempを指定したので1フレーム以内です!
詳しくは Unity NativeArrayで検索！
(*UnityDOTS*などでも使われるので、これからは必須の知識になりそう)