diff --git a/AgentScripts/WL816t_Bake.cs b/AgentScripts/WL816t_Bake.cs index cfce72a08..470692915 100644 --- a/AgentScripts/WL816t_Bake.cs +++ b/AgentScripts/WL816t_Bake.cs @@ -189,6 +189,12 @@ public static class WL816t_Bake 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]; } + // ── #816v 감아올리기 트림 (런타임 MeasureSwingCalibration 과 같은 헬퍼) ── + int nBefore = n; + if (settings.arcTrimWindupWrap) + n = SlashArcMeasure.TrimWindupWrap(tips, n, settings.arcFitMaxDeviation, Mathf.Max(settings.arcMinSamples, 3)); + hi = lo + n - 1; + float fullPath = 0f; Vector3 pd = (tips[0] - hilts[0]).normalized; float bladeAng = 0f; for (int i = 1; i < n; i++) { @@ -203,7 +209,7 @@ public static class WL816t_Bake float hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm); float dev = SlashArcMeasure.RadiusDeviation(tips, n, pivot, nrm, tipR); string pivotSrc = "칼끝원"; - if (tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f) + if (tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * Mathf.Max(settings.arcFitMaxDeviation, 0.01f)) { Vector3 tc = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]); pivot = SlashArcMeasure.CentroidOnPlane(hilts, n, nrm, tc); @@ -222,8 +228,8 @@ public static class WL816t_Bake pivot = pivot, normal = nrm, bisector = bis, startDir = sd2, tipR = tipR, hiltR = hiltR, sweep = sweep, path = fullPath, fitQ = fitQ, q = fullPath * Mathf.Max(fitQ, 0.05f), tipMid = tips[n / 2], - src = string.Format("wl816t {0:yyyy-MM-dd HH:mm} {1} f{2}~f{3}/{4}@60Hz n={5} PC={6} rootScale={7:F4} weaponK={8:F4} path={9:F2} bladeAng={10:F0}° sweep={11:F1}° pivot={12} tipR={13:F3} hiltR={14:F3} dev={15:F3} fitQ={16:F2} (런타임 로컬=회전만)", - System.DateTime.Now, clip.name, lo, hi, n0 - 1, n, pcName, rootScale, wk, fullPath, bladeAng, sweep, pivotSrc, tipR, hiltR, dev, fitQ) + src = string.Format("wl816v {0:yyyy-MM-dd HH:mm} {1} f{2}~f{3}/{4}@60Hz n={5}(감아올리기트림 {17}→{5}) PC={6} rootScale={7:F4} weaponK={8:F4} path={9:F2} bladeAng={10:F0}° sweep={11:F1}° pivot={12} tipR={13:F3} hiltR={14:F3} dev={15:F3} fitQ={16:F2} (런타임 로컬=회전만)", + System.DateTime.Now, clip.name, lo, hi, n0 - 1, n, pcName, rootScale, wk, fullPath, bladeAng, sweep, pivotSrc, tipR, hiltR, dev, fitQ, nBefore) }); } AnimationMode.StopAnimationMode(); animMode = false; diff --git a/AgentScripts/WL816v_Probe.cs b/AgentScripts/WL816v_Probe.cs new file mode 100644 index 000000000..10bf0fccf --- /dev/null +++ b/AgentScripts/WL816v_Probe.cs @@ -0,0 +1,262 @@ +// 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); } + + /// 클립별 애니메이션 이벤트 전수 — ShowEffect 시각·문자열 파라미터. + public static object Events(int classId) + { + var sb = new StringBuilder(); + var settings = AssetDatabase.LoadAssetAtPath(SettingsPath); + for (int stage = 1; stage <= 3; stage++) + { + var fbx = string.Format(AnimDir, stage); + var clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType().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; + } + + /// 창 후보 전수 스캔 — 시작 프레임 × 끝 프레임별 칼끝 원 피팅 품질. + public static object Scan(int classId, int stage) { return Windows(classId, stage); } + + /// 베이크 창(클립 전체 속도트림) vs 런타임 창(ShowEffect → +swingWindowNormalized) 비교. + 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(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("Assets/Res_Addr/PC/" + pcName + ".prefab"); + var weaponPrefab = AssetDatabase.LoadAssetAtPath("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(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(); + if (fitter == null) fitter = go.AddComponent(); + fitter.Fit(w.transform, socket); + + Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f; + foreach (var r in socket.GetComponentsInChildren(true)) + { + var mf2 = r.GetComponent(); 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(); + 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().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(); + } +} diff --git a/Assets/WL/Combat/SlashArcMeasure.cs b/Assets/WL/Combat/SlashArcMeasure.cs index 209942f71..1d307e786 100644 --- a/Assets/WL/Combat/SlashArcMeasure.cs +++ b/Assets/WL/Combat/SlashArcMeasure.cs @@ -393,6 +393,40 @@ namespace WL.Combat return sum / count; } + /// + /// #816v — 칼끝 궤적 창에서 **감아올리기(한 바퀴 넘게 감기는 구간)** 를 잘라 낸다. + /// + /// 속도 트림()은 "칼이 멈춘 꼬리"만 자른다. + /// 1타처럼 클립이 타격 뒤에도 빠르게 계속 감기면 속도 트림은 그 구간을 남기고, 그러면 칼끝이 + /// 한 바퀴를 넘겨 원 피팅이 깨진다(반경 편차비 0.27) → 호출부가 손잡이 중심 폴백으로 떨어져 + /// 피벗이 머리 높이·몸 옆으로 올라간다. + /// + /// 여기서는 창 끝을 한 프레임씩 앞으로 당기며 **편차비가 기준 이하가 되는 가장 긴 창**의 + /// 길이를 돌려준다. 기준을 만족하는 창이 없으면 원래 길이를 그대로 돌려준다(동작 불변). + /// + /// 칼끝 궤적(창 시작부터 순서대로) + /// 현재 창 길이 + /// 합격 기준 = 반경 편차 / 평균 반경 + /// 이보다 짧게는 자르지 않는다 + /// 새 창 길이(≤ count) + public static int TrimWindupWrap(Vector3[] tips, int count, float maxDevRatio, int minSamples) + { + if (tips == null || count < 4) return count; + int floor = Mathf.Max(minSamples, 3); + if (count <= floor) return count; + float th = Mathf.Max(maxDevRatio, 0.01f); + + for (int n = count; n >= floor; n--) + { + var fit = FitArcOrdered(tips, n); + if (!fit.valid) continue; + float r = MeanRadius(tips, n, fit.center, fit.normal); + if (r < 1e-3f) continue; + if (RadiusDeviation(tips, n, fit.center, fit.normal, r) / r <= th) return n; + } + return count; + } + /// 정점 집합을 원호로 피팅한다. 실패하면 Invalid. public static CrescentFrame FitCrescent(Vector3[] pts, int[] tris, Vector2[] uvs, bool flipSweep) { diff --git a/Assets/WL/Combat/SlashTrailSettings.cs b/Assets/WL/Combat/SlashTrailSettings.cs index 8003fcba7..b91ac690f 100644 --- a/Assets/WL/Combat/SlashTrailSettings.cs +++ b/Assets/WL/Combat/SlashTrailSettings.cs @@ -781,6 +781,20 @@ namespace WL.Combat "반지름이 0.04 m 로 붕괴한다. 0 이면 자르지 않는다")] [Range(0f, 0.6f)] public float swingTrimSpeedRatio = 0.2f; + [Tooltip("■ #816v — 속도 트림 뒤에도 칼끝이 **한 바퀴 넘게 감기는** 구간(감아올리기)이 남으면 " + + "원 피팅이 깨진다. 창 끝을 한 프레임씩 앞으로 당기며 반경 편차 비율이 " + + "arcFitMaxDeviation 이하가 되는 가장 긴 창을 고른다.\n" + + "근거(실측 2026-09-15 · 에디트 모드 60 Hz) — Attack1_S_10101 은 ShowEffect 직후 4~5 프레임이 " + + "실제 타격 호(sweep 69~106° · 편차비 0.02~0.03)이고 그 뒤 프레임부터 칼끝이 한 바퀴 이상 " + + "감긴다. 창 전체(24 프레임)를 피팅하면 sweep 398.8° · 편차비 0.27 로 칼끝 원 피팅이 실패해 " + + "**손잡이 중심 폴백**으로 떨어지고 피벗이 머리 높이·몸 왼쪽으로 올라간다. " + + "2·3타는 편차비 0.14/0.17 로 원래 성립하므로 이 트림에 걸리지 않는다(무변경).\n" + + "끄면 즉시 #816t 동작으로 돌아간다(롤백)")] + public bool arcTrimWindupWrap = true; + + [Tooltip("칼끝 원 피팅 합격 기준 = 반경 편차 / 평균 반경. 이 값을 넘으면 피팅 실패로 본다")] + [Range(0.05f, 0.6f)] public float arcFitMaxDeviation = 0.25f; + [Header("□ #792 호(휘두르기) 정합")] [Tooltip("호의 반지름을 무엇에 맞출지. 균등 스케일이라 하나만 정확히 맞출 수 있다")] public ArcRadiusFit arcRadiusFit = ArcRadiusFit.Outer; diff --git a/Assets/WL/Combat/WeaponTrailDriver.cs b/Assets/WL/Combat/WeaponTrailDriver.cs index 07f2b4ace..1b190c1f6 100644 --- a/Assets/WL/Combat/WeaponTrailDriver.cs +++ b/Assets/WL/Combat/WeaponTrailDriver.cs @@ -1243,6 +1243,11 @@ namespace WL.Combat var hilts = new Vector3[n]; for (int i = 0; i < n; i++) { tips[i] = rawTips[lo + i]; hilts[i] = rawHilts[lo + i]; } + // ── #816v 감아올리기 트림 — 속도 트림 뒤에도 칼끝이 한 바퀴 넘게 감기면 원 피팅이 깨진다 ── + // (실측 — Attack1_S_* 는 타격 4~5 프레임 뒤에도 계속 감겨 sweep 399~417°·편차비 0.27) + if (_settings.arcTrimWindupWrap) + n = SlashArcMeasure.TrimWindupWrap(tips, n, _settings.arcFitMaxDeviation, minSamples); + var fit = SlashArcMeasure.FitArcOrdered(tips, n); // ── 분류 (칼끝 궤적으로) ──────────────────────────────────────── @@ -1328,7 +1333,8 @@ namespace WL.Combat // tipR=0.042 · hiltR=0.682 처럼 **안팎이 뒤집힌다**(스케일 0.035 → 이펙트가 사라진다). // 물리적으로 칼끝은 항상 손잡이보다 피벗에서 멀고, 반지름 편차는 작아야 한다. // 둘 중 하나라도 어기면 발주 정의 그대로 "**손잡이 궤적의 중심**"을 피벗으로 쓴다. - bool bad = tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f; + bool bad = tipR < 1e-3f || tipR <= hiltR * 1.15f + || dev > tipR * Mathf.Max(_settings.arcFitMaxDeviation, 0.01f); // #816v — SO 로 이관(C45) if (bad) { Vector3 tipCentroid = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]); diff --git a/Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset b/Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset index 9c268f420..de6b89058 100644 --- a/Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset +++ b/Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset @@ -410,20 +410,20 @@ MonoBehaviour: - classId: 10101 clipName: Effect_Slash_10101_1 kind: 1 - planeNormal: {x: 0.6155671, y: -0.7459609, z: 0.2542036} - pivot: {x: -0.44185022, y: 0.90176433, z: -0.14577419} - bisector: {x: -0.18893999, y: -0.452843, z: -0.8713409} - startDir: {x: 0.4323041, y: 0.5893181, z: 0.6825082} - tipRadius: 0.71769434 - hiltRadius: 0.2310255 - sweepDeg: 398.79218 + planeNormal: {x: 0.36657068, y: -0.8561612, z: 0.364162} + pivot: {x: -0.10471258, y: 1.0030102, z: -0.30329537} + bisector: {x: -0.31154686, y: -0.48176682, z: -0.8190478} + startDir: {x: -0.53341776, y: 0.12729353, z: 0.8362188} + tipRadius: 0.4728649 + hiltRadius: 0.30291337 + sweepDeg: 250.90732 stabAxis: {x: 0, y: 0, z: 1} - stabStart: {x: -0.44185022, y: 0.90176433, z: -0.14577419} - stabLength: 0.71769434 + stabStart: {x: -0.10471258, y: 1.0030102, z: -0.30329537} + stabLength: 0.4728649 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack1_S_10101 f12~f35/50@60Hz n=24 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=6.88 bladeAng=544\xB0 sweep=398.8\xB0 - pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.718 hiltR=0.231 dev=0.195 fitQ=0.46 + source: "wl816v 2026-09-15 13:39 Attack1_S_10101 f12~f17/50@60Hz n=6(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 24\u21926) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=1.91 bladeAng=189\xB0 + sweep=250.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.473 hiltR=0.303 dev=0.043 fitQ=0.82 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10101 clipName: Effect_Slash_10101_2 @@ -439,10 +439,10 @@ MonoBehaviour: stabStart: {x: -0.07215572, y: 0.45991144, z: -0.0608187} stabLength: 0.65453726 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack2_S_10101 f14~f44/52@60Hz n=31 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 sweep=220.7\xB0 - pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 (\uB7F0\uD0C0\uC784 - \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack2_S_10101 f14~f44/52@60Hz n=31(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 31\u219231) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 + sweep=220.7\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10101 clipName: Effect_Slash_10101_3 kind: 1 @@ -457,26 +457,27 @@ MonoBehaviour: stabStart: {x: -0.37444147, y: 0.6109033, z: 0.22184418} stabLength: 0.71797794 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack3_S_10101 f16~f54/64@60Hz n=39 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0 - tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack3_S_10101 f16~f54/64@60Hz n=39(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 39\u219239) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 + sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10102 clipName: Effect_Slash_10102_1 kind: 1 - planeNormal: {x: 0.6155671, y: -0.7459609, z: 0.2542036} - pivot: {x: -0.44185022, y: 0.90176433, z: -0.14577419} - bisector: {x: -0.18893999, y: -0.452843, z: -0.8713409} - startDir: {x: 0.4323041, y: 0.5893181, z: 0.6825082} - tipRadius: 0.71769434 - hiltRadius: 0.2310255 - sweepDeg: 398.79218 + planeNormal: {x: 0.36657068, y: -0.8561612, z: 0.364162} + pivot: {x: -0.10471258, y: 1.0030102, z: -0.30329537} + bisector: {x: -0.31154686, y: -0.48176682, z: -0.8190478} + startDir: {x: -0.53341776, y: 0.12729353, z: 0.8362188} + tipRadius: 0.4728649 + hiltRadius: 0.30291337 + sweepDeg: 250.90732 stabAxis: {x: 0, y: 0, z: 1} - stabStart: {x: -0.44185022, y: 0.90176433, z: -0.14577419} - stabLength: 0.71769434 + stabStart: {x: -0.10471258, y: 1.0030102, z: -0.30329537} + stabLength: 0.4728649 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack1_S_10102 f12~f35/50@60Hz n=24 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=6.88 bladeAng=544\xB0 sweep=398.8\xB0 - pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.718 hiltR=0.231 dev=0.195 fitQ=0.46 + source: "wl816v 2026-09-15 13:39 Attack1_S_10102 f12~f17/50@60Hz n=6(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 24\u21926) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=1.91 bladeAng=189\xB0 + sweep=250.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.473 hiltR=0.303 dev=0.043 fitQ=0.82 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10102 clipName: Effect_Slash_10102_2 @@ -492,10 +493,10 @@ MonoBehaviour: stabStart: {x: -0.07215572, y: 0.45991144, z: -0.0608187} stabLength: 0.65453726 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack2_S_10102 f14~f44/52@60Hz n=31 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 sweep=220.7\xB0 - pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 (\uB7F0\uD0C0\uC784 - \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack2_S_10102 f14~f44/52@60Hz n=31(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 31\u219231) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 + sweep=220.7\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10102 clipName: Effect_Slash_10102_3 kind: 1 @@ -510,26 +511,27 @@ MonoBehaviour: stabStart: {x: -0.37444147, y: 0.6109033, z: 0.22184418} stabLength: 0.71797794 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack3_S_10102 f16~f54/64@60Hz n=39 PC=LH_M05 - rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0 - tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack3_S_10102 f16~f54/64@60Hz n=39(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 39\u219239) PC=LH_M05 rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 + sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10501 clipName: Effect_Slash_10501_1 kind: 1 - planeNormal: {x: 0.6043122, y: -0.7630855, z: 0.22914462} - pivot: {x: -0.5143255, y: 0.99384046, z: -0.13518894} - bisector: {x: -0.2910512, y: -0.47915196, z: -0.8280716} - startDir: {x: 0.60951185, y: 0.6279901, z: 0.48386335} - tipRadius: 0.7389534 - hiltRadius: 0.23210584 - sweepDeg: 416.96194 + planeNormal: {x: 0.6203164, y: -0.780581, z: 0.07681709} + pivot: {x: -0.23079482, y: 0.5742873, z: 0.1707264} + bisector: {x: 0.4605102, y: 0.44173047, z: 0.76993793} + startDir: {x: 0.67965496, y: 0.4860465, z: -0.54938877} + tipRadius: 0.75863856 + hiltRadius: 0.2804911 + sweepDeg: 167.98088 stabAxis: {x: 0, y: 0, z: 1} - stabStart: {x: -0.5143255, y: 0.99384046, z: -0.13518894} - stabLength: 0.7389534 + stabStart: {x: -0.23079482, y: 0.5742873, z: 0.1707264} + stabLength: 0.75863856 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack1_S_10501 f12~f35/50@60Hz n=24 PC=LH_M05 - rootScale=1.0527 weaponK=0.6587 path=7.35 bladeAng=545\xB0 sweep=417.0\xB0 - pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.739 hiltR=0.232 dev=0.204 fitQ=0.45 + source: "wl816v 2026-09-15 13:39 Attack1_S_10501 f12~f32/50@60Hz n=21(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 24\u219221) PC=LH_M05 rootScale=1.0527 weaponK=0.6587 path=6.71 bladeAng=504\xB0 + sweep=168.0\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.759 hiltR=0.280 dev=0.186 fitQ=0.51 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10501 clipName: Effect_Slash_10501_2 @@ -545,10 +547,10 @@ MonoBehaviour: stabStart: {x: -0.077203944, y: 0.43669456, z: -0.052627534} stabLength: 0.7294197 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack2_S_10501 f14~f44/52@60Hz n=31 PC=LH_M05 - rootScale=1.0527 weaponK=0.6587 path=8.04 bladeAng=616\xB0 sweep=221.9\xB0 - pivot=\uCE7C\uB05D\uC6D0 tipR=0.729 hiltR=0.282 dev=0.083 fitQ=0.77 (\uB7F0\uD0C0\uC784 - \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack2_S_10501 f14~f44/52@60Hz n=31(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 31\u219231) PC=LH_M05 rootScale=1.0527 weaponK=0.6587 path=8.04 bladeAng=616\xB0 + sweep=221.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.729 hiltR=0.282 dev=0.083 fitQ=0.77 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10501 clipName: Effect_Slash_10501_3 kind: 1 @@ -563,26 +565,28 @@ MonoBehaviour: stabStart: {x: -0.32562953, y: 0.5273627, z: 0.22276048} stabLength: 0.80060536 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack3_S_10501 f16~f54/64@60Hz n=39 PC=LH_M05 - rootScale=1.0527 weaponK=0.6587 path=4.20 bladeAng=254\xB0 sweep=84.3\xB0 pivot=\uCE7C\uB05D\uC6D0 - tipR=0.801 hiltR=0.421 dev=0.120 fitQ=0.70 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack3_S_10501 f16~f54/64@60Hz n=39(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 39\u219239) PC=LH_M05 rootScale=1.0527 weaponK=0.6587 path=4.20 bladeAng=254\xB0 + sweep=84.3\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.801 hiltR=0.421 dev=0.120 fitQ=0.70 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10502 clipName: Effect_Slash_10502_1 kind: 1 - planeNormal: {x: 0.6255864, y: -0.7492798, z: 0.21730489} - pivot: {x: -0.4383909, y: 0.96184254, z: -0.13046789} - bisector: {x: 0.32277644, y: 0.5021659, z: 0.80227476} - startDir: {x: 0.6897078, y: 0.6613513, z: 0.29481786} - tipRadius: 0.6846314 - hiltRadius: 0.21322028 - sweepDeg: 75.39421 + planeNormal: {x: 0.63217396, y: -0.7589478, z: 0.15605867} + pivot: {x: -0.18392368, y: 0.533262, z: 0.14431739} + bisector: {x: 0.56776035, y: 0.5907957, z: 0.573244} + startDir: {x: 0.69264877, y: 0.46327528, z: -0.55282336} + tipRadius: 0.6630484 + hiltRadius: 0.23265822 + sweepDeg: 139.0184 stabAxis: {x: 0, y: 0, z: 1} - stabStart: {x: -0.4383909, y: 0.96184254, z: -0.13046789} - stabLength: 0.6846314 + stabStart: {x: -0.18392368, y: 0.533262, z: 0.14431739} + stabLength: 0.6630484 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack1_S_10502 f12~f35/50@60Hz n=24 PC=LH_M05 - rootScale=1.0527 weaponK=0.5632 path=6.42 bladeAng=545\xB0 sweep=75.4\xB0 pivot=\uC190\uC7A1\uC774\uC911\uC2EC - tipR=0.685 hiltR=0.213 dev=0.167 fitQ=0.51 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack1_S_10502 f12~f34/50@60Hz n=23(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 24\u219223) PC=LH_M05 rootScale=1.0527 weaponK=0.5632 path=6.24 bladeAng=531\xB0 + sweep=139.0\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.663 hiltR=0.233 dev=0.158 fitQ=0.52 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10502 clipName: Effect_Slash_10502_2 kind: 1 @@ -597,10 +601,10 @@ MonoBehaviour: stabStart: {x: -0.07373327, y: 0.44671315, z: -0.044609603} stabLength: 0.6426512 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack2_S_10502 f14~f44/52@60Hz n=31 PC=LH_M05 - rootScale=1.0527 weaponK=0.5632 path=7.04 bladeAng=616\xB0 sweep=222.2\xB0 - pivot=\uCE7C\uB05D\uC6D0 tipR=0.643 hiltR=0.302 dev=0.076 fitQ=0.76 (\uB7F0\uD0C0\uC784 - \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack2_S_10502 f14~f44/52@60Hz n=31(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 31\u219231) PC=LH_M05 rootScale=1.0527 weaponK=0.5632 path=7.04 bladeAng=616\xB0 + sweep=222.2\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.643 hiltR=0.302 dev=0.076 fitQ=0.76 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" - classId: 10502 clipName: Effect_Slash_10502_3 kind: 1 @@ -615,15 +619,18 @@ MonoBehaviour: stabStart: {x: -0.28646088, y: 0.51465774, z: 0.23871267} stabLength: 0.70055455 quality: 50 - source: "wl816t 2026-09-15 09:35 Attack3_S_10502 f16~f54/64@60Hz n=39 PC=LH_M05 - rootScale=1.0527 weaponK=0.5632 path=3.83 bladeAng=254\xB0 sweep=84.1\xB0 pivot=\uCE7C\uB05D\uC6D0 - tipR=0.701 hiltR=0.399 dev=0.110 fitQ=0.69 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" + source: "wl816v 2026-09-15 13:39 Attack3_S_10502 f16~f54/64@60Hz n=39(\uAC10\uC544\uC62C\uB9AC\uAE30\uD2B8\uB9BC + 39\u219239) PC=LH_M05 rootScale=1.0527 weaponK=0.5632 path=3.83 bladeAng=254\xB0 + sweep=84.1\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.701 hiltR=0.399 dev=0.110 fitQ=0.69 + (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)" swingMinSweepDegrees: 150 swingMinTipHiltRatio: 3 stabMinStraightness: 0.86 stabMinForwardDot: 0.55 stabMinDistance: 0.18 swingTrimSpeedRatio: 0.2 + arcTrimWindupWrap: 1 + arcFitMaxDeviation: 0.25 arcRadiusFit: 0 arcSizeScale: 1 arcFitAttackRange: 1