using System; using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class FacialExpression { public string name; public BlendShapeWeight[] blendShapeWeights; [Range(0.1f, 5f)] public float duration = 1f; [Range(0f, 2f)] public float holdTime = 0.5f; } [System.Serializable] public class BlendShapeWeight { public string blendShapeName; [Range(0f, 1f)] public float weight = 0.5f; } public class RPMFacialAnimator : MonoBehaviour { [Header("Avatar Configuration")] [SerializeField] private Transform avatarRoot; [SerializeField] private string headRendererName = "Renderer_Head"; [Header("Animation Settings")] [SerializeField] private bool autoPlay = true; [SerializeField] private bool loopAnimations = true; [SerializeField] private float timeBetweenExpressions = 2f; [SerializeField] private bool smoothTransitions = true; [SerializeField] private AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); [Header("Predefined Expressions")] [SerializeField] private FacialExpression[] facialExpressions; [Header("Manual Controls")] [SerializeField] private bool enableManualControls = true; [Range(0f, 1f)] public float mouthSmile = 0f; [Range(0f, 1f)] public float mouthOpen = 0f; [Range(0f, 1f)] public float eyeBlinkLeft = 0f; [Range(0f, 1f)] public float eyeBlinkRight = 0f; [Range(0f, 1f)] public float browInnerUp = 0f; [Range(0f, 1f)] public float cheekPuff = 0f; [Range(0f, 1f)] public float jawOpen = 0f; [Header("Auto Blink Settings")] [SerializeField] private bool enableAutoBlink = true; [SerializeField] private float blinkInterval = 3f; [SerializeField] private float blinkDuration = 0.15f; [SerializeField] private float blinkRandomness = 1f; [Header("Debug")] [SerializeField] private bool showDebugInfo = false; [SerializeField] private bool listAllBlendShapes = false; private SkinnedMeshRenderer headRenderer; private Dictionary blendShapeIndexMap; private Coroutine expressionCoroutine; private Coroutine blinkCoroutine; private bool isAnimating = false; // RPM ARKit Blendshape names for reference private readonly string[] arkitBlendShapes = { // Eyes "eyeBlinkLeft", "eyeBlinkRight", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft", "eyeSquintLeft", "eyeWideLeft", "eyeLookDownRight", "eyeLookInRight", "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight", // Jaw "jawForward", "jawLeft", "jawRight", "jawOpen", // Mouth "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight", "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight", "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight", "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight", "mouthUpperUpLeft", "mouthUpperUpRight", // Brows "browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight", // Cheeks "cheekPuff", "cheekSquintLeft", "cheekSquintRight", // Nose "noseSneerLeft", "noseSneerRight", // Tongue "tongueOut", // Additional "mouthOpen", "mouthSmile", "eyesClosed", "eyesLookUp", "eyesLookDown" }; void Start() { InitializeAnimator(); SetupDefaultExpressions(); if (autoPlay && !enableManualControls) { StartExpressionAnimation(); } if (enableAutoBlink) { StartAutoBlinking(); } } void InitializeAnimator() { // Find avatar root if not assigned if (avatarRoot == null) { avatarRoot = transform; } // Find the head renderer Transform headRendererTransform = FindChildRecursive(avatarRoot, headRendererName); if (headRendererTransform == null) { Debug.LogError($"RPMFacialAnimator: Could not find child object with name '{headRendererName}' in avatar hierarchy."); return; } headRenderer = headRendererTransform.GetComponent(); if (headRenderer == null) { Debug.LogError($"RPMFacialAnimator: '{headRendererName}' does not have a SkinnedMeshRenderer component."); return; } // Build blend shape index map BuildBlendShapeIndexMap(); if (showDebugInfo) { Debug.Log($"RPMFacialAnimator: Initialized with {blendShapeIndexMap.Count} blend shapes on {headRendererName}"); } if (listAllBlendShapes) { ListAllBlendShapes(); } } void BuildBlendShapeIndexMap() { blendShapeIndexMap = new Dictionary(); if (headRenderer?.sharedMesh == null) return; for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++) { string blendShapeName = headRenderer.sharedMesh.GetBlendShapeName(i); blendShapeIndexMap[blendShapeName] = i; } } void ListAllBlendShapes() { if (headRenderer?.sharedMesh == null) return; Debug.Log("=== RPM Avatar Blend Shapes ==="); for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++) { string name = headRenderer.sharedMesh.GetBlendShapeName(i); bool isARKit = Array.Exists(arkitBlendShapes, bs => bs == name); Debug.Log($"Blend Shape {i}: {name} {(isARKit ? "(ARKit)" : "(Custom)")}"); } } Transform FindChildRecursive(Transform parent, string childName) { foreach (Transform child in parent) { if (child.name == childName) return child; Transform found = FindChildRecursive(child, childName); if (found != null) return found; } return null; } void SetupDefaultExpressions() { if (facialExpressions == null || facialExpressions.Length == 0) { facialExpressions = new FacialExpression[] { // Happy Expression new FacialExpression { name = "Happy", duration = 1.5f, holdTime = 1f, blendShapeWeights = new BlendShapeWeight[] { new BlendShapeWeight { blendShapeName = "mouthSmile", weight = 0.9f }, new BlendShapeWeight { blendShapeName = "mouthSmileLeft", weight = 0.8f }, new BlendShapeWeight { blendShapeName = "mouthSmileRight", weight = 0.8f }, new BlendShapeWeight { blendShapeName = "cheekSquintLeft", weight = 0.5f }, new BlendShapeWeight { blendShapeName = "cheekSquintRight", weight = 0.5f } } }, // Surprised Expression new FacialExpression { name = "Surprised", duration = 1f, holdTime = 0.8f, blendShapeWeights = new BlendShapeWeight[] { new BlendShapeWeight { blendShapeName = "mouthOpen", weight = 0.8f }, new BlendShapeWeight { blendShapeName = "jawOpen", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "eyeWideLeft", weight = 1.0f }, new BlendShapeWeight { blendShapeName = "eyeWideRight", weight = 1.0f }, new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.9f }, new BlendShapeWeight { blendShapeName = "browOuterUpLeft", weight = 0.7f }, new BlendShapeWeight { blendShapeName = "browOuterUpRight", weight = 0.7f } } }, // Sad Expression new FacialExpression { name = "Sad", duration = 2f, holdTime = 1.5f, blendShapeWeights = new BlendShapeWeight[] { new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.8f }, new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.8f }, new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "browDownRight", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.5f } } }, // Angry Expression new FacialExpression { name = "Angry", duration = 1.2f, holdTime = 1f, blendShapeWeights = new BlendShapeWeight[] { new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 1.0f }, new BlendShapeWeight { blendShapeName = "browDownRight", weight = 1.0f }, new BlendShapeWeight { blendShapeName = "eyeSquintLeft", weight = 0.7f }, new BlendShapeWeight { blendShapeName = "eyeSquintRight", weight = 0.7f }, new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "noseSneerLeft", weight = 0.5f }, new BlendShapeWeight { blendShapeName = "noseSneerRight", weight = 0.5f } } }, // Thinking Expression new FacialExpression { name = "Thinking", duration = 1.8f, holdTime = 2f, blendShapeWeights = new BlendShapeWeight[] { new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.6f }, new BlendShapeWeight { blendShapeName = "mouthPucker", weight = 0.5f }, new BlendShapeWeight { blendShapeName = "eyeLookUpLeft", weight = 0.4f }, new BlendShapeWeight { blendShapeName = "eyeLookUpRight", weight = 0.4f } } } }; } } void Update() { if (enableManualControls && headRenderer != null) { ApplyManualControls(); } } void ApplyManualControls() { SetBlendShapeWeight("mouthSmile", mouthSmile); SetBlendShapeWeight("mouthOpen", mouthOpen); SetBlendShapeWeight("eyeBlinkLeft", eyeBlinkLeft); SetBlendShapeWeight("eyeBlinkRight", eyeBlinkRight); SetBlendShapeWeight("browInnerUp", browInnerUp); SetBlendShapeWeight("cheekPuff", cheekPuff); SetBlendShapeWeight("jawOpen", jawOpen); } public void SetBlendShapeWeight(string blendShapeName, float weight) { if (headRenderer == null || blendShapeIndexMap == null) return; if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index)) { headRenderer.SetBlendShapeWeight(index, Mathf.Clamp(weight, 0f, 1f)); } else if (showDebugInfo) { Debug.LogWarning($"RPMFacialAnimator: Blend shape '{blendShapeName}' not found."); } } public float GetBlendShapeWeight(string blendShapeName) { if (headRenderer == null || blendShapeIndexMap == null) return 0f; if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index)) { return headRenderer.GetBlendShapeWeight(index); } return 0f; } public void PlayExpression(string expressionName) { FacialExpression expression = Array.Find(facialExpressions, expr => expr.name == expressionName); if (expression != null) { StartCoroutine(PlayExpressionCoroutine(expression)); } else { Debug.LogWarning($"RPMFacialAnimator: Expression '{expressionName}' not found."); } } public void PlayExpression(int expressionIndex) { if (expressionIndex >= 0 && expressionIndex < facialExpressions.Length) { StartCoroutine(PlayExpressionCoroutine(facialExpressions[expressionIndex])); } else { Debug.LogWarning($"RPMFacialAnimator: Expression index {expressionIndex} is out of range."); } } IEnumerator PlayExpressionCoroutine(FacialExpression expression) { isAnimating = true; // Store starting weights Dictionary startWeights = new Dictionary(); foreach (var blendWeight in expression.blendShapeWeights) { startWeights[blendWeight.blendShapeName] = GetBlendShapeWeight(blendWeight.blendShapeName); } // Animate to target weights float elapsedTime = 0f; while (elapsedTime < expression.duration) { float progress = elapsedTime / expression.duration; float curveValue = transitionCurve.Evaluate(progress); foreach (var blendWeight in expression.blendShapeWeights) { float startWeight = startWeights.ContainsKey(blendWeight.blendShapeName) ? startWeights[blendWeight.blendShapeName] : 0f; float targetWeight = blendWeight.weight; float currentWeight = Mathf.Lerp(startWeight, targetWeight, curveValue); SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight); } elapsedTime += Time.deltaTime; yield return null; } // Hold the expression if (expression.holdTime > 0f) { yield return new WaitForSeconds(expression.holdTime); } // Return to neutral elapsedTime = 0f; while (elapsedTime < expression.duration) { float progress = elapsedTime / expression.duration; float curveValue = transitionCurve.Evaluate(progress); foreach (var blendWeight in expression.blendShapeWeights) { float currentWeight = Mathf.Lerp(blendWeight.weight, 0f, curveValue); SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight); } elapsedTime += Time.deltaTime; yield return null; } // Ensure all weights are reset to 0 foreach (var blendWeight in expression.blendShapeWeights) { SetBlendShapeWeight(blendWeight.blendShapeName, 0f); } isAnimating = false; } public void StartExpressionAnimation() { if (expressionCoroutine != null) { StopCoroutine(expressionCoroutine); } expressionCoroutine = StartCoroutine(ExpressionAnimationLoop()); } public void StopExpressionAnimation() { if (expressionCoroutine != null) { StopCoroutine(expressionCoroutine); expressionCoroutine = null; } } IEnumerator ExpressionAnimationLoop() { while (loopAnimations && !enableManualControls) { if (facialExpressions.Length > 0 && !isAnimating) { int randomIndex = UnityEngine.Random.Range(0, facialExpressions.Length); yield return StartCoroutine(PlayExpressionCoroutine(facialExpressions[randomIndex])); } yield return new WaitForSeconds(timeBetweenExpressions); } } public void StartAutoBlinking() { if (blinkCoroutine != null) { StopCoroutine(blinkCoroutine); } blinkCoroutine = StartCoroutine(AutoBlinkLoop()); } public void StopAutoBlinking() { if (blinkCoroutine != null) { StopCoroutine(blinkCoroutine); blinkCoroutine = null; } } IEnumerator AutoBlinkLoop() { while (enableAutoBlink) { // Wait for random interval float waitTime = blinkInterval + UnityEngine.Random.Range(-blinkRandomness, blinkRandomness); yield return new WaitForSeconds(Mathf.Max(0.5f, waitTime)); // Blink yield return StartCoroutine(BlinkCoroutine()); } } IEnumerator BlinkCoroutine() { // Close eyes float elapsedTime = 0f; float halfDuration = blinkDuration * 0.5f; while (elapsedTime < halfDuration) { float progress = elapsedTime / halfDuration; float blinkWeight = Mathf.Lerp(0f, 1f, progress); SetBlendShapeWeight("eyeBlinkLeft", blinkWeight); SetBlendShapeWeight("eyeBlinkRight", blinkWeight); elapsedTime += Time.deltaTime; yield return null; } // Open eyes elapsedTime = 0f; while (elapsedTime < halfDuration) { float progress = elapsedTime / halfDuration; float blinkWeight = Mathf.Lerp(1f, 0f, progress); SetBlendShapeWeight("eyeBlinkLeft", blinkWeight); SetBlendShapeWeight("eyeBlinkRight", blinkWeight); elapsedTime += Time.deltaTime; yield return null; } // Ensure eyes are fully open SetBlendShapeWeight("eyeBlinkLeft", 0f); SetBlendShapeWeight("eyeBlinkRight", 0f); } public void ResetAllBlendShapes() { if (headRenderer == null || blendShapeIndexMap == null) return; foreach (var kvp in blendShapeIndexMap) { headRenderer.SetBlendShapeWeight(kvp.Value, 0f); } } // Public API Methods public void Smile() => PlayExpression("Happy"); public void Surprise() => PlayExpression("Surprised"); public void Frown() => PlayExpression("Sad"); public void Angry() => PlayExpression("Angry"); public void Think() => PlayExpression("Thinking"); public void SetAvatarRoot(Transform newRoot) { avatarRoot = newRoot; InitializeAnimator(); } public void SetManualControlMode(bool enabled) { enableManualControls = enabled; if (enabled) { // Stop auto-expressions StopExpressionAnimation(); ResetAllBlendShapes(); } else if (autoPlay) { // Restart auto-expressions StartExpressionAnimation(); } } void OnValidate() { // Update manual controls in real-time during edit mode if (Application.isPlaying && enableManualControls) { // Stop auto-expressions when manual controls are enabled if (expressionCoroutine != null) { StopCoroutine(expressionCoroutine); expressionCoroutine = null; } // Reset any ongoing animations isAnimating = false; ApplyManualControls(); } else if (Application.isPlaying && autoPlay && !enableManualControls) { // Restart auto-expressions if manual controls are disabled if (expressionCoroutine == null) { StartExpressionAnimation(); } } } }