// WL772_Repick.cs — PD 지시 #772 보정 : 베이크된 NavMesh 의 「최대 연결 성분」으로 시작 지점·플레이 영역을 다시 고른다. // ... --entry WL772_Repick.Run --args '["Assets/WL/Settings/WL_Nature07_NavMesh.asset", "Assets/Res_Addr/Map/WL_Nature07.prefab", -9999.0]' (minY = 수면 위 하한) // // 1단계 자동 선정은 「평탄 + 수면 근접」만 봤기 때문에 고립된 대지/섬을 고르는 경우가 있었다(07·08·10). // 여기서는 NavMesh 삼각분할을 union-find 로 묶어 가장 큰 연결 성분을 찾고, 그 성분 안에서 // 중심에 가깝고 법선이 위를 향하는 삼각형을 시작 지점으로 삼는다. 읽기 전용(에셋 미변경). using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; using UnityEngine.AI; #if UNITY_EDITOR using UnityEditor; #endif public static class WL772_Repick { #if UNITY_EDITOR class UF { int[] p; public UF(int n) { p = new int[n]; for (int i = 0; i < n; i++) p[i] = i; } public int F(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; } public void U(int a, int b) { a = F(a); b = F(b); if (a != b) p[a] = b; } } public static object Run(string navAsset, string prefabPath, float minY) { var data = AssetDatabase.LoadAssetAtPath(navAsset); if (data == null) return "NavMeshData 로드 실패: " + navAsset; var inst = NavMesh.AddNavMeshData(data); try { var tri = NavMesh.CalculateTriangulation(); int nt = tri.indices.Length / 3; if (nt == 0) return "삼각형 0"; // 정점 좌표를 양자화해서 공유 정점을 찾는다 (float 오차 흡수) var map = new Dictionary(); var vid = new int[tri.vertices.Length]; for (int i = 0; i < tri.vertices.Length; i++) { var v = tri.vertices[i]; long k = ((long)Mathf.RoundToInt(v.x * 100f) * 73856093) ^ ((long)Mathf.RoundToInt(v.y * 100f) * 19349663) ^ ((long)Mathf.RoundToInt(v.z * 100f) * 83492791); int id; if (!map.TryGetValue(k, out id)) { id = map.Count; map[k] = id; } vid[i] = id; } // 정점 → 삼각형, union-find var uf = new UF(nt); var byVert = new Dictionary>(); for (int t = 0; t < nt; t++) for (int j = 0; j < 3; j++) { int v = vid[tri.indices[t * 3 + j]]; List l; if (!byVert.TryGetValue(v, out l)) { l = new List(); byVert[v] = l; } l.Add(t); } foreach (var kv in byVert) for (int i = 1; i < kv.Value.Count; i++) uf.U(kv.Value[0], kv.Value[i]); // 성분별 면적 var area = new Dictionary(); var cent = new Dictionary(); var wsum = new Dictionary(); for (int t = 0; t < nt; t++) { var a = tri.vertices[tri.indices[t * 3]]; var b = tri.vertices[tri.indices[t * 3 + 1]]; var c = tri.vertices[tri.indices[t * 3 + 2]]; float ar = Vector3.Cross(b - a, c - a).magnitude * 0.5f; int r = uf.F(t); area[r] = (area.TryGetValue(r, out var v0) ? v0 : 0f) + ar; cent[r] = (cent.TryGetValue(r, out var v1) ? v1 : Vector3.zero) + (a + b + c) / 3f * ar; wsum[r] = (wsum.TryGetValue(r, out var v2) ? v2 : 0f) + ar; } var top = area.OrderByDescending(k => k.Value).First(); var centroid = cent[top.Key] / Mathf.Max(0.0001f, wsum[top.Key]); float totalArea = area.Values.Sum(); // 최대 성분의 XZ 범위 var bmin = new Vector3(9e9f, 9e9f, 9e9f); var bmax = -bmin; var pool = new List(); for (int t = 0; t < nt; t++) { if (uf.F(t) != top.Key) continue; var a = tri.vertices[tri.indices[t * 3]]; var b = tri.vertices[tri.indices[t * 3 + 1]]; var c = tri.vertices[tri.indices[t * 3 + 2]]; var ctr = (a + b + c) / 3f; var nrm = Vector3.Cross(b - a, c - a).normalized; bmin = Vector3.Min(bmin, ctr); bmax = Vector3.Max(bmax, ctr); if (Vector3.Angle(nrm, Vector3.up) < 12f && ctr.y >= minY) pool.Add(ctr); } if (pool.Count == 0) pool.Add(centroid); // 성분 중심에 가장 가까운 평탄 삼각형 var start = pool.OrderBy(v => Vector2.Distance(new Vector2(v.x, v.z), new Vector2(centroid.x, centroid.z))).First(); NavMeshHit hit; // 스냅은 같은 층으로만 (아래 층 NavMesh 로 끌려가는 것을 막는다) if (NavMesh.SamplePosition(start, out hit, 2f, NavMesh.AllAreas) && Mathf.Abs(hit.position.y - start.y) < 1f) start = hit.position; float halfX = (bmax.x - bmin.x) * 0.5f, halfZ = (bmax.z - bmin.z) * 0.5f; float half = Mathf.Clamp(Mathf.Min(halfX, halfZ), 40f, 100f); float cx = Mathf.Clamp(start.x, bmin.x + half * 0.5f, bmax.x - half * 0.5f); float cz = Mathf.Clamp(start.z, bmin.z + half * 0.5f, bmax.z - half * 0.5f); if (bmax.x - bmin.x < half) cx = (bmin.x + bmax.x) * 0.5f; if (bmax.z - bmin.z < half) cz = (bmin.z + bmax.z) * 0.5f; // camYaw = 프리팹의 수면 방향(있으면), 없으면 영역 중심 방향 float yaw = 0f; var root = PrefabUtility.LoadPrefabContents(prefabPath); try { var waters = root.GetComponentsInChildren(true) .Where(r => r.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0 || (r.sharedMaterial != null && r.sharedMaterial.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0)).ToArray(); Vector3 look; if (waters.Length > 0) { var nearest = waters.OrderBy(w => Vector2.Distance(new Vector2(start.x, start.z), new Vector2(w.bounds.center.x, w.bounds.center.z))).First(); look = new Vector3(nearest.bounds.center.x - start.x, 0f, nearest.bounds.center.z - start.z); } else look = new Vector3(cx - start.x, 0f, cz - start.z); if (look.sqrMagnitude < 1f) look = Vector3.forward; yaw = Mathf.Repeat(Quaternion.LookRotation(look).eulerAngles.y, 360f); } finally { PrefabUtility.UnloadPrefabContents(root); } return string.Format( "CSV,{0:F3},{1:F3},{2:F3},{3:F1},{4:F1},{5:F1},{6:F1} | 성분 {7}개 · 최대성분 면적 {8:F0}/{9:F0} ({10:F0}%) · XZ {11:F0}~{12:F0} x {13:F0}~{14:F0} · 평탄삼각형 {15}", start.x, start.y, start.z, yaw, cx, cz, half, area.Count, top.Value, totalArea, 100f * top.Value / totalArea, bmin.x, bmax.x, bmin.z, bmax.z, pool.Count); } finally { NavMesh.RemoveNavMeshData(inst); } } #endif }