【Unity】顔だけVTuberになるARアプリを作った【実装編】

こんにちはっ🌟八ツ橋まろんです

iPhoneの顔認識の機能を使った **顔だけVTuberになるiPhoneアプリ『Virtual Face』を作りました**

この記事では、内部の動作と実装について書いていきます。

動作デモ↓↓

https://twitter.com/Maron_Vtuber/status/1564733099950321664?s=20&t=ExLluQH39mlfEcohYJ6jGA

# 0. 初回起動時のチュートリアル

アプリを最初に起動したときに出てくる使い方画面。どんなアプリでも実装されていますが、Unityで開発するときは、PlayerPrefesを使うことで簡単に作成できました。

PlayerPrefesを使えば、int, float, stringの簡単な変数なら簡単に保存したり、呼び出したりできます。

```csharp:
public GameObject tutorial;

// PlayerPrefesに"Tutorial"という名前でデータが保存されているか確認し、
//データ内容が1ならチュートリアルをスキップする
void Start()
{
    if(PlayerPrefes.GetInt("Tutorial", 0) == 1)
        tutorial.SetActive(false);
}

// チュートリアル終了時に呼び出し、PlayerPrefesにデータを保存する
public void Tutorial_End()
{
    PlayerPrefes.SetInt("Tutorial", 1);
}
```

# 1. 初回起動時のフォルダ作成

Virtual Faceでは、任意のVRMファイルを読み込むことができます。
アプリ起動時、特定のフォルダ内にあるVRMファイルを全て読み込みます。
特定のフォルダはアプリ側で作る必要があるので、起動時フォルダの有無を確認し、無ければ作成します。

フォルダを作成する時はApplication.persistentdatapathを使うと、iPhone標準のファイルアプリで扱えるパスが取得できます。

以下のようなコードでフォルダ作成をすることで、Virtual Face用のVRMのフォルダを作成できました。

```csharp:
using System.IO;

void Start()
{
    var path = Application.persistentDataPath + "/VRM/";
    Directory.CreateDirectory(path);
}
```

まだ続きがあります。iPhoneの標準のファイルアプリを使って上記で作成したフォルダを閲覧&VRMファイルを入れるわけですが、XCode上できちんと設定しないとファイルアプリで閲覧できないそうです。

以下のようにXCode上で設定を2つ追加し、ValueをYESにすることで、iPhoneのファイルアプリで閲覧できるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/5daef616-c7f8-a073-e72d-eec330efd92d.png)

そしてまだ続きがあります。Application.persistentdatapath以下のフォルダはデフォルトでiCloudにデータのバックアップをとるようになっているそうです。今回はバックアップしなくていいので、アプリ起動時に以下のようにコードで「バックアップしないで」と明示します。

```csharp:
void Start()
{
#if UNIYT_IOS
    UnityEngine.iOS.Device.SetNoBackupFlag(Application.persistentDataPath);
#endif
}
```


# 2. VRMの読み込み
いろんな方の記事を参考に、VRMの一斉ロード&表示を実装しました。実装しましたとは言っても、正直なところソフトウェアエンジニアじゃないので、VRMの読み込み～表示までの実装内容は参考にしたコードたちを読んでも全然わかりませんでした。

[【UniVRMを使ってVRMモデルをランタイムロードする方法】](https://qiita.com/sh_akira/items/8155e4b69107c2a7ede6)


読み込んだ後、Tポーズ状態で表示されると画面が埋め尽くされて邪魔なので、ロード時に上空3000mに飛ばしてから表示します。



# 3. VRMの顔変換

Virtual Faceでは読み込んだVRMから顔だけのモデルを作成します。
やることは単純で、キャラクターのスケールを1/1000にしたあと、Headボーンのスケールを1000倍します。単純な作り方ですが、おおむね上手く動作しています。

愚直にそのままスケール変更するとSpringBoneが爆発して髪型が大変なことになので、いったんSpringBoneをオフにしてスケール変更し、その後SpringBoneを有効にします。

# 4. 前回の設定の読み込み

Virtual Faceでは、

アプリ起動 → ボタンを押して使用するVRMをフォルダから選ぶ　という手順ではなく、
アプリ起動 → VRMを自動でロード → 前回使用したVRMを自動で選択
という動作をします。

アプリ起動時に、前回使ったVRMファイル名と、顔のオフセット値(縦、奥行き、角度、サイズ)をPlayerPrefesから読み出すことで、毎回キャラクター選択やオフセット調整しなくていいようにしました。

PlayerPrefes、すごく便利です。

VRMを作り直した場合でも、ファイル名(例：Maron.vrm)が一致していれば同じオフセットが適用されます。

# 5. VRMの変更

これが一番大変でした。

Virtual FaceはAR Foundation Sampleをもとに作っており、顔として使いたいPrefabはAR Face Managerというコンポーネントにアタッチするのですが、動的に変更したい場合、コンポーネントへの再アタッチだけでは反映されない作りになっていました。

調べると、どうやら一度ARの機能をリセットする必要があるとのことだったのですが、そのリセット方法が見つからず、関連しそうなコンポーネントの関数や、オブジェクト、コンポーネントの削除&再生成などいろいろ試しました。

AR機能はMac上では動かないので、毎回iPhoneにビルドして確認するのが非常に面倒で時間がかかりましたが、最終的にAR SessionコンポーネントのARSession.subsystem.Reset()を呼んであげることで正常にリセットできるとわかりました。
頭部モデルの変更やオフセットの設定の際に都度リセットしています。

# 6. OSCの実装

これは[hecomi様のuOSC](https://github.com/hecomi/uOSC)に感謝する他ありません。とても簡単にOSCが実装できました。

アプリ内で、受信側のIPアドレスを設定するところがあります。入力値がIPアドレスに準拠しているか否かを調べる関数(IPAddress.TryParse)があると知り、入力間違いやエラー防止にこれを使いました。

```csharp:
using System.Net;
 
IPAddress iPAddress;
public void CheckIPAddress(string address)
{
    if(!IPAddress.TryParse(address, out iPAddress))
    {
        // 入力がIPアドレスの形式じゃなかった時の処理
    }
} 
```

# 7. フィルターの実装

おまけとして、色変換と手書き風エフェクトの実装をしました。
前に[Comic Toon Shader](https://maronvtuber.booth.pm/items/3400911)を販売した時に作ったPostProcess用のシェーダーを流用し、色変換・手書き風エフェクトを実装しようと思ったのですが、(おそらく)描画順の関係で、そもそもPostProcessがキャラクターやカメラ映像に対して効きませんでした。
[画像：Comic Toon Shaderに同梱している手書き風エフェクト例]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/38648bd5-0137-0043-0403-104116cc2595.png)


疑似的にポストプロセスをするシェーダーをいくつか作ったことがあったので、自作シェーダーの中からいくつか試して試行錯誤したところ、[VRChat・Styly用に作ったシェーダー](https://maron.fanbox.cc/posts/2916046)が有効だとわかったので、これを移植して使うことで疑似的にPostProcessを再現しました。
(このシェーダーを使ったマテリアルを持つオブジェクトが視界に入るとポストプロセスがかかるシェーダーです。VRChatやStylyでポストプロセスを動的に動かすのに有効な面白いシェーダーですが、性質上視界ハックに使えてしまうので限定公開です。)

[画像：最終的に実装できた手書き風エフェクト]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/c209d38a-1d94-b6ae-9eac-fec4028cf0c7.png)


# 8. 首のマスク機能の実装

キャラクターを頭部だけに変換し、カメラ映像に上書きしている都合上、リアルの首が後ろ髪によって上書きされてしまいます。

そこで、スクリプトによって頭部に円柱っぽいオブジェクトを刺すことで、疑似的な首付きの頭部にしました。この円柱オブジェクトは、デバッグモードでは水色で可視化できますが、通常時はマスクとして機能し、オブジェクトが描画されない領域になります。

[画像：左の画像の点線の位置で、後ろ髪がカットされているのがわかる。右がスクリプトで頭部に刺した首オブジェクト]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/b7637401-8672-20de-ba68-954fe29b9de6.png)

実際には、首オブジェクトはカメラ映像を投影する特殊なシェーダーで作っているため、首を描画する=カメラ映像を描画するという作りになっています。

首オブジェクトとして描画しているので、上から重ねるようなやり方と違い前後関係もしっかり反映してくれるのがいいところですね。

次に紹介する美肌フィルターの仕組みと合わせると、以下の画像の右側のようにとても上手く首のマスクができます。

[画像：左：首のマスク機能、中央：マスク範囲、右：後述の美肌フィルターの仕組みを合わせた首のマスク機能]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/ce194fd3-5c33-a8cb-ae8e-ba9a2d5016bf.png)


# 9. 美肌フィルターの実装

自撮りと言えば美肌フィルターですが、Virtual Faceではキャラクターの頭部と色を合わせる目的で実装しています。

[画像：左は元々のカメラ映像、右は美肌フィルター適用後のカメラ映像。キャラクターの顔色により近づいて自然になっている。]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/d9a4b55b-4efa-a3eb-3a40-4ae2c8e937cf.png)


『カメラ映像の特定の色部分のみ色変更を施す』ということをやりました。

AR Foundationでは、カメラ映像をCustomARBackGroundシェーダーでいじることができます。

[【Unity】[保存版] 自作Shader向けの便利な変数変換やノイズ関数集](https://qiita.com/Maron_Vtuber/items/7e8e5f55dfbdf4b5da9e)

で紹介している色変換の関数を使い、カメラ映像の特定の色範囲について、色相や彩度を調整しています。

色範囲の選択はどう実装しようか悩んだのですが、最終的に

『自身の肌の色を画面をタップして取得してもらい、それに近い色を標準正規分布を上手く使って判定する』

という実装にしました。具体的には、
①カメラ映像をRGB→HSVに変換する(変換後のHSVは0～1に正規化)
②一般的な標準正規分布の数式を少しいじり、[x = ±1 で 4σ]、[x = 0 で 1]となるように改造
③画面タップで得られた色(c)とカメラ映像のピクセル(p)の色が近いかどうかの係数を計算(x = |c-p|として②の標準正規分布から算出) 
④H(色相)とS(彩度)について③を計算し、掛け合わせたものを係数とする
⑤カメラ映像全体に色変更をかけるが、係数で影響度合いが変わるため、実質選択した色範囲のみ適用される

という具合です。背景に影響を及ぼさないが、ある程度の肌色はおおむね影響を受けるような係数作りに少し苦労しましたが、アプリではおおむね上手く動いています。



以上、顔だけVTuberになるアプリ「Virtual Face」の実装解説の記事でしたっ🌟

良ければアプリで遊んでみてください✨

App Storeのリンク↓↓

https://t.co/6U9ok0WhJS

アプリのUI編は[こちら](https://qiita.com/Maron_Vtuber/items/3df1b638d08f10d2c2d0)

またね🌟

八ツ橋まろん