using System.Collections.Generic; using System.Linq; // PD 지시 #771 2단계 — WL_Nature 맵 프리팹 밀도 보강(드레싱) // // 실행 (전부 --timeout 600 권장) // 목록 확인 : unity command run_script --file AgentScripts/WL_NatureDressing.cs --entry WL_NatureDressing.Pools // 배치 : ... --entry WL_NatureDressing.Run --args '[12345, 209.0, -148.0, 100.0, 12.0]' // 제거 : ... --entry WL_NatureDressing.Remove // // 원칙 // · 멱등 — 실행 전 기존 'Dressing' 컨테이너를 통째로 지우고 다시 만든다. // · 시드 고정 — 같은 인자면 같은 배치가 나온다 (System.Random). // · 좌표·개수는 전부 인자 / 아래 튜닝 필드. 프리팹 목록은 폴더 스캔으로 얻는다 (이름 하드코딩 금지 · C45). // · 배치 규칙: 경사 <= kSlopeLimit · 수면 아래 금지 · 카테고리별 최소 간격 · 시작 개활지/길목 비움. // // 프리팹은 씬에 인스턴스화한 뒤 언팩(최상위만)해서 다루므로 Physics.Raycast 가 정상 동작한다. // 중첩 프리팹 인스턴스(나무·바위 등)는 그대로 유지된다. public static class WL_NatureDressing { // ── 경로 ─────────────────────────────────────────────────── const string kTargetPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; const string kPackRoot = "Assets/LMHPOLY/Low Poly Nature Bundle"; const string kContainer = "Dressing"; const string kLogDir = @"E:\NerdNavis\nn_himminji\AgentScripts\staging\WL_Nature2\out"; // ── 배치 규칙 (발주 #771) ────────────────────────────────── const float kSlopeLimit = 30f; // 경사 상한(도) const float kWaterMargin = 0.35f; // 수면 위 최소 여유(m) const float kGapTree = 3.0f; const float kGapRock = 2.0f; const float kGapProp = 1.5f; // 통나무·그루터기·덤불 const float kGapGrass = 0.8f; const float kDetailClearFactor = 0.7f; // 개활지 안쪽 이 비율부터는 풀/꽃 허용 const int kLaneCount = 4; // 길목 수 const float kLaneHalfWidth = 2.5f; const float kLaneLength = 45f; // ── 목표 개수 (합 = 790 · 발주 상한 900) ──────────────────── static int nTree = 240, nRock = 110, nProp = 40, nBush = 80, nGrass = 180, nFlower = 100, nMush = 40; static int kTreeClusters = 6, kRockClusters = 6; static float kTreeSigma = 18f, kRockSigma = 8f; static float kScatterRatio = 0.20f; // 군락 밖 산개 비율 // ── 상태 ─────────────────────────────────────────────────── static System.Text.StringBuilder s_log; static System.Random s_rnd; static List s_water; static UnityEngine.Vector3 s_start; static float s_topY, s_rayLen; static float s_cx, s_cz, s_half, s_clear; static float s_camYaw; static UnityEngine.Transform s_container; static readonly List s_blockers = new List(); // 나무·바위·통나무·덤불 static readonly List s_details = new List(); // 풀·꽃·버섯 static readonly UnityEngine.RaycastHit[] s_hits = new UnityEngine.RaycastHit[24]; struct Cell { public UnityEngine.Vector3 p; public float gap; } static void L(string s) { s_log.AppendLine(s); } static void W(string s) { s_log.AppendLine("[경고] " + s); } static void E(string s) { s_log.AppendLine("[오류] " + s); } // ═══════════════════════════════════════════════════════════ // 프리팹 풀 확인 (배치 없이 목록·머티리얼 상태만 보고) // ═══════════════════════════════════════════════════════════ public static object Pools() { s_log = new System.Text.StringBuilder(); var pools = BuildPools(); int total = 0; foreach (var kv in pools) { L("== " + kv.Key + " : " + kv.Value.Count + "개 =="); total += kv.Value.Count; for (int i = 0; i < System.Math.Min(3, kv.Value.Count); i++) L(" " + kv.Value[i]); } L(""); L("== 풀 전체가 참조하는 머티리얼 셰이더 =="); var mats = new Dictionary(); foreach (var kv in pools) foreach (var p in kv.Value) { var go = UnityEditor.AssetDatabase.LoadAssetAtPath(p); if (go == null) continue; foreach (var r in go.GetComponentsInChildren(true)) foreach (var m in r.sharedMaterials) { if (m == null) continue; var mp = UnityEditor.AssetDatabase.GetAssetPath(m); if (!mats.ContainsKey(mp)) mats[mp] = m.shader == null ? "" : m.shader.name; } } foreach (var kv in mats.OrderBy(k => k.Value)) L(" " + kv.Value + " <- " + kv.Key); Flush("D0_pools.txt"); return "pools=" + pools.Count + " prefabs=" + total + " materials=" + mats.Count; } // ═══════════════════════════════════════════════════════════ public static object Remove() { s_log = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var root = UnityEditor.PrefabUtility.LoadPrefabContents(kTargetPrefab); try { var old = root.transform.Find(kContainer); if (old == null) { Flush("D1_remove.txt"); return "'" + kContainer + "' 없음 - 변경 없음"; } int n = CountDeep(old); UnityEngine.Object.DestroyImmediate(old.gameObject); UnityEditor.PrefabUtility.SaveAsPrefabAsset(root, kTargetPrefab); L("Dressing 제거: 하위 " + n + "개"); Flush("D1_remove.txt"); return "removed " + n + " objects"; } finally { UnityEditor.PrefabUtility.UnloadPrefabContents(root); } } // ═══════════════════════════════════════════════════════════ // seed / 플레이 영역 중심(cx,cz) / 반폭 half / 시작 개활지 반경 clear // ═══════════════════════════════════════════════════════════ public static object Run(int seed, float cx, float cz, float half, float clear) { s_log = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; s_rnd = new System.Random(seed); s_cx = cx; s_cz = cz; s_half = half; s_clear = clear; s_blockers.Clear(); s_details.Clear(); s_container = null; L("===== WL_NatureDressing seed=" + seed + " center=(" + cx + "," + cz + ") half=" + half + " clear=" + clear + " ====="); var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); UnityEngine.GameObject root = null; string result; try { // 1) 빈 씬에 프리팹을 올려 언팩 (Physics.Raycast 를 쓰기 위해) UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); var src = UnityEditor.AssetDatabase.LoadAssetAtPath(kTargetPrefab); if (src == null) { E("프리팹 없음: " + kTargetPrefab); Flush("D2_dress.txt"); return "no prefab"; } root = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src); UnityEditor.PrefabUtility.UnpackPrefabInstance(root, UnityEditor.PrefabUnpackMode.OutermostRoot, UnityEditor.InteractionMode.AutomatedAction); root.transform.position = UnityEngine.Vector3.zero; UnityEngine.Physics.SyncTransforms(); // 2) 기존 Dressing 제거 (멱등) var old = root.transform.Find(kContainer); if (old != null) { L("기존 '" + kContainer + "' 제거 (하위 " + CountDeep(old) + "개)"); UnityEngine.Object.DestroyImmediate(old.gameObject); } // 3) 실측 — 시작 지점 · 수역 · 레이 시작 높이 var portal = root.transform.Find("Portal_Start"); if (portal == null) { E("Portal_Start 가 없다"); Flush("D2_dress.txt"); return "no Portal_Start"; } s_start = portal.position + UnityEngine.Vector3.forward; // LoadMapMgr.Get_PortalPos 와 동일 var md = root.GetComponent(); s_camYaw = (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0) ? md.m_MapZoneData[0].CamRot_Portal : 0f; L("시작 지점(실측) = " + s_start.ToString("F3") + " CamRot_Portal=" + s_camYaw.ToString("F2")); var b = WorldBounds(root); s_topY = b.max.y + 50f; s_rayLen = b.size.y + 300f; s_water = FindWater(root); L("수역 " + s_water.Count + "개 · 레이 시작 y=" + s_topY.ToString("F1") + " 길이=" + s_rayLen.ToString("F1")); // 4) 컨테이너 var container = new UnityEngine.GameObject(kContainer); container.transform.SetParent(root.transform, false); s_container = container.transform; var gTree = Group(container, "Trees"); var gRock = Group(container, "Rocks"); var gProp = Group(container, "Props"); var gVeg = Group(container, "Vegetation"); var pools = BuildPools(); foreach (var kv in pools) L("풀 " + kv.Key + " = " + kv.Value.Count + "개"); // 5) 배치 int placedTree = Clustered(gTree, pools["tree"], nTree, kTreeClusters, kTreeSigma, kGapTree, true, 0.85f, 1.25f); int placedRock = Clustered(gRock, pools["rock"], nRock, kRockClusters, kRockSigma, kGapRock, true, 0.7f, 1.6f); int placedProp = Scatter(gProp, pools["prop"], nProp, kGapProp, true, 0.8f, 1.2f); int placedBush = Scatter(gVeg, pools["bush"], nBush, kGapProp, true, 0.8f, 1.3f); int placedGrass = Scatter(gVeg, pools["grass"], nGrass, kGapGrass, false, 0.7f, 1.4f); int placedFlower = Scatter(gVeg, pools["flower"], nFlower, kGapGrass, false, 0.8f, 1.3f); int placedMush = Scatter(gVeg, pools["mushroom"], nMush, kGapGrass, false, 0.8f, 1.4f); L(""); L("== 배치 결과 =="); L(string.Format(" 나무 {0,4} / {1}", placedTree, nTree)); L(string.Format(" 바위 {0,4} / {1}", placedRock, nRock)); L(string.Format(" 통나무 {0,4} / {1}", placedProp, nProp)); L(string.Format(" 덤불 {0,4} / {1}", placedBush, nBush)); L(string.Format(" 풀 {0,4} / {1}", placedGrass, nGrass)); L(string.Format(" 꽃 {0,4} / {1}", placedFlower, nFlower)); L(string.Format(" 버섯 {0,4} / {1}", placedMush, nMush)); int total = placedTree + placedRock + placedProp + placedBush + placedGrass + placedFlower + placedMush; L(" 합계 " + total + "개"); // 6) static 마킹 + 통계 MarkStatic(container); var stat = Stats(container); L(stat); // 7) 저장 UnityEditor.PrefabUtility.SaveAsPrefabAsset(root, kTargetPrefab); UnityEditor.AssetDatabase.SaveAssets(); L("프리팹 저장: " + kTargetPrefab); result = "placed=" + total + " | " + stat.Replace("\n", " "); } catch (System.Exception ex) { E("예외: " + ex); result = "EXCEPTION " + ex.Message; } finally { Flush("D2_dress.txt"); ClearDirty(); try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } return result; } // ── 군락 배치 ──────────────────────────────────────────────── static int Clustered(UnityEngine.Transform parent, List pool, int target, int clusters, float sigma, float gap, bool blocking, float sMin, float sMax) { if (pool.Count == 0) { W("풀이 비었다 - 건너뜀"); return 0; } // 군락 중심: 시작점에서 25m 이상, 영역 안, 서로 (2*sigma) 이상 var centers = new List(); for (int guard = 0; guard < 4000 && centers.Count < clusters; guard++) { float x = s_cx + Rf(-s_half + 15f, s_half - 15f); float z = s_cz + Rf(-s_half + 15f, s_half - 15f); UnityEngine.Vector3 p; UnityEngine.Vector3 n; if (!Ground(x, z, out p, out n)) continue; if (Flat2D(p, s_start) < s_clear + 15f) continue; bool ok = true; for (int i = 0; i < centers.Count; i++) if (Flat2D(p, centers[i]) < UnityEngine.Mathf.Max(sigma * 2f, s_half * 0.55f)) { ok = false; break; } // 군락을 영역 전체로 벌린다 (#771) if (ok) centers.Add(p); } L("군락 중심 " + centers.Count + "/" + clusters + "개"); for (int i = 0; i < centers.Count; i++) L(" center " + i + " = " + centers[i].ToString("F1")); // 군락마다 수종(폴더)을 1~2개만 쓴다 — 자연스럽고 고유 메시 수도 줄어 배칭에 유리하다. var groups = ByFolder(pool); L("수종 그룹 " + groups.Count + "개"); int scatterN = UnityEngine.Mathf.RoundToInt(target * kScatterRatio); int clusterN = target - scatterN; int placed = 0; int perCluster = centers.Count == 0 ? 0 : clusterN / centers.Count; for (int c = 0; c < centers.Count; c++) { var g1 = groups[s_rnd.Next(groups.Count)]; var g2 = groups[s_rnd.Next(groups.Count)]; var mix = new List(g1); if (!object.ReferenceEquals(g1, g2)) mix.AddRange(g2); L(" cluster " + c + " 수종 = " + Folder(g1[0]) + (object.ReferenceEquals(g1, g2) ? "" : " + " + Folder(g2[0])) + " (" + mix.Count + "종)"); int want = (c == centers.Count - 1) ? clusterN - perCluster * (centers.Count - 1) : perCluster; int attempts = want * 12; int got = 0; for (int a = 0; a < attempts && got < want; a++) { float ang = Rf(0f, 6.2831853f); float r = UnityEngine.Mathf.Abs(Gauss()) * sigma; float x = centers[c].x + UnityEngine.Mathf.Cos(ang) * r; float z = centers[c].z + UnityEngine.Mathf.Sin(ang) * r; if (TryPlace(parent, mix, x, z, gap, blocking, sMin, sMax)) { got++; placed++; } } } placed += Scatter(parent, pool, scatterN, gap, blocking, sMin, sMax); return placed; } // 폴더별로 프리팹을 묶는다 (수종/암종 단위) static List> ByFolder(List pool) { var d = new Dictionary>(); for (int i = 0; i < pool.Count; i++) { var k = Folder(pool[i]); if (!d.ContainsKey(k)) d[k] = new List(); d[k].Add(pool[i]); } return d.OrderBy(k => k.Key, System.StringComparer.Ordinal).Select(k => k.Value).ToList(); } static string Folder(string assetPath) { int i = assetPath.LastIndexOf('/'); return i < 0 ? assetPath : assetPath.Substring(0, i); } // ── 산개 배치 ──────────────────────────────────────────────── static int Scatter(UnityEngine.Transform parent, List pool, int target, float gap, bool blocking, float sMin, float sMax) { if (pool.Count == 0) { W("풀이 비었다 - 건너뜀"); return 0; } int placed = 0; int attempts = target * 15; for (int a = 0; a < attempts && placed < target; a++) { float x = s_cx + Rf(-s_half, s_half); float z = s_cz + Rf(-s_half, s_half); if (TryPlace(parent, pool, x, z, gap, blocking, sMin, sMax)) placed++; } return placed; } // ── 1개 배치 시도 ──────────────────────────────────────────── static bool TryPlace(UnityEngine.Transform parent, List pool, float x, float z, float gap, bool blocking, float sMin, float sMax) { if (x < s_cx - s_half || x > s_cx + s_half || z < s_cz - s_half || z > s_cz + s_half) return false; UnityEngine.Vector3 p, n; if (!Ground(x, z, out p, out n)) return false; float dStart = Flat2D(p, s_start); if (blocking) { if (dStart < s_clear) return false; if (InLane(p)) return false; } else if (dStart < s_clear * kDetailClearFactor) return false; // 간격 판정 — 막는 소품끼리는 서로, 디테일(풀·꽃·버섯)끼리는 서로. // 디테일은 막는 소품에서 최소 0.6 m 만 떨어지면 된다(나무 밑동 풀은 허용). if (blocking) { for (int i = 0; i < s_blockers.Count; i++) { float need = UnityEngine.Mathf.Max(gap, s_blockers[i].gap); if (Flat2D(p, s_blockers[i].p) < need) return false; } } else { for (int i = 0; i < s_details.Count; i++) if (Flat2D(p, s_details[i].p) < gap) return false; for (int i = 0; i < s_blockers.Count; i++) if (Flat2D(p, s_blockers[i].p) < 0.6f) return false; } var path = pool[s_rnd.Next(pool.Count)]; var src = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (src == null) return false; var go = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src, parent); go.transform.position = p; go.transform.rotation = UnityEngine.Quaternion.Euler(0f, Rf(0f, 360f), 0f); float s = Rf(sMin, sMax); go.transform.localScale = new UnityEngine.Vector3(s, s, s); (blocking ? s_blockers : s_details).Add(new Cell { p = p, gap = gap }); return true; } // ── 지형 판정 ──────────────────────────────────────────────── // 이미 배치한 드레싱 오브젝트(콜라이더)는 무시하고 원래 지형만 본다. static bool Ground(float x, float z, out UnityEngine.Vector3 pos, out UnityEngine.Vector3 nrm) { pos = UnityEngine.Vector3.zero; nrm = UnityEngine.Vector3.up; int n = UnityEngine.Physics.RaycastNonAlloc(new UnityEngine.Vector3(x, s_topY, z), UnityEngine.Vector3.down, s_hits, s_rayLen); if (n <= 0) return false; float bestD = float.MaxValue; int best = -1; for (int i = 0; i < n; i++) { if (s_hits[i].distance >= bestD) continue; if (s_container != null && s_hits[i].collider != null && s_hits[i].collider.transform.IsChildOf(s_container)) continue; bestD = s_hits[i].distance; best = i; } if (best < 0) return false; var h = s_hits[best]; if (h.normal.y < UnityEngine.Mathf.Cos(kSlopeLimit * UnityEngine.Mathf.Deg2Rad)) return false; if (InWater(h.point)) return false; pos = h.point; nrm = h.normal; return true; } static bool InWater(UnityEngine.Vector3 p) { for (int i = 0; i < s_water.Count; i++) { var b = s_water[i]; if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue; if (p.y < b.max.y + kWaterMargin) return true; } return false; } // 시작 지점에서 뻗는 방사형 길목 안인가 (막는 소품 제외 구역) static bool InLane(UnityEngine.Vector3 p) { var d = new UnityEngine.Vector2(p.x - s_start.x, p.z - s_start.z); float len = d.magnitude; if (len > kLaneLength || len < 0.01f) return false; for (int i = 0; i < kLaneCount; i++) { float yaw = (s_camYaw + i * (360f / kLaneCount)) * UnityEngine.Mathf.Deg2Rad; var dir = new UnityEngine.Vector2(UnityEngine.Mathf.Sin(yaw), UnityEngine.Mathf.Cos(yaw)); float along = UnityEngine.Vector2.Dot(d, dir); if (along <= 0f) continue; float side = UnityEngine.Mathf.Abs(d.x * dir.y - d.y * dir.x); if (side <= kLaneHalfWidth) return true; } return false; } // ── 프리팹 풀 (폴더 스캔) ──────────────────────────────────── static Dictionary> BuildPools() { var d = new Dictionary>(); // 나무 = LOD 변형 우선(모바일). Snow/Palm 제외(온대 침엽·활엽 바이옴). d["tree"] = Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Trees/LOD/No_Bottoms", null, new[] { "Palm" }); if (d["tree"].Count == 0) d["tree"] = Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Trees/NoLOD/No_Bottoms/Capsule_Colliders", null, new[] { "Palm" }); // 바위 = 메시 콜라이더 1색. 인공물(벽돌·타일·스톤헨지·아치·벽)·크리스탈 제외. d["rock"] = Scan(kPackRoot + "/Rocks/Rock Assets/Prefabs/With_Mesh_Colliders/1 Color", null, new[] { "Bricks", "Tiles", "Stonehenge", "Rock_Arches", "Rock_Walls", "Crystals", "Block_Rocks" }); // Block_Rocks = Concrete_Block (인공물) // 통나무·그루터기 var prop = Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Logs/LOD", null, null); prop.AddRange(Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Stumps/LOD", null, null)); if (prop.Count == 0) { prop = Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Logs/NoLOD/Capsule_Colliders", null, null); prop.AddRange(Scan(kPackRoot + "/Trees/Tree Assets/Prefabs/Stumps/NoLOD/Capsule_Colliders", null, null)); } d["prop"] = prop; // 덤불 (마른 덤불 제외 — 초원 룩) var bush = Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Bushes/Bush", null, null); bush.AddRange(Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Bushes/FlowerBush", null, null)); d["bush"] = bush; // 풀 var grass = Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Grass/Grass3D", null, null); grass.AddRange(Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Grass/GrassPlane/TwoSided", null, null)); d["grass"] = grass; // 꽃 · 버섯 d["flower"] = Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Flowers/TwoSided", null, null); d["mushroom"] = Scan(kPackRoot + "/Vegetation/Vegetation Assets/Prefabs/Mushrooms", null, null); return d; } static List Scan(string folder, string[] mustContain, string[] exclude) { var list = new List(); if (!UnityEditor.AssetDatabase.IsValidFolder(folder)) return list; foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { folder })) { var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g); if (p.Contains("_Snow") || p.Contains("Snow/")) continue; bool skip = false; if (exclude != null) for (int i = 0; i < exclude.Length; i++) if (p.Contains(exclude[i])) { skip = true; break; } if (skip) continue; if (mustContain != null) { bool ok = false; for (int i = 0; i < mustContain.Length; i++) if (p.Contains(mustContain[i])) { ok = true; break; } if (!ok) continue; } list.Add(p); } list.Sort(System.StringComparer.Ordinal); // 시드 재현성 return list; } // ── 보조 ───────────────────────────────────────────────────── static UnityEngine.Transform Group(UnityEngine.GameObject parent, string name) { var go = new UnityEngine.GameObject(name); go.transform.SetParent(parent.transform, false); return go.transform; } static void MarkStatic(UnityEngine.GameObject container) { var flags = UnityEditor.StaticEditorFlags.BatchingStatic | UnityEditor.StaticEditorFlags.OccluderStatic | UnityEditor.StaticEditorFlags.OccludeeStatic; int n = 0; foreach (var t in container.GetComponentsInChildren(true)) { UnityEditor.GameObjectUtility.SetStaticEditorFlags(t.gameObject, flags); n++; } L("static 마킹 " + n + "개 (Batching/Occluder/Occludee)"); } static string Stats(UnityEngine.GameObject container) { long tris = 0, trisLod0 = 0; int rend = 0, col = 0; var mats = new HashSet(); foreach (var mr in container.GetComponentsInChildren(true)) { rend++; foreach (var m in mr.sharedMaterials) if (m != null) mats.Add(m); var mf = mr.GetComponent(); if (mf == null || mf.sharedMesh == null) continue; long t = mf.sharedMesh.triangles.Length / 3; tris += t; if (!mr.name.Contains("LOD1") && !mr.name.Contains("LOD2")) trisLod0 += t; } col = container.GetComponentsInChildren(true).Length; int lodGroups = container.GetComponentsInChildren(true).Length; return "통계: 렌더러=" + rend + " (LODGroup " + lodGroups + ") 삼각형 합=" + tris + " LOD0만=" + trisLod0 + " 콜라이더=" + col + " 고유머티리얼=" + mats.Count; } static UnityEngine.Bounds WorldBounds(UnityEngine.GameObject root) { bool has = false; var b = new UnityEngine.Bounds(); foreach (var mr in root.GetComponentsInChildren(true)) { if (!has) { b = mr.bounds; has = true; } else b.Encapsulate(mr.bounds); } if (!has) b = new UnityEngine.Bounds(UnityEngine.Vector3.zero, UnityEngine.Vector3.one * 100f); return b; } static List FindWater(UnityEngine.GameObject root) { var list = new List(); 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) continue; if (m.name.ToLowerInvariant().Contains("water")) { w = true; break; } if (m.shader != null && m.shader.name.ToLowerInvariant().Contains("water")) { w = true; break; } } if (w) list.Add(r.bounds); } return list; } static float Flat2D(UnityEngine.Vector3 a, UnityEngine.Vector3 b) { float dx = a.x - b.x, dz = a.z - b.z; return UnityEngine.Mathf.Sqrt(dx * dx + dz * dz); } static float Rf(float a, float b) { return a + (float)s_rnd.NextDouble() * (b - a); } static float Gauss() { double u1 = 1.0 - s_rnd.NextDouble(), u2 = 1.0 - s_rnd.NextDouble(); return (float)(System.Math.Sqrt(-2.0 * System.Math.Log(u1)) * System.Math.Cos(2.0 * System.Math.PI * u2)); } 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 void ClearDirty() { var ty = typeof(UnityEditor.SceneManagement.EditorSceneManager); var mi = ty.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++) { try { mi.Invoke(null, new object[] { UnityEngine.SceneManagement.SceneManager.GetSceneAt(i) }); } catch { } } } static void Flush(string name) { try { if (!System.IO.Directory.Exists(kLogDir)) System.IO.Directory.CreateDirectory(kLogDir); System.IO.File.WriteAllText(System.IO.Path.Combine(kLogDir, name), s_log.ToString(), new System.Text.UTF8Encoding(true)); } catch { } } }