using System.Collections.Generic; using System.Linq; // PD 지시 #771 2단계 — WL_Nature 맵 마무리(조명·MapData·볼륨·기상연출·인스턴싱) + 검증 + 캡처 // 실행 예 // 조명 이식 : unity command run_script --file AgentScripts/WL_NatureFinish.cs --entry WL_NatureFinish.Lighting --args '["Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes/Demo_01.unity","Assets/Res_Addr/Map/WL_Nature.prefab", 30.0, 250.0]' // 구성 : ... --entry WL_NatureFinish.Setup --args '["Assets/Res_Addr/Map/WL_Nature.prefab"]' // 인스턴싱 : ... --entry WL_NatureFinish.Instancing --args '["Assets/Res_Addr/Map/WL_Nature.prefab"]' // 검증 : ... --entry WL_NatureFinish.Verify --args '["Assets/Res_Addr/Map/WL_Nature.prefab"]' // 도달성 : ... --entry WL_NatureFinish.Reach --args '["Assets/Res_Addr/Map/WL_Nature.prefab","Assets/WL/Settings/WL_Nature_NavMesh.asset", 4.0]' // 캡처 : ... --entry WL_NatureFinish.Shots --args '["Assets/Res_Addr/Map/WL_Nature.prefab", 100.0]' public static class WL_NatureFinish { const string OUT = @"E:\NerdNavis\nn_himminji\AgentScripts\staging\WL_Nature2\out"; const string SHOT = @"E:\NerdNavis\nn_himminji\Screenshots_WL\nature"; const string kWakeSettings = "Assets/WL/Settings/StartWakeSettings.asset"; const string kPostProfile = "Assets/WL/Settings/WL_Nature_Post.asset"; // 게임 시점 카메라 실측값 (Main Camera.prefab arr_cCamData[0] · RealCamera.cs · 1단계와 동일) const float CAM_PITCH = 65f, CAM_DIST = 3f, CAM_UP = 2f, CAM_FOV = 60f; const int W = 1280, H = 720; static System.Text.StringBuilder sb; static void L(string s) { sb.AppendLine(s); } // ═══════════════════════════════════════════════════════════ // 0) 지정한 머티리얼만 Built-in → URP 변환 (Q3 · 세미콜론 구분 경로 목록) // ═══════════════════════════════════════════════════════════ public static object UpgradeMats(string pathsSemicolon) { sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var ups = UnityEditor.Rendering.MaterialUpgrader.FetchAllUpgradersForPipeline( typeof(UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset)); L("업그레이더 " + ups.Count + "개"); int changed = 0, kept = 0; foreach (var raw in pathsSemicolon.Split(';')) { var p = raw.Trim(); if (p.Length == 0) continue; var m = UnityEditor.AssetDatabase.LoadAssetAtPath(p); if (m == null) { L(" [없음] " + p); continue; } string before = m.shader == null ? "" : m.shader.name; string bColor = m.HasProperty("_Color") ? m.GetColor("_Color").ToString() : "-"; string msg = null; try { UnityEditor.Rendering.MaterialUpgrader.Upgrade(m, ups, UnityEditor.Rendering.MaterialUpgrader.UpgradeFlags.None, ref msg); } catch (System.Exception e) { msg = "EXCEPTION " + e.Message; } string after = m.shader == null ? "" : m.shader.name; string aColor = m.HasProperty("_BaseColor") ? m.GetColor("_BaseColor").ToString() : (m.HasProperty("_Color") ? m.GetColor("_Color").ToString() : "-"); if (before != after) { changed++; L(" OK " + before + " -> " + after + " color " + bColor + " -> " + aColor + " " + p); } else { kept++; L(" -- " + before + " (변화 없음) " + p + (string.IsNullOrEmpty(msg) ? "" : " msg=" + msg)); } } UnityEditor.AssetDatabase.SaveAssets(); UnityEditor.AssetDatabase.Refresh(); Flush("F0_upgrade.txt"); return "changed=" + changed + " unchanged=" + kept; } // ═══════════════════════════════════════════════════════════ // 1) 원본 씬의 조명·안개·스카이박스·앰비언트를 MapData 로 이식 // ═══════════════════════════════════════════════════════════ public static object Lighting(string srcScene, string prefabPath, float fogStart, float fogEnd) { sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); UnityEngine.Rendering.AmbientMode am; UnityEngine.Color aSky, aEq, aGr, fogColor; float aInt, refl; UnityEngine.Material sky; bool fogOn; UnityEngine.FogMode fogMode; float fogDensity; try { UnityEditor.SceneManagement.EditorSceneManager.OpenScene(srcScene, UnityEditor.SceneManagement.OpenSceneMode.Single); am = UnityEngine.RenderSettings.ambientMode; aSky = UnityEngine.RenderSettings.ambientSkyColor; aEq = UnityEngine.RenderSettings.ambientEquatorColor; aGr = UnityEngine.RenderSettings.ambientGroundColor; aInt = UnityEngine.RenderSettings.ambientIntensity; refl = UnityEngine.RenderSettings.reflectionIntensity; sky = UnityEngine.RenderSettings.skybox; fogOn = UnityEngine.RenderSettings.fog; fogMode = UnityEngine.RenderSettings.fogMode; fogColor = UnityEngine.RenderSettings.fogColor; fogDensity = UnityEngine.RenderSettings.fogDensity; L("원본 씬 실측 " + srcScene); L(" ambientMode=" + am + " ambientLight=" + UnityEngine.RenderSettings.ambientLight + " sky=" + aSky + " eq=" + aEq + " gr=" + aGr + " intensity=" + aInt + " reflection=" + refl); L(" fog=" + fogOn + " mode=" + fogMode + " color=" + fogColor + " start=" + UnityEngine.RenderSettings.fogStartDistance + " end=" + UnityEngine.RenderSettings.fogEndDistance + " density=" + fogDensity); L(" skybox=" + (sky == null ? "(없음)" : UnityEditor.AssetDatabase.GetAssetPath(sky))); if (am == UnityEngine.Rendering.AmbientMode.Flat) aSky = UnityEngine.RenderSettings.ambientLight; // Flat 은 ambientLight 가 SOT ClearDirty(); } finally { try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } var root = UnityEditor.PrefabUtility.LoadPrefabContents(prefabPath); try { var md = root.GetComponent(); if (md == null) { L("[오류] MapData 없음"); Flush("F1_lighting.txt"); return "no MapData"; } md.material_skybox = sky; md.ambientMode = am; md.ambientSky = aSky; md.ambientEquator = aEq; md.ambientGround = aGr; md.ambientIntensity = aInt; md.reflectionIntensity = refl; md.updateEnvironmentFromSkybox = (am == UnityEngine.Rendering.AmbientMode.Skybox); md.showWeaponInLobby = true; // #756 관례 — 시작 맵은 무기 표시 // 안개 (MapData 신규 필드 · 기본값 = 기존 동작(끔)) var so = new UnityEditor.SerializedObject(md); SetB(so, "fogEnabled", fogOn); SetI(so, "fogMode", (int)fogMode); SetC(so, "fogColor", fogColor); SetF(so, "fogStart", fogStart); SetF(so, "fogEnd", fogEnd); SetF(so, "fogDensity", fogDensity); so.ApplyModifiedPropertiesWithoutUndo(); UnityEditor.EditorUtility.SetDirty(md); UnityEditor.PrefabUtility.SaveAsPrefabAsset(root, prefabPath); L("MapData 갱신: ambientMode=" + am + " ambientSky=" + aSky + " skybox=" + (sky == null ? "-" : sky.name) + " fog=" + fogOn + "/" + fogMode + " start=" + fogStart + " end=" + fogEnd); } finally { UnityEditor.PrefabUtility.UnloadPrefabContents(root); } UnityEditor.AssetDatabase.SaveAssets(); Flush("F1_lighting.txt"); return "lighting applied (fog " + fogStart + "~" + fogEnd + ")"; } static void SetB(UnityEditor.SerializedObject so, string n, bool v) { var p = so.FindProperty(n); if (p != null) p.boolValue = v; else L(" [주의] 필드 없음 " + n); } static void SetI(UnityEditor.SerializedObject so, string n, int v) { var p = so.FindProperty(n); if (p != null) p.intValue = v; else L(" [주의] 필드 없음 " + n); } static void SetF(UnityEditor.SerializedObject so, string n, float v) { var p = so.FindProperty(n); if (p != null) p.floatValue = v; else L(" [주의] 필드 없음 " + n); } static void SetC(UnityEditor.SerializedObject so, string n, UnityEngine.Color v) { var p = so.FindProperty(n); if (p != null) p.colorValue = v; else L(" [주의] 필드 없음 " + n); } // ═══════════════════════════════════════════════════════════ // 2) 구성 — MobControlMgrList · #764 필드 · StartWakeSequence · Global Volume // ═══════════════════════════════════════════════════════════ public static object Setup(string prefabPath) { sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; // 포스트 프로파일 복제 (WL_Start_Post.asset → WL_Nature_Post.asset) if (UnityEditor.AssetDatabase.LoadMainAssetAtPath(kPostProfile) == null) { bool ok = UnityEditor.AssetDatabase.CopyAsset("Assets/WL/Settings/WL_Start_Post.asset", kPostProfile); UnityEditor.AssetDatabase.SaveAssets(); L("포스트 프로파일 복제 " + (ok ? "OK" : "실패") + " → " + kPostProfile); } else L("포스트 프로파일 이미 존재: " + kPostProfile); var prof = UnityEditor.AssetDatabase.LoadMainAssetAtPath(kPostProfile); var wake = UnityEditor.AssetDatabase.LoadAssetAtPath(kWakeSettings); if (wake == null) L("[주의] StartWakeSettings 에셋 없음: " + kWakeSettings); var root = UnityEditor.PrefabUtility.LoadPrefabContents(prefabPath); try { // MobControlMgrList (루트) if (root.GetComponent() == null) { root.AddComponent(); L("MobControlMgrList 추가"); } else L("MobControlMgrList 이미 있음"); // #764 필드 var md = root.GetComponent(); if (md == null) L("[오류] MapData 없음"); else { md.spawnRequireReachableFromPlayer = true; md.spawnSnapRadius = 2f; md.spawnSampleTries = 50; md.mobNavSnapRadius = 2f; UnityEditor.EditorUtility.SetDirty(md); L("#764 필드: reachable=1 snap=2 tries=50 mobSnap=2"); } // StartWakeSequence var seq = root.GetComponent(); if (seq == null) { seq = root.AddComponent(); L("StartWakeSequence 추가"); } if (wake != null) { var sox = new UnityEditor.SerializedObject(seq); var p = sox.FindProperty("settings"); if (p != null) { p.objectReferenceValue = wake; sox.ApplyModifiedPropertiesWithoutUndo(); L("StartWakeSequence.settings = " + kWakeSettings); } else L("[주의] settings 프로퍼티 없음"); } // SceneAssets / Global Volume var holder = root.transform.Find("SceneAssets"); if (holder == null) { var go = new UnityEngine.GameObject("SceneAssets"); go.transform.SetParent(root.transform, false); holder = go.transform; L("SceneAssets 생성"); } var volType = System.Type.GetType("UnityEngine.Rendering.Volume, Unity.RenderPipelines.Core.Runtime"); if (volType == null) L("[주의] Volume 타입을 찾지 못했다 — 포스트 볼륨 생략"); else { var vt = holder.Find("Global Volume"); UnityEngine.GameObject vgo; if (vt == null) { vgo = new UnityEngine.GameObject("Global Volume"); vgo.transform.SetParent(holder, false); L("Global Volume 생성"); } else vgo = vt.gameObject; var vol = vgo.GetComponent(volType); if (vol == null) vol = vgo.AddComponent(volType); var vso = new UnityEditor.SerializedObject(vol); var sp = vso.FindProperty("sharedProfile"); if (sp != null && prof != null) sp.objectReferenceValue = prof; var ig = vso.FindProperty("isGlobal"); if (ig != null) ig.boolValue = true; var pr = vso.FindProperty("priority"); if (pr != null) pr.floatValue = 0f; vso.ApplyModifiedPropertiesWithoutUndo(); L("Global Volume: profile=" + (prof == null ? "(null)" : prof.name) + " isGlobal=true"); } UnityEditor.PrefabUtility.SaveAsPrefabAsset(root, prefabPath); L("프리팹 저장: " + prefabPath); } finally { UnityEditor.PrefabUtility.UnloadPrefabContents(root); } UnityEditor.AssetDatabase.SaveAssets(); Flush("F2_setup.txt"); return "setup done"; } // ═══════════════════════════════════════════════════════════ // 3) GPU 인스턴싱 — 맵이 쓰는 모든 머티리얼 // ═══════════════════════════════════════════════════════════ public static object Instancing(string prefabPath) { sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var go = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); if (go == null) return "no prefab"; var mats = new HashSet(); foreach (var r in go.GetComponentsInChildren(true)) foreach (var m in r.sharedMaterials) if (m != null) mats.Add(m); int on = 0, already = 0; foreach (var m in mats.OrderBy(x => x.name)) { var p = UnityEditor.AssetDatabase.GetAssetPath(m); if (string.IsNullOrEmpty(p) || !p.StartsWith("Assets/")) { L(" [건너뜀 · 에셋 아님] " + m.name); continue; } if (m.enableInstancing) { already++; L(" [이미 켜짐] " + m.shader.name + " " + p); continue; } m.enableInstancing = true; UnityEditor.EditorUtility.SetDirty(m); on++; L(" [켬] " + m.shader.name + " " + p); } UnityEditor.AssetDatabase.SaveAssets(); L("머티리얼 " + mats.Count + "개 중 인스턴싱 신규 " + on + "개 · 기존 " + already + "개"); Flush("F3_instancing.txt"); return "materials=" + mats.Count + " enabled=" + on + " already=" + already; } // ═══════════════════════════════════════════════════════════ // 4) 검증 — 셰이더 전수 · 스포너 · 통계 // ═══════════════════════════════════════════════════════════ public static object Verify(string prefabPath) { sb = new System.Text.StringBuilder(); var go = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); if (go == null) return "no prefab"; // 셰이더 전수 (마젠타 판정) var byShader = new Dictionary(); var bad = new List(); int nullSlots = 0, rend = 0; long tris = 0, trisLod0 = 0; foreach (var r in go.GetComponentsInChildren(true)) { rend++; var ms = r.sharedMaterials; for (int i = 0; i < ms.Length; i++) { if (ms[i] == null) { nullSlots++; continue; } var sn = ms[i].shader == null ? "" : ms[i].shader.name; if (!byShader.ContainsKey(sn)) byShader[sn] = 0; byShader[sn]++; if (sn == "Hidden/InternalErrorShader" || sn == "Standard" || sn.StartsWith("Standard (") || sn.StartsWith("Legacy Shaders/") || sn.StartsWith("Mobile/")) bad.Add(sn + " " + Path(r.transform) + " <- " + UnityEditor.AssetDatabase.GetAssetPath(ms[i])); } var mf = r.GetComponent(); if (mf != null && mf.sharedMesh != null) { long t = mf.sharedMesh.triangles.Length / 3; tris += t; if (!r.name.Contains("LOD1") && !r.name.Contains("LOD2")) trisLod0 += t; } } L("== 렌더러 " + rend + "개 · 셰이더 분포 =="); foreach (var kv in byShader.OrderByDescending(k => k.Value)) L(string.Format(" [{0,5}] {1}", kv.Value, kv.Key)); L("null 머티리얼 슬롯 = " + nullSlots); L("== 마젠타 위험 슬롯 = " + bad.Count + " =="); for (int i = 0; i < System.Math.Min(bad.Count, 40); i++) L(" " + bad[i]); L("삼각형 합 = " + tris + " (LOD1/LOD2 제외 = " + trisLod0 + ")"); L("콜라이더 = " + go.GetComponentsInChildren(true).Length + " · LODGroup = " + go.GetComponentsInChildren(true).Length + " · 프리팹 인스턴스 루트 = " + CountPrefabRoots(go)); // 구성 var md = go.GetComponent(); L(""); L("== 구성 =="); L("MapData = " + (md == null ? "없음 🔴" : "있음")); if (md != null) { L(" skybox=" + (md.material_skybox == null ? "(없음)" : md.material_skybox.name) + " ambientMode=" + md.ambientMode + " ambientSky=" + md.ambientSky + " reflection=" + md.reflectionIntensity + " showWeaponInLobby=" + md.showWeaponInLobby); L(" #764: reachable=" + md.spawnRequireReachableFromPlayer + " snap=" + md.spawnSnapRadius + " tries=" + md.spawnSampleTries + " mobSnap=" + md.mobNavSnapRadius); var so = new UnityEditor.SerializedObject(md); var fe = so.FindProperty("fogEnabled"); if (fe != null) L(" fog: enabled=" + fe.boolValue + " mode=" + Get(so, "fogMode") + " color=" + GetC(so, "fogColor") + " start=" + Get(so, "fogStart") + " end=" + Get(so, "fogEnd")); else L(" fog: MapData 에 안개 필드가 없다 (미적용)"); if (md.m_MapZoneData != null && md.m_MapZoneData.Count > 0) { var z = md.m_MapZoneData[0]; L(" zone0 portal=" + (z.go_portal == null ? "(null)🔴" : z.go_portal.transform.position.ToString("F3")) + " CamRot_Portal=" + z.CamRot_Portal + " reinc=" + (z.go_reincarnation == null ? "(null)🔴" : "OK")); } } L("MobControlMgrList = " + (go.GetComponent() == null ? "없음 🔴" : "있음")); L("StartWakeSequence = " + (go.GetComponent() == null ? "없음 🔴" : "있음")); L("카메라 = " + go.GetComponentsInChildren(true).Length + "대 (0 이어야 함)"); var lights = go.GetComponentsInChildren(true); L("라이트 = " + lights.Length + "개"); foreach (var lt in lights) L(" " + Path(lt.transform) + " type=" + lt.type + " color=" + lt.color + " intensity=" + lt.intensity + " shadows=" + lt.shadows + " euler=" + lt.transform.rotation.eulerAngles.ToString("F2")); // 스포너 var mgrs = go.GetComponentsInChildren(true); var ids = new List(); int nullLv = 0; foreach (var m in mgrs) { ids.Add(m.SpawnerID); if (m.text_lv == null) nullLv++; } ids.Sort(); L("스포너 = " + mgrs.Length + "개 IDs=" + string.Join(",", ids.Select(x => x.ToString()).ToArray()) + " text_lv null=" + nullLv); // 드레싱 var dress = go.transform.Find("Dressing"); if (dress != null) { L(""); L("== Dressing =="); foreach (UnityEngine.Transform g in dress) L(" " + g.name + " = " + g.childCount + "개"); L(" 총 " + CountDeep(dress) + " 오브젝트"); } else L("Dressing 컨테이너 없음 🔴"); Flush("F4_verify.txt"); return "renderers=" + rend + " magentaRisk=" + bad.Count + " tris=" + tris + " spawners=" + mgrs.Length; } static string Get(UnityEditor.SerializedObject so, string n) { var p = so.FindProperty(n); if (p == null) return "?"; if (p.propertyType == UnityEditor.SerializedPropertyType.Float) return p.floatValue.ToString("F2"); if (p.propertyType == UnityEditor.SerializedPropertyType.Integer) return p.intValue.ToString(); if (p.propertyType == UnityEditor.SerializedPropertyType.Boolean) return p.boolValue.ToString(); return p.propertyType.ToString(); } static string GetC(UnityEditor.SerializedObject so, string n) { var p = so.FindProperty(n); return p == null ? "?" : p.colorValue.ToString(); } // ═══════════════════════════════════════════════════════════ // 5) NavMesh 도달 가능 비율 + 스포너 도달성 // ═══════════════════════════════════════════════════════════ public static object Reach(string prefabPath, string navAsset, float step) { sb = new System.Text.StringBuilder(); var go = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); var nav = UnityEditor.AssetDatabase.LoadAssetAtPath(navAsset); if (go == null || nav == null) return "prefab or navmesh missing"; var portal = go.transform.Find("Portal_Start"); if (portal == null) return "no Portal_Start"; var spawn = portal.transform.position + UnityEngine.Vector3.forward; var inst = UnityEngine.AI.NavMesh.AddNavMeshData(nav); string res; try { var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); L("NavMesh 삼각형 " + (tri.indices.Length / 3) + "개 · 정점 " + tri.vertices.Length); if (tri.vertices.Length == 0) { Flush("F5_reach.txt"); return "empty navmesh"; } var min = tri.vertices[0]; var max = tri.vertices[0]; for (int i = 1; i < tri.vertices.Length; i++) { min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); } L("NavMesh 범위 min=" + min.ToString("F1") + " max=" + max.ToString("F1") + " (XZ " + (max.x - min.x).ToString("F1") + " x " + (max.z - min.z).ToString("F1") + " m)"); UnityEngine.AI.NavMeshHit ph; if (!UnityEngine.AI.NavMesh.SamplePosition(spawn, out ph, 5f, UnityEngine.AI.NavMesh.AllAreas)) { L("🔴 시작 지점이 NavMesh 위가 아니다: " + spawn.ToString("F3")); Flush("F5_reach.txt"); return "spawn off navmesh"; } var pc = ph.position; L("시작 지점 " + spawn.ToString("F3") + " → NavMesh " + pc.ToString("F3") + " (거리 " + UnityEngine.Vector3.Distance(spawn, pc).ToString("F2") + "m)"); int onNav = 0, reach = 0; for (float x = min.x; x <= max.x; x += step) for (float z = min.z; z <= max.z; z += step) { UnityEngine.AI.NavMeshHit h; var probe = new UnityEngine.Vector3(x, (min.y + max.y) * 0.5f, z); if (!UnityEngine.AI.NavMesh.SamplePosition(probe, out h, (max.y - min.y) * 0.5f + step, UnityEngine.AI.NavMesh.AllAreas)) continue; if (UnityEngine.Mathf.Abs(h.position.x - x) > step || UnityEngine.Mathf.Abs(h.position.z - z) > step) continue; onNav++; if (Reachable(pc, h.position)) reach++; } int pct = onNav == 0 ? 0 : reach * 100 / onNav; L("격자 " + step + "m: NavMesh 위 " + onNav + "점 중 시작 지점에서 도달 가능 " + reach + "점 = " + pct + "%"); // 스포너 var mgrs = go.GetComponentsInChildren(true); int okS = 0; var lines = new List(); foreach (var m in mgrs.OrderBy(x => x.SpawnerID)) { UnityEngine.AI.NavMeshHit sh; bool on = UnityEngine.AI.NavMesh.SamplePosition(m.transform.position, out sh, 3f, UnityEngine.AI.NavMesh.AllAreas); bool rc = on && Reachable(pc, sh.position); if (rc) okS++; lines.Add(string.Format(" {0,2} pos={1} onNav={2} reachable={3} PC거리={4:F1}m", m.SpawnerID, m.transform.position.ToString("F1"), on, rc, UnityEngine.Vector3.Distance(m.transform.position, pc))); } L("== 스포너 도달성 " + okS + "/" + mgrs.Length + " =="); foreach (var s in lines) L(s); res = "reachPct=" + pct + " onNav=" + onNav + " spawnersReachable=" + okS + "/" + mgrs.Length; } finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } Flush("F5_reach.txt"); return res; } static bool Reachable(UnityEngine.Vector3 a, UnityEngine.Vector3 b) { var path = new UnityEngine.AI.NavMeshPath(); if (!UnityEngine.AI.NavMesh.CalculatePath(a, b, UnityEngine.AI.NavMesh.AllAreas, path)) return false; return path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete; } // ═══════════════════════════════════════════════════════════ // 6) 캡처 — 게임 시점 4장 + 위에서 본 배치도 1장 (에디터 렌더) // ═══════════════════════════════════════════════════════════ public static object Shots(string prefabPath, float half) { sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; System.IO.Directory.CreateDirectory(SHOT); var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); var made = new List(); try { UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); var src = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); var root = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src); root.transform.position = UnityEngine.Vector3.zero; // 프리팹 MapData 설정을 씬 RenderSettings 에 반영 (런타임과 같은 룩) var md = root.GetComponent(); if (md != null) { UnityEngine.RenderSettings.skybox = md.material_skybox; UnityEngine.RenderSettings.ambientMode = md.ambientMode; if (md.ambientMode == UnityEngine.Rendering.AmbientMode.Flat) UnityEngine.RenderSettings.ambientLight = md.ambientSky; else { UnityEngine.RenderSettings.ambientSkyColor = md.ambientSky; UnityEngine.RenderSettings.ambientEquatorColor = md.ambientEquator; UnityEngine.RenderSettings.ambientGroundColor = md.ambientGround; } UnityEngine.RenderSettings.reflectionIntensity = md.reflectionIntensity; var so = new UnityEditor.SerializedObject(md); var fe = so.FindProperty("fogEnabled"); if (fe != null && fe.boolValue) { UnityEngine.RenderSettings.fog = true; var fm = so.FindProperty("fogMode"); if (fm != null) UnityEngine.RenderSettings.fogMode = (UnityEngine.FogMode)fm.intValue; var fc = so.FindProperty("fogColor"); if (fc != null) UnityEngine.RenderSettings.fogColor = fc.colorValue; var fs = so.FindProperty("fogStart"); if (fs != null) UnityEngine.RenderSettings.fogStartDistance = fs.floatValue; var fed = so.FindProperty("fogEnd"); if (fed != null) UnityEngine.RenderSettings.fogEndDistance = fed.floatValue; } else UnityEngine.RenderSettings.fog = false; L("RenderSettings 적용: ambient=" + md.ambientMode + " fog=" + UnityEngine.RenderSettings.fog); } var portal = root.transform.Find("Portal_Start"); var start = portal.position + UnityEngine.Vector3.forward; float camYaw = (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0) ? md.m_MapZoneData[0].CamRot_Portal : 0f; // (1) 시작 지점 — 실제 게임 카메라 각도 made.Add(GameShot("01_start", start, camYaw)); // (2)(3)(4) 숲 / 강가 / 바위 — Dressing 그룹 중심을 실측해 대표점 선정 var dress = root.transform.Find("Dressing"); if (dress != null) { var t = dress.Find("Trees"); if (t != null && t.childCount > 0) made.Add(GameShot("02_forest", GroundAt(Densest(t, 15f)), camYaw + 40f)); var r = dress.Find("Rocks"); if (r != null && r.childCount > 0) made.Add(GameShot("04_rocks", GroundAt(Densest(r, 10f)), camYaw - 60f)); } var water = NearestWater(root, start); if (water.HasValue) made.Add(GameShot("03_river", GroundAt(water.Value), YawTo(water.Value, start))); // (5) 위에서 본 배치도 made.Add(TopShot("05_layout", start, half)); L("캡처 " + made.Count + "장"); foreach (var m in made) L(" " + m); UnityEngine.Object.DestroyImmediate(root); ClearDirty(); } catch (System.Exception e) { L("EXCEPTION " + e); } finally { try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } Flush("F6_shots.txt"); return string.Join(" | ", made.ToArray()); } // 반경 r 안에 같은 그룹 오브젝트가 가장 많은 지점 (군락 한복판) — 촬영 대상점 static UnityEngine.Vector3 Densest(UnityEngine.Transform group, float r) { var pts = new List(); foreach (UnityEngine.Transform c in group) pts.Add(c.position); int best = 0, bestN = -1; float r2 = r * r; for (int i = 0; i < pts.Count; i++) { int n = 0; for (int j = 0; j < pts.Count; j++) { float dx = pts[i].x - pts[j].x, dz = pts[i].z - pts[j].z; if (dx * dx + dz * dz <= r2) n++; } if (n > bestN) { bestN = n; best = i; } } L(" 군락 최밀점 = " + pts[best].ToString("F1") + " (반경 " + r + "m 안 " + bestN + "개)"); // 군락 한복판이 아니라 가장자리에서 안쪽을 보도록 살짝 물러난다 return pts[best]; } static UnityEngine.Vector3 Center(UnityEngine.Transform t) { var s = UnityEngine.Vector3.zero; int n = 0; foreach (UnityEngine.Transform c in t) { s += c.position; n++; } return n == 0 ? t.position : s / n; } static UnityEngine.Vector3 GroundAt(UnityEngine.Vector3 p) { UnityEngine.RaycastHit h; if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 200f, p.z), UnityEngine.Vector3.down, out h, 600f)) return h.point; return p; } static float YawTo(UnityEngine.Vector3 from, UnityEngine.Vector3 to) { var d = to - from; d.y = 0f; return d.sqrMagnitude < 0.01f ? 0f : UnityEngine.Quaternion.LookRotation(d).eulerAngles.y + 180f; } static UnityEngine.Vector3? NearestWater(UnityEngine.GameObject root, UnityEngine.Vector3 from) { UnityEngine.Vector3? best = null; float bd = float.MaxValue; foreach (var r in root.GetComponentsInChildren(true)) { bool w = r.name.ToLowerInvariant().Contains("water"); if (!w) foreach (var m in r.sharedMaterials) { if (m != null && m.name.ToLowerInvariant().Contains("water")) { w = true; break; } } if (!w) continue; var c = r.bounds.ClosestPoint(from); float d = UnityEngine.Vector3.Distance(c, from); if (d < bd) { bd = d; best = c + (from - c).normalized * 4f; } } if (best.HasValue) L("최근접 수역까지 " + bd.ToString("F1") + "m → 촬영점 " + best.Value.ToString("F1")); return best; } static string GameShot(string tag, UnityEngine.Vector3 target, float yaw) { var tmp = new UnityEngine.GameObject("~shot"); tmp.hideFlags = UnityEngine.HideFlags.HideAndDontSave; var c = tmp.AddComponent(); c.fieldOfView = CAM_FOV; c.nearClipPlane = 0.3f; c.farClipPlane = 1000f; c.clearFlags = UnityEngine.CameraClearFlags.Skybox; var rotV = UnityEngine.Quaternion.Euler(CAM_PITCH, yaw, 0f) * UnityEngine.Vector3.one; c.transform.position = target + UnityEngine.Vector3.up * CAM_UP - rotV * CAM_DIST; c.transform.LookAt(target + UnityEngine.Vector3.up); var path = System.IO.Path.Combine(SHOT, tag + ".png"); Render(c, path, W, H); L(tag + ": target=" + target.ToString("F1") + " campos=" + c.transform.position.ToString("F1") + " yaw=" + yaw.ToString("F0")); UnityEngine.Object.DestroyImmediate(tmp); return path; } static string TopShot(string tag, UnityEngine.Vector3 center, float half) { // 배치도는 안개를 끄고 찍는다 — Linear 안개(end 250 m)가 켜져 있으면 상공 카메라에서 화면이 통째로 안개색이 된다. bool fogWas = UnityEngine.RenderSettings.fog; UnityEngine.RenderSettings.fog = false; var tmp = new UnityEngine.GameObject("~top"); tmp.hideFlags = UnityEngine.HideFlags.HideAndDontSave; var c = tmp.AddComponent(); c.orthographic = true; c.orthographicSize = half * 1.15f; c.nearClipPlane = 1f; c.farClipPlane = 2000f; c.clearFlags = UnityEngine.CameraClearFlags.SolidColor; c.backgroundColor = new UnityEngine.Color(0.08f, 0.10f, 0.13f, 1f); c.transform.position = new UnityEngine.Vector3(center.x, center.y + 300f, center.z); c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f); var path = System.IO.Path.Combine(SHOT, tag + ".png"); Render(c, path, 1200, 1200); L(tag + ": center=" + center.ToString("F1") + " orthoSize=" + c.orthographicSize.ToString("F0") + " (안개 일시 해제)"); UnityEngine.Object.DestroyImmediate(tmp); UnityEngine.RenderSettings.fog = fogWas; return path; } static void Render(UnityEngine.Camera cam, string path, int w, int h) { var rt = new UnityEngine.RenderTexture(w, h, 24, UnityEngine.RenderTextureFormat.ARGB32); var prev = cam.targetTexture; cam.targetTexture = rt; cam.Render(); var active = UnityEngine.RenderTexture.active; UnityEngine.RenderTexture.active = rt; var tex = new UnityEngine.Texture2D(w, h, UnityEngine.TextureFormat.RGB24, false); tex.ReadPixels(new UnityEngine.Rect(0, 0, w, h), 0, 0); tex.Apply(); UnityEngine.RenderTexture.active = active; cam.targetTexture = prev; System.IO.File.WriteAllBytes(path, UnityEngine.ImageConversion.EncodeToPNG(tex)); UnityEngine.Object.DestroyImmediate(tex); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); } // ── 보조 ───────────────────────────────────────────────────── static int CountPrefabRoots(UnityEngine.GameObject go) { int n = 0; foreach (var t in go.GetComponentsInChildren(true)) if (UnityEditor.PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject)) n++; return n; } static int CountDeep(UnityEngine.Transform t) { int n = 0; for (int i = 0; i < t.childCount; i++) n += 1 + CountDeep(t.GetChild(i)); return n; } static string Path(UnityEngine.Transform t) { var s = t.name; var p = t.parent; while (p != null) { s = p.name + "/" + s; p = p.parent; } return s; } static void ClearDirty() { var t = typeof(UnityEditor.SceneManagement.EditorSceneManager); var mi = t.GetMethod("ClearSceneDirtiness", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (mi == null) return; for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) mi.Invoke(null, new object[] { UnityEngine.SceneManagement.SceneManager.GetSceneAt(i) }); } static void Flush(string name) { try { System.IO.Directory.CreateDirectory(OUT); System.IO.File.WriteAllText(System.IO.Path.Combine(OUT, name), sb.ToString(), new System.Text.UTF8Encoding(true)); } catch { } } }