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; } }