// WL791_FootProbe.cs — #791 공격 클립의 발 움직임 실측 → 자연스러운 프레임당 이동값 도출 (에디트 모드 · 읽기 전용 · 프리뷰 씬) // unity command run_script --file AgentScripts/WL791_FootProbe.cs --entry WL791_FootProbe.Run --args '["Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Attack1"]' // unity command run_script --file AgentScripts/WL791_FootProbe.cs --entry WL791_FootProbe.All // 방법: 실제 플레이어 모델(Assets/Res_Addr/PC/Ai01.prefab · Humanoid)을 프리뷰 씬에 띄우고 AnimationMode 로 클립을 30fps 프레임마다 샘플(루트모션 미적용) // → 좌/우 발(LeftFoot/RightFoot)의 루트 기준 XZ 를 기록. "접지" = 발 높이 ≤ 최저+5 cm. 접지한 발이 루트 기준으로 뒤로 흐르는 양(−dz) = 그 프레임에 // 캐릭터가 앞으로 가야 발이 땅에 붙어 보이는 양(제자리 애니메이션의 발 슬라이딩 보정). 접지 발이 앞으로 가는 프레임(딛는 순간)은 0. using System.Linq; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; public static class WL791_FootProbe { const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab"; static readonly string[][] Targets = { new[] { "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Attack1" }, new[] { "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack2_S.FBX", "Attack2" }, new[] { "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX", "Attack3" }, }; public static object All() { var sb = new StringBuilder(); foreach (var t in Targets) sb.Append(Run(t[0], t[1])).AppendLine(); return sb.ToString(); } public static object Run(string fbxPath, string clipName) { var model = AssetDatabase.LoadAssetAtPath(ModelPath); var clip = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType().FirstOrDefault(c => c.name == clipName); if (model == null || clip == null) return "load fail model=" + (model != null) + " clip=" + (clip != null) + " " + fbxPath; var scene = EditorSceneManager.NewPreviewScene(); var sb = new StringBuilder(); bool animMode = false; try { var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene); go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; var anim = go.GetComponentInChildren(); if (anim == null || anim.avatar == null) return "animator/avatar null on " + ModelPath; var animGo = anim.gameObject; anim.applyRootMotion = false; var lf = anim.GetBoneTransform(HumanBodyBones.LeftFoot); var rf = anim.GetBoneTransform(HumanBodyBones.RightFoot); var hips = anim.GetBoneTransform(HumanBodyBones.Hips); if (lf == null || rf == null || hips == null) return "foot/hips bones null avatar=" + anim.avatar.name; float fps = clip.frameRate; int n = Mathf.RoundToInt(clip.length * fps); var L = new Vector3[n + 1]; var R = new Vector3[n + 1]; var H = new Vector3[n + 1]; AnimationMode.StartAnimationMode(); animMode = true; for (int i = 0; i <= n; i++) { AnimationMode.BeginSampling(); AnimationMode.SampleAnimationClip(animGo, clip, i / fps); AnimationMode.EndSampling(); L[i] = animGo.transform.InverseTransformPoint(lf.position); R[i] = animGo.transform.InverseTransformPoint(rf.position); H[i] = animGo.transform.InverseTransformPoint(hips.position); } float minY = Mathf.Min(L.Min(p => p.y), R.Min(p => p.y)); float groundThr = minY + 0.05f; sb.AppendLine("== " + clipName + " len=" + clip.length.ToString("F3") + "s frames=" + n + " fps=" + fps + " model=" + anim.avatar.name + " minFootY=" + minY.ToString("F3") + " =="); sb.AppendLine(" f | Lz Ly | Rz Ry | 접지 | 루트전진(cm) | 누적(cm) | hipsZ(cm)"); float accum = 0f; var perFrame = new float[n + 1]; for (int i = 1; i <= n; i++) { bool lG = L[i].y <= groundThr, rG = R[i].y <= groundThr; float lDz = L[i].z - L[i - 1].z, rDz = R[i].z - R[i - 1].z; float step = 0f; string g = "-"; if (lG && rG) { step = Mathf.Max(-lDz, -rDz); g = "LR"; } else if (lG) { step = -lDz; g = "L"; } else if (rG) { step = -rDz; g = "R"; } if (step < 0f) step = 0f; perFrame[i] = step; accum += step; sb.AppendLine(string.Format(" {0,2} | {1,5:F2} {2,5:F2} | {3,5:F2} {4,5:F2} | {5,-3} | {6,7:F1} | {7,7:F1} | {8,6:F1}", i, L[i].z, L[i].y, R[i].z, R[i].y, g, step * 100f, accum * 100f, H[i].z * 100f)); } sb.AppendLine("합계 전진 = " + (accum * 100f).ToString("F1") + " cm · 프레임당(cm): " + string.Join(",", perFrame.Skip(1).Select(v => (v * 100f).ToString("F1")))); float maxStride = Enumerable.Range(0, n + 1).Max(i => Mathf.Abs(L[i].z - R[i].z)); sb.AppendLine("최대 보폭(두 발 z 차) = " + (maxStride * 100f).ToString("F1") + " cm · hips z 범위 " + (H.Min(p => p.z) * 100f).ToString("F0") + "~" + (H.Max(p => p.z) * 100f).ToString("F0") + " cm · 발 z 범위 L " + (L.Min(p => p.z) * 100f).ToString("F0") + "~" + (L.Max(p => p.z) * 100f).ToString("F0") + " R " + (R.Min(p => p.z) * 100f).ToString("F0") + "~" + (R.Max(p => p.z) * 100f).ToString("F0")); } finally { if (animMode) AnimationMode.StopAnimationMode(); EditorSceneManager.ClosePreviewScene(scene); } return sb.ToString(); } }