447 lines
22 KiB
C#
447 lines
22 KiB
C#
|
|
// WL788_Measure.cs — PD 지시 #788 크레센트 로컬 프레임 실측 (에디트 모드 · Assets 무수정)
|
|||
|
|
// unity command run_script --file AgentScripts/WL788_Measure.cs --entry WL788_Measure.Dump --args '["Effect_Slash_10101_1"]'
|
|||
|
|
// unity command run_script --file AgentScripts/WL788_Measure.cs --entry WL788_Measure.DumpMany --args '["Effect_Slash_10101_1,Effect_Slash_10501_1"]'
|
|||
|
|
//
|
|||
|
|
// 측정 방법 (구 방식 "메시 바운즈 최소축" 의 한계를 넘기 위한 정밀화):
|
|||
|
|
// ① 프리팹을 프리뷰 씬에 인스턴스(현재 씬 무영향 · isDirty 유발 없음)
|
|||
|
|
// ② 크레센트 이미터를 수명 30% 지점까지 Simulate → ParticleSystemRenderer.BakeMesh
|
|||
|
|
// = 정렬(alignment) · startRotation · startSize · 스트레치가 모두 반영된 **실제 렌더 지오메트리**
|
|||
|
|
// ③ 베이크 메시의 삼각형을 UV 로 리샘플하며 머티리얼 텍스처의 알파×휘도(잉크)를 가중치로 삼아
|
|||
|
|
// 루트 로컬 공간의 "보이는 픽셀 구름" 을 만든다 (크레센트가 메시 모양이든 텍스처든 동일 처리)
|
|||
|
|
// ④ 가중 PCA → 평면 법선 · 평면 투영 후 Kasa 원 적합 → 호 중심 · 내/외 반지름 · 각도 범위
|
|||
|
|
// → 시위 방향(start→end) · 볼록 방향(중심→호 중점) 확정
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEditor.SceneManagement;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.SceneManagement;
|
|||
|
|
|
|||
|
|
public static class WL788_Measure
|
|||
|
|
{
|
|||
|
|
// ── 결과 구조 ────────────────────────────────────────────────────────────
|
|||
|
|
public struct Frame
|
|||
|
|
{
|
|||
|
|
public string emitter;
|
|||
|
|
public Vector3 normal; // 루트 로컬 — 크레센트 평면 법선
|
|||
|
|
public Vector3 chord; // 루트 로컬 — 호 시작→끝 (시위) 방향, 정규화
|
|||
|
|
public Vector3 convex; // 루트 로컬 — 호 중심→호 중점 (볼록) 방향, 정규화
|
|||
|
|
public Vector3 center; // 루트 로컬 — 호의 원 중심
|
|||
|
|
public float innerR, outerR, midR;
|
|||
|
|
public float arcDeg; // 호가 덮는 각도
|
|||
|
|
public float ink; // 잉크 총량(가중치 합)
|
|||
|
|
public float planarity; // 평면성 = 1 - (최소분산/중간분산). 1 에 가까울수록 평평
|
|||
|
|
public float circleRms; // 원 적합 잔차 RMS / midR
|
|||
|
|
public int samples;
|
|||
|
|
public string inkMode;
|
|||
|
|
public bool oriented; // 방향 정보를 담고 있는가(카메라 정렬이 아닌가)
|
|||
|
|
public string tile; // 스프라이트 시트에서 고른 타일
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static object DumpMany(string csv)
|
|||
|
|
{
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
foreach (var n in csv.Split(',')) { sb.AppendLine(Dump(n.Trim()) as string); }
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static object Dump(string prefabName)
|
|||
|
|
{
|
|||
|
|
string path = FindPrefab(prefabName);
|
|||
|
|
if (path == null) return "prefab not found: " + prefabName;
|
|||
|
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|||
|
|
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
sb.AppendLine("== " + prefabName + " " + path);
|
|||
|
|
sb.AppendLine(" rootScale=" + src.transform.localScale.ToString("F3") + " rootEuler=" + src.transform.localEulerAngles.ToString("F1"));
|
|||
|
|
|
|||
|
|
Scene preview = default(Scene);
|
|||
|
|
GameObject inst = null;
|
|||
|
|
GameObject camGo = null;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
preview = EditorSceneManager.NewPreviewScene();
|
|||
|
|
inst = Object.Instantiate(src);
|
|||
|
|
inst.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
|||
|
|
SceneManager.MoveGameObjectToScene(inst, preview);
|
|||
|
|
|
|||
|
|
camGo = new GameObject("wl788cam");
|
|||
|
|
SceneManager.MoveGameObjectToScene(camGo, preview);
|
|||
|
|
var cam = camGo.AddComponent<Camera>();
|
|||
|
|
camGo.transform.SetPositionAndRotation(new Vector3(0f, 0f, -10f), Quaternion.identity);
|
|||
|
|
cam.enabled = false;
|
|||
|
|
|
|||
|
|
var root = inst.transform;
|
|||
|
|
foreach (var ps in inst.GetComponentsInChildren<ParticleSystem>(true))
|
|||
|
|
{
|
|||
|
|
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
|||
|
|
if (psr == null) continue;
|
|||
|
|
var main = ps.main;
|
|||
|
|
string mat = psr.sharedMaterial != null ? psr.sharedMaterial.name : "-";
|
|||
|
|
var tex = GetTex(psr.sharedMaterial);
|
|||
|
|
var tsa = ps.textureSheetAnimation;
|
|||
|
|
|
|||
|
|
sb.AppendLine(string.Format(
|
|||
|
|
" -- {0,-16} on={1} render={2}{3} align={4} size={5:F2} life={6:F2} rot3D={7} rot={8:F0} sim={9} mat={10} tex={11}{12}",
|
|||
|
|
ps.gameObject.name, psr.enabled, psr.renderMode,
|
|||
|
|
psr.renderMode == ParticleSystemRenderMode.Mesh && psr.mesh != null ? "(" + psr.mesh.name + " v" + psr.mesh.vertexCount + ")" : "",
|
|||
|
|
psr.alignment, main.startSize.constantMax, main.startLifetime.constantMax,
|
|||
|
|
main.startRotation3D, main.startRotation.constantMax * Mathf.Rad2Deg, main.simulationSpace, mat,
|
|||
|
|
tex != null ? tex.name + " " + tex.width + "x" + tex.height : "-",
|
|||
|
|
tsa.enabled ? string.Format(" sheet={0}x{1}", tsa.numTilesX, tsa.numTilesY) : ""));
|
|||
|
|
|
|||
|
|
if (!psr.enabled) continue;
|
|||
|
|
|
|||
|
|
Frame f;
|
|||
|
|
if (!MeasureEmitter(ps, psr, root, cam, out f)) { sb.AppendLine(" (측정 불가 — 잉크 없음)"); continue; }
|
|||
|
|
sb.AppendLine(Format(f));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
if (camGo != null) Object.DestroyImmediate(camGo);
|
|||
|
|
if (inst != null) Object.DestroyImmediate(inst);
|
|||
|
|
if (preview.IsValid()) EditorSceneManager.ClosePreviewScene(preview);
|
|||
|
|
}
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string Format(Frame f)
|
|||
|
|
{
|
|||
|
|
return string.Format(
|
|||
|
|
" FRAME n={0} chord={1} convex={2} center={3}\n" +
|
|||
|
|
" innerR={4:F3} outerR={5:F3} midR={6:F3} arc={7:F1}deg ink={8:F0} planar={9:F3} circRms={10:F3} pts={11}",
|
|||
|
|
f.normal.ToString("F3"), f.chord.ToString("F3"), f.convex.ToString("F3"), f.center.ToString("F3"),
|
|||
|
|
f.innerR, f.outerR, f.midR, f.arcDeg, f.ink, f.planarity, f.circleRms, f.samples)
|
|||
|
|
+ " " + f.inkMode + (string.IsNullOrEmpty(f.tile) ? "" : " tile=" + f.tile);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 이미터 1개 실측 (결정론적) ───────────────────────────────────────────
|
|||
|
|
// Simulate + BakeMesh 는 **여러 파티클의 합집합**이라 shape 산포·랜덤 시드 때문에
|
|||
|
|
// 실행마다 호 중심·시위 방향이 달라진다(실측 2026-09-07: 같은 이미터 2회 측정에서
|
|||
|
|
// 법선은 4° 이내로 같았지만 chord 가 평면 안에서 60° 이상 회전). 캘리브레이션 값은
|
|||
|
|
// 재현 가능해야 하므로 **파티클 1개의 소스 지오메트리**를 결정론적으로 잰다.
|
|||
|
|
// · Mesh 렌더 → psr.mesh 의 정점/UV 를 그대로 (메시 로컬)
|
|||
|
|
// · Billboard/Local → 이미터 로컬 XY 평면의 단위 쿼드로 합성 (모양은 텍스처가 갖는다)
|
|||
|
|
// · View 정렬 → 항상 카메라를 보므로 방향 정보 없음 → 측정 제외
|
|||
|
|
// 그 뒤 이미터의 루트 기준 상대 회전·스케일·위치와 startSize·startRotation 을 곱한다.
|
|||
|
|
public static bool MeasureEmitter(ParticleSystem ps, ParticleSystemRenderer psr, Transform root, Camera cam, out Frame f)
|
|||
|
|
{
|
|||
|
|
f = new Frame();
|
|||
|
|
f.emitter = ps.gameObject.name;
|
|||
|
|
var main = ps.main;
|
|||
|
|
|
|||
|
|
bool oriented = psr.alignment != ParticleSystemRenderSpace.View;
|
|||
|
|
f.oriented = oriented;
|
|||
|
|
if (!oriented) return false;
|
|||
|
|
|
|||
|
|
// ① 파티클 로컬 지오메트리 (정점 + UV + 삼각형)
|
|||
|
|
Vector3[] verts; Vector2[] uvs; int[] tris;
|
|||
|
|
if (psr.renderMode == ParticleSystemRenderMode.Mesh && psr.mesh != null)
|
|||
|
|
{
|
|||
|
|
verts = psr.mesh.vertices; uvs = psr.mesh.uv; tris = psr.mesh.triangles;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 단위 쿼드 — 이미터 로컬 XY 평면, 법선 +Z
|
|||
|
|
verts = new[] { new Vector3(-0.5f, -0.5f, 0f), new Vector3(0.5f, -0.5f, 0f), new Vector3(0.5f, 0.5f, 0f), new Vector3(-0.5f, 0.5f, 0f) };
|
|||
|
|
uvs = new[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) };
|
|||
|
|
tris = new[] { 0, 1, 2, 0, 2, 3 };
|
|||
|
|
}
|
|||
|
|
if (verts.Length < 3 || tris.Length < 3) return false;
|
|||
|
|
if (uvs == null || uvs.Length != verts.Length) uvs = null;
|
|||
|
|
|
|||
|
|
// ② 텍스처 (스프라이트 시트면 잉크가 가장 많은 타일을 고른다)
|
|||
|
|
Texture2D readable = MakeReadable(GetTex(psr.sharedMaterial));
|
|||
|
|
Vector2 tileScale = Vector2.one, tileOffset = Vector2.zero;
|
|||
|
|
var tsa = ps.textureSheetAnimation;
|
|||
|
|
if (readable != null && tsa.enabled && tsa.numTilesX > 0 && tsa.numTilesY > 0)
|
|||
|
|
{
|
|||
|
|
tileScale = new Vector2(1f / tsa.numTilesX, 1f / tsa.numTilesY);
|
|||
|
|
float best = -1f;
|
|||
|
|
for (int ty = 0; ty < tsa.numTilesY; ty++)
|
|||
|
|
for (int tx = 0; tx < tsa.numTilesX; tx++)
|
|||
|
|
{
|
|||
|
|
float ink = TileInk(readable, new Vector2(tx * tileScale.x, ty * tileScale.y), tileScale);
|
|||
|
|
if (ink > best) { best = ink; tileOffset = new Vector2(tx * tileScale.x, ty * tileScale.y); }
|
|||
|
|
}
|
|||
|
|
f.tile = string.Format("{0}x{1}@{2:F2},{3:F2}", tsa.numTilesX, tsa.numTilesY, tileOffset.x, tileOffset.y);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// ③ 파티클 로컬 → 루트 로컬 변환
|
|||
|
|
// 루트 기준 상대 회전 · 스케일. startSize 는 파티클 크기 배수.
|
|||
|
|
Quaternion rel = Quaternion.Inverse(root.rotation) * ps.transform.rotation;
|
|||
|
|
float rotZ = main.startRotation.constantMax * Mathf.Rad2Deg;
|
|||
|
|
if (Mathf.Abs(rotZ) > 0.01f) rel = rel * Quaternion.Euler(0f, 0f, rotZ);
|
|||
|
|
Vector3 relScale = InvScale(ps.transform.lossyScale, root.lossyScale) * Mathf.Max(main.startSize.constantMax, 1e-4f);
|
|||
|
|
Vector3 origin = root.InverseTransformPoint(ps.transform.position);
|
|||
|
|
|
|||
|
|
float[] thresholds = { 0.05f, 0.01f, 0f }; // 마지막 0 = 지오메트리 전용 폴백
|
|||
|
|
for (int ti2 = 0; ti2 < thresholds.Length; ti2++)
|
|||
|
|
{
|
|||
|
|
float thr = thresholds[ti2];
|
|||
|
|
var pts = new List<Vector3>(8192);
|
|||
|
|
var wts = new List<float>(8192);
|
|||
|
|
const int GRID = 14;
|
|||
|
|
int triCount = tris.Length / 3;
|
|||
|
|
|
|||
|
|
for (int ti = 0; ti < triCount; ti++)
|
|||
|
|
{
|
|||
|
|
Vector3 a = verts[tris[ti * 3]], b = verts[tris[ti * 3 + 1]], c = verts[tris[ti * 3 + 2]];
|
|||
|
|
Vector2 ua = Vector2.zero, ub = Vector2.zero, uc = Vector2.zero;
|
|||
|
|
if (uvs != null) { ua = uvs[tris[ti * 3]]; ub = uvs[tris[ti * 3 + 1]]; uc = uvs[tris[ti * 3 + 2]]; }
|
|||
|
|
|
|||
|
|
for (int i = 0; i <= GRID; i++)
|
|||
|
|
for (int j = 0; i + j <= GRID; j++)
|
|||
|
|
{
|
|||
|
|
float w1 = (float)i / GRID, w2 = (float)j / GRID, w0 = 1f - w1 - w2;
|
|||
|
|
float w = 1f;
|
|||
|
|
if (readable != null && uvs != null && thr > 0f)
|
|||
|
|
{
|
|||
|
|
Vector2 uv = ua * w0 + ub * w1 + uc * w2;
|
|||
|
|
uv = new Vector2(tileOffset.x + Frac(uv.x) * tileScale.x, tileOffset.y + Frac(uv.y) * tileScale.y);
|
|||
|
|
var col = readable.GetPixelBilinear(uv.x, uv.y);
|
|||
|
|
// 잉크 = 알파 × 휘도. 가산 합성 텍스처는 알파가 1 이라 휘도가 지배한다.
|
|||
|
|
float lum = col.r * 0.299f + col.g * 0.587f + col.b * 0.114f;
|
|||
|
|
w = col.a * Mathf.Max(lum, 0.0001f);
|
|||
|
|
if (w < thr) continue;
|
|||
|
|
}
|
|||
|
|
Vector3 pLocal = a * w0 + b * w1 + c * w2;
|
|||
|
|
pLocal = new Vector3(pLocal.x * relScale.x, pLocal.y * relScale.y, pLocal.z * relScale.z);
|
|||
|
|
pts.Add(origin + rel * pLocal);
|
|||
|
|
wts.Add(w);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (pts.Count < 32) continue;
|
|||
|
|
f.inkMode = thr > 0f ? "tex>" + thr.ToString("F2") : "geom";
|
|||
|
|
bool ok = Solve(pts, wts, ref f);
|
|||
|
|
f.emitter = ps.gameObject.name;
|
|||
|
|
f.oriented = oriented;
|
|||
|
|
return ok;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
if (readable != null) Object.DestroyImmediate(readable);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float Frac(float v) { return v - Mathf.Floor(v); }
|
|||
|
|
|
|||
|
|
static Vector3 InvScale(Vector3 child, Vector3 parent)
|
|||
|
|
{
|
|||
|
|
return new Vector3(
|
|||
|
|
Mathf.Abs(parent.x) > 1e-6f ? child.x / parent.x : child.x,
|
|||
|
|
Mathf.Abs(parent.y) > 1e-6f ? child.y / parent.y : child.y,
|
|||
|
|
Mathf.Abs(parent.z) > 1e-6f ? child.z / parent.z : child.z);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float TileInk(Texture2D tex, Vector2 off, Vector2 scale)
|
|||
|
|
{
|
|||
|
|
float sum = 0f;
|
|||
|
|
const int N = 24;
|
|||
|
|
for (int y = 0; y < N; y++)
|
|||
|
|
for (int x = 0; x < N; x++)
|
|||
|
|
{
|
|||
|
|
var c = tex.GetPixelBilinear(off.x + (x + 0.5f) / N * scale.x, off.y + (y + 0.5f) / N * scale.y);
|
|||
|
|
sum += c.a * (c.r * 0.299f + c.g * 0.587f + c.b * 0.114f);
|
|||
|
|
}
|
|||
|
|
return sum;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 가중 PCA + 원 적합 ───────────────────────────────────────────────────
|
|||
|
|
static bool Solve(List<Vector3> pts, List<float> wts, ref Frame f)
|
|||
|
|
{
|
|||
|
|
int n = pts.Count;
|
|||
|
|
float wsum = 0f; Vector3 mean = Vector3.zero;
|
|||
|
|
for (int i = 0; i < n; i++) { mean += pts[i] * wts[i]; wsum += wts[i]; }
|
|||
|
|
if (wsum <= 1e-6f) return false;
|
|||
|
|
mean /= wsum;
|
|||
|
|
|
|||
|
|
// 3x3 공분산
|
|||
|
|
double[,] C = new double[3, 3];
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
Vector3 d = pts[i] - mean; float w = wts[i];
|
|||
|
|
C[0, 0] += w * d.x * d.x; C[0, 1] += w * d.x * d.y; C[0, 2] += w * d.x * d.z;
|
|||
|
|
C[1, 1] += w * d.y * d.y; C[1, 2] += w * d.y * d.z; C[2, 2] += w * d.z * d.z;
|
|||
|
|
}
|
|||
|
|
C[1, 0] = C[0, 1]; C[2, 0] = C[0, 2]; C[2, 1] = C[1, 2];
|
|||
|
|
for (int a = 0; a < 3; a++) for (int b = 0; b < 3; b++) C[a, b] /= wsum;
|
|||
|
|
|
|||
|
|
Vector3[] ev; float[] va;
|
|||
|
|
Jacobi(C, out ev, out va);
|
|||
|
|
// 오름차순 정렬 — va[0] 최소 = 법선
|
|||
|
|
for (int i = 0; i < 2; i++)
|
|||
|
|
for (int j = i + 1; j < 3; j++)
|
|||
|
|
if (va[j] < va[i]) { var tv = va[i]; va[i] = va[j]; va[j] = tv; var te = ev[i]; ev[i] = ev[j]; ev[j] = te; }
|
|||
|
|
|
|||
|
|
Vector3 nrm = ev[0].normalized;
|
|||
|
|
Vector3 e1 = ev[2].normalized; // 최대 분산 축(평면 내)
|
|||
|
|
Vector3 e2 = Vector3.Cross(nrm, e1).normalized;
|
|||
|
|
f.planarity = va[1] > 1e-12f ? 1f - Mathf.Sqrt(Mathf.Max(va[0], 0f) / va[1]) : 1f;
|
|||
|
|
|
|||
|
|
// 평면 2D 좌표
|
|||
|
|
var x = new float[n]; var y = new float[n];
|
|||
|
|
for (int i = 0; i < n; i++) { Vector3 d = pts[i] - mean; x[i] = Vector3.Dot(d, e1); y[i] = Vector3.Dot(d, e2); }
|
|||
|
|
|
|||
|
|
// Kasa 원 적합 (가중)
|
|||
|
|
double Sx = 0, Sy = 0, Sxx = 0, Syy = 0, Sxy = 0, Sxz = 0, Syz = 0, Sz = 0, W = 0;
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
double w = wts[i], xi = x[i], yi = y[i], zi = xi * xi + yi * yi;
|
|||
|
|
W += w; Sx += w * xi; Sy += w * yi; Sxx += w * xi * xi; Syy += w * yi * yi;
|
|||
|
|
Sxy += w * xi * yi; Sxz += w * xi * zi; Syz += w * yi * zi; Sz += w * zi;
|
|||
|
|
}
|
|||
|
|
double a11 = Sxx - Sx * Sx / W, a12 = Sxy - Sx * Sy / W, a22 = Syy - Sy * Sy / W;
|
|||
|
|
double b1 = 0.5 * (Sxz - Sx * Sz / W), b2 = 0.5 * (Syz - Sy * Sz / W);
|
|||
|
|
double det = a11 * a22 - a12 * a12;
|
|||
|
|
double cx, cy;
|
|||
|
|
bool circleOk = System.Math.Abs(det) > 1e-14;
|
|||
|
|
if (circleOk) { cx = (b1 * a22 - b2 * a12) / det; cy = (a11 * b2 - a12 * b1) / det; }
|
|||
|
|
else { cx = 0; cy = 0; }
|
|||
|
|
|
|||
|
|
// 반지름 분포 · 각도 범위
|
|||
|
|
var rad = new float[n]; var ang = new float[n];
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
double dx = x[i] - cx, dy = y[i] - cy;
|
|||
|
|
rad[i] = (float)System.Math.Sqrt(dx * dx + dy * dy);
|
|||
|
|
ang[i] = Mathf.Atan2((float)dy, (float)dx);
|
|||
|
|
}
|
|||
|
|
float rMid = WeightedPercentile(rad, wts, 0.5f);
|
|||
|
|
f.innerR = WeightedPercentile(rad, wts, 0.05f);
|
|||
|
|
f.outerR = WeightedPercentile(rad, wts, 0.95f);
|
|||
|
|
f.midR = rMid;
|
|||
|
|
|
|||
|
|
double rms = 0;
|
|||
|
|
for (int i = 0; i < n; i++) { double d = rad[i] - rMid; rms += wts[i] * d * d; }
|
|||
|
|
f.circleRms = rMid > 1e-6f ? (float)System.Math.Sqrt(rms / W) / rMid : 999f;
|
|||
|
|
|
|||
|
|
// 각도 평균 = 원형 평균(±π 경계 안전)
|
|||
|
|
double sa = 0, ca = 0;
|
|||
|
|
for (int i = 0; i < n; i++) { sa += wts[i] * Mathf.Sin(ang[i]); ca += wts[i] * Mathf.Cos(ang[i]); }
|
|||
|
|
float midAng = Mathf.Atan2((float)sa, (float)ca);
|
|||
|
|
// 중점 기준 상대각으로 펴서 범위를 잰다
|
|||
|
|
var rel = new float[n];
|
|||
|
|
for (int i = 0; i < n; i++) rel[i] = Mathf.DeltaAngle(midAng * Mathf.Rad2Deg, ang[i] * Mathf.Rad2Deg);
|
|||
|
|
float lo = WeightedPercentile(rel, wts, 0.03f), hi = WeightedPercentile(rel, wts, 0.97f);
|
|||
|
|
f.arcDeg = hi - lo;
|
|||
|
|
|
|||
|
|
// 프레임 확정
|
|||
|
|
Vector3 Dir(float degFromMid)
|
|||
|
|
{
|
|||
|
|
float t = midAng + degFromMid * Mathf.Deg2Rad;
|
|||
|
|
return (e1 * Mathf.Cos(t) + e2 * Mathf.Sin(t)).normalized;
|
|||
|
|
}
|
|||
|
|
Vector3 convex = Dir(0f);
|
|||
|
|
Vector3 start = Dir(lo), end = Dir(hi);
|
|||
|
|
Vector3 chord = end - start; // 시위 = 호 시작점 → 끝점
|
|||
|
|
if (chord.sqrMagnitude < 1e-8f) chord = Vector3.Cross(nrm, convex);
|
|||
|
|
chord.Normalize();
|
|||
|
|
// 법선 방향을 (chord × convex) 오른손 규약으로 고정한다 — 뒤집힘 없이 재현 가능
|
|||
|
|
Vector3 nFixed = Vector3.Cross(chord, convex).normalized;
|
|||
|
|
if (Vector3.Dot(nFixed, nrm) < 0f) nrm = -nrm;
|
|||
|
|
nrm = nFixed;
|
|||
|
|
|
|||
|
|
f.normal = nrm;
|
|||
|
|
f.chord = chord;
|
|||
|
|
f.convex = convex;
|
|||
|
|
f.center = mean + e1 * (float)cx + e2 * (float)cy;
|
|||
|
|
f.ink = wsum;
|
|||
|
|
f.samples = n;
|
|||
|
|
return circleOk;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float WeightedPercentile(float[] v, List<float> w, float p)
|
|||
|
|
{
|
|||
|
|
int n = v.Length;
|
|||
|
|
var idx = Enumerable.Range(0, n).ToArray();
|
|||
|
|
System.Array.Sort(idx, (a, b) => v[a].CompareTo(v[b]));
|
|||
|
|
float total = 0f; for (int i = 0; i < n; i++) total += w[i];
|
|||
|
|
float acc = 0f, target = total * p;
|
|||
|
|
for (int i = 0; i < n; i++) { acc += w[idx[i]]; if (acc >= target) return v[idx[i]]; }
|
|||
|
|
return v[idx[n - 1]];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 대칭 3x3 야코비 고유분해
|
|||
|
|
static void Jacobi(double[,] a, out Vector3[] vec, out float[] val)
|
|||
|
|
{
|
|||
|
|
double[,] v = new double[3, 3] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
|
|||
|
|
double[,] m = (double[,])a.Clone();
|
|||
|
|
for (int sweep = 0; sweep < 24; sweep++)
|
|||
|
|
{
|
|||
|
|
double off = m[0, 1] * m[0, 1] + m[0, 2] * m[0, 2] + m[1, 2] * m[1, 2];
|
|||
|
|
if (off < 1e-20) break;
|
|||
|
|
for (int p = 0; p < 2; p++)
|
|||
|
|
for (int q = p + 1; q < 3; q++)
|
|||
|
|
{
|
|||
|
|
if (System.Math.Abs(m[p, q]) < 1e-18) continue;
|
|||
|
|
double theta = (m[q, q] - m[p, p]) / (2 * m[p, q]);
|
|||
|
|
double tt = System.Math.Sign(theta) / (System.Math.Abs(theta) + System.Math.Sqrt(theta * theta + 1));
|
|||
|
|
if (theta == 0) tt = 1;
|
|||
|
|
double c = 1 / System.Math.Sqrt(tt * tt + 1), s = tt * c;
|
|||
|
|
for (int k = 0; k < 3; k++)
|
|||
|
|
{
|
|||
|
|
double mkp = m[k, p], mkq = m[k, q];
|
|||
|
|
m[k, p] = c * mkp - s * mkq; m[k, q] = s * mkp + c * mkq;
|
|||
|
|
}
|
|||
|
|
for (int k = 0; k < 3; k++)
|
|||
|
|
{
|
|||
|
|
double mpk = m[p, k], mqk = m[q, k];
|
|||
|
|
m[p, k] = c * mpk - s * mqk; m[q, k] = s * mpk + c * mqk;
|
|||
|
|
}
|
|||
|
|
for (int k = 0; k < 3; k++)
|
|||
|
|
{
|
|||
|
|
double vkp = v[k, p], vkq = v[k, q];
|
|||
|
|
v[k, p] = c * vkp - s * vkq; v[k, q] = s * vkp + c * vkq;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
vec = new Vector3[3]; val = new float[3];
|
|||
|
|
for (int i = 0; i < 3; i++)
|
|||
|
|
{
|
|||
|
|
vec[i] = new Vector3((float)v[0, i], (float)v[1, i], (float)v[2, i]);
|
|||
|
|
val[i] = (float)m[i, i];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 유틸 ────────────────────────────────────────────────────────────────
|
|||
|
|
public static string FindPrefab(string name)
|
|||
|
|
{
|
|||
|
|
var guids = AssetDatabase.FindAssets(name + " t:Prefab");
|
|||
|
|
return guids.Select(AssetDatabase.GUIDToAssetPath)
|
|||
|
|
.FirstOrDefault(p => System.IO.Path.GetFileNameWithoutExtension(p) == name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Texture GetTex(Material m)
|
|||
|
|
{
|
|||
|
|
if (m == null) return null;
|
|||
|
|
if (m.mainTexture != null) return m.mainTexture;
|
|||
|
|
string[] names = { "_MainTex", "_BaseMap", "_BaseColorMap", "_Texture" };
|
|||
|
|
foreach (var n in names) if (m.HasProperty(n) && m.GetTexture(n) != null) return m.GetTexture(n);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>읽기 불가 텍스처도 RenderTexture 경유로 픽셀을 읽는다(임포터 무수정).</summary>
|
|||
|
|
public static Texture2D MakeReadable(Texture src)
|
|||
|
|
{
|
|||
|
|
if (src == null) return null;
|
|||
|
|
int w = Mathf.Min(src.width, 256), h = Mathf.Min(src.height, 256);
|
|||
|
|
var rt = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
|
|||
|
|
var prev = RenderTexture.active;
|
|||
|
|
Graphics.Blit(src, rt);
|
|||
|
|
RenderTexture.active = rt;
|
|||
|
|
var tex = new Texture2D(w, h, TextureFormat.RGBA32, false, true);
|
|||
|
|
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
|
|||
|
|
tex.Apply();
|
|||
|
|
RenderTexture.active = prev;
|
|||
|
|
RenderTexture.ReleaseTemporary(rt);
|
|||
|
|
return tex;
|
|||
|
|
}
|
|||
|
|
}
|