// WL838_BowRelease.cs — 활 클립(.anim 또는 FBX)에서 「시위를 놓는 순간」을 수치로 찾는다 (에디트 모드 · 프리뷰 씬) // run_script --file AgentScripts/WL838_BowRelease.cs --entry WL838_BowRelease.Run --args '["Assets/WL/Combat/Anim/FR/FR_Bow0.anim", ""]' // 프레임(30 fps)마다 양손 거리(당김) · 오른손 속도 · 왼손(활팔) 앞 뻗음을 찍는다. 당김이 최대인 뒤 양손 거리가 급감하는 프레임 = 발사(Projectile) 후보. using System.Linq; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; public static class WL838_BowRelease { public static string Run(string src, string clipName) { string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01"); var model = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/PC/" + pcName + ".prefab"); if (model == null) return "PC 프리팹 없음"; AnimationClip clip; if (src.EndsWith(".anim")) clip = AssetDatabase.LoadAssetAtPath(src); else { var clips = AssetDatabase.LoadAllAssetsAtPath(src).OfType().Where(c => !c.name.StartsWith("__preview")).ToArray(); clip = string.IsNullOrEmpty(clipName) ? clips.FirstOrDefault() : (clips.FirstOrDefault(c => c.name == clipName) ?? clips.FirstOrDefault(c => c.name.Contains(clipName))); } if (clip == null) return "클립 없음 " + src; var scene = EditorSceneManager.NewPreviewScene(); 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) return "Animator 없음"; AnimationMode.StartAnimationMode(); animMode = true; var sb = new StringBuilder(); sb.Append("clip=").Append(clip.name).Append(" len=").Append(clip.length.ToString("0.000")).Append("s human=").Append(clip.isHumanMotion).Append('\n'); sb.Append("f t norm handDist rHandSpd lHandFwd rHandFwd rHandUp\n"); int frames = Mathf.Max(2, Mathf.RoundToInt(clip.length * 30f)); Vector3 prevR = Vector3.zero; float maxDist = 0f; int maxF = -1; var rows = new System.Collections.Generic.List(); var dists = new float[frames + 1]; for (int f = 0; f <= frames; f++) { float t = Mathf.Min(f / 30f, clip.length); AnimationMode.BeginSampling(); AnimationMode.SampleAnimationClip(anim.gameObject, clip, t); AnimationMode.EndSampling(); var lh = anim.GetBoneTransform(HumanBodyBones.LeftHand); var rh = anim.GetBoneTransform(HumanBodyBones.RightHand); var hips = anim.GetBoneTransform(HumanBodyBones.Hips); if (lh == null || rh == null || hips == null) return "손/힙 본 없음"; Vector3 fwd = go.transform.forward, up = Vector3.up; float d = Vector3.Distance(lh.position, rh.position); dists[f] = d; float spd = f == 0 ? 0f : Vector3.Distance(rh.position, prevR) * 30f; prevR = rh.position; if (d > maxDist) { maxDist = d; maxF = f; } rows.Add(string.Format("{0,-3} {1,5:0.000} {2,5:0.00} {3,8:0.000} {4,9:0.00} {5,9:0.000} {6,9:0.000} {7,8:0.000}", f, t, t / clip.length, d, spd, Vector3.Dot(lh.position - hips.position, fwd), Vector3.Dot(rh.position - hips.position, fwd), Vector3.Dot(rh.position - hips.position, up))); } foreach (var r in rows) sb.Append(r).Append('\n'); // 발사 후보 = 최대 당김 이후 양손 거리가 최대의 85% 아래로 처음 떨어지는 프레임 int rel = -1; for (int f = maxF + 1; f <= frames; f++) if (dists[f] < maxDist * 0.85f) { rel = f; break; } sb.Append("maxDraw f").Append(maxF).Append(" (norm ").Append((maxF / 30f / clip.length).ToString("0.00")).Append(") dist ").Append(maxDist.ToString("0.000")) .Append(" · release candidate f").Append(rel).Append(rel >= 0 ? " (norm " + (rel / 30f / clip.length).ToString("0.00") + ")" : ""); return sb.ToString(); } finally { if (animMode) AnimationMode.StopAnimationMode(); EditorSceneManager.ClosePreviewScene(scene); } } }