Project_WL/AgentScripts/WL816t_Bake.cs

312 lines
19 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.

// WL816t_Bake.cs — 검기 호 ↔ 무기 정합 실측·재베이크 (PD #816t)
// unity command run_script --file AgentScripts/WL816t_Bake.cs --entry WL816t_Bake.Probe --args "[10101, false]"
// unity command run_script --file AgentScripts/WL816t_Bake.cs --entry WL816t_Bake.Probe --args "[10101, true]" (SO 기록)
//
// WL804_Bake 와 같은 수식(SlashArcMeasure)·같은 프리뷰 샘플링을 쓰되 **런타임과 같은 캐릭터**로 잰다.
// · WL804_Bake : Assets/Res_Addr/PC/Ai01.prefab · root scale 1 · 무기 localScale 1 · root.InverseTransformPoint(스케일 포함)
// · 런타임 : WLCharacterSwapSettings.Resolve("Ai01") = LH_M05 · root scale = f_Scale × heightCompensation
// · 무기 = WLWeaponFitter.Fit(targetRatio 0.6) · ToLocalPos = 회전만(스케일 제외)
// 두 좌표계가 달라서 베이크 행이 현재 캐릭터의 칼끝 원과 어긋난다 — 그 편차를 재고 새 행을 만든다.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Combat;
public static class WL816t_Bake
{
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";
const float BakedQuality = 50f;
class Row
{
public int stage; public string clipName;
public Vector3 pivot, normal, bisector, startDir; public float tipR, hiltR, sweep, path, fitQ, q;
public Vector3 tipMid; // 스윙 중간 프레임의 칼끝(런타임 로컬)
public string src;
}
static readonly int[] Classes = { 10101, 10102, 10501, 10502 };
public static object Probe(int classId, bool apply) { return Run(classId, apply, true); }
public static object ProbeAll()
{
var sb = new StringBuilder();
foreach (var c in Classes) sb.AppendLine(Run(c, false, false));
return sb.ToString();
}
public static object ApplyAll()
{
var sb = new StringBuilder();
foreach (var c in Classes) sb.AppendLine(Run(c, true, false));
return sb.ToString();
}
/// <summary>공격 범위 정합 스위치 on/off + 배수. 되돌리기 = Set(false, 1)</summary>
public static object SetRangeFit(bool on, float rangeScale)
{
var s = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (s == null) return "설정 에셋 없음";
string before = string.Format("before: arcFitAttackRange={0} arcRangeScale={1:F2} arcSizeScale={2:F2}", s.arcFitAttackRange, s.arcRangeScale, s.arcSizeScale);
s.arcFitAttackRange = on; s.arcRangeScale = rangeScale;
EditorUtility.SetDirty(s); AssetDatabase.SaveAssets();
return before + string.Format(" → after: arcFitAttackRange={0} arcRangeScale={1:F2} arcSizeScale={2:F2}", s.arcFitAttackRange, s.arcRangeScale, s.arcSizeScale);
}
/// <summary>공격 범위(사거리) 정적 검산 — 런타임 ResolveAttackReach 와 같은 SOT.</summary>
public static object Reach(int classId)
{
float atk = 0f;
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr) { if ((string)row["n_ClassID"] == classId.ToString()) { float.TryParse((string)row["f_AttackRange"], out atk); break; } }
float margin = WL.Settings.WLTargetingSettings.ImmediateAttackMargin;
return string.Format("class {0} · f_AttackRange={1:F2} + AttackRange_Up(평시 0) + immediateAttackMargin={2:F2} = reach {3:F2} m", classId, atk, margin, atk + margin);
}
static string Run(int classId, bool apply, bool table)
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (settings == null) return "설정 에셋 없음";
// ── ClassConfig (C45 · 상수 금지) ────────────────────────────────────
string weaponName = null; int socketIndex = -1; float fScale = 1f, atkRange = 0f;
try
{
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);
float.TryParse((string)row["f_AttackRange"], out atkRange);
break;
}
}
catch (System.Exception ex) { return "ClassConfig parse fail: " + ex.Message; }
if (string.IsNullOrEmpty(weaponName) || socketIndex < 0) return "class " + classId + " weapon not found";
// ── 런타임과 같은 PC 프리팹·스케일 ──────────────────────────────────
string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01");
float heightComp = WL.Character.WLCharacterSwapSettings.HeightCompensation;
string pcPath = "Assets/Res_Addr/PC/" + pcName + ".prefab";
var model = AssetDatabase.LoadAssetAtPath<GameObject>(pcPath);
var weaponPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || weaponPrefab == null) return "load fail pc=" + pcPath + " weapon=" + weaponName;
float rootScale = Mathf.Max(fScale, 0.01f) * Mathf.Max(heightComp, 0.01f);
sb.AppendLine(string.Format("== class {0} · PC {1}(swap:{2}) rootScale={3:F4}(f_Scale {4:F2} × heightComp {5:F4}) · weapon {6}@{7} · f_AttackRange={8:F2}",
classId, pcName, pcName != "Ai01", rootScale, fScale, heightComp, weaponName, socketIndex, atkRange));
var scene = EditorSceneManager.NewPreviewScene();
bool animMode = false;
var rows = new List<Row>();
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);
if (pc == null || pc.tfs_weapon == null || socketIndex >= pc.tfs_weapon.Length) return sb + "\ntfs_weapon missing";
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;
// ── 814p 무기 축소(런타임과 동일 경로) ──────────────────────────
var fitter = go.GetComponent<WL.Character.WLWeaponFitter>();
if (fitter == null) fitter = go.AddComponent<WL.Character.WLWeaponFitter>();
float wk = fitter.Fit(w.transform, socket);
sb.AppendLine(string.Format(" 무기 보정 ×{0:F4} (WLWeaponFitter.Fit · targetRatio={1:F2})", wk, WL.Character.WLWeaponFitSettings.ResolveRatio(w.name)));
// ── 칼자루/칼끝 로컬(WL804_Bake·MeasureSlot 과 같은 규칙) ────────
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; }
}
if (best == null) return sb + "\nno renderer under socket";
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);
sb.AppendLine(string.Format(" 무기 월드 길이 {0:F3} m · 칼끝-자루 {1:F3} m", bestExtent,
Vector3.Distance(socket.TransformPoint(tipLocal), socket.TransformPoint(hiltLocal))));
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) { sb.AppendLine(" [" + stage + "타] 클립 없음 " + fbx); continue; }
// 클립 전체를 60 Hz 로 훑고 런타임과 같은 속도 트림으로 스윙 구간만 남긴다
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();
// 🔴 런타임 ToLocalPos 와 동일 = 회전만(스케일 제외)
tAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(tipLocal) - root.position);
hAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(hiltLocal) - root.position);
}
int lo = 0, hi = n0 - 1;
float ratio = settings.swingTrimSpeedRatio;
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; }
}
int n = hi - lo + 1;
if (n < 3) { lo = 0; hi = n0 - 1; n = n0; }
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++)
{
fullPath += Vector3.Distance(tips[i], tips[i - 1]);
var d = (tips[i] - hilts[i]).normalized; bladeAng += Vector3.Angle(pd, d); pd = d;
}
var fit = SlashArcMeasure.FitArcOrdered(tips, n);
if (!fit.valid) { sb.AppendLine(" [" + stage + "타] 원 피팅 실패 (n=" + n + ")"); continue; }
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);
string pivotSrc = "칼끝원";
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);
tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
pivotSrc = "손잡이중심";
}
Vector3 sd2, bis; float sweep;
if (!SlashArcMeasure.RecomputeAboutPivot(tips, n, pivot, ref nrm, out sd2, out bis, out sweep))
{ sb.AppendLine(" [" + stage + "타] RecomputeAboutPivot 실패"); continue; }
float fitQ = Mathf.Clamp01(1f - (dev / Mathf.Max(tipR, 1e-3f)) * 2f);
rows.Add(new Row
{
stage = stage, clipName = "Effect_Slash_" + classId + "_" + stage,
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("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;
// ── 비교 + 8방향 편차 ───────────────────────────────────────────
sb.AppendLine("");
sb.AppendLine("clip | old pivot(로컬) tipR | new pivot(로컬) tipR | Δpivot(m) ΔtipR(m) Δnormal(°)");
var cams = CamBasis();
var devTable = new StringBuilder();
foreach (var r in rows)
{
var old = settings.FindCalibration(classId, r.clipName);
if (old == null) { sb.AppendLine(string.Format("{0,-23} | (없음) | {1} {2:F3} | -", r.clipName, V(r.pivot), r.tipR)); continue; }
float dPiv = Vector3.Distance(old.pivot, r.pivot);
float dTip = r.tipR - old.tipRadius;
float dNrm = Vector3.Angle(old.planeNormal, r.normal);
sb.AppendLine(string.Format("{0,-23} | {1} {2:F3} | {3} {4:F3} | {5:F3} {6:+0.000;-0.000} {7:F1}",
r.clipName, V(old.pivot), old.tipRadius, V(r.pivot), r.tipR, dPiv, dTip, dNrm));
// 8방향: 구 행으로 배치한 호의 중심/테두리 vs 실제 칼끝
if (!table) continue;
devTable.AppendLine(" " + r.clipName);
devTable.AppendLine(" yaw | Δ중심(m) 화면Δ가로(m) 화면Δ세로(m) | 칼끝-호테두리 반경차(m)");
for (int k = 0; k < 8; k++)
{
float yaw = k * 45f;
var rot = Quaternion.Euler(0f, yaw, 0f);
Vector3 oldPivW = rot * old.pivot; // root at origin
Vector3 newPivW = rot * r.pivot;
Vector3 tipW = rot * r.tipMid;
Vector3 d = oldPivW - newPivW;
float radialGap = Vector3.Distance(tipW, oldPivW) - old.tipRadius * Mathf.Max(settings.arcSizeScale, 0.01f);
devTable.AppendLine(string.Format(" {0,3:F0} | {1:F3} {2,8:+0.000;-0.000} {3,8:+0.000;-0.000} | {4,8:+0.000;-0.000}",
yaw, d.magnitude, Vector3.Dot(d, cams[0]), Vector3.Dot(d, cams[1]), radialGap));
}
}
sb.AppendLine("");
sb.AppendLine("8방향 편차(카메라 기준 · PD 구도 yaw45 내려보기 45°)");
sb.Append(devTable);
if (apply)
{
int removed = 0;
foreach (var r in rows)
{
removed += settings.swingCalibrations.RemoveAll(c => c != null && c.classId == classId && c.clipName == r.clipName);
settings.swingCalibrations.Add(new SwingCalibration
{
classId = classId, clipName = r.clipName, kind = SwingKind.Swing,
planeNormal = r.normal, bisector = r.bisector, startDir = r.startDir,
pivot = r.pivot, tipRadius = r.tipR, hiltRadius = r.hiltR, sweepDeg = r.sweep,
stabAxis = Vector3.forward, stabStart = r.pivot, stabLength = Mathf.Max(r.tipR, 1e-3f),
quality = Mathf.Max(BakedQuality, r.q), source = r.src
});
}
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
sb.AppendLine(string.Format("APPLIED {0}행 (기존 {1}행 교체 · 총 {2}행)", rows.Count, removed, settings.swingCalibrations.Count));
}
else sb.AppendLine("(dry)");
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
/// <summary>PD 구도(비스듬히 내려보는 카메라)의 화면 가로·세로 축(월드).</summary>
static Vector3[] CamBasis()
{
var rot = Quaternion.Euler(45f, 45f, 0f);
return new[] { rot * Vector3.right, rot * Vector3.up };
}
static string V(Vector3 v) { return string.Format("({0,6:F3},{1,6:F3},{2,6:F3})", v.x, v.y, v.z); }
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
}