415 lines
18 KiB
C#
415 lines
18 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace NiloToon.NiloToonURP.MiscUtil
|
|
{
|
|
[Serializable]
|
|
public class RendererBlendshapes
|
|
{
|
|
[SerializeField]
|
|
public SkinnedMeshRenderer renderer;
|
|
|
|
[SerializeField]
|
|
public List<string> blendshapeNames = new List<string>();
|
|
|
|
[SerializeField]
|
|
[HideInInspector]
|
|
public List<int> blendshapeIndices = new List<int>();
|
|
|
|
[SerializeField]
|
|
public List<float> blendshapeMaxValues = new List<float>();
|
|
|
|
public RendererBlendshapes()
|
|
{
|
|
blendshapeNames = new List<string>();
|
|
blendshapeIndices = new List<int>();
|
|
blendshapeMaxValues = new List<float>();
|
|
}
|
|
}
|
|
|
|
public class AutoEyeBlink : MonoBehaviour
|
|
{
|
|
[SerializeField]
|
|
[Header("Targets")]
|
|
public List<RendererBlendshapes> rendererTargets = new List<RendererBlendshapes>();
|
|
|
|
[SerializeField]
|
|
[Header("Timing Settings")]
|
|
public float minCoolDown = 1f;
|
|
[SerializeField]
|
|
public float maxCoolDown = 3f;
|
|
[SerializeField]
|
|
public float minCloseAnimTime = 0.12f;
|
|
[SerializeField]
|
|
public float maxCloseAnimTime = 0.3f;
|
|
[SerializeField]
|
|
public float minHoldTime = 0.1f;
|
|
[SerializeField]
|
|
public float maxHoldTime = 0.2f;
|
|
|
|
private void OnValidate()
|
|
{
|
|
foreach (var target in rendererTargets)
|
|
{
|
|
if (target.renderer != null && target.renderer.sharedMesh != null)
|
|
{
|
|
target.blendshapeIndices.Clear();
|
|
foreach (var name in target.blendshapeNames)
|
|
{
|
|
target.blendshapeIndices.Add(target.renderer.sharedMesh.GetBlendShapeIndex(name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
rendererTargets.RemoveAll(target => target.renderer == null || target.blendshapeNames.Count == 0);
|
|
|
|
if (rendererTargets.Count > 0)
|
|
{
|
|
StartCoroutine(EyeBlinkLoop());
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("No valid blendshape targets set for AutoEyeBlink");
|
|
}
|
|
}
|
|
|
|
private IEnumerator EyeBlinkLoop()
|
|
{
|
|
while (true)
|
|
{
|
|
yield return new WaitForSeconds(UnityEngine.Random.Range(minCoolDown, maxCoolDown));
|
|
|
|
float closeTime = UnityEngine.Random.Range(minCloseAnimTime, maxCloseAnimTime);
|
|
|
|
yield return StartCoroutine(AnimateBlendshapes(0f, 100f, closeTime * 0.5f));
|
|
yield return new WaitForSeconds(UnityEngine.Random.Range(minHoldTime, maxHoldTime));
|
|
yield return StartCoroutine(AnimateBlendshapes(100f, 0f, closeTime * 0.5f));
|
|
}
|
|
}
|
|
|
|
private IEnumerator AnimateBlendshapes(float startValue, float endValue, float duration)
|
|
{
|
|
float elapsed = 0f;
|
|
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float normalizedTime = Mathf.Clamp01(elapsed / duration);
|
|
float currentValue = Mathf.Lerp(startValue, endValue, normalizedTime);
|
|
|
|
foreach (var target in rendererTargets)
|
|
{
|
|
if (target.renderer == null || target.renderer.sharedMesh == null) continue;
|
|
|
|
for (int i = 0; i < target.blendshapeIndices.Count; i++)
|
|
{
|
|
if (target.blendshapeIndices[i] != -1)
|
|
{
|
|
float maxValue = (target.blendshapeMaxValues.Count > i) ? target.blendshapeMaxValues[i] : 1f;
|
|
target.renderer.SetBlendShapeWeight(target.blendshapeIndices[i], currentValue * maxValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
foreach (var target in rendererTargets)
|
|
{
|
|
if (target.renderer == null || target.renderer.sharedMesh == null) continue;
|
|
|
|
for (int i = 0; i < target.blendshapeIndices.Count; i++)
|
|
{
|
|
if (target.blendshapeIndices[i] != -1)
|
|
{
|
|
float maxValue = (target.blendshapeMaxValues.Count > i) ? target.blendshapeMaxValues[i] : 1f;
|
|
target.renderer.SetBlendShapeWeight(target.blendshapeIndices[i], endValue * maxValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
[UnityEditor.CustomEditor(typeof(AutoEyeBlink))]
|
|
public class AutoEyeBlinkEditor : UnityEditor.Editor
|
|
{
|
|
private static class Styles
|
|
{
|
|
public static readonly GUIStyle headerStyle;
|
|
public static readonly GUIStyle sectionStyle;
|
|
public static readonly GUIStyle groupStyle;
|
|
|
|
static Styles()
|
|
{
|
|
headerStyle = new GUIStyle(UnityEditor.EditorStyles.boldLabel);
|
|
headerStyle.fontSize = 14;
|
|
headerStyle.margin = new RectOffset(4, 4, 12, 8);
|
|
|
|
sectionStyle = new GUIStyle(UnityEngine.GUI.skin.box);
|
|
sectionStyle.padding = new RectOffset(12, 12, 12, 12);
|
|
sectionStyle.margin = new RectOffset(0, 0, 8, 8);
|
|
|
|
groupStyle = new GUIStyle(UnityEditor.EditorStyles.helpBox);
|
|
groupStyle.padding = new RectOffset(8, 8, 8, 8);
|
|
groupStyle.margin = new RectOffset(0, 0, 4, 4);
|
|
}
|
|
}
|
|
|
|
private readonly Dictionary<string, string> tooltips = new Dictionary<string, string>
|
|
{
|
|
{ "minCoolDown", "Minimum time to wait between blinks" },
|
|
{ "maxCoolDown", "Maximum time to wait between blinks" },
|
|
{ "minCloseAnimTime", "Minimum duration of the blink animation" },
|
|
{ "maxCloseAnimTime", "Maximum duration of the blink animation" },
|
|
{ "minHoldTime", "Minimum time to keep eyes closed" },
|
|
{ "maxHoldTime", "Maximum time to keep eyes closed" }
|
|
};
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
serializedObject.Update();
|
|
AutoEyeBlink script = (AutoEyeBlink)target;
|
|
|
|
// Timing Settings
|
|
UnityEditor.EditorGUILayout.Space(4);
|
|
using (new UnityEditor.EditorGUILayout.VerticalScope(Styles.sectionStyle))
|
|
{
|
|
UnityEditor.EditorGUILayout.LabelField("Timing Settings", Styles.headerStyle);
|
|
|
|
using (new UnityEditor.EditorGUILayout.VerticalScope(Styles.groupStyle))
|
|
{
|
|
DrawFloatField("Min Cool Down", "minCoolDown", 0f, 10f);
|
|
DrawFloatField("Max Cool Down", "maxCoolDown", 0f, 10f);
|
|
DrawFloatField("Min Close Anim Time", "minCloseAnimTime", 0f, 1f);
|
|
DrawFloatField("Max Close Anim Time", "maxCloseAnimTime", 0f, 1f);
|
|
DrawFloatField("Min Hold Time", "minHoldTime", 0f, 1f);
|
|
DrawFloatField("Max Hold Time", "maxHoldTime", 0f, 1f);
|
|
}
|
|
|
|
ValidateTimings(script);
|
|
}
|
|
|
|
// Renderer Targets
|
|
UnityEditor.EditorGUILayout.Space(4);
|
|
using (new UnityEditor.EditorGUILayout.VerticalScope(Styles.sectionStyle))
|
|
{
|
|
UnityEditor.EditorGUILayout.LabelField("Renderer Targets", Styles.headerStyle);
|
|
|
|
SerializedProperty rendererTargetsProperty = serializedObject.FindProperty("rendererTargets");
|
|
|
|
for (int i = 0; i < script.rendererTargets.Count; i++)
|
|
{
|
|
using (new UnityEditor.EditorGUILayout.VerticalScope(Styles.groupStyle))
|
|
{
|
|
var target = script.rendererTargets[i];
|
|
SerializedProperty rendererTargetProperty = rendererTargetsProperty.GetArrayElementAtIndex(i);
|
|
SerializedProperty rendererProperty = rendererTargetProperty.FindPropertyRelative("renderer");
|
|
|
|
var rect = UnityEditor.EditorGUILayout.GetControlRect();
|
|
UnityEditor.EditorGUI.BeginProperty(rect, new GUIContent("Renderer"), rendererProperty);
|
|
|
|
using (var check = new UnityEditor.EditorGUI.ChangeCheckScope())
|
|
{
|
|
var newRenderer = (SkinnedMeshRenderer)UnityEditor.EditorGUI.ObjectField(
|
|
rect, "Renderer", target.renderer, typeof(SkinnedMeshRenderer), true);
|
|
|
|
if (check.changed)
|
|
{
|
|
UnityEditor.Undo.RecordObject(script, "Change Renderer");
|
|
target.renderer = newRenderer;
|
|
target.blendshapeNames.Clear();
|
|
target.blendshapeIndices.Clear();
|
|
target.blendshapeMaxValues.Clear();
|
|
UnityEditor.EditorUtility.SetDirty(script);
|
|
}
|
|
}
|
|
|
|
UnityEditor.EditorGUI.EndProperty();
|
|
|
|
if (target.renderer != null && target.renderer.sharedMesh != null)
|
|
{
|
|
DrawBlendshapeSection(target, target.renderer.sharedMesh, script, i);
|
|
}
|
|
else
|
|
{
|
|
UnityEditor.EditorGUILayout.HelpBox("Please assign a SkinnedMeshRenderer with a valid mesh", UnityEditor.MessageType.Info);
|
|
}
|
|
|
|
if (GUILayout.Button("Remove Renderer", GUILayout.Width(120)))
|
|
{
|
|
UnityEditor.Undo.RecordObject(script, "Remove Renderer");
|
|
script.rendererTargets.RemoveAt(i);
|
|
UnityEditor.EditorUtility.SetDirty(script);
|
|
i--;
|
|
}
|
|
}
|
|
UnityEditor.EditorGUILayout.Space(2);
|
|
}
|
|
|
|
UnityEditor.EditorGUILayout.Space(4);
|
|
if (GUILayout.Button("Add Renderer", GUILayout.Height(24)))
|
|
{
|
|
UnityEditor.Undo.RecordObject(script, "Add Renderer");
|
|
script.rendererTargets.Add(new RendererBlendshapes());
|
|
UnityEditor.EditorUtility.SetDirty(script);
|
|
}
|
|
}
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
private void DrawFloatField(string label, string propertyName, float min, float max)
|
|
{
|
|
var prop = serializedObject.FindProperty(propertyName);
|
|
var rect = UnityEditor.EditorGUILayout.GetControlRect();
|
|
|
|
UnityEditor.EditorGUI.BeginProperty(rect, new GUIContent(label, tooltips[propertyName]), prop);
|
|
float newValue = UnityEditor.EditorGUI.Slider(rect, label, prop.floatValue, min, max);
|
|
if (newValue != prop.floatValue)
|
|
{
|
|
prop.floatValue = newValue;
|
|
}
|
|
UnityEditor.EditorGUI.EndProperty();
|
|
}
|
|
|
|
private void ValidateTimings(AutoEyeBlink script)
|
|
{
|
|
if (script.minCoolDown > script.maxCoolDown)
|
|
UnityEditor.EditorGUILayout.HelpBox("Min Cool Down should be less than Max Cool Down", UnityEditor.MessageType.Warning);
|
|
|
|
if (script.minCloseAnimTime > script.maxCloseAnimTime)
|
|
UnityEditor.EditorGUILayout.HelpBox("Min Close Anim Time should be less than Max Close Anim Time", UnityEditor.MessageType.Warning);
|
|
|
|
if (script.minHoldTime > script.maxHoldTime)
|
|
UnityEditor.EditorGUILayout.HelpBox("Min Hold Time should be less than Max Hold Time", UnityEditor.MessageType.Warning);
|
|
}
|
|
|
|
private void DrawBlendshapeSection(RendererBlendshapes target, UnityEngine.Mesh mesh, AutoEyeBlink script, int targetIndex)
|
|
{
|
|
var blendshapeNames = new List<string>();
|
|
for (int j = 0; j < mesh.blendShapeCount; j++)
|
|
{
|
|
blendshapeNames.Add(mesh.GetBlendShapeName(j));
|
|
}
|
|
|
|
if (blendshapeNames.Count == 0)
|
|
{
|
|
UnityEditor.EditorGUILayout.HelpBox("This mesh has no blendshapes", UnityEditor.MessageType.Info);
|
|
return;
|
|
}
|
|
|
|
EditorGUILayout.LabelField("Blendshapes", EditorStyles.miniLabel);
|
|
EditorGUI.indentLevel++;
|
|
|
|
for (int j = 0; j < target.blendshapeNames.Count; j++)
|
|
{
|
|
while (target.blendshapeMaxValues.Count <= j)
|
|
target.blendshapeMaxValues.Add(1f);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
{
|
|
// Label
|
|
EditorGUILayout.LabelField($"Shape {j + 1}", EditorStyles.miniLabel, GUILayout.Width(60));
|
|
|
|
// Dropdown
|
|
int currentIndex = blendshapeNames.IndexOf(target.blendshapeNames[j]);
|
|
if (currentIndex == -1) currentIndex = 0;
|
|
|
|
using (var check = new EditorGUI.ChangeCheckScope())
|
|
{
|
|
var dropdownStyle = new GUIStyle(EditorStyles.miniPullDown)
|
|
{
|
|
fixedWidth = 120, // Increased width to show full name
|
|
alignment = TextAnchor.MiddleLeft,
|
|
clipping = TextClipping.Clip
|
|
};
|
|
|
|
// Create the popup content with the actual blendshape names
|
|
int newIndex = EditorGUILayout.Popup(currentIndex,
|
|
blendshapeNames.ToArray(),
|
|
dropdownStyle);
|
|
|
|
if (check.changed)
|
|
{
|
|
Undo.RecordObject(script, "Change Blendshape");
|
|
target.blendshapeNames[j] = blendshapeNames[newIndex];
|
|
EditorUtility.SetDirty(script);
|
|
}
|
|
}
|
|
|
|
// Slider
|
|
using (var check = new EditorGUI.ChangeCheckScope())
|
|
{
|
|
float newValue = EditorGUILayout.Slider(target.blendshapeMaxValues[j], 0f, 1f, GUILayout.ExpandWidth(true));
|
|
if (check.changed)
|
|
{
|
|
Undo.RecordObject(script, "Change Max Value");
|
|
target.blendshapeMaxValues[j] = newValue;
|
|
EditorUtility.SetDirty(script);
|
|
}
|
|
}
|
|
|
|
GUILayout.Space(4); // Add small space before the X button
|
|
|
|
// Remove button with dark theme style
|
|
var buttonStyle = new GUIStyle(EditorStyles.miniButton)
|
|
{
|
|
fixedWidth = 18,
|
|
fixedHeight = 16,
|
|
padding = new RectOffset(0, 0, 0, 0),
|
|
margin = new RectOffset(0, 0, 1, 0),
|
|
alignment = TextAnchor.MiddleCenter,
|
|
normal = new GUIStyleState
|
|
{
|
|
background = EditorGUIUtility.whiteTexture,
|
|
textColor = new Color(0.7f, 0.7f, 0.7f)
|
|
}
|
|
};
|
|
|
|
if (GUILayout.Button("X", buttonStyle))
|
|
{
|
|
Undo.RecordObject(script, "Remove Blendshape");
|
|
target.blendshapeNames.RemoveAt(j);
|
|
target.blendshapeMaxValues.RemoveAt(j);
|
|
EditorUtility.SetDirty(script);
|
|
j--;
|
|
}
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
EditorGUI.indentLevel--;
|
|
EditorGUILayout.Space(2);
|
|
|
|
// Add Blendshape button
|
|
var addButtonStyle = new GUIStyle(GUI.skin.button)
|
|
{
|
|
fontSize = 11,
|
|
alignment = TextAnchor.MiddleCenter,
|
|
normal = new GUIStyleState { textColor = new Color(0.7f, 0.7f, 0.7f) }
|
|
};
|
|
|
|
if (GUILayout.Button("Add Blendshape", addButtonStyle))
|
|
{
|
|
if (blendshapeNames.Count > 0)
|
|
{
|
|
Undo.RecordObject(script, "Add Blendshape");
|
|
target.blendshapeNames.Add(blendshapeNames[0]);
|
|
target.blendshapeMaxValues.Add(1f);
|
|
EditorUtility.SetDirty(script);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
} |