558 lines
29 KiB
C#
558 lines
29 KiB
C#
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<UnityEngine.Bounds> 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<Cell> s_blockers = new List<Cell>(); // 나무·바위·통나무·덤불
|
|
static readonly List<Cell> s_details = new List<Cell>(); // 풀·꽃·버섯
|
|
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<string, string>();
|
|
foreach (var kv in pools)
|
|
foreach (var p in kv.Value)
|
|
{
|
|
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
|
|
if (go == null) continue;
|
|
foreach (var r in go.GetComponentsInChildren<UnityEngine.Renderer>(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 ? "<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<UnityEngine.GameObject>(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<MapData>();
|
|
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<string> 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<UnityEngine.Vector3>();
|
|
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<string>(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<List<string>> ByFolder(List<string> pool)
|
|
{
|
|
var d = new Dictionary<string, List<string>>();
|
|
for (int i = 0; i < pool.Count; i++)
|
|
{
|
|
var k = Folder(pool[i]);
|
|
if (!d.ContainsKey(k)) d[k] = new List<string>();
|
|
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<string> 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<string> 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<UnityEngine.GameObject>(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<string, List<string>> BuildPools()
|
|
{
|
|
var d = new Dictionary<string, List<string>>();
|
|
// 나무 = 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<string> Scan(string folder, string[] mustContain, string[] exclude)
|
|
{
|
|
var list = new List<string>();
|
|
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<UnityEngine.Transform>(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<UnityEngine.Material>();
|
|
foreach (var mr in container.GetComponentsInChildren<UnityEngine.MeshRenderer>(true))
|
|
{
|
|
rend++;
|
|
foreach (var m in mr.sharedMaterials) if (m != null) mats.Add(m);
|
|
var mf = mr.GetComponent<UnityEngine.MeshFilter>();
|
|
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<UnityEngine.Collider>(true).Length;
|
|
int lodGroups = container.GetComponentsInChildren<UnityEngine.LODGroup>(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<UnityEngine.MeshRenderer>(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<UnityEngine.Bounds> FindWater(UnityEngine.GameObject root)
|
|
{
|
|
var list = new List<UnityEngine.Bounds>();
|
|
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) 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 { }
|
|
}
|
|
}
|