580 lines
20 KiB
C#
580 lines
20 KiB
C#
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<string, int> 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<SkinnedMeshRenderer>();
|
|
|
|
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<string, int>();
|
|
|
|
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<string, float> startWeights = new Dictionary<string, float>();
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
} |