// 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(path); if (asset == null) { sb.AppendLine(" !! 없음"); sb.AppendLine(); return; } GameObject root = null; try { root = PrefabUtility.LoadPrefabContents(path); var systems = root.GetComponentsInChildren(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(); int totalMaxParticles = 0; foreach (var ps in systems) { var psr = ps.GetComponent(); 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(true)) { var psr = ps.GetComponent(); 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(); 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(xs); var keepY = new List(ys); for (int pass = 0; pass < 5 && keepX.Count >= 12; pass++) { var rr = new List(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(rr); sortedR.Sort(); double cut = sortedR[sortedR.Count / 2]; // 중앙값 이상만 남긴다 var nx = new List(); var ny = new List(); 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(); var nrm = new List(); var wgt = new List(); var nm = new List(); // 🔴 반경을 등방(max extent)으로 쓰면 축 방향 길이가 과대평가된다(1차 측정 13.4 m). // 이미터의 실제 방향성 반크기 벡터를 그대로 담아 두었다가 축이 정해진 뒤 투영한다. var halfEdge = new List(); // 월드 반변(半邊) 3개 foreach (var ps in root.GetComponentsInChildren(true)) { var psr = ps.GetComponent(); 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(); 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 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(); 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; } }