イネ科

すっぺらぴっちょん

VRMのBlendShapeを良い感じに遷移させてみる

表情を変えるだけなら公式を参照すれば解決なのですが、作成中のアダルトアプリにおいて、表情の遷移が重要なエッセンスであると感じ、実装してみました。背景としてBlendShapeをAnimationに組み込むと、スクリプトからの変更が効かなくなるのに加え、同じ体位中(Animation)でも表情だけを変更したく、コードから変更したほうが良さそうだった為です。Mecanimのアバターマスクでの遷移は地獄なので。

前提として、またたきの処理についてはVRM標準のもの(Blinker.cs)をモデルにアタッチしています。
C#の理解度が低いので、かなりゴリ押しです。

KeepChangeBlendshapeEmoteでBlendShapeを5分割して遷移させてあげるだけの、シンプルな実装ですが、引数で細かい制御を可能にしています。emoteがBlendShapeの名前、ratioが表情の遷移分パラメータ、secが表情のキープ時間、lateが遅延時間、transitionが表情の遷移にかける時間、startが表情の初期パラメータです。transitionは実際には最低0.02secほどかかってしまうので、この時間を10倍した値を考慮して、遷移間隔を算出してあげる必要があります。

以下が、実際に動作させたイメージです。かわいいですね。
f:id:sesleria:20190502152409g:plain:w250f:id:sesleria:20190502152414g:plain:w250

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の値をキープしつつ、変更先までのパラメータを加算なり減算してあげれば、一度リセットしてやらなくても済むのかもしれない。

作業進捗風景