// WL762_RootCurve.cs — PD 지시 #762 M1 실측 (에디트 모드 · Assets 무수정) // unity command run_script --file AgentScripts/WL762_RootCurve.cs --entry WL762_RootCurve.Run // 휴머노이드 클립은 AnimationMode.SampleAnimationClip 으로 본 위치를 못 읽는 경우가 있어 // (실측 2026-09-06 — 전 구간 0.00), 클립의 **루트 모션 커브(RootT.x/y/z)** 를 직접 읽는다. // 아울러 서브클립 .meta 에 걸린 애니메이션 이벤트(Move·ShowEffect) 시각도 함께 덤프한다. using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; public static class WL762_RootCurve { public static object Run() { var sb = new StringBuilder(); // 원본 FBX (Paladin = Sword&Shield 계열 · Knight = OneHand 계열) string[] fbx = { "Assets/Res_Addr/Animations/Animation/Paladin/attack.FBX", "Assets/Res_Addr/Animations/Animation/Paladin/Stander@Sword&Shield_Attack2.FBX", "Assets/Res_Addr/Animations/Animation/Paladin/Stander@Sword&Shield_Attack3.FBX", "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack2_S.FBX", "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX", }; foreach (var p in fbx) sb.Append(DumpFbx(p)); return sb.ToString(); } /// 서브클립을 품은 FBX 를 이름으로 찾아 전부 덤프한다. public static object Find(string clipNameFilter) { var sb = new StringBuilder(); foreach (var guid in AssetDatabase.FindAssets("t:Model")) { string path = AssetDatabase.GUIDToAssetPath(guid); var imp = AssetImporter.GetAtPath(path) as ModelImporter; if (imp == null) continue; if (!imp.clipAnimations.Any(c => c.name.Contains(clipNameFilter))) continue; sb.Append(DumpFbx(path)); } return sb.Length == 0 ? "no fbx with clip containing: " + clipNameFilter : sb.ToString(); } static string DumpFbx(string fbxPath) { var sb = new StringBuilder(); var imp = AssetImporter.GetAtPath(fbxPath) as ModelImporter; if (imp == null) return "importer not found: " + fbxPath + "\n"; sb.AppendLine("### " + fbxPath + " animType=" + imp.animationType); var clips = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType() .Where(c => !c.name.StartsWith("__preview__")).ToDictionary(c => c.name, c => c); foreach (var ca in imp.clipAnimations) { AnimationClip clip; clips.TryGetValue(ca.name, out clip); sb.AppendLine(string.Format(" -- {0} len={1} frames[{2}..{3}] bakeXZ(loopBlendPositionXZ)={4} bakeY={5} bakeRot={6} keepOrigXZ={7} keepOrigY={8}", ca.name, clip != null ? clip.length.ToString("F3") : "?", ca.firstFrame, ca.lastFrame, ca.lockRootPositionXZ, ca.lockRootHeightY, ca.lockRootRotation, ca.keepOriginalPositionXZ, ca.keepOriginalPositionY)); if (clip != null) sb.Append(DumpRoot(clip)); var evs = ca.events; if (evs != null && evs.Length > 0) { var parts = evs.Select(e => string.Format("{0}@{1:F5}(i={2},f={3:F3},s='{4}')", e.functionName, e.time, e.intParameter, e.floatParameter, e.stringParameter)); sb.AppendLine(" events: " + string.Join(" | ", parts.ToArray())); } else sb.AppendLine(" events: (none)"); } return sb.ToString(); } /// 클립의 RootT/RootQ 커브를 직접 읽어 전진량을 잰다. static string DumpRoot(AnimationClip clip) { var sb = new StringBuilder(); var binds = AnimationUtility.GetCurveBindings(clip); var names = new List(); AnimationCurve cx = null, cy = null, cz = null; foreach (var b in binds) { if (b.propertyName == "RootT.x") cx = AnimationUtility.GetEditorCurve(clip, b); else if (b.propertyName == "RootT.y") cy = AnimationUtility.GetEditorCurve(clip, b); else if (b.propertyName == "RootT.z") cz = AnimationUtility.GetEditorCurve(clip, b); if (b.propertyName.StartsWith("Root") || b.propertyName.StartsWith("Motion")) names.Add(b.propertyName); } sb.AppendLine(" rootBindings=[" + string.Join(",", names.Distinct().ToArray()) + "] hasRootCurves=" + clip.hasRootCurves + " hasMotion=" + clip.hasMotionCurves); if (cz == null) { sb.AppendLine(" (RootT.z curve 없음 — 루트 전진 커브 미존재)"); return sb.ToString(); } float z0 = cz.Evaluate(0f), x0 = cx != null ? cx.Evaluate(0f) : 0f; float maxZ = 0f, minZ = 0f, tMax = 0f, maxX = 0f; var rows = new List(); int n = 10; for (int i = 0; i <= n; i++) { float t = clip.length * i / n; float dz = cz.Evaluate(t) - z0; float dx = cx != null ? cx.Evaluate(t) - x0 : 0f; if (dz > maxZ) { maxZ = dz; tMax = t; } if (dz < minZ) minZ = dz; if (Mathf.Abs(dx) > Mathf.Abs(maxX)) maxX = dx; rows.Add(string.Format("{0:F2}:{1:F3}", t, dz)); } sb.AppendLine(" RootT.z Δ: " + string.Join(" ", rows.ToArray())); sb.AppendLine(string.Format(" => 루트 전진 max +{0:F3} m (t={1:F2}s), min {2:F3} m, 최종 {3:F3} m, 측면 max {4:F3} m", maxZ, tMax, minZ, cz.Evaluate(clip.length) - z0, maxX)); return sb.ToString(); } }