Help us understand the problem. What is going on with this article?

CVVTuberExampleでVTuber配信アプリケーションを作る + 拡張あれこれ

このアドベントカレンダーについて

こちらは武蔵野アドベントカレンダー2018の24日目の記事になります。
なぜこのアドベントカレンダーに僕が記事を書くことになったかについてはお察しください
ちなみに20日目の@alfredplplさんと同様、普段は武蔵野ではなく横須賀にいます。
まぁ僕のリアルアバターについてのあれこれはさておき、今後共末永くお付き合いいただければと思います。

はじめに

アドベントカレンダーを書き始めた当初は、UnityのCVVTuberExampleというアセットを使って、WebカメラでVTuberになれるアプリケーションの作り方を記事にしようと思っていました。
ちょうどVTuber Tech #1 アドベントカレンダーの5日目の記事の導入にあたる部分です。

しかし、最新のアセットをダウンロードしたところ、何度試してもセットアップがうまくいきませんでした。

「どういうことだ?」といろいろ調べていると、最新のCVVtuberExampleに必要なアセットがまだAsset Storeに出てないことが判明しました。

CVVTuberExampleのリリースノートを確認しますと、

1.0.2
[Common]Updated for OpenCV for Unity v2.3.3.( This asset requires OpenCVforUnity 2.3.3 or later.)
[Common]Updated for Dlib FaceLandmark Detector v1.2.5.( This asset requires Dlib FaceLandmark Detector 1.2.5 or later.)

となっており、最新のCVVtuberEXampleを用いてアプリケーションを作成しようとするとOpenCV for Unity v2.3.3が必要であることがわかります。

しかし、OpenCV for Unity のバージョンをAseet storeで確認すると、

Releases current ver. 2.3.2

となっており、CVVTuberExampleが必要としているバージョンより低いバージョンがリリースされていることがわかります。

現状のUnity Asset Storeの仕様では、過去のバージョンのアセットをインストールすることはできません。

私自身も環境構築の再現を試みようと、Asset storeより両方のバージョンをインストールして確かめてみましたが、どうしてもコンパイルエラー等が発生してしまいうまくビルドすることができませんでした。

一応、こちらにはCVVTuberExampleの旧バージョンがアップロードされていますが、CVVTuberExampleの使用に必要なもう一つの有料アセットであるDlib FaceLandmark Detectorは最新バージョンへのアップデートが行われており、こちらでもビルドするのは厳しいという判断に至りました。

……ということで、現状CVVtuberExampleを使ってWebカメラでVtuberになることは実質不可能です。

ので、今回はこちらの内容については「保留」とさせていただきます。

そこで、当初から予定を変更してVTuber Tech #1 アドベントカレンダーの5日目の記事と多少被る部分もありますが、今回はCVVtuberに対して私が行った拡張についてお話していきたいと思います。

各種アセットの開発元である ENOX SOFTWARE 様には、一刻も早くバージョンの不整合を解決していただきたく思います。

成果物

まず、今回CVVtuberを用いて僕が作成したアプリケーションを用いて撮影した動画を以下に貼ります。

……勘違いされる方がもしかしたらいらっしゃるかもしれませんので一応言っておきますと、僕の本来(リアル)の性別は「男の子」です。

この動画ではいわゆるボイスチェンジャーソフトを使って女の子っぽい声を出しています。

使用している3Dモデルについては僕のTwiterアイコンをモチーフにして、VRoidという3Dモデリングアプリケーションを用いて作成していただいたものを使用させていただいております。

以下、この動画を作るまでに僕が行った拡張について述べていきます。

動作環境

  • Windows10
  • Unity2018.2.6f1

外部利用ライブラリ/スクリプト(CVVTuberExample関連以外)

利用デバイス

初期状態

こちらがCVVTuberをセットアップしたばかりの状況です。

こちらでも述べられている通り、カクカクしてとてもではないですが見れた状況ではないことがわかります。

ここから少しづつ拡張を行っていきます。

拡張

1. 頭部動作のLerp設定をする

デフォルトの設定だと、頭の動きが全くなめらかではないので、こちらで述べられている通りVRMHeadLotationControllerのLerp Tの値を変更して動きを滑らかにします。

まだ大した設定は行っていませんが、この時点でそれなりにVTuberとして成立していることがわかります。この当時はまだ自分のモデルを所持していなかったのでプロ生ちゃんの3Dモデルを使用させて頂いています。

2. 表情制御

VRMCVtuberではVRMKeyInputFaceBlendShapeControllerというスクリプトを用いて、キーボード操作を用いた手付けによる表情の制御を行うことができます。
しかし、デフォルトの状態では非アクティブ時等のキーボード操作を受け付けることができずゲーム配信時等には使用しづらいですし、また瞬きとの排他制御がなされていないので、非常に使いづらいです。

そこで、以下のようなスクリプトを作成してRawKeyから表情の制御を行うようにし、さらにデフォルト表情以外のときはまばたきを行わないようにすることで排他制御を行うようにしました。

MyVRMKeyInputFaceBlendShapeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityRawInput;

using VRM;

namespace CVVTuber.VRM
{
    public class VRMKeyInputFaceBlendShapeController : CVVTuberProcess
    {
        public Blinker blinker;
        public VRMBlendShapeProxy target;
        [Range(0.0f, 1.0f)] public float FaceWeight = 0.5f;

        public class Face {
            string morph;
            UnityRawInput.RawKey Key;

            public Face(string morphname, UnityRawInput.RawKey Keyname) {
                morph = morphname;
                Key = Keyname;
            }

            public string GetMorph() {
                return morph;
            }

            public UnityRawInput.RawKey GetKey()
            {
                return Key;
            }
        }

        public List<Face> faces;

        public string[] morphname;
        public UnityRawInput.RawKey[] Keyname;

        public override string GetDescription()
        {
            return "Update face BlendShape of VRM using KeyInput.";
        }

        public override void Setup()
        {
            faces = new List<Face>();
            for (int n = 0; n < morphname.Length; n++)
            {
                Debug.Log("face added");
                Face faceelem = new Face(morphname[n], Keyname[n]);
                faces.Add(faceelem);

            }
            RawKeyInput.Start(true);

        }

        public override void UpdateValue()
        {
            float value;

            foreach (Face f in faces) {
                if (RawKeyInput.IsKeyDown(f.GetKey())) {
                    Debug.Log("Pushed" + f.GetMorph());

                    if (f.GetMorph().Equals("NEUTRAL"))
                    {
                        blinker.enabled = true;
                    }
                    else {
                        blinker.enabled = false;
                    }

                    float targetvalue = Mathf.Clamp01(target.GetValue(f.GetMorph()) + FaceWeight);

                    foreach (Face f2 in faces) {
                        value = Mathf.Clamp01(target.GetValue(f2.GetMorph()) - FaceWeight);
                        target.SetValues(f2.GetMorph(), value, false);
                    }

                    target.SetValues(f.GetMorph(), targetvalue, false);
                    break;

                }
            }
        }

        public override void LateUpdateValue ()
        {
            target.Apply();
        }

        private void OnApplicationQuit()
        {
            RawKeyInput.Stop();
        }
    }
}

ただ、このままですとキーを押すと一瞬で表情が切り替わってしまうため、切り替え時間を設けて徐々に表情が変わるようにする等の処理を入れたいところです。

3. FOVを狭くする

先程の動画を見ていただければわかるかも知れませんが、画面に映るプロ生ちゃんのモデルは非常に頬がぷっくりとした印象を受けると思われます。
こちらの記事でも触れられていますが、これは元からそのようなモデルの作りになっているわけではなく、Unityのカメラが広角レンズのような役割を果たしており、今回のアプリケーションのようにキャラクターの顔に超接近した場合、画面に歪みが生じてしまうためです。

普通のUnityアプリケーションならば画面全体にゲーム画面が表示されるためこれで良いのですが、今回のアプリケーションのような用途の場合画面いっぱいにキャラクターの顔が表示される……といった状況になりますのでFOV60という値は大きすぎます。

そのため、MainCameraのFOVをできるだけ小さく(今回の場合は1)にして、遠くにMainカメラを置くことで、その問題を回避します。

camera.PNG

4. LipSyncを改良する

CVVtuberではVRMDlibFaceBlendShapeControllerを用いて口の動作を認識してその動きをモデルに反映する機能があるのですが、ボイスチェンジャーを使用する関係上遅延が発生してしまうため今回の用途には不向きです。

最初は凹さんが作成されたリップシンクアセットをVRM用に拡張したものを用いて音声認識ベースのリップシンクを行っていましたが、キャリブレーションが必要だったりいろいろと難がありました(記事公開時はまだOVRLipSyncはリリース前だったと記憶しています)。

その後、OVRLipSyncが公開され、さらに坪倉さん(@kohack_v)がVRMモデルにOVRLipSyncを反映させるスクリプトを公開していただいたため、最終的にこちらを採用することにしました。

比較対象としてAniLipSyncの利用も考えましたが試してみたところ僕の3Dモデルの雰囲気と合わなかったので今回は見送り、シンプルにOVRLipSyncの値をそのまま反映させることにしました。このあたりはキャラクター性との兼ね合いになりますので個人の好みの部分も大きいかと思います。

また、OVRLipSyncは通常の場合ですとバックグラウンド時にマイクによるリップシンクが反映されないようになっていますので、OVRLipSync内にあるOVRLipSyncMicInputの該当ソースコード(主にStopMicrophone();回り)をコメントアウトし、バックグラウンド時もリップシンクが反映されるようにしました。(アドバイス頂いたizm(@izm))さん、ありがとうございます)

5. 手の表現をつける

首の動きだけでは寂しいので、LeapMotionを用いて手の表現も付け加えてみることにしました。

実装にあたってはこちらのページを参考にさせていただきました。

ただ、そのままだとCVVTuberのAnimationControllerとLeapmotionのAnimationControllerが衝突してしまっていたため、VRMCVVTuberControllManagerの Animator Controllerの出力先をNone(Runtime Animator)にすることで頭の動きと指の動きが同時に反映されるようにしています。

controll.PNG

さらに、LeapMotionで指を動かす場合、Model Finger Pointingの値の調整が必要になるのですが、これはモデルごとに微妙に使用が異なるため、値調整が必要になります。

一応VRoidで作成された僕のモデルの値をこちらに記載しておきます。何かの参考になれば幸いです。

  • 左手人差し指~小指
    index-l.PNG

  • 左手親指
    thumb-l.PNG

  • 右手人差し指~小指
    index-r.PNG

  • 右手親指
    thumb-r.PNG

また、Leapmotionの手のトラッキングが外れた時に動作がおかしくなるのが不満だったので、こちらのページの汎用クラスを引用 + 参考にしながら以下のようなスクリプトを作成し、トラッキングが外れたときは手が徐々にデフォルト位置に戻るようにしました。

HandTransFormProxy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HandTransformProxy : MonoBehaviour {
    public GameObject SourceObject;
    public GameObject ActivationObject;
    GameObject OriginalObject;
    public float deltaactive = 3.0f;
    public float deltainactive = 1.0f;



    private Vector3Spring _springvector = new Vector3Spring(Vector3.left, Vector3.right);
    private QuaternionSpring _springquaternion = new QuaternionSpring(Quaternion.Euler(0,0,180), Quaternion.Euler(0, 0, -180));

    // Use this for initialization
    void Awake () {

        OriginalObject = new GameObject();
        OriginalObject.transform.SetPositionAndRotation(transform.position, transform.rotation);
        _springvector.Play(value => transform.position = value);
        _springvector.Pingpong = false;
        _springquaternion.Play(value => transform.rotation = value);
        _springquaternion.Pingpong = false;
    }

    // Update is called once per frame
    void Update () {
        Debug.Log(ActivationObject.activeSelf);
        _springvector.StartValue = transform.position;
        _springquaternion.StartValue = transform.rotation;
        if (ActivationObject.activeSelf)
        {
            _springvector.EndValue = SourceObject.transform.position;
            _springquaternion.EndValue = SourceObject.transform.rotation;
            _springvector.Update(Time.deltaTime * deltaactive);
            _springquaternion.Update(Time.deltaTime * deltaactive);
        }
        else {
            _springvector.EndValue = OriginalObject.transform.position;
            _springquaternion.EndValue = OriginalObject.transform.rotation;
            _springvector.Update(Time.deltaTime * deltainactive);
            _springquaternion.Update(Time.deltaTime * deltainactive);
        }
    }
}

最初は位置のみを徐々に戻すようにしていたのですが、その場合手の回転角の関係でFinalIKの挙動が怪しくなる時があり、手の回転角についてもLerpを使って徐々に戻すように設定しています。その関係上こちらで紹介されている汎用クラスに新たにQuaternion用の派生クラスを追記しました。

public class QuaternionSpring : Spring<Quaternion>
{
    public QuaternionSpring(Quaternion startValue, Quaternion endValue) : base(startValue, endValue)
    {
    }

    protected override float Progress
    {
        get { return (Quaternion.Dot(CurrentValue,StartValue) / Quaternion.Dot(EndValue, StartValue)); }
    }

    protected override Quaternion Lerp(Quaternion a, Quaternion b, float rate)
    {
        return Quaternion.Lerp(a, b, rate);
    }
}

これで、手が検出状態が切り替わったでも違和感のない表現ができるようになりました。

今回はLeapMotionを机の上に直置きで置いた関係上、キーボードの入力等の机面に対する動きをとることができなかったので、こちらのようにLeapMotionを胸につけたり、頭につけたりした場合の指トラッキングも試してみたいです。

一応机にLeapMotionを置くメリットとして、「自分の髪を触れる」というのがあり、これはこれで割と気に入っています。

https://twitter.com/Nam0naki_/status/1054760329949806592

6. 声をどうにかする

初期の動画(プロ生ちゃんのやつ)を見ていただければ分かる通り、最初の僕の声は非常に引きつった感じで、あまり聞くに堪えるものではありませんでした。

そこで、試行錯誤してちゃんと女の子っぽい声が出るように工夫をしました。

……と端折って書きましたが、ぶっちゃけこの領域についてはです。

「女の子の声を出す方法」については、両声類になるためのボイストレーニング等のアナログな方法からボイスチェンジャーを使用する等のデジタルな方法までまさに初めから終わりまで様々な方法が検討されています。

さらに、「声」という個人の依存性の高い信号を扱う関係上、一概に「このツールを使えば女の子の声になれる」という絶対的な指標は(ほぼ)存在しません。

実際、この活動を通じて僕が新規に購入した機材についてはこの「女の子の声をちゃんと出す」ためにほぼ使用されています。

また、ボイスチェンジャーによるノイズの影響を少しでも緩和するために、発声の時点で高い声を出し、ピッチを下げる(大体130%以下)等の工夫を行っています。

使用するボイスチェンジャーソフトについても、今回の完成動画までに恋声REAPERClownfish Voice Changerと二度のマイナーチェンジを行っています。

真の沼とされるハードウェアボイスチェンジャーについても、今大人気のRoland VT-4をすでに予約済みで、入荷次第試して見る所存です。

……というように完全にこの範囲についてはなので、これ以上の言及は避けさせていただきます。

幸いねこますさんが女の子の姿でも地声を出して良い、という地平を切り開いてくれた(参考)ので声については別に変える必要はないと思われます(男性VTuberになる選択肢も普通にありますし)。

それでもどうしても「女の子の声を出したい」ならば、いろいろと検索して試行錯誤されてみると良いかと思います。個人的におすすめはしませんが。

成果物(再掲)

これらの実装プロセスを踏まえた上で、もう一度初期の動画(プロ生ちゃんのやつ)と最新の動画を比較してみましょう。

ここまで来るのに約5ヶ月という非常にゆっくりなペースでの更新でしたが、初期と比べて格段に表現力が上がっているかと思われます(モデルが変わった影響も大きいですが)。

まとめと今後の発展

ということで、CVVtuberを用いてVTuber配信用のアプリケーションを作成し、そちらに私が行った様々な拡張についてご紹介させていただきました。

昨今、VTuberに「なれる」アプリケーションとしてはWebカメラのみを使用したLive2DからHMDヘッドセット + トラッカーを用いたバーチャルキャストまで様々なツールが存在します。

しかし、上に挙げられているとおり一口にVTuberに「なる」といってもWebカメラのみを使用する方法、iPhoneX以降のface tracking APIを使用する方法、HMDを使用する方法、ハンドコントローラーやトラッカーを用いる方法、さらにその組み合わせ等、実に様々です。

これは、VTuberという概念が「魂を持った一つの(あるいは複数の)キャラクターを作り上げる」という『目的』によって存在しており、そのための『手段』は一切問われていないという所から来ているのだと思います。

そのため、『VTuber配信用アプリケーションを作る』という行為自体が無限の手段や可能性を秘めていると言えると思います。

僕も、他の方がQiita等に書いていた記事を見て「この機能いいな」とか「これはこんな風に実装する方法もあるんだ」等、VTuber配信アプリケーションを作っていなければ興味を持たなかったであろう様々な気づきを得ることが出来ました。

特に、視線制御関係の処理(今のモデルは都合上目線制御ができなくなっているためモデル差し替えたらやりたい)ですとか、アバターの外部読み込み(このような活動も行っているため複数のモデルを扱えるようにしたい)等は今後特にやってみたいところであります。

このように、一度VTuber配信アプリケーションを作ると目的に向けて実装したいことは無尽蔵に増えていきますし、機能を増やすことでそれ自体がポートフォリオ(自身のスキル公開)の役割を持ちます。

個人VTuberの先駆けとなった(元)VTuberのねこますさんも、元々はUnityの勉強 + ポートフォリオの作成の目的でVTuberを始めていらっしゃいます(参考)。

僕自身も半年ほど実際に配信用アプリケーション作ってみた感触として、VTuberアプリ作りは

配信用アプリケーションにアップデートがあると見てもらいたくなる!→配信 or 動画をアップする→反応が帰ってくる→フィードバックを受けて次はこの機能を入れてみよう!と思う

といったサイクルが確かに回りやすい題材なのかなという風に感じました。

今後、より良質なVTuber配信アプリケーションがリリースされ、個人での開発はどうしても車輪の再発明になることは避けられないと思いますが、それでも個人でVTuber配信アプリケーションを作ることには大きな意義があると考えています(少なくとも勉強にはなりますし)。

この記事を読んでもし興味を持たれた方がいましたらこれを機に「あなただけの」VTuber配信アプリを作ってみてはいかがでしょうか。

(そのためにもENOX SOFTWAREさんには早くAsset Storeの不整合を直して欲しい)

今回の実装についてなにかわからないことがありましたらアプリ作者 & Qiita記事執筆者のなもなき(@Nam0naki_)までお気軽にご連絡ください。

また、今回が僕の初のQiita記事 + 最初のほうに書いた様々な不手際があったため、僕担当のアドベントカレンダーの更新が想定より大幅に遅れてしまいました。

自身の未熟さを痛感するとともに、今後は余裕を持って記事を書く習慣を身に着けたいと思った次第です。

以上、よろしくお願いたします。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away