363 lines
22 KiB
C#
363 lines
22 KiB
C#
// 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();
|
||
}
|
||
}
|