120 lines
4.0 KiB
C#
120 lines
4.0 KiB
C#
using UnityEngine;
|
|
using UniVRM10;
|
|
|
|
/// <summary>
|
|
/// VRM 1.0 모델용 ICharacterAvatar 구현.
|
|
///
|
|
/// FBX 와 달리 매핑 작업이 필요 없다. VRM 규격이 표정과 입모양의 이름을
|
|
/// 강제하므로 어떤 VRM 모델이든 같은 코드로 동작한다. 런타임 로딩으로
|
|
/// 사용자가 임의의 모델을 넣는 구조에서는 이 점이 필수적이다.
|
|
/// </summary>
|
|
public class VrmAvatar : MonoBehaviour, ICharacterAvatar
|
|
{
|
|
[Tooltip("VRM 자체 자동 깜빡임/시선을 쓰지 않고 우리가 제어할지. " +
|
|
"우리 HeadLookAt 과 충돌하지 않게 기본은 켬")]
|
|
[SerializeField] bool overrideVrmAutoExpressions = true;
|
|
|
|
Vrm10Instance instance;
|
|
|
|
float[] emotionWeights;
|
|
float[] visemeWeights;
|
|
float blinkWeight;
|
|
|
|
public Transform Root => transform;
|
|
public Animator Animator { get; private set; }
|
|
|
|
/// <summary>로더가 인스턴스를 만든 직후 호출한다.</summary>
|
|
public void Bind(Vrm10Instance vrm)
|
|
{
|
|
instance = vrm;
|
|
Animator = vrm != null ? vrm.GetComponent<Animator>() : null;
|
|
|
|
emotionWeights = new float[System.Enum.GetValues(typeof(AvatarEmotion)).Length];
|
|
visemeWeights = new float[System.Enum.GetValues(typeof(AvatarViseme)).Length];
|
|
|
|
if (instance != null && overrideVrmAutoExpressions)
|
|
{
|
|
// VRM 이 스스로 깜빡이고 시선을 돌리면 우리 제어와 싸운다.
|
|
instance.LookAtTargetType = VRM10ObjectLookAt.LookAtTargetTypes.SpecifiedTransform;
|
|
}
|
|
}
|
|
|
|
// ---------------- ICharacterAvatar ----------------
|
|
|
|
public void SetEmotion(AvatarEmotion emotion, float weight)
|
|
{
|
|
if (emotionWeights == null) return;
|
|
emotionWeights[(int)emotion] = Mathf.Clamp01(weight);
|
|
}
|
|
|
|
public void ClearEmotions()
|
|
{
|
|
if (emotionWeights == null) return;
|
|
System.Array.Clear(emotionWeights, 0, emotionWeights.Length);
|
|
}
|
|
|
|
public void SetViseme(AvatarViseme viseme, float weight)
|
|
{
|
|
if (visemeWeights == null) return;
|
|
visemeWeights[(int)viseme] = Mathf.Clamp01(weight);
|
|
}
|
|
|
|
public void ClearVisemes()
|
|
{
|
|
if (visemeWeights == null) return;
|
|
System.Array.Clear(visemeWeights, 0, visemeWeights.Length);
|
|
}
|
|
|
|
public void SetBlink(float weight) => blinkWeight = Mathf.Clamp01(weight);
|
|
|
|
// ---------------- 반영 ----------------
|
|
|
|
void LateUpdate()
|
|
{
|
|
if (instance == null || instance.Runtime == null) return;
|
|
|
|
var expr = instance.Runtime.Expression;
|
|
if (expr == null) return;
|
|
|
|
// 감정. Neutral(0) 은 "표정 없음"이라 대응 키가 없다.
|
|
for (int i = 1; i < emotionWeights.Length; i++)
|
|
{
|
|
expr.SetWeight(ToKey((AvatarEmotion)i), emotionWeights[i]);
|
|
}
|
|
|
|
// 입 모양. Silence(0) 도 마찬가지로 대응 키가 없다.
|
|
for (int i = 1; i < visemeWeights.Length; i++)
|
|
{
|
|
expr.SetWeight(ToKey((AvatarViseme)i), visemeWeights[i]);
|
|
}
|
|
|
|
expr.SetWeight(ExpressionKey.Blink, blinkWeight);
|
|
}
|
|
|
|
static ExpressionKey ToKey(AvatarEmotion e)
|
|
{
|
|
switch (e)
|
|
{
|
|
case AvatarEmotion.Happy: return ExpressionKey.Happy;
|
|
case AvatarEmotion.Angry: return ExpressionKey.Angry;
|
|
case AvatarEmotion.Sad: return ExpressionKey.Sad;
|
|
case AvatarEmotion.Relaxed: return ExpressionKey.Relaxed;
|
|
case AvatarEmotion.Surprised: return ExpressionKey.Surprised;
|
|
default: return ExpressionKey.Neutral;
|
|
}
|
|
}
|
|
|
|
static ExpressionKey ToKey(AvatarViseme v)
|
|
{
|
|
switch (v)
|
|
{
|
|
case AvatarViseme.A: return ExpressionKey.Aa;
|
|
case AvatarViseme.I: return ExpressionKey.Ih;
|
|
case AvatarViseme.U: return ExpressionKey.Ou;
|
|
case AvatarViseme.E: return ExpressionKey.Ee;
|
|
case AvatarViseme.O: return ExpressionKey.Oh;
|
|
default: return ExpressionKey.Neutral;
|
|
}
|
|
}
|
|
}
|