using UnityEngine; using UniVRM10; /// /// VRM 1.0 모델용 ICharacterAvatar 구현. /// /// FBX 와 달리 매핑 작업이 필요 없다. VRM 규격이 표정과 입모양의 이름을 /// 강제하므로 어떤 VRM 모델이든 같은 코드로 동작한다. 런타임 로딩으로 /// 사용자가 임의의 모델을 넣는 구조에서는 이 점이 필수적이다. /// 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; } /// 로더가 인스턴스를 만든 직후 호출한다. public void Bind(Vrm10Instance vrm) { instance = vrm; Animator = vrm != null ? vrm.GetComponent() : 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; } } }