3Dなキャラクターで動画作ってみようかなと思い、お手軽そうなVroidを使ってみた。Unityで操作するとゲームにも使えるんじゃない?という目論見でVRMをVroidから吐き出させ導入。unity-chan!のアニメーションなどを適用して悦に入るも、しばらくして気づく。顔が動かねぇ…
VRMはどうやら数ヶ月前に1.0というものに進化を遂げたらしく、その影響で調査難航するも、なんとかスクリプトから動かせることがわかり一安心。自前で表情切り替えのやつをつくってみた。
こういうスクリプト公開みたいなのって初めてだから正直自信ない。でももし御役に立てるならと思い、一応置いときますね。
using UnityEngine;
using UniVRM10;
using System.Collections.Generic;
public class VRMController : MonoBehaviour
{
[SerializeField] private Vrm10Instance vrm;
private Vrm10RuntimeExpression vrm10RuntimeExpression;
private List<VRMCtrl_ExpressionTransition> expressionTransitions;
private void Awake()
{
vrm10RuntimeExpression = vrm.Runtime.Expression;
expressionTransitions = new List<VRMCtrl_ExpressionTransition>();
}
private void Update()
{
if (expressionTransitions == null || expressionTransitions.Count < 1) { return; }
List<VRMCtrl_ExpressionTransition> deleteList = new List<VRMCtrl_ExpressionTransition>();
Dictionary<ExpressionKey, float> facial = new Dictionary<ExpressionKey, float>();
float deltaTime = Time.deltaTime;
for (int i = 0; i < expressionTransitions.Count; i++)
{
VRMCtrl_ExpressionTransition expTransition = expressionTransitions[i];
ExpressionKey key = expTransition.expressionKey;
if (expTransition.delay <= expTransition.counter)
{
if (!expTransition.startFlg)
{
expTransition.startValue = vrm10RuntimeExpression.GetWeight(key);
expTransition.startFlg = true;
}
float startValue = expTransition.startValue;
float targetValue = expTransition.targetValue;
float tempCounter = expTransition.counter - expTransition.delay;//計算用
//変化量を計算
float lerp = 0;
if (0.0f < expTransition.duration && tempCounter < expTransition.duration)
{
lerp = tempCounter / expTransition.duration;
}
else
{
tempCounter = expTransition.duration;
lerp = 1;
}
float value = startValue + (targetValue - startValue) * lerp;
if (facial.ContainsKey(key))
{
facial[key] = value;
}
else
{
facial.Add(key, value);
}
}
if (expTransition.duration + expTransition.delay <= expTransition.counter)
{
//変化終了
deleteList.Add(expTransition);
}
expTransition.counter += deltaTime;
}
vrm10RuntimeExpression.SetWeights(facial);//値を渡す
//変化情報削除
foreach (VRMCtrl_ExpressionTransition data in deleteList)
{
this.expressionTransitions.Remove(data);
}
}
public void ResetExpression(float duration = 0.0f)
{//expressionTransitionsをすべて消す
expressionTransitions.Clear();
//現在初期状態にないExpressionをもとにもどす予約
Dictionary<ExpressionKey, float> currentExpressions
= (Dictionary<ExpressionKey, float>)vrm10RuntimeExpression.GetWeights();
foreach (KeyValuePair<ExpressionKey, float> entry in currentExpressions)
{
VRMCtrl_ExpressionTransition data = new VRMCtrl_ExpressionTransition();
data.expressionKey = entry.Key;
data.targetValue = 0.0f;
data.duration = duration;
expressionTransitions.Add(data);
}
}
/*******************************
* 指定したキーの表情変化を中断
* *****************************/
private void RemoveTransition(ExpressionKey expressionKey)
{
List<VRMCtrl_ExpressionTransition> deleteList = new List<VRMCtrl_ExpressionTransition>();
foreach (VRMCtrl_ExpressionTransition expTransition in expressionTransitions)
{
if (expTransition.expressionKey.Equals(expressionKey))
{
deleteList.Add(expTransition);
}
}
foreach (VRMCtrl_ExpressionTransition data in deleteList)
{
expressionTransitions.Remove(data);
}
}
/**************************
* 標準の表情に戻す
* ************************/
public void ResetExpression(float duration = 0.0f, float delay = 0.0f)
{//expressionTransitionsをすべて消す
expressionTransitions.Clear();
//現在初期状態にないExpressionをもとにもどす予約
Dictionary<ExpressionKey, float> currentExpressions
= (Dictionary<ExpressionKey, float>)vrm10RuntimeExpression.GetWeights();
foreach (KeyValuePair<ExpressionKey, float> entry in currentExpressions)
{
VRMCtrl_ExpressionTransition data = new VRMCtrl_ExpressionTransition();
data.expressionKey = entry.Key;
data.targetValue = 0.0f;
data.duration = duration;
data.delay = delay;
expressionTransitions.Add(data);
}
}
/**************************
* 表情の変化を追加
* ************************/
public void AddExpressionTransition(ExpressionKey expressionKey, float value,
float duration = 0.0f, float delay = 0.0f)
{
VRMCtrl_ExpressionTransition data = new VRMCtrl_ExpressionTransition();
data.expressionKey = expressionKey;
data.targetValue = value;
data.duration = duration;
data.delay = delay;
expressionTransitions.Add(data);
}
/**************************
* 表情の変化を更新
* ************************/
public void UpdateExpressionTransition(ExpressionKey expressionKey, float value,
float duration = 0.0f, float delay = 0.0f)
{
this.RemoveTransition(expressionKey);//変化を中断
VRMCtrl_ExpressionTransition data = new VRMCtrl_ExpressionTransition();
data.expressionKey = expressionKey;
data.targetValue = value;
data.duration = duration;
data.delay = delay;
expressionTransitions.Add(data);
}
class VRMCtrl_ExpressionTransition
{
public ExpressionKey expressionKey;
public float targetValue = 0;
public float startValue = 0;
public float duration = 0;
public float delay = 0;
public float counter = 0;
public bool startFlg = false;
}
}
呼び出し側。
[SerializeField] public VRMController vrmCtrl;
-----------------------中略-------------------------
vrmCtrl.AddExpressionTransition(ExpressionKey.CreateCustom("_Custom"), 1.0f, 0.5f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 1.0f, 0.0f, 0.0f);
vrmCtrl.ResetExpression(0.0f);
UpdateExpressionTransitionとAddExpressionTransitionの違いは、すでに設定したExpressionKeyに対して上書きするか否かの違い。
例えば瞬き3連続とかの設定をするのはこう書く
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 1.0f, 0.0f, 0.0f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 0.0f, 0.0f, 0.1f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 1.0f, 0.0f, 0.6f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 0.0f, 0.0f, 0.7f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 1.0f, 0.0f, 1.0f);
vrmCtrl.AddExpressionTransition(ExpressionKey.Blink, 0.0f, 0.0f, 1.1f);
これがもし全部UpdateExpressionTransitionを使ったとしたら、適用されるのは最後の1行だけで目をつむりっぱなしになってしまう。基本的にDelayオプションを指定したときはAddExpressionTransitionを使うことになるはず。
UpdateExpressionTransitionの使い所は…指示済みのExpressionを条件によって途中で中断させるとかかな?自分でもまだ本格的には使ってないから分かんね。
コメント
I agree, very useful message