474 lines
25 KiB
C#
474 lines
25 KiB
C#
// WL792_Probe.cs — #792 이펙트 프리팹 기하 실측 (읽기 전용 · Assets 무수정 · Play 불필요)
|
|
//
|
|
// unity command run_script --file AgentScripts/WL792_Probe.cs --entry WL792_Probe.Dump
|
|
//
|
|
// 하는 일
|
|
// ① NamuFX `Slash_B_recolor_1 Variant` 와 EricWang `FX_Blue Stab` 을 프리팹 컨텐츠로 열어
|
|
// (씬을 건드리지 않는다 — PrefabUtility.LoadPrefabContents 는 격리된 프리뷰 씬을 쓴다)
|
|
// 계층 · 렌더 모드 · 정렬 · 메시 · 머티리얼 · 셰이더 유효성 · 파티클 예산을 덤프한다.
|
|
// ② 슬래시 호 메시(SlashMesh_00)의 정점을 읽어 **원 피팅**으로 호의 로컬 프레임을 실측한다.
|
|
// (평면 법선 · 원 중심 · 안/바깥 반지름 · 스윕 각도 · UV 진행 방향)
|
|
// ③ 찌르기 이펙트의 이미터 배치에서 진행 축 · 시작점 · 길이 · 면 법선을 실측한다.
|
|
//
|
|
// 왜 이 실측이 필요한가
|
|
// 기존 #761 배치는 크레센트의 **바운딩 박스 최장축**을 "호가 뻗는 방향"으로 봤는데,
|
|
// 원호의 최장축은 언제나 현(chord · 접선)이라 호가 스윙 평면 안에서 90° 돌아간 채 놓인다.
|
|
// PD #792 의 "무기에 딱 맞게"를 만족하려면 호의 **원 중심·반지름·스윕**을 알아야 한다.
|
|
//
|
|
// 산출물: Screenshots_WL/combat6/wl792_probe.txt (에셋을 하나도 수정하지 않는다)
|
|
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public static class WL792_Probe
|
|
{
|
|
const string Out = "Screenshots_WL/combat6/wl792_probe.txt";
|
|
|
|
const string PathSwing = "Assets/NamuFX/Simple Stylized Slash vol2/Prefabs/Slash_B_recolor_1 Variant.prefab";
|
|
const string PathSwingBase = "Assets/NamuFX/Simple Stylized Slash vol2/Prefabs/Slash_B.prefab";
|
|
const string PathStab = "Assets/ResWork/EricWang/Game VFX - Cartoon Skill Collection Vol2/Prefabs/FX_Blue Stab.prefab";
|
|
const string PathStab7 = "Assets/ResWork/EricWang/Game VFX - Cartoon Skill Collection Vol2/Prefabs/FX_7Blue Stab.prefab";
|
|
|
|
public static void Dump()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("# WL #792 이펙트 기하 실측 (읽기 전용)");
|
|
sb.AppendLine("생성 " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|
sb.AppendLine();
|
|
|
|
// FX_7Blue Stab 은 1차 측정에서 렌더 이미터 85개 · 고유 머티리얼 22개로 확인됐다 —
|
|
// 모바일 예산(스윙당 추가 드로우 ≤ 3)과 자릿수가 달라 후보에서 제외하고 덤프도 생략한다.
|
|
foreach (var p in new[] { PathSwing, PathStab })
|
|
DumpPrefab(sb, p);
|
|
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Out));
|
|
System.IO.File.WriteAllText(Out, sb.ToString(), new System.Text.UTF8Encoding(false));
|
|
Debug.Log("[WL #792] probe -> " + Out + "\n" + sb.ToString());
|
|
}
|
|
|
|
static void DumpPrefab(StringBuilder sb, string path)
|
|
{
|
|
sb.AppendLine("══════════════════════════════════════════════════════════════");
|
|
sb.AppendLine("PREFAB " + path);
|
|
|
|
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|
if (asset == null) { sb.AppendLine(" !! 없음"); sb.AppendLine(); return; }
|
|
|
|
GameObject root = null;
|
|
try
|
|
{
|
|
root = PrefabUtility.LoadPrefabContents(path);
|
|
var systems = root.GetComponentsInChildren<ParticleSystem>(true);
|
|
sb.AppendLine(string.Format(" 루트 scale={0} rot={1} · ParticleSystem {2}개",
|
|
root.transform.localScale.ToString("F3"), root.transform.localEulerAngles.ToString("F1"), systems.Length));
|
|
|
|
int rendering = 0, matCount = 0;
|
|
var mats = new HashSet<string>();
|
|
int totalMaxParticles = 0;
|
|
|
|
foreach (var ps in systems)
|
|
{
|
|
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
|
var tm = ps.transform;
|
|
var main = ps.main;
|
|
|
|
string chain = Chain(root.transform, tm);
|
|
string mat = psr != null && psr.sharedMaterial != null ? psr.sharedMaterial.name : "(null)";
|
|
string sh = psr != null && psr.sharedMaterial != null && psr.sharedMaterial.shader != null
|
|
? psr.sharedMaterial.shader.name : "(null)";
|
|
bool shOk = psr != null && psr.sharedMaterial != null && psr.sharedMaterial.shader != null
|
|
&& psr.sharedMaterial.shader.isSupported
|
|
&& sh != "Hidden/InternalErrorShader";
|
|
|
|
sb.AppendLine(string.Format(
|
|
" · {0,-28} render={1,-10} align={2,-8} enabled={3} | mesh={4,-16} startSize={5:F3} dur={6:F2} loop={7} maxP={8} burst={9}",
|
|
chain,
|
|
psr != null ? psr.renderMode.ToString() : "?",
|
|
psr != null ? psr.alignment.ToString() : "?",
|
|
psr != null && psr.enabled ? 1 : 0,
|
|
psr != null && psr.mesh != null ? psr.mesh.name : "-",
|
|
main.startSize.constantMax, main.duration, main.loop ? 1 : 0, main.maxParticles,
|
|
BurstTotal(ps)));
|
|
sb.AppendLine(string.Format(
|
|
" localPos={0} localEuler={1} lossyScale={2} | mat={3} shader='{4}' 지원={5}",
|
|
tm.localPosition.ToString("F3"), tm.localEulerAngles.ToString("F1"), tm.lossyScale.ToString("F3"),
|
|
mat, sh, shOk ? "OK" : "!! 마젠타 위험"));
|
|
|
|
if (psr != null && psr.enabled) { rendering++; totalMaxParticles += main.maxParticles; if (psr.sharedMaterial != null) mats.Add(psr.sharedMaterial.name); }
|
|
}
|
|
matCount = mats.Count;
|
|
sb.AppendLine(string.Format(" 예산 — 렌더 이미터 {0}개 · 고유 머티리얼 {1}개(≈ SetPass 추가) · maxParticles 합 {2}",
|
|
rendering, matCount, totalMaxParticles));
|
|
|
|
// ── 호 피팅 ──────────────────────────────────────────────────────
|
|
MeasureArc(sb, root);
|
|
|
|
// ── 찌르기 축 ────────────────────────────────────────────────────
|
|
// 전체 이미터 · 예산 후보(3개)를 각각 잰다. 남길 이미터가 곧 시각 길이를 정하므로
|
|
// 드로우 예산(≤3)과 기하 정합은 같은 결정이다.
|
|
MeasureStab(sb, root, null, "전체");
|
|
MeasureStab(sb, root, new[] { "lb", "blue_flash", "head" }, "예산후보 lb+blue_flash+head");
|
|
MeasureStab(sb, root, new[] { "lb", "blue_flash", "head", "Trail", "fm_g01" }, "확장후보 +Trail+fm_g01");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
sb.AppendLine(" !! 예외: " + e.Message);
|
|
}
|
|
finally
|
|
{
|
|
if (root != null) PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 호(크레센트) 원 피팅
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
static void MeasureArc(StringBuilder sb, GameObject root)
|
|
{
|
|
ParticleSystem best = null;
|
|
float bestScore = 0f, bestSize = 0f;
|
|
|
|
foreach (var ps in root.GetComponentsInChildren<ParticleSystem>(true))
|
|
{
|
|
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
|
if (psr == null || !psr.enabled) continue;
|
|
if (psr.renderMode != ParticleSystemRenderMode.Mesh || psr.mesh == null) continue;
|
|
if (psr.alignment == ParticleSystemRenderSpace.View) continue;
|
|
|
|
float size = Mathf.Max(ps.main.startSize.constantMax, 0f);
|
|
var ms = psr.mesh.bounds.size;
|
|
var ls = ps.transform.lossyScale;
|
|
float score = size * Mathf.Max(ms.x * Mathf.Abs(ls.x), Mathf.Max(ms.y * Mathf.Abs(ls.y), ms.z * Mathf.Abs(ls.z)));
|
|
if (score > bestScore) { bestScore = score; best = ps; bestSize = size; }
|
|
}
|
|
if (best == null) { sb.AppendLine(" [호] Mesh/비-View 이미터 없음 — 호 피팅 생략"); return; }
|
|
|
|
var r = best.GetComponent<ParticleSystemRenderer>();
|
|
var mesh = r.mesh;
|
|
sb.AppendLine(string.Format(" [호] 후보 이미터 '{0}' mesh='{1}' verts={2} isReadable={3} bounds={4}",
|
|
best.gameObject.name, mesh.name, mesh.vertexCount, mesh.isReadable, mesh.bounds.ToString()));
|
|
|
|
Vector3[] v;
|
|
int[] tri;
|
|
Vector2[] uv;
|
|
try { v = mesh.vertices; tri = mesh.triangles; uv = mesh.uv; }
|
|
catch (System.Exception e) { sb.AppendLine(" !! 정점 읽기 실패: " + e.Message); return; }
|
|
if (v == null || v.Length < 8) { sb.AppendLine(" !! 정점 부족"); return; }
|
|
|
|
// 루트를 원점·항등·스케일1 로 놓고 잰다 → 자식의 world 좌표 = 루트 로컬(스케일 1)
|
|
var rt = root.transform;
|
|
var sp = rt.position; var sr = rt.rotation; var ss = rt.localScale;
|
|
rt.position = Vector3.zero; rt.rotation = Quaternion.identity; rt.localScale = Vector3.one;
|
|
|
|
var tmw = best.transform;
|
|
var pts = new Vector3[v.Length];
|
|
for (int i = 0; i < v.Length; i++) pts[i] = tmw.TransformPoint(v[i] * bestSize);
|
|
|
|
rt.position = sp; rt.rotation = sr; rt.localScale = ss;
|
|
|
|
// ① 평면 법선 — 면적 가중 · 부호 정렬
|
|
Vector3 refN = Vector3.zero; float refA = 0f;
|
|
for (int i = 0; i + 2 < tri.Length; i += 3)
|
|
{
|
|
var cr = Vector3.Cross(pts[tri[i + 1]] - pts[tri[i]], pts[tri[i + 2]] - pts[tri[i]]);
|
|
if (cr.magnitude > refA) { refA = cr.magnitude; refN = cr; }
|
|
}
|
|
if (refA < 1e-12f) { sb.AppendLine(" !! 삼각형 면적 0"); return; }
|
|
refN /= refA;
|
|
Vector3 n = Vector3.zero;
|
|
for (int i = 0; i + 2 < tri.Length; i += 3)
|
|
{
|
|
var cr = Vector3.Cross(pts[tri[i + 1]] - pts[tri[i]], pts[tri[i + 2]] - pts[tri[i]]);
|
|
n += Vector3.Dot(cr, refN) >= 0f ? cr : -cr;
|
|
}
|
|
n.Normalize();
|
|
|
|
// 평면성 — 두께
|
|
Vector3 o = Vector3.zero;
|
|
for (int i = 0; i < pts.Length; i++) o += pts[i];
|
|
o /= pts.Length;
|
|
float thick = 0f;
|
|
for (int i = 0; i < pts.Length; i++) thick = Mathf.Max(thick, Mathf.Abs(Vector3.Dot(pts[i] - o, n)));
|
|
|
|
Vector3 u = Vector3.Cross(n, Mathf.Abs(n.y) < 0.9f ? Vector3.up : Vector3.right).normalized;
|
|
Vector3 w = Vector3.Cross(n, u);
|
|
|
|
int cnt = pts.Length;
|
|
var xs = new double[cnt]; var ys = new double[cnt];
|
|
for (int i = 0; i < cnt; i++) { var d = pts[i] - o; xs[i] = Vector3.Dot(d, u); ys[i] = Vector3.Dot(d, w); }
|
|
|
|
// ── 원 피팅 ────────────────────────────────────────────────────────
|
|
// 🔴 Kåsa 대수 피팅을 리본 정점 **전체**에 걸면 중심이 치우친다.
|
|
// 호는 안/바깥 반지름이 크게 다른 고리(annulus)라 대수 잔차가 큰 반지름 쪽으로 끌린다.
|
|
// (1차 측정에서 중심이 0.22 m 어긋나 안쪽 반지름이 0.47→0.27 로 축소돼 나왔다)
|
|
// → **바깥 가장자리만** 남기며 반복 재피팅한다. 바깥 경계는 진짜 원이라 정확히 수렴한다.
|
|
double cx, cy;
|
|
if (!FitCircle(xs, ys, out cx, out cy)) { sb.AppendLine(" !! 원 피팅 실패"); return; }
|
|
sb.AppendLine(string.Format(" [피팅] 1차(전체) 중심=({0:F4}, {1:F4}) 상대", cx, cy));
|
|
|
|
var keepX = new List<double>(xs); var keepY = new List<double>(ys);
|
|
for (int pass = 0; pass < 5 && keepX.Count >= 12; pass++)
|
|
{
|
|
var rr = new List<double>(keepX.Count);
|
|
for (int i = 0; i < keepX.Count; i++)
|
|
{
|
|
double dx = keepX[i] - cx, dy = keepY[i] - cy;
|
|
rr.Add(System.Math.Sqrt(dx * dx + dy * dy));
|
|
}
|
|
var sortedR = new List<double>(rr); sortedR.Sort();
|
|
double cut = sortedR[sortedR.Count / 2]; // 중앙값 이상만 남긴다
|
|
var nx = new List<double>(); var ny = new List<double>();
|
|
for (int i = 0; i < keepX.Count; i++) if (rr[i] >= cut) { nx.Add(keepX[i]); ny.Add(keepY[i]); }
|
|
if (nx.Count < 8) break;
|
|
keepX = nx; keepY = ny;
|
|
double ncx, ncy;
|
|
if (!FitCircle(keepX.ToArray(), keepY.ToArray(), out ncx, out ncy)) break;
|
|
cx = ncx; cy = ncy;
|
|
}
|
|
// 바깥 링 잔차 — 이 값이 작아야 "진짜 원"이다
|
|
double rSum = 0;
|
|
for (int i = 0; i < keepX.Count; i++)
|
|
{
|
|
double dx = keepX[i] - cx, dy = keepY[i] - cy;
|
|
rSum += System.Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
double rOutFit = rSum / System.Math.Max(keepX.Count, 1);
|
|
double rDev = 0;
|
|
for (int i = 0; i < keepX.Count; i++)
|
|
{
|
|
double dx = keepX[i] - cx, dy = keepY[i] - cy;
|
|
double d = System.Math.Sqrt(dx * dx + dy * dy) - rOutFit;
|
|
rDev += d * d;
|
|
}
|
|
rDev = System.Math.Sqrt(rDev / System.Math.Max(keepX.Count, 1));
|
|
sb.AppendLine(string.Format(" [피팅] 바깥링 재피팅 중심=({0:F4}, {1:F4}) r={2:F4} 잔차σ={3:F4} ({4:F1}%) · 표본 {5}/{6}",
|
|
cx, cy, rOutFit, rDev, rOutFit > 1e-6 ? rDev / rOutFit * 100.0 : 0.0, keepX.Count, cnt));
|
|
|
|
Vector3 center = o + u * (float)cx + w * (float)cy;
|
|
|
|
float rMin = float.MaxValue, rMax = 0f;
|
|
var ang = new float[cnt];
|
|
for (int i = 0; i < cnt; i++)
|
|
{
|
|
double dx = xs[i] - cx, dy = ys[i] - cy;
|
|
float rr = (float)System.Math.Sqrt(dx * dx + dy * dy);
|
|
if (rr < rMin) rMin = rr;
|
|
if (rr > rMax) rMax = rr;
|
|
ang[i] = Mathf.Atan2((float)dy, (float)dx);
|
|
}
|
|
|
|
var sorted = (float[])ang.Clone();
|
|
System.Array.Sort(sorted);
|
|
float gap = sorted[0] + Mathf.PI * 2f - sorted[cnt - 1];
|
|
float startAng = sorted[0];
|
|
for (int i = 1; i < cnt; i++)
|
|
{
|
|
float g = sorted[i] - sorted[i - 1];
|
|
if (g > gap) { gap = g; startAng = sorted[i]; }
|
|
}
|
|
float sweep = Mathf.PI * 2f - gap;
|
|
|
|
// UV 진행 방향
|
|
string uvNote = "(UV 없음)";
|
|
if (uv != null && uv.Length == cnt)
|
|
{
|
|
double su = 0, sa = 0;
|
|
var uu = new double[cnt]; var aa = new double[cnt];
|
|
for (int i = 0; i < cnt; i++)
|
|
{
|
|
float rel = Mathf.Repeat(ang[i] - startAng, Mathf.PI * 2f);
|
|
if (rel > sweep + 1e-3f) rel -= Mathf.PI * 2f;
|
|
uu[i] = uv[i].x; aa[i] = rel; su += uu[i]; sa += aa[i];
|
|
}
|
|
su /= cnt; sa /= cnt;
|
|
double cov = 0;
|
|
for (int i = 0; i < cnt; i++) cov += (uu[i] - su) * (aa[i] - sa);
|
|
uvNote = cov < 0 ? "UV u 가 각도 증가와 반대 → 호 진행 = CW(법선 반전 필요)"
|
|
: "UV u 가 각도 증가와 같음 → 호 진행 = CCW(법선 그대로)";
|
|
}
|
|
|
|
float midAng = startAng + sweep * 0.5f;
|
|
Vector3 bis = (u * Mathf.Cos(midAng) + w * Mathf.Sin(midAng)).normalized;
|
|
Vector3 sd = (u * Mathf.Cos(startAng) + w * Mathf.Sin(startAng)).normalized;
|
|
|
|
sb.AppendLine(string.Format(" 법선 n={0} (평면 두께 {1:F4} m)", n.ToString("F4"), thick));
|
|
sb.AppendLine(string.Format(" 원 중심 c={0} · r_in={1:F4} r_out={2:F4} (in/out={3:F3})",
|
|
center.ToString("F4"), rMin, rMax, rMax > 1e-6f ? rMin / rMax : 0f));
|
|
sb.AppendLine(string.Format(" 스윕={0:F1}° · 볼록(bisector)={1} · 시작 방향={2}",
|
|
sweep * Mathf.Rad2Deg, bis.ToString("F4"), sd.ToString("F4")));
|
|
sb.AppendLine(" " + uvNote);
|
|
sb.AppendLine(string.Format(" ▶ SO 베이크 값: arcNormal={0} arcCenter={1} arcBisector={2} arcStartDir={3} rOut={4:F4} rIn={5:F4} sweep={6:F2}",
|
|
F(n), F(center), F(bis), F(sd), rMax, rMin, sweep * Mathf.Rad2Deg));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 찌르기 축
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
static void MeasureStab(StringBuilder sb, GameObject root, string[] keep, string label)
|
|
{
|
|
var rt = root.transform;
|
|
var sp = rt.position; var sr = rt.rotation; var ss = rt.localScale;
|
|
rt.position = Vector3.zero; rt.rotation = Quaternion.identity; rt.localScale = Vector3.one;
|
|
|
|
var pos = new List<Vector3>(); var nrm = new List<Vector3>();
|
|
var wgt = new List<float>(); var nm = new List<string>();
|
|
// 🔴 반경을 등방(max extent)으로 쓰면 축 방향 길이가 과대평가된다(1차 측정 13.4 m).
|
|
// 이미터의 실제 방향성 반크기 벡터를 그대로 담아 두었다가 축이 정해진 뒤 투영한다.
|
|
var halfEdge = new List<Vector3[]>(); // 월드 반변(半邊) 3개
|
|
|
|
foreach (var ps in root.GetComponentsInChildren<ParticleSystem>(true))
|
|
{
|
|
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
|
if (psr == null || !psr.enabled) continue;
|
|
float size = Mathf.Max(ps.main.startSize.constantMax, 0f);
|
|
if (size <= 0f) continue;
|
|
if (keep != null && System.Array.IndexOf(keep, ps.gameObject.name) < 0) continue;
|
|
|
|
var tm = ps.transform;
|
|
float sx = Mathf.Abs(tm.lossyScale.x), sy = Mathf.Abs(tm.lossyScale.y), sz = Mathf.Abs(tm.lossyScale.z);
|
|
Vector3 ext;
|
|
if (psr.renderMode == ParticleSystemRenderMode.Mesh && psr.mesh != null)
|
|
{
|
|
var ms = psr.mesh.bounds.size;
|
|
ext = new Vector3(ms.x * sx, ms.y * sy, ms.z * sz) * size;
|
|
}
|
|
else ext = new Vector3(size * sx, size * sy, 0f);
|
|
|
|
pos.Add(tm.position);
|
|
halfEdge.Add(new[] { tm.right * (ext.x * 0.5f), tm.up * (ext.y * 0.5f), tm.forward * (ext.z * 0.5f) });
|
|
nrm.Add(tm.TransformDirection(AxisVec(MinAxis(ext))).normalized);
|
|
wgt.Add(Mathf.Max(ext.x, Mathf.Max(ext.y, ext.z)));
|
|
nm.Add(ps.gameObject.name);
|
|
}
|
|
|
|
if (pos.Count < 2) { rt.position = sp; rt.rotation = sr; rt.localScale = ss; sb.AppendLine(" [찌르기:" + label + "] 이미터 부족"); return; }
|
|
|
|
Vector3 c = Vector3.zero; float wsum = 0f;
|
|
for (int i = 0; i < pos.Count; i++) { c += pos[i] * wgt[i]; wsum += wgt[i]; }
|
|
c /= Mathf.Max(wsum, 1e-6f);
|
|
|
|
Vector3 axis = Principal(pos, c);
|
|
float far = 0f, farT = 0f;
|
|
for (int i = 0; i < pos.Count; i++)
|
|
{
|
|
float t = Vector3.Dot(pos[i] - c, axis);
|
|
if (Mathf.Abs(t) > far) { far = Mathf.Abs(t); farT = t; }
|
|
}
|
|
if (farT < 0f) axis = -axis;
|
|
|
|
float tMin = float.MaxValue, tMax = float.MinValue, thick = 0f;
|
|
var rows = new List<string>();
|
|
for (int i = 0; i < pos.Count; i++)
|
|
{
|
|
float t = Vector3.Dot(pos[i] - c, axis);
|
|
var he = halfEdge[i];
|
|
// 축 방향 반크기 = 세 반변을 축에 투영한 절댓값 합 (OBB 의 축 지지 함수)
|
|
float ha = Mathf.Abs(Vector3.Dot(he[0], axis)) + Mathf.Abs(Vector3.Dot(he[1], axis)) + Mathf.Abs(Vector3.Dot(he[2], axis));
|
|
if (t - ha < tMin) tMin = t - ha;
|
|
if (t + ha > tMax) tMax = t + ha;
|
|
|
|
Vector3 perp = (pos[i] - c) - axis * t;
|
|
float hp = (he[0] - axis * Vector3.Dot(he[0], axis)).magnitude
|
|
+ (he[1] - axis * Vector3.Dot(he[1], axis)).magnitude
|
|
+ (he[2] - axis * Vector3.Dot(he[2], axis)).magnitude;
|
|
thick = Mathf.Max(thick, perp.magnitude + hp);
|
|
rows.Add(string.Format(" {0,-20} t={1,7:F3} 축반크기={2:F3} → [{3,7:F3}, {4,7:F3}]", nm[i], t, ha, t - ha, t + ha));
|
|
}
|
|
float len = tMax - tMin;
|
|
|
|
Vector3 planeN = Vector3.zero;
|
|
for (int i = 0; i < pos.Count; i++) planeN += nrm[i] * wgt[i];
|
|
planeN -= axis * Vector3.Dot(planeN, axis);
|
|
if (planeN.sqrMagnitude < 1e-8f) planeN = Vector3.Cross(axis, Vector3.up);
|
|
planeN.Normalize();
|
|
|
|
Vector3 start = c + axis * tMin;
|
|
|
|
// 프리팹 루트 원점 = 작가가 의도한 부착점(= 손/칼자루 쪽). 이 지점부터 앞쪽으로 뻗은 거리를
|
|
// "전방 도달 길이" 로 따로 잰다 — 뒤로 흐르는 스피드 라인까지 포함한 전체 길이로 크기를
|
|
// 맞추면 이펙트가 지나치게 작아진다(전체 12.8 m vs 전방 도달 5.9 m).
|
|
float tRoot = Vector3.Dot(Vector3.zero - c, axis);
|
|
float forward = tMax - tRoot;
|
|
Vector3 rootOnAxis = c + axis * tRoot;
|
|
|
|
rt.position = sp; rt.rotation = sr; rt.localScale = ss;
|
|
|
|
sb.AppendLine(string.Format(" [찌르기:{0}] 이미터 {1}개 · 축={2} 면법선={3}",
|
|
label, pos.Count, axis.ToString("F4"), planeN.ToString("F4")));
|
|
sb.AppendLine(string.Format(" 전체 길이={0:F3} m (시작 {1}) · 두께={2:F3} m · 루트 t={3:F3} → 전방 도달={4:F3} m",
|
|
len, start.ToString("F3"), thick * 2f, tRoot, forward));
|
|
sb.AppendLine(" 축 위 이미터 배치 (t = 축 방향 좌표 · 큰 값이 칼끝 쪽):");
|
|
rows.Sort((a, b) => a.CompareTo(b));
|
|
foreach (var r in rows) sb.AppendLine(r);
|
|
sb.AppendLine(string.Format(" ▶ SO 베이크(전체 기준): stabAxis={0} stabStart={1} stabLength={2:F4} stabPlaneNormal={3}",
|
|
F(axis), F(start), len, F(planeN)));
|
|
sb.AppendLine(string.Format(" ▶ SO 베이크(전방 기준): stabStart={0} stabLength={1:F4}", F(rootOnAxis), forward));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
static bool FitCircle(double[] xs, double[] ys, out double cx, out double cy)
|
|
{
|
|
cx = cy = 0;
|
|
int n = xs.Length;
|
|
double sx = 0, sy = 0, sxx = 0, syy = 0, sxy = 0, sxz = 0, syz = 0, sz = 0;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
double x = xs[i], y = ys[i], z = x * x + y * y;
|
|
sx += x; sy += y; sz += z; sxx += x * x; syy += y * y; sxy += x * y; sxz += x * z; syz += y * z;
|
|
}
|
|
double a11 = sxx, a12 = sxy, a13 = sx, a21 = sxy, a22 = syy, a23 = sy, a31 = sx, a32 = sy, a33 = n;
|
|
double b1 = -sxz, b2 = -syz, b3 = -sz;
|
|
double det = a11 * (a22 * a33 - a23 * a32) - a12 * (a21 * a33 - a23 * a31) + a13 * (a21 * a32 - a22 * a31);
|
|
if (System.Math.Abs(det) < 1e-14) return false;
|
|
double dD = b1 * (a22 * a33 - a23 * a32) - a12 * (b2 * a33 - a23 * b3) + a13 * (b2 * a32 - a22 * b3);
|
|
double dE = a11 * (b2 * a33 - a23 * b3) - b1 * (a21 * a33 - a23 * a31) + a13 * (a21 * b3 - b2 * a31);
|
|
cx = -(dD / det) * 0.5; cy = -(dE / det) * 0.5;
|
|
return !double.IsNaN(cx) && !double.IsNaN(cy);
|
|
}
|
|
|
|
static Vector3 Principal(List<Vector3> pts, Vector3 c)
|
|
{
|
|
float xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
|
|
foreach (var p in pts)
|
|
{
|
|
var d = p - c;
|
|
xx += d.x * d.x; xy += d.x * d.y; xz += d.x * d.z; yy += d.y * d.y; yz += d.y * d.z; zz += d.z * d.z;
|
|
}
|
|
Vector3 v = new Vector3(1f, 0.7f, 0.3f);
|
|
for (int it = 0; it < 32; it++)
|
|
{
|
|
var w = new Vector3(xx * v.x + xy * v.y + xz * v.z, xy * v.x + yy * v.y + yz * v.z, xz * v.x + yz * v.y + zz * v.z);
|
|
float m = w.magnitude;
|
|
if (m < 1e-12f) return Vector3.forward;
|
|
v = w / m;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static int BurstTotal(ParticleSystem ps)
|
|
{
|
|
var em = ps.emission;
|
|
if (!em.enabled) return 0;
|
|
var bursts = new ParticleSystem.Burst[em.burstCount];
|
|
em.GetBursts(bursts);
|
|
int t = 0;
|
|
for (int i = 0; i < bursts.Length; i++) t += (int)bursts[i].count.constantMax;
|
|
return t;
|
|
}
|
|
|
|
static string Chain(Transform root, Transform t)
|
|
{
|
|
var parts = new List<string>();
|
|
while (t != null && t != root) { parts.Insert(0, t.name); t = t.parent; }
|
|
parts.Insert(0, root.name);
|
|
return string.Join("/", parts);
|
|
}
|
|
|
|
static string F(Vector3 v) { return string.Format("({0:F5}, {1:F5}, {2:F5})", v.x, v.y, v.z); }
|
|
static Vector3 AxisVec(int i) { var v = Vector3.zero; v[i] = 1f; return v; }
|
|
static int MinAxis(Vector3 s) { if (s.x <= s.y && s.x <= s.z) return 0; return s.y <= s.z ? 1 : 2; }
|
|
}
|