Project_WL/AgentScripts/WL816v_Probe.cs

263 lines
16 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>창 후보 전수 스캔 — 시작 프레임 × 끝 프레임별 칼끝 원 피팅 품질.</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();
}
}