246 lines
14 KiB
C#
246 lines
14 KiB
C#
// 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<Plan>();
|
|
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<MeshRenderer>();
|
|
int pfi = 0;
|
|
foreach (var r in roots)
|
|
{
|
|
rends.AddRange(r.GetComponentsInChildren<MeshRenderer>(false));
|
|
foreach (var t in r.GetComponentsInChildren<Transform>(true))
|
|
if (PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject)) pfi++;
|
|
}
|
|
p.renderers = rends.Count; p.prefabInstances = pfi;
|
|
|
|
// 지형(콜라이더) 범위 = 실제로 서 있을 수 있는 곳
|
|
var cols = new List<Collider>();
|
|
foreach (var r in roots) cols.AddRange(r.GetComponentsInChildren<Collider>(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<Bounds>();
|
|
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<Light>(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
|
|
}
|