Unityとモバイル端末からのセンシング（Unityゆるふわサマーアドベントカレンダー2019）

#はじめに
この記事は[Unityゆるふわサマーアドベントカレンダー 2019](https://qiita.com/nkjzm/items/7c933bb5c772a0b75512)の22日目の記事です。
Unityで開発した、趣味で作っているアプリの紹介です。

#作ろうとしているもの
バーチャルモトブロガー向けライブ配信システム
###なぜ作ろうとしているか
バイク界隈には、**モトブロガー(Motovlogger)**というツーリングやバイクに関する動画を配信（**Motovrog**という）しているYoutuberがいます。

[Motovrog（wikipedia）](https://en.wikipedia.org/wiki/Motovlog)
>A motovlog is a type of video log recorded by a person while riding a motorcycle or any motorized vehicle. The word is a neologism and portmanteau derived from "motorcycle", "video" and "log".

このモトブロガーですが、このバーチャルユーチューバーの乱世の中、準備中の方は何人かお見受けしましたが、いまだにバーチャルなモトブロガーとして活動している人はいません。
頻度はともかく、3Dモデルを使ってバイクに関する活動をしていたとしても、通常のツーリングや動画の右下に擬人化したバイクのモデルを表示したり、バ美声したりして実況しているにとどまっています。

動画配信をしているモトブロガーとバ美肉って絶対相性いいですよね？
少なくともバーチャル化したときの意義とネタには困らなさそうです。
そこで、バーチャルモトブロガーのための配信システムを作ろうと試みました。
**Youtuber ⊃ バーチャルYoutuber ⊃ バーチャルモトブロガー**とし、以下に、ニコニコ大百科の[バーチャルYoutuber](https://dic.nicovideo.jp/a/%E3%83%90%E3%83%BC%E3%83%81%E3%83%A3%E3%83%AByoutuber)の定義に則って、***この記事中でのバーチャルモトブロガー***を定義してみます。

###バーチャルモトブロガー(VirtualMotovlogger)
- 3Dモデル、CGを用いて、バイクに関する動画配信をする者
- カメラや、モバイル端末や車載ECUからのセンシングによって得たデータからバイクやライダーの動きを動画中で再現

２つ目のセンシングは、慣性センサ式の**モーションキャプチャ**です。
走行しているバイクを光学式やビデオ式のモーションキャプチャで再現するのは、少し難しそうです。コストさえ無視すれば、スロットルバイワイヤ（ドライブバイワイヤ）などの電子制御システムを使って、筐体（例えば、[Hondaライディングシミュレーター](https://www.honda.co.jp/safetyinfo/simulator/motorcycle/)のような）を作って部屋の中で光学式やビデオ式のモーションキャプチャを用い、ライダーとバイクの動きは再現できそうですが。（逆に言えば、その筐体と点群データなどで作成した仮想空間を作れば実車が無くても3D空間上で走行配信ができそうです。）

#使ったもの
使ったハードやソフト、アセットなどです。
###開発環境　
- MacBook Pro (13-inch, 2019)(バージョン 10.14.6（18G84）)
- Pixel 3a（Androidバージョン 9）
- Unity（バージョン 2019.1.7.f1）

###アセットなど
- [UniVRM(Github)](https://github.com/vrm-c/UniVRM)
    - UnityにVRMをインポート・エクスポートするためのものです。
- [ニコニ立体ちゃん（VRM）（ニコニ立体）](https://3d.nicovideo.jp/works/td32797)
    - ご存知、教授ことAliciaSolidで知られているニコニ立体ちゃんのVRMモデルです。
- [Brat(Artistic Mechanics)（UnityAssetStore）](https://assetstore.unity.com/packages/3d/vehicles/land/brat-74351)
    - ARTISTIC　MECHANICSから提供されている無料のオートバイの3Dモデルです。

#作ったもの
###Github
[https://github.com/Sunmax0731/getSensor](https://github.com/Sunmax0731/getSensor)

###ソースコード（抜粋）
```csharp:getsensor.cs
using UnityEngine;
using System.Collections;
using TMPro;
using System.Timers;

public class getSensor : MonoBehaviour
{

    [SerializeField]
    public TextMeshProUGUI SensorData;

    private static Vector3 acceleration;
    private static Compass compass;
    private static Vector3 gyro;
    private GUIStyle labelStyle;
    public static float nowTime;
    public static float updateSpan = 1f;

    public static Vector3 Acceleration { get => acceleration; set => acceleration = Input.acceleration; }
    public static Compass Compass { get => compass; set => compass = Input.compass; }
    public static Vector3 Gyro { get => gyro; set => gyro = Input.gyro.rotationRate; }

    void Start()
    {
        nowTime = 0f;
        Input.compass.enabled = true;
        Input.gyro.enabled = true;
    }

    void Update()
    {
        acceleration = Input.acceleration;
        compass = Input.compass;
        gyro = Input.gyro.rotationRateUnbiased;

        if (LoggerButton.UpdateFlag)
            nowTime += Time.deltaTime;

        //if (nowTime > updateSpan && LoggerButton.UpdateFlag)
        //{
            SensorData.text = UpdateText(Input.acceleration, Input.compass, Input.gyro.attitude);
            nowTime = 0f;
        //}
    }

    string UpdateText(Vector3 Accel, Compass Compass, Quaternion Gyro)
    {
        return string.Format(
            "Timestamp:{0:#.#####}\n" +
            "Acceleration = X:{1:#.#####} Y:{2:#.#####} Z:{3:#.#####}\n" +
            "Commpass = magnetic:{4:#.#####} true:{5:#.#####}\n" +
            "Gyroscope = X:{6:#.#####} Y:{7:#.#####} Z:{8:#.#####}",
            LoggerButton.totalTime,
            Accel.x, Accel.y, Accel.z,
            Compass.magneticHeading, Compass.trueHeading,
            Gyro.x, Gyro.y, Gyro.z);
    }
}

```

```csharp:moveObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
//using System.Numerics;

public class moveObject : MonoBehaviour
{
    public GameObject model;
    public GameObject head;
    static List<Vector3> listGyro = new List<Vector3>();
    static List<Vector3> listAcceleration = new List<Vector3>();
    bool flg = false;
    float angle;
    static int currentPOV = 0;
    static List<string> strPOVkey = new List<string> { "TPS", "FPS", "nulmodel" };
    Dictionary<string, Vector3> dicPOV = new Dictionary<string, Vector3>()
    {
        {"TPS",new Vector3(0f,0f,0f)},
        {"FPS",new Vector3(0f,-2f,-64f)},
        {"nulmodel",new Vector3(0f,0f,-350f)}
    };

    // Start is called before the first frame update
    void Start()
    {
        //model = GameObject.Find("Model");
        //head = GameObject.Find("Head");
        model.transform.localPosition = dicPOV[strPOVkey[currentPOV]];
        listAcceleration.Add(model.transform.position);
        listGyro.Add(model.transform.rotation.eulerAngles);
    }

    [System.Obsolete]
    void Update()
    {
        if (strPOVkey[currentPOV] != "FPS")
        {
            model.transform.localPosition =
            Vector3.Lerp(
                listAcceleration.Last(),
                SetPosition(getSensor.Acceleration) + dicPOV[strPOVkey[currentPOV]],
                0.5f);
            model.transform.Rotate(SetRotate(getSensor.Gyro));

            head.transform.localEulerAngles = new Vector3(
                0f, 0f, model.transform.localEulerAngles.z);

        }

        listAcceleration.Add(model.transform.position);
        listGyro.Add(model.transform.rotation.eulerAngles);
 
    }

    public void OnResetObjectClick()
    {
        model.transform.localPosition = dicPOV[strPOVkey[currentPOV]];
        do
        {
            model.transform.Rotate(listGyro[0] - model.transform.rotation.eulerAngles);
        }
        while (model.transform.eulerAngles != new Vector3(0f, 0f, 0f));
    }

    private Vector3 SetPosition(Vector3 vec)
    {
        return new Vector3(vec.x, vec.y, vec.z) * 5f;
    }

    private Vector3 SetRotate(Vector3 vec)
    {
        //ピッチ角は０度で固定
        //ヨー角は端末の動きに追従
        return new Vector3(
            0f,
            //Mathf.Clamp(-vec.x, -30f, 30f),
            Mathf.Clamp(-vec.y * listGyro[0].y, -30f, 30f),
            Mathf.Clamp(vec.z, -30f, 30f)) / 5;

            //0f,
            ////Mathf.Clamp(-vec.x, -30f, 30f),
            //Mathf.Clamp(vec.z * listGyro[0].z, -30f, 30f),
            //Mathf.Clamp(-vec.y, -30f, 30f)) / 5;
    }

    //視線（頭）の向き
    private Vector3 HeadOrientation(Vector3 vec)
    {
        //ピッチ,ロール軸の調整
        float limOri = 45f;
        float threshold = 20f;

        Debug.Log(vec.z);

        if (vec.z > threshold)
        {
            return new Vector3(
                0f,
                Mathf.Clamp(Mathf.Abs(vec.z), -limOri, limOri),
                0f);
        }

        if (vec.z < -threshold)
        {
            return new Vector3(
                0f,
                Mathf.Clamp(-Mathf.Abs(vec.z), -limOri, limOri),
                0f);
        }

        return new Vector3(0f, 0f, 0f);

    }

    //バイクのロール角に応じて体や膝の角度の調整
    private Vector3 LeanIn(Vector3 vec)
    {
        return new Vector3(0f, 0f, 0f);
    }


    public void ChangePOV()
    {
        if (currentPOV < strPOVkey.Count - 1)
            currentPOV++;
        else
            currentPOV = 0;

        OnResetObjectClick();
    }
}
```

###Scene
<img width="889" alt="スクリーンショット 2019-08-02 9.56.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147257/52f108bb-ef4f-2015-b9c9-9c19523a5d44.png">

###動作画面（gif動画、JPEG静止画）
|(1)スマホを手に持って傾けたとき|(2)オートバイにスマホを固定して動作させたとき|(3)加速度センサの値の反映なし|
|---|---|---|
|<img width="430" alt="20190801_082153.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147257/a45dd45a-e302-6201-d116-27c15e0bf069.gif">|<img width="430" alt="20190801_081023.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147257/f461971c-939a-7344-efe7-1d36129c5113.gif">|<img width="430" alt="20190821_162534.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147257/9dfd43ef-57fc-5290-57a7-c94fb98c6352.gif">|

- (1)は当然ですが端末のロー角の変化しか影響がないので、3Dモデルのブレなどは観測できていません。
- (2)は(1)のプログラムの実走行テストをしたときのキャプチャ動画です。車体の振動や道路の轍などをセンサが拾って、このような挙動になっていると感がられます。
- (3)は3Dモデルにジャイロセンサの値の値を反映するのみに止まっていて、実車の傾きを3Dモデルの傾きに反映させています。
- 加速度センサの値を反映させることで、実車により近く没入感の得られる3Dモデルの挙動になると予想していましたが、車体の振動を予想以上に拾ってしまいました。
- アプリを起動している端末は、フロントカウルのカウルステーに市販のハンドルブレースを装着して、そのハンドルブレースに装着していましたが、ピッチ角が少しでも傾いていると、実車のロー角の値が傾いて元に戻った時に、端末側では3Dモデルが元の角度に戻りませんでした。Quaternionの計算上の問題でしょうか。

動作画面（静止画）
<img width="200" alt="Screenshot_20190719-084847.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147257/6d1d5b38-9947-be84-5e6d-d867fcf92032.png">



#苦労したところ
アプリを開発する中で、得られた知見などを紹介します。

- Quaternion
    - 四苦八苦試行錯誤しながら調べた結果、結局はオイラー角で色々やりました。

> [UnityEngine.Quaternion - Unityスクリプトカンファレンス](https://docs.unity3d.com/ja/2018.3/ScriptReference/Quaternion.html)
>>クォータ二オンは複素数に基づいており、直感的に理解するのは容易ではありません。個々のクォータニオンの成分 (x,y,z,w) にアクセスしたり変更したりすることはほとんどありません。

>> |w|クォータ二オンの W 成分。クォータ二オンを熟知していない限り、この値を直接変更しないでください。|
|---|---|
|x|クォータ二オンの X 成分。クォータ二オンを熟知していない限り、この値を直接変更しないでください。|
|y|クォータ二オンの Y 成分。クォータ二オンを熟知していない限り、この値を直接変更しないでください。|
|z|クォータ二オンの Z 成分。クォータ二オンを熟知していない限り、この値を直接変更しないでください。|

わからないままにするのももったいないので、金谷健一先生の[３次元回転](https://www.amazon.co.jp/gp/product/4320113829/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4320113829&linkCode=as2&tag=sunmax0731-22&linkId=b262ab42c721c88cb492ec59e3bcee0d)を読んで勉強することにします。

- [VRMファイル](https://vrm.dev/)
    - 規格化されているので、AliciaSolidさんのモデルから[他のモデル](https://3d.nicovideo.jp/works/td35076)に、容易に変更できるかと思ったらGameObjectのファイル名も子オブジェクトの構成も違った（特に規定されていない模様）ので、スクリプト上ではなくてインスペクタ上でモデルをアタッチしないといけませんでした。最終的には、アプリ上でVRMモデルを変更できるようにしたいです。

- UVCカメラ
    - いろいろ調査したけど、まだアプリに組み込めてません。
    - [ExternalUSBCamera](https://source.android.com/devices/camera/external-usb-cameras?authuser=1)
    - [AndroidにUSBでカメラを繋ぎた〜い(^o^)／〜その１](https://serenegiant.com/blog/?p=1117)
    - [（上記同ブログ）アプリの紹介: UsbWebカメラ](https://serenegiant.com/blog/?p=1458)
    - [(上記同ブログのGithub)saki4510t/UVCCamera](https://github.com/saki4510t/UVCCamera)
    - 接続できたとして、センサと映像の遅延の差はどれくらいあるのか。特に許容値は設けていませんが、時速約60km/hで走行した場合、1秒あたりの移動距離は単純計算で約16.7mなので、1秒の遅延で距離にして約16.7mの差が生まれ、車体の3Dモデルと表示する映像に無視できない齟齬が発生します。（例：実際はコーナーを走行中でセンサの値も車体が傾いていることを示しているのに、カメラからの映像の表示が遅延して、アプリの表示上は映像は直進の道路で、3Dモデルは傾いているなどの表示になる。）

#参考にしたもの
- [Unity スクリプトリファレンス Transform](https://docs.unity3d.com/ja/2018.1/ScriptReference/Transform.html)
    - 初歩的な問題であれば、このページを見ればだいたい解決すると思います。
- [Unity スクリプトリファレンス Input.acceleration](https://docs.unity3d.com/ja/current/ScriptReference/Input-acceleration.html)]
    - モバイル端末からの入力に関することを調べるのに参考にしました。
- [おもちゃラボ - 【Unity】Webカメラの画像を加工して表示する](http://nn-hokuson.hatenablog.com/entry/2017/08/09/192813)
    - スマホカメラやPCの内蔵カメラを利用する際に参考にしました。また、PCに接続したUSBカメラもこの方法で認識されました。スマホに接続したUSBカメラは、端末には認識されましたが、Unityアプリ上では認識されませんでした。
- [バーチャルキャストでアイリスちゃんを使ってみる](https://qiita.com/miyumiyu/items/192e1beb95d6e3507533)
    - 髪やスカートなどの揺れものを設定するときに参考にしました。デフォルトではスカートにもVRM Spring Boneがアタッチされていましたが、しつこいくらい揺れるので、今回は揺れものは髪のみにしました。


#課題
- モバイル端末とUVCカメラの接続
    - 実装すれば複数の視点からの映像を取得可能なので、よりライブ感が増加すると思います。
- サーバーサイドシステム（ライブ配信をする仕組み）
    - 今回は全く触ってません。
- センシングしたデータのフィルタリング
    - 現時点では、実走行した際にエンジンからの振動で３Dモデルの振動が止まらない。
    - ローパスフィルターなどで調整できるかと思いましたが、車種ごとに振動の仕方が違うので、Adaptiveなフィルターにしないといけなそうです。

#最後に
８月２２日までちょこちょこ記事の修正や更新などします。
１月に退職予定で、現在転職活動中です。会社訪問などさせてもらえる企業さんがいらっしゃいましたが、[Twitter](https://twitter.com/sunmax0731)のDMやリプなどで声をかけてもらえると嬉しいです。