2026-09-15 00:37:12 +00:00
|
|
|
|
// 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]; }
|
|
|
|
|
|
|
2026-09-15 04:41:59 +00:00
|
|
|
|
// ── #816v 감아올리기 트림 (런타임 MeasureSwingCalibration 과 같은 헬퍼) ──
|
|
|
|
|
|
int nBefore = n;
|
|
|
|
|
|
if (settings.arcTrimWindupWrap)
|
|
|
|
|
|
n = SlashArcMeasure.TrimWindupWrap(tips, n, settings.arcFitMaxDeviation, Mathf.Max(settings.arcMinSamples, 3));
|
|
|
|
|
|
hi = lo + n - 1;
|
|
|
|
|
|
|
2026-09-15 00:37:12 +00:00
|
|
|
|
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 = "칼끝원";
|
2026-09-15 04:41:59 +00:00
|
|
|
|
if (tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * Mathf.Max(settings.arcFitMaxDeviation, 0.01f))
|
2026-09-15 00:37:12 +00:00
|
|
|
|
{
|
|
|
|
|
|
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],
|
2026-09-15 04:41:59 +00:00
|
|
|
|
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)
|
2026-09-15 00:37:12 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
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)); }
|
|
|
|
|
|
}
|