Edited at

CV VTuber Exampleで遊ぶ&プレゼンスを高める


はじめに

こんにちは、かいき(@kaikiofkaiki)です。

OpenCVとDlibを使って開発をしていたところ、CV VTuber Exampleというアセットを見つけ、非常に簡単にVtuberのシステムを作ることができました。

せっかくなので、いろいろとプレゼンスを高める工夫をしてみました。本記事はその記録となります。なお、実装順に書いているため、設計などの甘さにはご了承ください。

なお、記事中にある僕の書いたソースコードはパブリックドメインとします。


1.動作環境


1-1.開発環境


  • Windows10

  • Unity2017.4.12f1


1-2.利用アセット


1-3.利用外部ライブラリ/スクリプト


1-4.利用デバイス

※OpenCVとDlibの処理がかなり重いため、スムーズに動かすためにはPCスペックが必要です。


2.CV VTuber Exampleとは

2018年5月ごろにアセットストアで無料リリースされた、簡単にVtuberになれるアセットです。

ただし、無料といっても本体のみでOpenCV for Unity(\$95)とDlib FaceLandmark Detector(\$40)が動作に必要なため、動かすためにはそれなりの費用がかかります。キャリブレーションに必要な機材はWebカメラのみで、Webカメラから取得した映像をDlibにかけて特徴点を割り出し、位置推定と表情への反映を行ってくれます。

また、様々なモデルにも対応しており、ユニティちゃんのような汎用モデルからVRMモデル、Live2Dモデルにまで対応しています。

ただし、Dlibで用いられている学習データが商業利用禁止のライセンスである可能性が高いため、収益を得る場合は自前で学習データを用意しましょう。

image.png


3.CV VTuber Example導入&セッティング

[CV VTuber Exampleセッティング~VRMをwebカメラで動かせるまで~]を参考にしてください。

以下の画像のようにVRMCVVtuberのセッティングが完了した状態として次項以降進んでいきます。

image.png


4.プレゼンスの向上

VRMCVVtuberも初期状態ではガタガタしていて可愛くは見えず、機械的に見えます。少しずつ工夫をかけてプレゼンスの向上を目指します。

ezgif.com-video-to-gif.gif

人間は大体無表情ですが、アニメキャラクターは大体しっかりとした表情があります。無表情は可愛げがないので、ある程度詐欺ってもいいから豊富な感情表現を与えてみます。

僕個人的な使い道として作業配信やゲーム実況でVtuberシステムを使いたいと考えており、僕の手は別の作業で忙しいので、表情は手付けでなく自動で行うこととします


4-1.体を人間っぽく動かす

シーン上のVRMCVVTuberExample以下にあるVRMHeadLotationControllerが推定した頭の向きを使用して3Dモデルの頭の回転を制御しています。

このコンポーネントの値を調整して動きのプレゼンス向上を図ります。


4-1.1.動きを滑らかにする

人間は急激に動くことはなく、ある程度滑らかに動くものです。急激にカクカク動くと機械らしさが出てしまいます。VRMHeadLotationControllerも滑らかに動くよう線形的に変位する実装になっていますが、デフォルトではまだ機械的に見えてしまいます。ありがたいことにLeapTという動かす割合を制御できるパラメータが用意されているので、こちらのパラメータを調整して滑らかにしましょう。

僕はデフォルトでは0.6と早すぎるので、0.2にしました。

以下の画像のように調整しました。

image.png


4-1.2.女の子らしくする

女の子は直線的ではなく、ヒネリのあるS字のような動きになることが多いです。

[かわいい女の子になりたいんや! UE4の最新機能を使ってVTuberしてみた!]のスライド66枚目にその効果が示されており、比較画像を見る限りでもプレゼンスの高まりを感じます。これを再現してみます。

VRMHeadLotationControllerにはInvert 〇 Axis,Rotate 〇 Axcis(〇はXYZのいずれかとします)というパラメータが用意されており、Invert 〇 Axisをアクティブにすることでその方向の回転が逆になり、Rotate 〇 Axcisはアクティブすることでその方向にしか回転しないようになります。

デフォルトではInvert X AxisとInvert Z Axisだけがアクティブになっていますが、追加でInvert Y Axisもアクティブにしてy座標回転も反転させることで、頭と体の回転が逆になり、女の子らしいS字の動きになります。


結果

カクつきが減って人間らしく、曲線的な動きで女の子らしくなりました。

ezgif.com-video-to-gif (1).gif


4-2.揺れもの設定

VRMにはVRMSpringBoneというものが用意されており、このスクリプトを用いれば簡単に揺れものを設定することができます。。

設定の方法は普通のSpringBoneに近く、以下が使い方の参考になりました。

[Unity]エンジニアが揺れもの(髪、胸、服)を揺らすときに使った知識まとめ

アリシア・ソリッドちゃんは初めからVRMSpringBoneの設定がなされており(AliciaSolid/secondaryで設定されています)、既に素晴らしいのですが、ちょっと調整し、リボンのパラメータを以下のようにしました。

image.png

リボンがより揺れるようにしました。Vtuberは一般的なゲームなどと比べてバストアップで映る機会が多いので、小物で画面に動きがあると可愛く映ると思います。(KizunaAIさんのピョコピョコみたいな)


結果

画面に動きができ、見ていて楽しくなりました。

ezgif.com-video-to-gif (2).gif


4-3.カメラ目線

VRMにはVRMLookAtHeadというものがあり、こちらをアタッチして注視したいオブジェクトをTargetに入れればそのオブジェクトを注視し続けてくれます。今回はカメラ目線にしたいのでMainCameraを入れました。

image.png

VRMLookAtBoneApplyerがVRMLookAtHeadの計算した視線方向のパラメーターをEyeBoneに適用してくれます。こちらも忘れずアタッチしておきましょう。

image.png

この視線制御に関してパラメータなどより詳しくはドワンゴさんのGitHubページが参考になります。

視線制御

アリシアソリッドちゃんの目のハイライトと眼球は別のオブジェクトで構成されておりながら、親子関係を持っておらず、眼球の動きに対してハイライトが置いてけぼりにされてしまったので構成をかえました。AliciaSolid/Character001/root/waist/upperbody/upperbody01/neck/以下に空のオブジェクトを作り、その子オブジェクトに左右の目と対応するハイライトを配置しました。

image.png

また、アリシア・ソリッドちゃんは眼球が大きく、眼を動かしてもキョロキョロ感が薄くて可愛くなかったので、AliciaSolid\eyeSkinnedMeshRenderBlendShapeseyeMinを15に設定して少し眼球を小さくしました

image.png


結果

キョロキョロしてる感じが出ました。目に空白があると動きを感じやすく、プレゼンスも高まる気がします

(動画では分かりやすくするために表情制御を一時的に切っています。)

ezgif.com-video-to-gif (3).gif


4-4.表情の手付け

VRMCVVtuberは手付けが想定されており、VRMKeyInputFaceBlendShapeControllerによってキーボードによるフェイシャル操作が提供されています。

VRMにはVRMBlendShapeProxyというBlendShapeのプリセットを登録して表情変化させる仕組みがあり、このVRMKeyInputFaceBlendShapeControllerはキーボード入力に応じてそのプリセットを適応させます。

VRMKeyInputFaceBlendShapeControllerのキーマップ

・Zキー: Fun

・Xキー: Angry

・Cキー: Joy

・Vキー: Sorrow


結果

これは手動なので使いません。また、使ったとしても瞬きとの排他制御がなされておらず、表情変化も線形的でなく急激に行われるので、改良が必要そうです。

ezgif.com-video-to-gif (4).gif


4-5.VRMKeyInputFaceBlendShapeControllerVRMBlendShapeProxyを非アクティブ化

自動で表情付けを行うため、VRMKeyInputFaceBlendShapeControllerはいらないので切ります。

本来、VRMの特性を活かし、汎用的なシステムするためにはVRMBlendShapeProxyは使うべきなのですが、後述の理由により、VRMBlendShapeProxyを切ってBlendShapeを直接操作しています。


4-5.1.VRMBlendShapeProxyを切った理由

BlendShape同士の競合が苦しかったというのが一番の理由です。

VRMBlendShapeProxyでは複数のBlendShapeを使ったプリセットを登録し、それを操作して表情の変化を行います。どんなプリセットが登録されているかによりますが、「笑顔」には目や眉毛、口のBlendShapeが含まれていることでしょう。

今回は、音や映像から感情を推定し、眉・目・口の3つのパーツをバラバラに動かさせて感情表現を行っていっています。目を笑顔にするのに口や眉毛まで動かれるのは不都合でした。

もちろんうまく排他制御をすればVRMBlendShapeProxyでもうまくできると思いますが、今回はどれだけプレゼンスを高められるかに注力したかったので直接BlendShapeを操作することにしました。

VRMBlendShapeProxyでは記号的な表情を表現するなら楽ですが、バラバラに組み合わせた中途半端な表情表現は難しかったです。


4-6.無表情を減らす

瞬きを笑顔に変えてしまいます。詐欺ります。

VRMDlibFaceBlendShapeController.csのソースコードを以下のように変更します。

VRMBlendShapeProxyではなくBlendShape直接指定による変更と、目の開閉の判定をより極端にしています。

目の開閉の判定をより極端にしたのは、瞬きと比べて笑顔は派手で、誤認識がよく目立ったからです。


VRMDlibFaceBlendShapeController.cs


//追加
public SkinnedMeshRenderer Skin;
(中略)
protected virtual void FaceBlendShapeUpdate (List<Vector2> points)
{
if (enableEye) {
float eyeOpen = (getLeftEyeOpenRatio (points) + getRightEyeOpenRatio (points)) / 2.0f;
//Debug.Log("eyeOpen " + eyeOpen);

//0.4を0.1に変更
if (eyeOpen >= 0.1f) {
eyeOpen = 1.0f;
} else {
eyeOpen = 0.0f;
}
EyeParam = Mathf.Lerp (EyeParam, 1.0f - eyeOpen, eyeLeapT);

//コメントアウト
//target.SetValue(BlendShapePreset.Blink, EyeParam, false);

//追加
Skin.SetBlendShapeWeight(25,100f*EyeParam);
}
(中略)


また、動きをゆったりさせるため、RMDlibFaceBlendShapeControllerEyeLeapTの値を0.6から0.2に変更しました

image.png


結果

Webカメラのみの完全自動フェイシャルですが、とても可愛くなりました。怒りっぽい人なら怒り顔、優し気な人なら穏やかな顔など、演者の特性に合わせて表情はある程度偏らせていいかと思います。

ezgif.com-video-to-gif (5).gif


4-7.口の動きを豊富にする

VRMCVVtuberはVRMDlibFaceBlendShapeControllerで、リップシンク操作を行っていますが、精度はあまりよろしくないです。

OVRLipSyncを用いて音声から自然なリップシンクを行うように変更します。

VRMDlibFaceBlendShapeControllerのEnableMouthにチェックを入れて、VRMDlibFaceBlendShapeControllerによるリップシンクは動かないようにしておきましょう。

image.png

次に、Oculus Lipsync UnityからOVRLipSyncをDLし、

[Unity でリップシンクができる OVRLipSync を試してみた]を参考にモデルにOVRLipSyncを適応させます。

image.png

設定し終わったAliciaSolidのコンポーネントは以下のようになりました。

あまり滑らかに動くとむしろ気味が悪く感じてしまうため、OVRLipSyncContextMorphTargetSmoothAmountを少しデフォルトから下げています。

image.png

なお、レイテンシの軽減とリミテッドアニメーションを実現するAniLipSyncというのもありますが、今回は口モーフの変化には使っていません。後で徐々に口の形を変形(笑顔など)させる処理をさせるため、リミテッドアニメーションと相性と悪かったためです。

ただし、レイテンシの軽減は魅力的なのでその部分だけ活用しました

AniLipSyncのレイテンシ活用部分だけ利用する方法はこちらを参考に組み替えてください。


結果

だいぶ生き生きとしてきました。リップシンクの重要性を感じますね。

ezgif.com-video-to-gif (6).gif


4-8.眼球微細運動を付ける

人間はほんの少し眼球が揺れ続けています。それを再現することでプレゼンスを上げます。

効果は[VR MAGIC! ~キャラクターに命を吹き込んだこの4年間の記録~]で詳しく話されています。

スクリプトは[Unityで微細眼球運動っぽい何か]にて公開されています。

上記スクリプトをDLし、EyeJitter.csをAliciaSolidにアタッチしてパラメータを調整したコンポーネントが以下です。

眼球を揺らすので左右の目を入れます。

image.png

また、EyeJitter.csをもう一つアタッチし、目のハイライトも微妙に揺らします。

image.png

実は、眼球微細運動を行うスクリプトが公開されてるとは知らずに、1回自分で作成したスクリプトもあるので供養で公開しておきます。Unityで微細眼球運動っぽい何かの方がプレゼンスが高まったので今は使ってません。(左右の目で別の動きをするあたりが良かった。)


ShakeEye.cs


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

namespace AddonScripts{
public class ShakeEye : MonoBehaviour {

public Transform Eye;
public Vector3 FirstPositionRotate;

// Use this for initialization
void Start () {
FirstPositionRotate = Eye.transform.localEulerAngles;
}

// Update is called once per frame
void Update () {
//目を乱数で揺らす
Eye.Rotate(new Vector3(Random.Range(0.08f*-1f,0.08f),Random.Range(0.08f*-1f,0.08f),0));
//乱数の偏りで目が変なところにいったら戻す
Eye.transform.localEulerAngles = new Vector3(
Mathf.Clamp(Eye.transform.localEulerAngles.x,FirstPositionRotate.x-0.5f,FirstPositionRotate.x+0.5f),
Mathf.Clamp(Eye.transform.localEulerAngles.y,FirstPositionRotate.y-0.5f,FirstPositionRotate.y+0.5f),
0
);
}
}
}



結果

やりすぎない程度に挙動不審な動きがあると人間っぽさが上がりますね。

ezgif.com-video-to-gif (7).gif


4-9.瞳孔サイズを変更する

人は喋るときに大きく瞳孔が開き、ぼーっとしてるときは瞳孔は小さくなります。これを再現してプレゼンスの向上を図ります。

作成したスクリプトは以下です。


PupilControler.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;

namespace AddonScripts{
public class PupilControler : MonoBehaviour {

public SkinnedMeshRenderer MouthSkin;
public SkinnedMeshRenderer EyeSkin;
public float MinEyeParam = 15;
public float EyeLeapSmall = 0.1f;
public float EyeLeapBig = 0.5f;
public float EyeParam;
public float LeapSum;

void Update () {
if(CheckMouthBlendShap()){
if(LeapSum > 0f){
LeapSum -= EyeLeapBig*Time.deltaTime;
EyeParam = Mathf.Lerp(0,MinEyeParam,LeapSum);
EyeSkin.SetBlendShapeWeight(1,EyeParam);
}
}else{
if(LeapSum < 1.0f){
LeapSum += EyeLeapSmall*Time.deltaTime;
EyeParam = Mathf.Lerp(0,MinEyeParam,LeapSum);
EyeSkin.SetBlendShapeWeight(1,EyeParam);
}
}

}
//母音BlendShapeが変化しているか調べ、喋ってる状態か判断する
bool CheckMouthBlendShap(){
for(int i = 0;i<5;i++){
if(MouthSkin.GetBlendShapeWeight(i) != 0){
return true;
}
}
return false;
}
}
}


喋るとOVRLipSyncが反応し、母音のBlendShapeが動くはずなのでそこを監視して瞳孔の大きさを変えています。また、急激に変化させると違和感があり、プレゼンスが下がるため、Lerpによって徐々に変化するようにしました。

眼球が小さくなるときに比べて大きくなるときを早くしておくことでより元気が出てくる感じを演出しています。

設定した図が以下です。

image.png


結果

喋りだしたときに一気に活力が漲ってくる感じたまらなくないですか。

ezgif.com-video-to-gif (8).gif


4-10.口に感情をのせる

目に笑顔が多いので、口にも笑顔を多くします。口の横広がりに応じて口の形も笑顔にするとします。「い」の音がどうしても笑顔になりますが、それはそれで可愛いのでよしとします。

VRMDlibFaceBlendShapeController.csを編集します。


VRMDlibFaceBlendShapeController.cs


//追加
public bool enableMouthFace;
(中略)
protected virtual void FaceBlendShapeUpdate (List<Vector2> points)
{
(中略)
if(enableMouthFace){
float mouthOpen = getMouthSmaileRatio (points);
Skin.SetBlendShapeWeight(14,mouthOpen*100 - 120f);
}
}
(中略)
protected virtual float getMouthSmaileRatio (List<Vector2> points)
{
float size = Mathf.Abs (points[48].x - points[54].x) / (Mathf.Abs(points[31].x - points[35].x));

return size;
}
}
}


単純計算で口の右端と左端の特徴点の差を取り、鼻の大きさで比率を取っています(比率で取るのは、映っている顔の場所によって値の変動があるため。カメラに近いと値が大きくなり、遠いと小さくなる。)。


結果

表情がだいぶ増えて見えますね。

ezgif.com-video-to-gif (9).gif


4-11.無表情を減らすPart2

何もしていないときに何もないのは正解なのですが、やはり寂しく感じます。なので、何もしていないときに何もしていない雰囲気を出します。しばらく何も喋っていないと退屈そうな顔をしてもらうようにしました。


BoredController.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;

namespace AddonScripts{
public class BoredController : MonoBehaviour {

public SkinnedMeshRenderer MouthSkin;
public SkinnedMeshRenderer EyeSkin;
public float MinEyeParam = 20;
public float EyeLeapSmallSpeed = 0.1f;
public float EyeLeapBigSpeed = 0.5f;
public float EyeLeapBoredSmallSpeed = 0.1f;
public float EyeLeapBoredBigSpeed = 0.5f;
public float BoredStartTime = 5.0f;

private float eyeParam;
private float eyeParamBored;
private float leapSum;
private float leapSumBored;
private float notTalkCount;
private bool boredEyeFlag = false;

void Update () {
if(CheckMouthBlendShap()){
notTalkCount = 0f;
if(leapSum > 0f){
//眼球を大きくしている
leapSum -= EyeLeapBigSpeed*Time.deltaTime;
eyeParam = Mathf.Lerp(0,MinEyeParam,leapSum);
EyeSkin.SetBlendShapeWeight(1,eyeParam);
}
//暇な目を戻している
if(leapSumBored > 0f){
leapSumBored -= EyeLeapBoredBigSpeed*Time.deltaTime;
eyeParamBored = Mathf.Lerp(0,100,leapSumBored);
MouthSkin.SetBlendShapeWeight(33,eyeParamBored);
}
}else{
//一定時間何も喋ってないと暇そうな目をしだす
notTalkCount += Time.deltaTime;
if(notTalkCount >= 5.0f){
boredEyeFlag = true;
}else{
boredEyeFlag = false;
}

//眼球を小さくしている
if(leapSum < 1.0f){
leapSum += EyeLeapSmallSpeed*Time.deltaTime;
eyeParam = Mathf.Lerp(0,MinEyeParam,leapSum);
EyeSkin.SetBlendShapeWeight(1,eyeParam);
}
if(boredEyeFlag){
//暇な目にしてる
if(leapSumBored <= 1.0f){
leapSumBored += EyeLeapBoredSmallSpeed*Time.deltaTime;
eyeParamBored = Mathf.Lerp(0,100,leapSumBored);
MouthSkin.SetBlendShapeWeight(33,eyeParamBored);
}
}
}
}
//母音BlendShapeが変化しているか調べ、喋ってる状態か判断する
private bool CheckMouthBlendShap(){
for(int i = 0;i<5;i++){
if(MouthSkin.GetBlendShapeWeight(i) != 0){
return true;
}
}
return false;
}
}
}


上記スクリプトは【4-9.瞳孔サイズを変更する】でのPupilController.csの改変で、実装も似たようなものです。母音のBlendShapeを監視し、動いていない時間を計り、その時間に応じて表情を変化させています。こちらも急激に変化させると違和感があり、プレゼンスが下がるため、Lerpによって徐々に変化するようにしました。

PupilControllerコンポーネントを外し、BoredControllerを付け、パラメータを調整した図が以下です。

image.png


結果

瞳孔サイズの変更と合わせて喋るときに命が宿ってきましたね。(分かりやすくするため、移行する時間は早めてあります)

ezgif-1-c31915020d9a.gif


4-11.大きい声に反応する

Vtuberがリアクションとして大きい声を出すというのをよく見るので、その反応を作ってみます。大きい声と言っても驚いたときや嬉しいときなど、状況は一つに絞れないのである程度汎用性を持たせた表情をさせるとしましょう。

UnityでAudioSource.Mute=trueにした時にMicrophone(マイク)から音量が取得できない時の対処法を参考に以下のようなスクリプトを作成しました。


MicChangeFace.cs

using UnityEngine;

using System.Collections;

public class MicChangeFace : MonoBehaviour
{
public AudioSource Audio;
public SkinnedMeshRenderer Face;
public int SupriseChangeratio;
public int BorrowUPChangeratio;
public float FaceChangeVol;
private static int borrowUp = 40;
private static int supriseEye = 32;
private float borrowUpParam;
private float supriseParam;
public float UpLeap = 0.1f;
public float DownLeap = 0.9f;

void Update()
{
float vol = GetAveragedVolume();
Debug.Log(vol);
if(vol >= FaceChangeVol){
//目を大きく開かせる
supriseParam = Mathf.Lerp (supriseParam, vol*SupriseChangeratio, UpLeap);
Face.SetBlendShapeWeight(supriseEye,supriseParam);
//眉毛を上げる
borrowUpParam = Mathf.Lerp (borrowUpParam, vol*BorrowUPChangeratio, UpLeap);
Face.SetBlendShapeWeight(borrowUp,borrowUpParam);
}else{
//目を通常の状態へ
supriseParam = Mathf.Lerp (0, supriseParam, DownLeap);
Face.SetBlendShapeWeight(supriseEye,supriseParam);
//眉毛を通常の状態へ
borrowUpParam = Mathf.Lerp (0, borrowUpParam, DownLeap);
Face.SetBlendShapeWeight(borrowUp,borrowUpParam);
}
}

float GetAveragedVolume()
{
float[] data = new float[256];
float a = 0;
Audio.GetOutputData(data, 0);
foreach (float s in data)
{
a += Mathf.Abs(s);
}
return a / 256.0f;
}
}


マイクから取得した音声が一定以上であればマイク音量に応じて目を大きく開き、眉毛を上に上げます(supriseのBlendShapeを選んでいるのは、目を大きく開くという表現に近かったため)。こちらも同じくLerpによって徐々に変化させています。

驚いた顔に近いですが、嬉しいとき、怖いとき、怒っているとき、問わず大きな声を出しているなら間違った表現になっていないでしょう

上記のスクリプトをアタッチし、パラメータを調整した図が以下です。

image.png


結果

また一つ違和感なく表情が豊かになった。

ezgif-1-f1c4caa66364.gif


5.まとめ

普通のWebカメラとマイクのみでも豊かな感情表現ができました。今回は主に目に工夫を入れてみましたが、正に「命は目に宿る」という感じでしたね。

最後に、初期とプレゼンスを高めた後のgifを並べて置いておきます。

ezgif.com-video-to-gif.gifezgif.com-video-to-gif (10).gif

※プレゼンスを高めた後のgifが更新前で止まっています。Qiitaの毎月の画像アップロード制限に引っ掛かりました。Gifをいっぱいあげるとすぐ引っ掛かるので気を付けよう!!


6.余談

2年くらい昔に「有料アセット?他人ができるなら自分でもできるだろう、自作してやるぜ!!」の根性で、ネイティブプラグインやOpenCVSharp、標準IKを駆使してVtuberらしきものの開発を頑張ったことがありますが、今思うととても効率的とは言い難い行動でしたね。

今やCVVtuberやVRMのおかげで基本的なVtuberのシステムが一瞬でできるようになりました。感謝しかないですね。

おかげ様でクリエイティブな活動に専念できます。クリエイティブに専念できるならそれに越したことはないです。車輪の再開発も楽しいですがほどほどにですね。

まだまだCVVtuberをベースに実装をしていて、解説しきれていない部分があるので、そちらもいつか紹介していきたいと思います。


7.参考