Project_WL/AgentScripts/WL804_TipProbe.cs

131 lines
9.1 KiB
C#
Raw Permalink Normal View History

// WL804_TipProbe.cs — #804 공격 클립의 칼끝 궤적 실측 → ShowEffect 이벤트 시각을 "실제 스윙 구간"으로 옮기기 위한 근거 (에디트 모드 · 읽기 전용 · 프리뷰 씬)
// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.All
// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.Run --args "[\"Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX\",\"Attack3\",\"Elven_Sword_01\",2,0.29305]"
// 방법: WL791_FootProbe 와 같은 프리뷰 씬 샘플링. Ai01.prefab(PCActor.tfs_weapon 소켓) 에 클래스 무기 프리팹(ClassConfig s_WeaponRHPrefab)을
// 런타임 PCActor.Make_Weapon 과 같은 방식(소켓 자식 · 로컬 0/identity/1)으로 붙이고, WeaponTrailDriver.MeasureSlot 과 같은 규칙
// (가장 긴 렌더러 바운딩의 장축 양끝 · 소켓에 가까운 쪽 = 자루)으로 칼끝/자루를 정한 뒤 30fps 프레임마다 루트 기준 위치를 기록한다.
// 출력: 프레임별 칼끝 이동(cm) · 칼날 방향 변화(deg) · 누적 경로 → "스윙 구간"(이동량 ≥ 최대의 30% 연속 구간) 과
// 현재 ShowEffect 시각 + 창(swingWindowNormalized 0.35) 이 스윙을 얼마나 덮는지(경로 비율).
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public static class WL804_TipProbe
{
const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab";
const float Window = 0.35f; // SlashTrailSettings.swingWindowNormalized (실측값 · 표시용)
public static object All()
{
var sb = new StringBuilder();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Attack1", "Elven_Sword_01", 2, 0.29821f)).AppendLine();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack2_S.FBX", "Attack2", "Elven_Sword_01", 2, 0.26643f)).AppendLine();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX", "Attack3", "Elven_Sword_01", 2, 0.29305f)).AppendLine();
return sb.ToString();
}
public static object Run(string fbxPath, string clipName, string weaponName, int socketIndex, float currentEventNorm)
{
var model = AssetDatabase.LoadAssetAtPath<GameObject>(ModelPath);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == clipName) ?? clips.FirstOrDefault(c => c.name.Contains(clipName));
var weapon = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || clip == null || weapon == null)
return "load fail model=" + (model != null) + " clip=" + (clip != null) + " weapon=" + (weapon != null) + " clips=[" + string.Join(",", clips.Select(c => c.name)) + "]";
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 pc = go.GetComponentInChildren<PCActor>(true);
if (pc == null || pc.tfs_weapon == null || socketIndex < 0 || socketIndex >= pc.tfs_weapon.Length) return "PCActor/tfs_weapon null or index out of range";
var socket = pc.tfs_weapon[socketIndex];
if (socket == null) return "socket null idx=" + socketIndex;
var w = (GameObject)PrefabUtility.InstantiatePrefab(weapon, scene);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
// MeasureSlot 규칙
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.sharedMesh : null);
if (mesh == null) continue;
var ws = Vector3.Scale(mesh.bounds.size, Abs(r.transform.lossyScale));
float longest = Mathf.Max(ws.x, Mathf.Max(ws.y, ws.z));
if (longest > bestExtent) { bestExtent = longest; best = r; bestMesh = mesh; }
}
if (best == null) return "no renderer under socket " + socket.name;
var b = bestMesh.bounds; var size = Vector3.Scale(b.size, Abs(best.transform.lossyScale));
int axis = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2);
Vector3 axisDir = Vector3.zero; axisDir[axis] = 1f;
Vector3 eA = best.transform.TransformPoint(b.center - axisDir * b.extents[axis]);
Vector3 eB = best.transform.TransformPoint(b.center + axisDir * b.extents[axis]);
bool aIsHilt = (eA - socket.position).sqrMagnitude <= (eB - socket.position).sqrMagnitude;
Vector3 hiltLocal = socket.InverseTransformPoint(aIsHilt ? eA : eB);
Vector3 tipLocal = socket.InverseTransformPoint(aIsHilt ? eB : eA);
float bladeLen = Vector3.Distance(hiltLocal, tipLocal);
var anim = go.GetComponentInChildren<Animator>();
if (anim == null) return "animator null";
var animGo = anim.gameObject; anim.applyRootMotion = false;
float fps = clip.frameRate; int n = Mathf.RoundToInt(clip.length * fps);
var T = 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();
T[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(tipLocal));
H[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(hiltLocal));
}
sb.AppendLine("== " + clipName + " (" + clip.name + ") len=" + clip.length.ToString("F3") + "s frames=" + n + " fps=" + fps
+ " weapon=" + weaponName + " socket=" + socket.name + " blade=" + best.name + " len=" + bladeLen.ToString("F2") + "m · currentShowEffect=" + currentEventNorm.ToString("F3") + " window=" + Window + " ==");
sb.AppendLine(" f norm | tip x y z | dTip(cm) | dDir(deg) | cum(cm)");
var d = new float[n + 1]; float accum = 0f;
for (int i = 1; i <= n; i++)
{
d[i] = Vector3.Distance(T[i], T[i - 1]); accum += d[i];
float ang = Vector3.Angle(T[i] - H[i], T[i - 1] - H[i - 1]);
sb.AppendLine(string.Format(" {0,3} {1,5:F3} | {2,6:F2} {3,5:F2} {4,5:F2} | {5,7:F1} | {6,6:F1} | {7,7:F1}",
i, i / (float)n, T[i].x, T[i].y, T[i].z, d[i] * 100f, ang, accum * 100f));
}
float dmax = d.Max(); int imax = System.Array.IndexOf(d, dmax);
float thr = dmax * 0.30f;
int s = imax, e = imax;
while (s - 1 >= 1 && d[s - 1] >= thr) s--;
while (e + 1 <= n && d[e + 1] >= thr) e++;
float swingPath = 0f; for (int i = s; i <= e; i++) swingPath += d[i];
sb.AppendLine(string.Format("SWING(dTip >= 30% of max {0:F1}cm @f{1}) = f{2}~f{3} -> norm {4:F3}~{5:F3} · path {6:F2} m / total {7:F2} m",
dmax * 100f, imax, s, e, s / (float)n, e / (float)n, swingPath, accum));
float cover = Coverage(d, n, currentEventNorm, Window);
sb.AppendLine(string.Format("CURRENT ShowEffect {0:F3} + window {1:F2} (f{2}~f{3}) covers path {4:F2} m ({5:P0} of total)",
currentEventNorm, Window, Mathf.CeilToInt(currentEventNorm * n), Mathf.Min(n, Mathf.FloorToInt((currentEventNorm + Window) * n)), cover, accum > 0 ? cover / accum : 0f));
float rec = Mathf.Max(0f, (s - 1) / (float)n);
float recCover = Coverage(d, n, rec, Window);
sb.AppendLine(string.Format("RECOMMEND ShowEffect ~ {0:F3} (f{1}) -> window covers {2:F2} m ({3:P0}) · window end f{4}{5}",
rec, s - 1, recCover, accum > 0 ? recCover / accum : 0f, Mathf.Min(n, Mathf.FloorToInt((rec + Window) * n)), (rec + Window) * n < e ? " !! window shorter than swing end f" + e : ""));
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
static float Coverage(float[] d, int n, float startNorm, float window)
{
int a = Mathf.Max(1, Mathf.CeilToInt(startNorm * n)), z = Mathf.Min(n, Mathf.FloorToInt((startNorm + window) * n));
float c = 0f; for (int i = a; i <= z; i++) c += d[i]; return c;
}
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
}