using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
///
/// Editor utility that pins the humanoid knee muscles to fully straight on selected animation clips.
///
/// On this project's auto-generated PaintmanAvatar, a fully straight leg corresponds to the
/// "Lower Leg Stretch" muscle sitting at its maximum (1.0); anything below reads as a visibly bent
/// knee (~8 deg per 0.1). Authoring tools (UMotion, Mixamo retargets) commonly land at ~0.93, which
/// shows as the "slightly bent knees on an idle that should be straight" symptom. This tool sets the
/// Left/Right Lower Leg Stretch curves to a constant 1.0 so the knees read as a true 180 deg.
///
/// Only native .anim humanoid clips can be edited — imported clips (FBX/Blend) are regenerated on
/// import, so their curves can't be changed here and are skipped with a warning.
///
public static class StraightenKneesTool
{
private const string LeftStretch = "Left Lower Leg Stretch";
private const string RightStretch = "Right Lower Leg Stretch";
private const float StraightValue = 1f; // muscle value that maps to a fully straight knee on this avatar
[MenuItem("Tools/Chameleon/Straighten Knees (Selected Clips)", false, 100)]
private static void StraightenSelected()
{
List clips = GetSelectedClips();
if (clips.Count == 0)
{
EditorUtility.DisplayDialog("Straighten Knees",
"Select one or more native (.anim) humanoid AnimationClips in the Project window.", "OK");
return;
}
int changed = 0, skipped = 0;
foreach (AnimationClip clip in clips)
{
if (StraightenClip(clip)) changed++;
else skipped++;
}
if (changed > 0)
AssetDatabase.SaveAssets();
Debug.Log($"[Straighten Knees] Lower-leg stretch set to {StraightValue:0.##} on {changed} clip(s)" +
(skipped > 0 ? $", skipped {skipped} (non-.anim or non-humanoid)." : "."));
}
[MenuItem("Tools/Chameleon/Straighten Knees (Selected Clips)", true)]
private static bool StraightenSelectedValidate() => GetSelectedClips().Count > 0;
private static bool StraightenClip(AnimationClip clip)
{
if (!clip)
return false;
string path = AssetDatabase.GetAssetPath(clip);
if (!path.EndsWith(".anim", System.StringComparison.OrdinalIgnoreCase))
{
Debug.LogWarning($"[Straighten Knees] '{clip.name}' is an imported clip ({path}); its curves are " +
"regenerated on import and can't be edited. Make an editable .anim copy first.", clip);
return false;
}
if (!clip.humanMotion)
{
Debug.LogWarning($"[Straighten Knees] '{clip.name}' is not a humanoid clip; skipped.", clip);
return false;
}
Undo.RegisterCompleteObjectUndo(clip, "Straighten Knees");
SetMuscleConstant(clip, LeftStretch, StraightValue);
SetMuscleConstant(clip, RightStretch, StraightValue);
EditorUtility.SetDirty(clip);
return true;
}
/// Replaces (or adds) the named muscle curve with a flat constant across the whole clip.
private static void SetMuscleConstant(AnimationClip clip, string muscle, float value)
{
var binding = EditorCurveBinding.FloatCurve(string.Empty, typeof(Animator), muscle);
float end = clip.length > 0f ? clip.length : 1f / Mathf.Max(1f, clip.frameRate);
var curve = new AnimationCurve(new Keyframe(0f, value), new Keyframe(end, value));
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
private static List GetSelectedClips()
{
var result = new List();
foreach (Object o in Selection.objects)
{
if (o is AnimationClip clip && !result.Contains(clip))
result.Add(clip);
}
return result;
}
}