// WL772_Maps.cs — PD 지시 #772 : LMHPOLY _Demo Scenes/Demo_02~Demo_10 을 사용 가능한 맵으로 일괄 생성 // 씬 실측 : unity command run_script --file AgentScripts/WL772_Maps.cs --entry WL772_Maps.ScanAll --timeout 600 // 1개 실측 : ... --entry WL772_Maps.Scan --args '["Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes/Demo_02.unity", 100.0]' // 빌드 파라미터 출력(JSON) : ... --entry WL772_Maps.PlanAll --timeout 900 // // 측정 전용 — 씬을 열기만 하고 ClearDirty 로 저장하지 않는다. 에셋 변경 0. // 실제 맵 생성은 WL_StartMapBuilder.RunFor / Rebake, WL_MonsterSpawnerBuilder.RunFor, // WL_NatureFinish.Lighting/Setup/Instancing 을 이 스캔 결과 수치로 호출해서 수행한다. using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif public static class WL772_Maps { const string SceneDir = "Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes"; const string Out = "AgentScripts/staging/WL_Maps/out"; #if UNITY_EDITOR static void W(string f, string s) { System.IO.Directory.CreateDirectory(Out); System.IO.File.WriteAllText(System.IO.Path.Combine(Out, f), s, new System.Text.UTF8Encoding(true)); } public class Plan { public string scene, name; public float minX, maxX, minZ, maxZ, minY, maxY; public int prefabInstances, renderers, waterRenderers; public float waterY = float.NaN; public float startX, startY, startZ, centerX, centerZ, half, camYaw; public float dWater = -1f, walkPct; public string skybox = "", fog = "", sun = "", ambient = ""; public string note = ""; } // ── 씬 1개 실측 + 시작 지점/영역 선정 ─────────────────────────────────── public static object Scan(string scenePath, float half) { var p = ScanCore(scenePath, half); return Describe(p); } public static object ScanAll() { var sb = new StringBuilder(); var plans = new List(); for (int i = 2; i <= 10; i++) { var sp = string.Format("{0}/Demo_{1:00}.unity", SceneDir, i); var p = ScanCore(sp, 100f); plans.Add(p); sb.AppendLine(Describe(p)); sb.AppendLine(); } // 표 sb.AppendLine("| 씬 | XZ 범위 | 높이 | 수면 y | 프리팹 인스턴스 | 렌더러 | 시작 지점 | 영역 중심 ±반폭 | 수면거리 | 보행가능 | camYaw |"); sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|"); foreach (var p in plans) sb.AppendLine(string.Format("| {0} | {1:F0}~{2:F0} x {3:F0}~{4:F0} | {5:F1}~{6:F1} | {7} | {8} | {9} | ({10:F1}, {11:F2}, {12:F1}) | ({13:F0}, {14:F0}) ±{15:F0} | {16:F1} | {17:F1}% | {18:F0} |", p.name, p.minX, p.maxX, p.minZ, p.maxZ, p.minY, p.maxY, float.IsNaN(p.waterY) ? "없음" : p.waterY.ToString("F2"), p.prefabInstances, p.renderers, p.startX, p.startY, p.startZ, p.centerX, p.centerZ, p.half, p.dWater, p.walkPct, p.camYaw)); var s = sb.ToString(); W("M1_scan.txt", s); return s; } // 빌드에 필요한 인자만 CSV 로 (셸에서 파싱해 순차 호출) public static object PlanAll() { var sb = new StringBuilder(); sb.AppendLine("idx,scene,startX,startY,startZ,camYaw,centerX,centerZ,half,navCY,navSY,waterY"); for (int i = 2; i <= 10; i++) { var sp = string.Format("{0}/Demo_{1:00}.unity", SceneDir, i); var p = ScanCore(sp, 100f); float navCY = (p.minY + p.maxY) * 0.5f; float navSY = Mathf.Max(60f, (p.maxY - p.minY) + 40f); sb.AppendLine(string.Format("{0},{1},{2:F3},{3:F3},{4:F3},{5:F1},{6:F1},{7:F1},{8:F1},{9:F2},{10:F1},{11}", i, sp, p.startX, p.startY, p.startZ, p.camYaw, p.centerX, p.centerZ, p.half, navCY, navSY, float.IsNaN(p.waterY) ? "nan" : p.waterY.ToString("F3"))); } var s = sb.ToString(); W("M1_plan.csv", s); return s; } static string Describe(Plan p) { return string.Format( "## {0}\n 경로 {1}\n XZ {2:F1}~{3:F1} x {4:F1}~{5:F1} 높이 {6:F2}~{7:F2}\n 프리팹인스턴스 {8} · 렌더러 {9} · 수면렌더러 {10} · 수면y {11}\n 시작 ({12:F2}, {13:F2}, {14:F2}) camYaw {15:F0} · 영역중심 ({16:F1}, {17:F1}) ±{18:F0} · 수면거리 {19:F1} · 보행가능 {20:F1}%\n 스카이박스 {21}\n 안개 {22}\n 태양 {23}\n 앰비언트 {24}\n 비고 {25}", p.name, p.scene, p.minX, p.maxX, p.minZ, p.maxZ, p.minY, p.maxY, p.prefabInstances, p.renderers, p.waterRenderers, float.IsNaN(p.waterY) ? "없음" : p.waterY.ToString("F3"), p.startX, p.startY, p.startZ, p.camYaw, p.centerX, p.centerZ, p.half, p.dWater, p.walkPct, p.skybox, p.fog, p.sun, p.ambient, p.note); } static Plan ScanCore(string scenePath, float half) { var p = new Plan { scene = scenePath, name = System.IO.Path.GetFileNameWithoutExtension(scenePath), half = half }; var setup = EditorSceneManager.GetSceneManagerSetup(); var sc = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); try { Physics.SyncTransforms(); var roots = sc.GetRootGameObjects(); var rends = new List(); int pfi = 0; foreach (var r in roots) { rends.AddRange(r.GetComponentsInChildren(false)); foreach (var t in r.GetComponentsInChildren(true)) if (PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject)) pfi++; } p.renderers = rends.Count; p.prefabInstances = pfi; // 지형(콜라이더) 범위 = 실제로 서 있을 수 있는 곳 var cols = new List(); foreach (var r in roots) cols.AddRange(r.GetComponentsInChildren(false)); Bounds tb; if (cols.Count > 0) { tb = cols[0].bounds; foreach (var c in cols) tb.Encapsulate(c.bounds); } else if (rends.Count > 0) { tb = rends[0].bounds; foreach (var r in rends) tb.Encapsulate(r.bounds); } else tb = new Bounds(Vector3.zero, Vector3.one * 100f); p.minX = tb.min.x; p.maxX = tb.max.x; p.minZ = tb.min.z; p.maxZ = tb.max.z; p.minY = tb.min.y; p.maxY = tb.max.y; // 수면 var waters = rends.Where(r => r.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0 || (r.sharedMaterial != null && r.sharedMaterial.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0)).ToList(); p.waterRenderers = waters.Count; var waterBounds = new List(); if (waters.Count > 0) { p.waterY = waters.Max(w => w.bounds.max.y); foreach (var w in waters) waterBounds.Add(w.bounds); } // 환경 p.skybox = RenderSettings.skybox != null ? RenderSettings.skybox.name : "(none)"; p.fog = string.Format("enabled={0} mode={1} color={2} start={3:F1} end={4:F1} density={5:F4}", RenderSettings.fog, RenderSettings.fogMode, ColorStr(RenderSettings.fogColor), RenderSettings.fogStartDistance, RenderSettings.fogEndDistance, RenderSettings.fogDensity); var lights = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None) .Where(l => l.type == LightType.Directional).ToArray(); p.sun = lights.Length == 0 ? "(directional 없음)" : string.Join(" / ", lights.Select(l => string.Format("{0} rot({1:F2},{2:F2}) {3} i={4:F2} {5}", l.name, l.transform.eulerAngles.x, l.transform.eulerAngles.y, ColorStr(l.color), l.intensity, l.shadows))); p.ambient = string.Format("mode={0} sky={1} intensity={2:F2}", RenderSettings.ambientMode, ColorStr(RenderSettings.ambientLight), RenderSettings.ambientIntensity); // ── 시작 지점 선정 ──────────────────────────────────────────── // 조건: 경사 <= 15° · 수면 위(+0.5 m) · 반경 10 m 개활지 단차 <= 1.5 m · 지형 가장자리에서 여유 float pad = Mathf.Min(half * 0.5f, Mathf.Min(tb.size.x, tb.size.z) * 0.35f); float x0 = tb.min.x + pad, x1 = tb.max.x - pad, z0 = tb.min.z + pad, z1 = tb.max.z - pad; float step = Mathf.Max(4f, Mathf.Min(tb.size.x, tb.size.z) / 40f); Vector3 best = Vector3.zero; float bestScore = float.NegativeInfinity, bestDW = -1f; bool found = false; for (float x = x0; x <= x1; x += step) for (float z = z0; z <= z1; z += step) { RaycastHit h; if (!Physics.Raycast(new Vector3(x, tb.max.y + 50f, z), Vector3.down, out h, tb.size.y + 200f)) continue; if (!float.IsNaN(p.waterY) && h.point.y < p.waterY + 0.5f) continue; float slope = Vector3.Angle(h.normal, Vector3.up); if (slope > 15f) continue; // 반경 10 m 평탄도 float lo = h.point.y, hi = h.point.y; bool ok = true; for (int a = 0; a < 8 && ok; a++) { float ang = a * Mathf.PI / 4f; var q = new Vector3(x + Mathf.Cos(ang) * 10f, tb.max.y + 50f, z + Mathf.Sin(ang) * 10f); RaycastHit h2; if (!Physics.Raycast(q, Vector3.down, out h2, tb.size.y + 200f)) { ok = false; break; } lo = Mathf.Min(lo, h2.point.y); hi = Mathf.Max(hi, h2.point.y); } if (!ok || hi - lo > 1.5f) continue; // 수면까지 거리 float dw = float.MaxValue; foreach (var wb in waterBounds) { var cp = wb.ClosestPoint(new Vector3(x, wb.center.y, z)); dw = Mathf.Min(dw, Vector2.Distance(new Vector2(x, z), new Vector2(cp.x, cp.z))); } if (waterBounds.Count == 0) dw = -1f; // 점수: 평탄할수록 · 수면 6~40 m 근접 우선 · 지형 중앙 근접 float score = -(hi - lo) * 4f - slope * 0.2f; if (dw >= 0f) score += (dw >= 5f && dw <= 45f) ? 20f - Mathf.Abs(dw - 20f) * 0.3f : -Mathf.Min(20f, Mathf.Abs(dw - 20f) * 0.2f); score -= Vector2.Distance(new Vector2(x, z), new Vector2(tb.center.x, tb.center.z)) * 0.05f; if (score > bestScore) { bestScore = score; best = h.point; bestDW = dw; found = true; } } if (!found) { RaycastHit hc; if (Physics.Raycast(new Vector3(tb.center.x, tb.max.y + 50f, tb.center.z), Vector3.down, out hc, tb.size.y + 200f)) best = hc.point; else best = new Vector3(tb.center.x, 0f, tb.center.z); p.note += "적합 후보 없음 → 지형 중심 채택. "; } p.startX = best.x; p.startY = best.y; p.startZ = best.z; p.dWater = bestDW; // 영역 반폭 = 지형에 맞춰 축소 (최대 100) float maxHalf = Mathf.Min(100f, Mathf.Min(tb.size.x, tb.size.z) * 0.45f); p.half = Mathf.Max(40f, maxHalf); // 영역 중심 = 시작 지점을 지형 안으로 클램프 p.centerX = Mathf.Clamp(best.x, tb.min.x + p.half, tb.max.x - p.half); p.centerZ = Mathf.Clamp(best.z, tb.min.z + p.half, tb.max.z - p.half); if (tb.size.x < p.half * 2f) p.centerX = tb.center.x; if (tb.size.z < p.half * 2f) p.centerZ = tb.center.z; // camYaw = 수면 방향(없으면 영역 중심 방향) Vector3 look; if (waterBounds.Count > 0) { var nearest = waterBounds.OrderBy(b => Vector2.Distance(new Vector2(best.x, best.z), new Vector2(b.center.x, b.center.z))).First(); look = new Vector3(nearest.center.x - best.x, 0f, nearest.center.z - best.z); } else look = new Vector3(p.centerX - best.x, 0f, p.centerZ - best.z); if (look.sqrMagnitude < 1f) look = Vector3.forward; p.camYaw = Mathf.Repeat(Quaternion.LookRotation(look).eulerAngles.y, 360f); // 영역 보행가능 비율(경사 45° 이하 · 수면 위) int tot = 0, walk = 0; for (float x = p.centerX - p.half; x <= p.centerX + p.half; x += 5f) for (float z = p.centerZ - p.half; z <= p.centerZ + p.half; z += 5f) { tot++; RaycastHit h; if (!Physics.Raycast(new Vector3(x, tb.max.y + 50f, z), Vector3.down, out h, tb.size.y + 200f)) continue; if (!float.IsNaN(p.waterY) && h.point.y < p.waterY) continue; if (Vector3.Angle(h.normal, Vector3.up) > 45f) continue; walk++; } p.walkPct = 100f * walk / Mathf.Max(1, tot); } finally { // 저장 없이 원상복귀 — 씬을 열기만 했으므로 dirty 를 만들지 않았다 if (setup != null && setup.Length > 0) EditorSceneManager.RestoreSceneManagerSetup(setup); } return p; } static string ColorStr(Color c) { return string.Format("RGBA({0:F3},{1:F3},{2:F3},{3:F3})", c.r, c.g, c.b, c.a); } #endif }