캐릭터 로드
This commit is contained in:
119
Assets/02_Scripts/Character/VrmAvatar.cs
Normal file
119
Assets/02_Scripts/Character/VrmAvatar.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user