Files
2026-08-26 03:39:36 +09:00

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;
}
}
}