VRMのBlendShapeを良い感じに遷移させてみる
表情を変えるだけなら公式を参照すれば解決なのですが、作成中のアダルトアプリにおいて、表情の遷移が重要なエッセンスであると感じ、実装してみました。背景としてBlendShapeをAnimationに組み込むと、スクリプトからの変更が効かなくなるのに加え、同じ体位中(Animation)でも表情だけを変更したく、コードから変更したほうが良さそうだった為です。Mecanimのアバターマスクでの遷移は地獄なので。
前提として、またたきの処理についてはVRM標準のもの(Blinker.cs)をモデルにアタッチしています。
※C#の理解度が低いので、かなりゴリ押しです。
KeepChangeBlendshapeEmote
でBlendShapeを5分割して遷移させてあげるだけの、シンプルな実装ですが、引数で細かい制御を可能にしています。emote
がBlendShapeの名前、ratio
が表情の遷移分パラメータ、sec
が表情のキープ時間、late
が遅延時間、transition
が表情の遷移にかける時間、start
が表情の初期パラメータです。transition
は実際には最低0.02secほどかかってしまうので、この時間を10倍した値を考慮して、遷移間隔を算出してあげる必要があります。
以下が、実際に動作させたイメージです。かわいいですね。
ManageFacialExpressions.cs
VRoidで作成したVRMモデルにBlinker.csと、ManageFacialExpressions.csをアタッチすれば動きます。C
V
B
で遷移、N
で中断。
using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using VRM; public class ManageFacialExpressions : MonoBehaviour { private Blinker blink; private VRMBlendShapeProxy proxy; private bool endFlag = true; private bool startFlag = true; private int newFace = 100; void Start() { proxy = this.GetComponent<VRMBlendShapeProxy>(); blink = this.GetComponent<Blinker>(); } void Update() { if(Input.GetKeyDown(KeyCode.C)){ startFlag = false; newFace = 0; } if(Input.GetKeyDown(KeyCode.V)){ startFlag = false; newFace = 1; } if(Input.GetKeyDown(KeyCode.B)){ startFlag = false; newFace = 2; } if(Input.GetKeyDown(KeyCode.N)){ blink.enabled = true; startFlag = false; newFace = 100; } if(endFlag && newFace != 100){ blink.enabled = false; startFlag = true; endFlag = false; StartCoroutine(FaceAnimationStart(newFace)); } } //表情を初期化 public void ResetBlendShape(){ proxy.SetValue("NEWTRAL",0.0f); proxy.SetValue("A",0.0f); proxy.SetValue("I",0.0f); proxy.SetValue("U",0.0f); proxy.SetValue("E",0.0f); proxy.SetValue("O",0.0f); proxy.SetValue("BLINK",0.0f); proxy.SetValue("BLINK_L",0.0f); proxy.SetValue("BLINK_R",0.0f); proxy.SetValue("JOY",0.0f); proxy.SetValue("ANGRY",0.0f); proxy.SetValue("SORROW",0.0f); proxy.SetValue("FUN",0.0f); proxy.SetValue("SURPRISED",0.0f); proxy.SetValue("EXTRA",0.0f); } //表情制御(個別) public void KeepChangeBlendshapeEmote(string emote, float ratio, float sec, float late = 0.0f, float transition = 0.01f, float start = 0.0f){ StartCoroutine(ChangingBlendshape(emote, ratio, sec, late, transition, start)); } private IEnumerator ChangingBlendshape(string emote, float ratio, float sec, float late ,float transition, float start){ yield return new WaitForSeconds(late); for(int i=5; i>=1; i--){ proxy.SetValue(emote,start + ratio / i); yield return new WaitForSeconds(transition); } yield return new WaitForSeconds(sec); for(int i=1; i<= 5; i++){ proxy.SetValue(emote,start + ratio / i); yield return new WaitForSeconds(transition); } proxy.SetValue(emote,start); } //表情制御(指示) private IEnumerator FaceAnimationStart(int faceNo){ int blinkCount = 0; if(faceNo == 0){ KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); while(startFlag){ KeepChangeBlendshapeEmote("FUN", 0.15f, 1.2f, 0.0f, 0.1f, 0.15f); KeepChangeBlendshapeEmote("JOY", -0.15f, 1.2f, 0.0f, 0.1f, 0.3f); yield return new WaitForSeconds(2.5f); blinkCount++; if(blinkCount == 3){ blinkCount = 0; KeepChangeBlendshapeEmote("BLINK", 0.6f, 0.0f, 0.0f, 0.01f, 0.0f); } } } if(faceNo == 1){ KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.55f); while(startFlag){ KeepChangeBlendshapeEmote("FUN", 0.15f, 1.2f, 0.0f, 0.1f, 0.15f); KeepChangeBlendshapeEmote("JOY", -0.15f, 1.2f, 0.0f, 0.1f, 0.3f); KeepChangeBlendshapeEmote("BLINK", 0.0f, 2.5f, 0.0f, 0.0f, 0.55f); yield return new WaitForSeconds(2.5f); } } if(faceNo == 2){ KeepChangeBlendshapeEmote("BLINK", 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); while(startFlag){ KeepChangeBlendshapeEmote("A", 1.0f, 0.0f, 0.0f, 0.02f, 0.0f); KeepChangeBlendshapeEmote("I", 1.0f, 0.0f, 0.3f, 0.02f, 0.0f); KeepChangeBlendshapeEmote("U", 1.0f, 0.0f, 0.6f, 0.02f, 0.0f); KeepChangeBlendshapeEmote("E", 1.0f, 0.0f, 0.9f, 0.02f, 0.0f); KeepChangeBlendshapeEmote("O", 1.0f, 0.0f, 1.2f, 0.02f, 0.0f); yield return new WaitForSeconds(1.7f); } } endFlag = true; ResetBlendShape(); } }
課題 : 元のBlendShapeの値をキープしつつ、変更先までのパラメータを加算なり減算してあげれば、一度リセットしてやらなくても済むのかもしれない。
作業進捗風景
えっち時のVRMの表情を細かく制御したく、テコ入れした結果。表情制御大変すぎでは。 #VRM憑依ちゃん pic.twitter.com/Vw5tNCI1hI
— せすれりあ (@sesleria) 2019年4月27日
表情は5フレームくらいで遷移させるのが妥当なとこかしら。いかんせんUnity経験が足りなさすぎるゾ。 pic.twitter.com/E5IQzwtCSy
— せすれりあ (@sesleria) 2019年4月28日
VRoidの場合、AIUEOが高めのパラメータで被ると歯が飛び出ちゃう事故が起きる事があるので、そこさえ気を付ければ多少重ねたほうが自然に遷移しますな。 pic.twitter.com/gAh7F31hMP
— せすれりあ (@sesleria) 2019年4月28日
へんたい。 pic.twitter.com/wRCcDyw9VX
— せすれりあ (@sesleria) 2019年4月28日
えっち。 pic.twitter.com/Vo3tWlXVVS
— せすれりあ (@sesleria) 2019年4月28日
BLINKにマイナス入れたらどうなるんだろうと思ってやってみたら、壊れたアンドロイドみたいになってしまった。 pic.twitter.com/MYyVwRTiCM
— せすれりあ (@sesleria) 2019年4月28日
シェイプの初期値も考慮するようにしたけど、引数多すぎて地獄絵図だなぁ。 pic.twitter.com/3nYCdrOvQP
— せすれりあ (@sesleria) 2019年4月28日