Project_WL/AgentScripts/WL816v_Probe.cs

363 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL816v_Probe.cs — 1타 검기 정합 원인 실측 (PD #816v)
// unity command run_script --file AgentScripts/WL816v_Probe.cs --entry WL816v_Probe.Events --args "[10101]"
// unity command run_script --file AgentScripts/WL816v_Probe.cs --entry WL816v_Probe.Windows --args "[10101]"
//
// 목적: 베이크 행(클립 전체 속도트림 구간)과 **런타임이 실제로 이펙트를 띄우는 순간부터의 구간**
// (ShowEffect 이벤트 → +swingWindowNormalized) 의 호 피팅이 타별로 얼마나 다른지 잰다.
// WL816t_Bake 와 같은 캐릭터·스케일·무기보정·로컬 규약(회전만)을 쓴다.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Combat;
public static class WL816v_Probe
{
const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset";
const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json";
const string AnimDir = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack{0}_S.FBX";
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
static string V(Vector3 v) { return string.Format("({0,6:F3},{1,6:F3},{2,6:F3})", v.x, v.y, v.z); }
/// <summary>클립별 애니메이션 이벤트 전수 — ShowEffect 시각·문자열 파라미터.</summary>
public static object Events(int classId)
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
for (int stage = 1; stage <= 3; stage++)
{
var fbx = string.Format(AnimDir, stage);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == "Attack" + stage + "_S_" + classId) ?? clips.FirstOrDefault(c => c.name.StartsWith("Attack" + stage));
if (clip == null) { sb.AppendLine(stage + "타 클립 없음 " + fbx); continue; }
sb.AppendLine(string.Format("[{0}타] {1} len={2:F3}s frameRate={3:F0}", stage, clip.name, clip.length, clip.frameRate));
foreach (var ev in AnimationUtility.GetAnimationEvents(clip))
sb.AppendLine(string.Format(" ev {0,-14} t={1:F4}s norm={2:F4} int={3} str='{4}' float={5:F3}",
ev.functionName, ev.time, clip.length > 0 ? ev.time / clip.length : 0f, ev.intParameter, ev.stringParameter, ev.floatParameter));
if (settings != null)
{
foreach (var ev in AnimationUtility.GetAnimationEvents(clip))
{
if (ev.functionName != "ShowEffect" || string.IsNullOrEmpty(ev.stringParameter)) continue;
var cal = settings.FindCalibration(classId, ev.stringParameter);
sb.AppendLine(string.Format(" → FindCalibration({0},'{1}') = {2}", classId, ev.stringParameter,
cal == null ? "없음(런타임 실측 폴백)" : string.Format("pivot{0} tipR={1:F3} sweep={2:F1}deg kind={3}", V(cal.pivot), cal.tipRadius, cal.sweepDeg, cal.kind)));
}
}
}
sb.AppendLine(string.Format("settings: swingWindowNormalized={0:F3} swingTrimSpeedRatio={1:F3} arcFitAttackRange={2} arcRangeScale={3:F2}",
settings.swingWindowNormalized, settings.swingTrimSpeedRatio, settings.arcFitAttackRange, settings.arcRangeScale));
return sb.ToString();
}
class Fit
{
public bool ok; public Vector3 pivot, normal, bisector, startDir; public float tipR, hiltR, sweep, path, bladeAng, dev; public int lo, hi, n;
}
static Fit FitRange(Vector3[] tAll, Vector3[] hAll, int lo, int hi, SlashTrailSettings settings)
{
var f = new Fit { lo = lo, hi = hi };
int n = hi - lo + 1;
if (n < 3) return f;
var tips = new Vector3[n]; var hilts = new Vector3[n];
for (int i = 0; i < n; i++) { tips[i] = tAll[lo + i]; hilts[i] = hAll[lo + i]; }
f.n = n;
Vector3 pd = (tips[0] - hilts[0]).normalized;
for (int i = 1; i < n; i++)
{
f.path += Vector3.Distance(tips[i], tips[i - 1]);
var d = (tips[i] - hilts[i]).normalized; f.bladeAng += Vector3.Angle(pd, d); pd = d;
}
var fit = SlashArcMeasure.FitArcOrdered(tips, n);
if (!fit.valid) return f;
Vector3 nrm = fit.normal, pivot = fit.center;
float tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
float hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
float dev = SlashArcMeasure.RadiusDeviation(tips, n, pivot, nrm, tipR);
if (tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f)
{
Vector3 tc = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]);
pivot = SlashArcMeasure.CentroidOnPlane(hilts, n, nrm, tc);
tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
}
Vector3 sd2, bis; float sweep;
if (!SlashArcMeasure.RecomputeAboutPivot(tips, n, pivot, ref nrm, out sd2, out bis, out sweep)) return f;
f.ok = true; f.pivot = pivot; f.normal = nrm; f.bisector = bis; f.startDir = sd2;
f.tipR = tipR; f.hiltR = hiltR; f.sweep = sweep; f.dev = dev;
return f;
}
static int TrimLo(Vector3[] tAll, float ratio, out int hiOut)
{
int n0 = tAll.Length; int lo = 0, hi = n0 - 1;
if (ratio > 0f)
{
float maxStep = 0f; var step = new float[n0];
for (int i = 1; i < n0; i++) { step[i] = Vector3.Distance(tAll[i], tAll[i - 1]); if (step[i] > maxStep) maxStep = step[i]; }
float th = maxStep * ratio; int f = -1, l = -1;
for (int i = 1; i < n0; i++) if (step[i] >= th) { if (f < 0) f = i - 1; l = i; }
if (f >= 0 && l > f) { lo = f; hi = l; }
}
hiOut = hi; return lo;
}
/// <summary>새 SO 필드를 에셋에 직렬화한다(기본값이 파일에 보이도록).</summary>
public static object SaveCoreSettings()
{
var cfg = Resources.Load<WL.Combat.Core.WLCombatCoreSettings>(WL.Combat.Core.WLCombatCoreSettings.ResourcesPath);
if (cfg == null) return "WLCombatCoreSettings 없음";
EditorUtility.SetDirty(cfg); AssetDatabase.SaveAssets();
return string.Format("saved · hit1LookAtTarget={0} pattern='{1}' fn='{2}' poll={3:F2}",
cfg.hit1LookAtTarget, cfg.hit1ClipNamePattern, cfg.hit1EventFunctionName, cfg.hit1PollSeconds);
}
/// <summary>
/// 이슈 ① — 1타 LookAtTarget 런타임 주입을 **에디트 모드에서 같은 코드 경로로** 검증한다.
/// ① 컨트롤러가 물고 있는 Attack1/2/3 클립의 이벤트 전/후
/// ② Actor.LookAtTarget() = transform.LookAt(target) 의 정렬 각 오차 8방향 표
/// ③ 검증 뒤 FBX 재임포트로 메모리 변경 폐기(에셋 무오염)
/// </summary>
public static object Hit1(int classId)
{
var sb = new StringBuilder();
var cfg = Resources.Load<WL.Combat.Core.WLCombatCoreSettings>(WL.Combat.Core.WLCombatCoreSettings.ResourcesPath);
if (cfg == null) return "WLCombatCoreSettings 없음";
sb.AppendLine(string.Format("SO: hit1LookAtTarget={0} pattern='{1}' fn='{2}' poll={3:F2}s",
cfg.hit1LookAtTarget, cfg.hit1ClipNamePattern, cfg.hit1EventFunctionName, cfg.hit1PollSeconds));
// ── 컨트롤러 = ClassConfig s_AnimationController (C45) ──
string ctrlName = null;
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr)
if ((string)row["n_ClassID"] == classId.ToString()) { ctrlName = (string)row["s_AnimationController"]; break; }
if (string.IsNullOrEmpty(ctrlName)) return sb + "\ns_AnimationController 비어 있음";
string ctrlPath = null;
foreach (var g in AssetDatabase.FindAssets(System.IO.Path.GetFileNameWithoutExtension(ctrlName) + " t:AnimatorController"))
{
var p = AssetDatabase.GUIDToAssetPath(g);
if (System.IO.Path.GetFileNameWithoutExtension(p) == System.IO.Path.GetFileNameWithoutExtension(ctrlName)) { ctrlPath = p; break; }
}
if (ctrlPath == null) return sb + "\n컨트롤러 못 찾음: " + ctrlName;
var ctrl = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(ctrlPath);
sb.AppendLine("controller = " + ctrlPath + " · clips=" + ctrl.animationClips.Length);
string[] want = { "Attack1_S_" + classId, "Attack2_S_" + classId, "Attack3_S_" + classId };
System.Action<string> dump = tag =>
{
foreach (var wn in want)
foreach (var c in ctrl.animationClips)
{
if (c == null || c.name != wn) continue;
var names = new List<string>();
foreach (var e in c.events) names.Add(string.Format("{0}@{1:F4}s(int={2},str='{3}')", e.functionName, e.time, e.intParameter, e.stringParameter));
sb.AppendLine(string.Format(" [{0}] {1,-20} 이벤트 {2}개 : {3}", tag, c.name, c.events.Length, string.Join(" · ", names.ToArray())));
break;
}
};
dump("전");
int added = WL.Combat.Auto.Hit1LookAtTarget.Inject(ctrl.animationClips, classId, cfg.hit1ClipNamePattern);
sb.AppendLine(" Inject() → 주입 " + added + "건 · " + WL.Combat.Auto.Hit1LookAtTarget.LastResult);
dump("후");
// ── ② 정렬 각 오차 — Actor.LookAtTarget() 은 transform.LookAt(m_Target.Get_position()) ──
sb.AppendLine("");
sb.AppendLine("정렬 각 오차 (Actor.cs:2406 transform.LookAt · 대상 거리 = 공격 범위 1.70 m)");
sb.AppendLine(" 캐릭터yaw | 대상방위 | yaw오차 전 | yaw오차 후 | pitch 후(대상 높이차 0 / +0.5 m)");
var probe = new GameObject("WL816v_yawProbe");
try
{
float[] yaws = { 0, 45, 90, 135, 180, 225, 270, 315 };
for (int i = 0; i < yaws.Length; i++)
{
float charYaw = yaws[i];
float tgtYaw = (yaws[i] + 37f) % 360f; // 임의 비정렬(오차가 0 이 아닌 상태)
Vector3 tgt = Quaternion.Euler(0f, tgtYaw, 0f) * Vector3.forward * 1.70f;
probe.transform.position = Vector3.zero;
probe.transform.rotation = Quaternion.Euler(0f, charYaw, 0f);
float before = Mathf.Abs(Mathf.DeltaAngle(probe.transform.eulerAngles.y, tgtYaw));
probe.transform.LookAt(tgt);
float after = Mathf.Abs(Mathf.DeltaAngle(probe.transform.eulerAngles.y, tgtYaw));
float pitch0 = Mathf.DeltaAngle(0f, probe.transform.eulerAngles.x);
probe.transform.rotation = Quaternion.Euler(0f, charYaw, 0f);
probe.transform.LookAt(tgt + Vector3.up * 0.5f);
float pitchH = Mathf.DeltaAngle(0f, probe.transform.eulerAngles.x);
sb.AppendLine(string.Format(" {0,8:F0}° | {1,7:F0}° | {2,9:F2}° | {3,9:F2}° | {4:F2}° / {5:F2}°",
charYaw, tgtYaw, before, after, pitch0, pitchH));
}
}
finally { Object.DestroyImmediate(probe); }
// ── ③ 메모리 변경 폐기 — 임포트된 서브클립을 원본에서 다시 읽는다 ──
var fbxSet = new HashSet<string>();
foreach (var c in ctrl.animationClips)
{
var p = AssetDatabase.GetAssetPath(c);
if (!string.IsNullOrEmpty(p) && p.EndsWith(".FBX", System.StringComparison.OrdinalIgnoreCase)) fbxSet.Add(p);
}
foreach (var p in fbxSet) AssetDatabase.ImportAsset(p, ImportAssetOptions.ForceUpdate);
sb.AppendLine("");
sb.AppendLine("메모리 변경 폐기 — FBX 재임포트 " + fbxSet.Count + "건");
dump("원복");
return sb.ToString();
}
/// <summary>창 후보 전수 스캔 — 시작 프레임 × 끝 프레임별 칼끝 원 피팅 품질.</summary>
public static object Scan(int classId, int stage) { return Windows(classId, stage); }
/// <summary>베이크 창(클립 전체 속도트림) vs 런타임 창(ShowEffect → +swingWindowNormalized) 비교.</summary>
public static object Windows(int classId) { return Windows(classId, 0); }
public static object Windows(int classId, int scanStage)
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (settings == null) return "설정 에셋 없음";
string weaponName = null; int socketIndex = -1; float fScale = 1f;
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr)
{
if ((string)row["n_ClassID"] != classId.ToString()) continue;
weaponName = (string)row["s_WeaponRHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponRHIndex"]);
if (string.IsNullOrEmpty(weaponName)) { weaponName = (string)row["s_WeaponLHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponLHIndex"]); }
float.TryParse((string)row["f_Scale"], out fScale);
break;
}
string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01");
float rootScale = Mathf.Max(fScale, 0.01f) * Mathf.Max(WL.Character.WLCharacterSwapSettings.HeightCompensation, 0.01f);
var model = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/PC/" + pcName + ".prefab");
var weaponPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || weaponPrefab == null) return "load fail";
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;
go.transform.localScale = Vector3.one * rootScale;
var pc = go.GetComponentInChildren<PCActor>(true);
var socket = pc.tfs_weapon[socketIndex];
var w = (GameObject)PrefabUtility.InstantiatePrefab(weaponPrefab, scene);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
var fitter = go.GetComponent<WL.Character.WLWeaponFitter>();
if (fitter == null) fitter = go.AddComponent<WL.Character.WLWeaponFitter>();
fitter.Fit(w.transform, socket);
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf2 = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf2 != null ? mf2.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; }
}
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);
var anim = go.GetComponentInChildren<Animator>();
var animGo = anim.gameObject; anim.applyRootMotion = false;
var root = go.transform;
AnimationMode.StartAnimationMode(); animMode = true;
for (int stage = 1; stage <= 3; stage++)
{
var fbx = string.Format(AnimDir, stage);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == "Attack" + stage + "_S_" + classId) ?? clips.FirstOrDefault(c => c.name.StartsWith("Attack" + stage));
if (clip == null) continue;
int n0 = Mathf.Max(8, Mathf.RoundToInt(clip.length * 60f) + 1);
var tAll = new Vector3[n0]; var hAll = new Vector3[n0];
for (int i = 0; i < n0; i++)
{
float t = clip.length * i / (n0 - 1);
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(animGo, clip, t);
AnimationMode.EndSampling();
tAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(tipLocal) - root.position);
hAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(hiltLocal) - root.position);
}
if (scanStage > 0 && stage != scanStage) continue;
int bHi; int bLo = TrimLo(tAll, settings.swingTrimSpeedRatio, out bHi);
var bakeFit = FitRange(tAll, hAll, bLo, bHi, settings);
if (scanStage > 0)
{
float evT0 = -1f;
foreach (var ev in AnimationUtility.GetAnimationEvents(clip))
if (ev.functionName == "ShowEffect") { evT0 = ev.time; break; }
int evF = Mathf.Clamp(Mathf.RoundToInt(evT0 / clip.length * (n0 - 1)), 0, n0 - 1);
sb.AppendLine(string.Format("== SCAN [{0}타] {1} n0={2} bake f{3}~f{4} ev f{5}", stage, clip.name, n0 - 1, bLo, bHi, evF));
sb.AppendLine("start end n | tipR dev dev/R | sweep path | pivot");
int[] starts = { bLo, evF, evF + 1, evF + 2 };
foreach (int st in starts)
{
for (int e = st + 3; e <= Mathf.Min(bHi + 4, n0 - 1); e++)
{
var g = FitRange(tAll, hAll, st, e, settings);
if (!g.ok) continue;
sb.AppendLine(string.Format("f{0,-5} f{1,-4} {2,2} | {3:F3} {4:F3} {5,5:F2} | {6,6:F1} {7,5:F2} | {8}",
st, e, g.n, g.tipR, g.dev, g.tipR > 1e-4f ? g.dev / g.tipR : 9f, g.sweep, g.path, V(g.pivot)));
}
sb.AppendLine("");
}
continue;
}
float evT = -1f; string evStr = null;
foreach (var ev in AnimationUtility.GetAnimationEvents(clip))
if (ev.functionName == "ShowEffect") { evT = ev.time; evStr = ev.stringParameter; break; }
sb.AppendLine(string.Format("== [{0}타] {1} len={2:F3}s · ShowEffect t={3:F4}s(norm {4:F3}) str='{5}'",
stage, clip.name, clip.length, evT, clip.length > 0 ? evT / clip.length : -1f, evStr));
sb.AppendLine(string.Format(" 베이크창 f{0}~f{1}/{2} n={3} · pivot{4} tipR={5:F3} sweep={6:F1}deg bladeAng={7:F0}deg path={8:F2} dev={9:F3} normal{10} bis{11}",
bakeFit.lo, bakeFit.hi, n0 - 1, bakeFit.n, V(bakeFit.pivot), bakeFit.tipR, bakeFit.sweep, bakeFit.bladeAng, bakeFit.path, bakeFit.dev, V(bakeFit.normal), V(bakeFit.bisector)));
if (evT >= 0f)
{
int rLo = Mathf.Clamp(Mathf.RoundToInt(evT / clip.length * (n0 - 1)), 0, n0 - 1);
float winSec = settings.swingWindowNormalized * clip.length;
int rHi = Mathf.Clamp(Mathf.RoundToInt((evT + winSec) / clip.length * (n0 - 1)), rLo + 1, n0 - 1);
// 런타임은 이 구간을 다시 속도 트림한다(MeasureSwingCalibration 과 같은 규칙)
int sub = rHi - rLo + 1;
var subT = new Vector3[sub]; var subH = new Vector3[sub];
for (int i = 0; i < sub; i++) { subT[i] = tAll[rLo + i]; subH[i] = hAll[rLo + i]; }
int tHi; int tLo = TrimLo(subT, settings.swingTrimSpeedRatio, out tHi);
var runFit = FitRange(subT, subH, tLo, tHi, settings);
sb.AppendLine(string.Format(" 런타임창 f{0}~f{1}(ev f{2} +win {3:F3}s) n={4} · pivot{5} tipR={6:F3} sweep={7:F1}deg bladeAng={8:F0}deg path={9:F2} dev={10:F3} normal{11} bis{12}",
rLo + tLo, rLo + tHi, rLo, winSec, runFit.n, V(runFit.pivot), runFit.tipR, runFit.sweep, runFit.bladeAng, runFit.path, runFit.dev, V(runFit.normal), V(runFit.bisector)));
if (bakeFit.ok && runFit.ok)
sb.AppendLine(string.Format(" Δ(베이크→런타임): pivot={0:F3} m · normal={1:F1}deg · bisector={2:F1}deg · tipR={3:F3} m · sweep={4:F1}deg",
Vector3.Distance(bakeFit.pivot, runFit.pivot), Vector3.Angle(bakeFit.normal, runFit.normal),
Vector3.Angle(bakeFit.bisector, runFit.bisector), runFit.tipR - bakeFit.tipR, runFit.sweep - bakeFit.sweep));
// 이벤트 프레임의 칼끝·칼자루 (검기가 뜨는 그 순간의 실제 무기 위치)
sb.AppendLine(string.Format(" ev프레임 칼끝{0} 칼자루{1} · 창끝 프레임 칼끝{2}",
V(tAll[rLo]), V(hAll[rLo]), V(tAll[rHi])));
}
sb.AppendLine("");
}
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
}