Project_WL/AgentScripts/WL_NatureFinish.cs

664 lines
38 KiB
C#
Raw Permalink Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
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<UnityEngine.Material>(p);
if (m == null) { L(" [없음] " + p); continue; }
string before = m.shader == null ? "<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 ? "<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<MapData>();
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<WL.Start.StartWakeSettings>(kWakeSettings);
if (wake == null) L("[주의] StartWakeSettings 에셋 없음: " + kWakeSettings);
var root = UnityEditor.PrefabUtility.LoadPrefabContents(prefabPath);
try
{
// MobControlMgrList (루트)
if (root.GetComponent<MobControlMgrList>() == null) { root.AddComponent<MobControlMgrList>(); L("MobControlMgrList 추가"); }
else L("MobControlMgrList 이미 있음");
// #764 필드
var md = root.GetComponent<MapData>();
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<WL.Start.StartWakeSequence>();
if (seq == null) { seq = root.AddComponent<WL.Start.StartWakeSequence>(); 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<UnityEngine.GameObject>(prefabPath);
if (go == null) return "no prefab";
var mats = new HashSet<UnityEngine.Material>();
foreach (var r in go.GetComponentsInChildren<UnityEngine.Renderer>(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<UnityEngine.GameObject>(prefabPath);
if (go == null) return "no prefab";
// 셰이더 전수 (마젠타 판정)
var byShader = new Dictionary<string, int>();
var bad = new List<string>();
int nullSlots = 0, rend = 0;
long tris = 0, trisLod0 = 0;
foreach (var r in go.GetComponentsInChildren<UnityEngine.Renderer>(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 ? "<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<UnityEngine.MeshFilter>();
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<UnityEngine.Collider>(true).Length
+ " · LODGroup = " + go.GetComponentsInChildren<UnityEngine.LODGroup>(true).Length
+ " · 프리팹 인스턴스 루트 = " + CountPrefabRoots(go));
// 구성
var md = go.GetComponent<MapData>();
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<MobControlMgrList>() == null ? "없음 🔴" : "있음"));
L("StartWakeSequence = " + (go.GetComponent<WL.Start.StartWakeSequence>() == null ? "없음 🔴" : "있음"));
L("카메라 = " + go.GetComponentsInChildren<UnityEngine.Camera>(true).Length + "대 (0 이어야 함)");
var lights = go.GetComponentsInChildren<UnityEngine.Light>(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<MobControlMgr>(true);
var ids = new List<int>(); 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<UnityEngine.GameObject>(prefabPath);
var nav = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(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<MobControlMgr>(true);
int okS = 0;
var lines = new List<string>();
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<string>();
try
{
UnityEditor.SceneManagement.EditorSceneManager.NewScene(
UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single);
var src = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(prefabPath);
var root = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src);
root.transform.position = UnityEngine.Vector3.zero;
// 프리팹 MapData 설정을 씬 RenderSettings 에 반영 (런타임과 같은 룩)
var md = root.GetComponent<MapData>();
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<UnityEngine.Vector3>();
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<UnityEngine.Renderer>(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<UnityEngine.Camera>();
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<UnityEngine.Camera>();
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<UnityEngine.Transform>(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 { }
}
}