// WL-814q — 시범 전투장 씬 신규 제작 (Critter Demo 만듦새 재현 · 반경 14 m) // 데모 실측값(AgentScripts/WL814q_Probe.cs · Demo.unity YAML)을 그대로 재현한다. 원본 에셋 수정 0(참조만). public static class WL814q_BuildArena { const string SceneDir = "Assets/WL/Look/Arena/Scenes"; const string TerrDir = "Assets/WL/Look/Arena/Terrain"; const string ScenePath = SceneDir + "/WL_ArenaProto.unity"; const string TerrPath = TerrDir + "/WL_ArenaTerrain.asset"; const string PfxDir = "Assets/3DPixelArtEnvironment/Prefabs/"; const string DemoDir = "Assets/3DPixelArtEnvironment/Demo/"; const string MatDir = "Assets/3DPixelArtEnvironment/Materials/"; const float TerrSize = 80f; // m (데모 200 m · 반경 14 m 아레나에 맞춰 축소) const float TerrHeight = 12f; // m (데모 100 m) const int HmRes = 65; // 64×64 쿼드 = 8,192 tri (데모 513 은 삼각형 예산 초과) const int AlphaRes = 256; const float TerrY = -1.0f; // 아레나 바닥이 월드 y=0 이 되도록 const float BaseH = 1.0f / TerrHeight; const float Barrier = 14f; // WLStageTable barrierRadius const float StartR = 2f; // WLStageTable startRadius static readonly UnityEngine.Vector2 PondC = new UnityEngine.Vector2(17.5f, -10.5f); const float PondR = 6.0f; const float WaterY = -0.10f; static System.Random rnd; static UnityEngine.Terrain terrain; static System.Text.StringBuilder log; /// HLSL smoothstep. 🔴 Unity 의 Mathf.SmoothStep(from,to,t) 은 이것과 다르다(from~to 보간). static float SStep(float e0, float e1, float x) { float t = UnityEngine.Mathf.Clamp01((x - e0) / (e1 - e0)); return t * t * (3f - 2f * t); } static UnityEngine.GameObject Pfx(string n) { return UnityEditor.AssetDatabase.LoadAssetAtPath(PfxDir + n + ".prefab"); } static float Rf(float a, float b) { return a + (float)rnd.NextDouble() * (b - a); } static float Mirror() { return (rnd.NextDouble() < 0.5) ? -1f : 1f; } static UnityEngine.GameObject Place(string prefab, UnityEngine.Transform parent, string name, float wx, float wz, float yaw, UnityEngine.Vector3 scale, float sink, bool alignByBounds) { var src = Pfx(prefab); if (src == null) { log.AppendLine("MISSING PREFAB " + prefab); return null; } var go = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src, parent); go.name = name; go.transform.localScale = scale; go.transform.rotation = UnityEngine.Quaternion.Euler(0f, yaw, 0f); go.transform.position = new UnityEngine.Vector3(wx, 0f, wz); float groundY = terrain != null ? terrain.SampleHeight(new UnityEngine.Vector3(wx, 0f, wz)) + terrain.transform.position.y : 0f; var b = Bounds(go); if (alignByBounds) { go.transform.position += new UnityEngine.Vector3(wx - b.center.x, 0f, wz - b.center.z); b = Bounds(go); } go.transform.position += new UnityEngine.Vector3(0f, groundY - b.min.y - sink, 0f); return go; } static UnityEngine.Bounds Bounds(UnityEngine.GameObject go) { var rs = go.GetComponentsInChildren(true); var b = new UnityEngine.Bounds(go.transform.position, UnityEngine.Vector3.zero); bool f = true; foreach (var r in rs) { if (f) { b = r.bounds; f = false; } else b.Encapsulate(r.bounds); } return b; } static float PondAngle { get { return UnityEngine.Mathf.Atan2(PondC.y, PondC.x) * UnityEngine.Mathf.Rad2Deg; } } static bool NearPond(float a) { return UnityEngine.Mathf.Abs(UnityEngine.Mathf.DeltaAngle(a, PondAngle)) < 15f; } public static string Run() { log = new System.Text.StringBuilder(); rnd = new System.Random(814); if (!UnityEditor.AssetDatabase.IsValidFolder("Assets/WL/Look/Arena")) UnityEditor.AssetDatabase.CreateFolder("Assets/WL/Look", "Arena"); if (!UnityEditor.AssetDatabase.IsValidFolder(SceneDir)) UnityEditor.AssetDatabase.CreateFolder("Assets/WL/Look/Arena", "Scenes"); if (!UnityEditor.AssetDatabase.IsValidFolder(TerrDir)) UnityEditor.AssetDatabase.CreateFolder("Assets/WL/Look/Arena", "Terrain"); var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); // ── 1. 지형 (데모와 같은 방식: Unity Terrain + Demo 터레인 레이어 2종 + Terrain.mat[ToonTerrain]) var td = new UnityEngine.TerrainData(); td.heightmapResolution = HmRes; td.size = new UnityEngine.Vector3(TerrSize, TerrHeight, TerrSize); td.alphamapResolution = AlphaRes; td.SetDetailResolution(0, 8); float half = TerrSize * 0.5f; var h = new float[HmRes, HmRes]; for (int y = 0; y < HmRes; y++) for (int x = 0; x < HmRes; x++) { float wx = (float)x / (HmRes - 1) * TerrSize - half; float wz = (float)y / (HmRes - 1) * TerrSize - half; float r = UnityEngine.Mathf.Sqrt(wx * wx + wz * wz); // 전투장 안은 평지 + 아주 얕은 기복(툰 셰이딩 단차가 보이게 · 이동엔 영향 없음) float ripple = (UnityEngine.Mathf.PerlinNoise(wx * 0.14f + 31.7f, wz * 0.14f + 19.3f) - 0.5f) * 0.14f; float v = BaseH + ripple / TerrHeight; if (r > 15.5f) { float t = SStep(15.5f, 38f, r); float n = UnityEngine.Mathf.PerlinNoise(wx * 0.035f + 11.3f, wz * 0.035f + 7.1f); v += (0.4f + n * 2.2f) * t / TerrHeight; } float pr = UnityEngine.Mathf.Sqrt((wx - PondC.x) * (wx - PondC.x) + (wz - PondC.y) * (wz - PondC.y)); if (pr < PondR) v = UnityEngine.Mathf.Lerp(v, 0.15f / TerrHeight, SStep(PondR, PondR * 0.25f, pr)); h[y, x] = v; } var grass = UnityEditor.AssetDatabase.LoadAssetAtPath(DemoDir + "Demo_Grass.terrainlayer"); var dirt = UnityEditor.AssetDatabase.LoadAssetAtPath(DemoDir + "Demo_Dirt.terrainlayer"); td.terrainLayers = new UnityEngine.TerrainLayer[] { grass, dirt }; // 0=R채널(풀 초록) · 1=G채널(흙 갈색) — Terrain.mat 실측 var alpha = new float[AlphaRes, AlphaRes, 2]; for (int y = 0; y < AlphaRes; y++) for (int x = 0; x < AlphaRes; x++) { float wx = (float)x / (AlphaRes - 1) * TerrSize - half; float wz = (float)y / (AlphaRes - 1) * TerrSize - half; float n = UnityEngine.Mathf.PerlinNoise(wx * 0.16f + 3.7f, wz * 0.16f + 5.2f); float r = UnityEngine.Mathf.Sqrt(wx * wx + wz * wz) + (n - 0.5f) * 3.2f; float d = 1f - SStep(11.0f, 15.0f, r); // 흙 = 전투장 바닥(반경 ~13 m) float pr = UnityEngine.Mathf.Sqrt((wx - PondC.x) * (wx - PondC.x) + (wz - PondC.y) * (wz - PondC.y)); d = UnityEngine.Mathf.Max(d, 1f - SStep(PondR - 1.0f, PondR + 1.8f, pr)); // 물가 = 흙 alpha[y, x, 1] = UnityEngine.Mathf.Clamp01(d); alpha[y, x, 0] = 1f - UnityEngine.Mathf.Clamp01(d); } // 🔴 높이·스플랫은 에셋을 만든 뒤에 써야 남는다(CreateAsset 전에 쓰면 ch1 이 0 으로 날아간다 — 실측) UnityEditor.AssetDatabase.CreateAsset(td, TerrPath); td.SetHeights(0, 0, h); td.SetAlphamaps(0, 0, alpha); UnityEditor.EditorUtility.SetDirty(td); UnityEditor.AssetDatabase.SaveAssets(); var terrGo = UnityEngine.Terrain.CreateTerrainGameObject(td); terrGo.name = "Terrain"; terrGo.transform.position = new UnityEngine.Vector3(-half, TerrY, -half); terrain = terrGo.GetComponent(); terrain.materialTemplate = UnityEditor.AssetDatabase.LoadAssetAtPath(MatDir + "Terrain.mat"); terrain.heightmapPixelError = 5f; terrain.drawTreesAndFoliage = false; UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(terrGo, scene); AddTerrainInstancing(terrGo); // ── 2. 조명 (데모 실측 그대로) var lightRoot = new UnityEngine.GameObject("Lighting"); var dl = new UnityEngine.GameObject("Directional Light"); dl.transform.SetParent(lightRoot.transform); var dlc = dl.AddComponent(); dlc.type = UnityEngine.LightType.Directional; dlc.color = UnityEngine.Color.white; dlc.intensity = 1f; dlc.shadows = UnityEngine.LightShadows.Soft; dl.transform.position = new UnityEngine.Vector3(0f, 3f, 0f); dl.transform.rotation = new UnityEngine.Quaternion(0.40821788f, -0.23456968f, 0.10938163f, 0.8754261f); var warm = new UnityEngine.Color(1f, 0.9639759f, 0.63836473f, 1f); // ── 3. 렌더 설정 (데모 실측) UnityEngine.RenderSettings.fog = false; UnityEngine.RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Skybox; UnityEngine.RenderSettings.ambientSkyColor = new UnityEngine.Color(0.212f, 0.227f, 0.259f, 1f); UnityEngine.RenderSettings.ambientEquatorColor = new UnityEngine.Color(0.114f, 0.125f, 0.133f, 1f); UnityEngine.RenderSettings.ambientGroundColor = new UnityEngine.Color(0.047f, 0.043f, 0.035f, 1f); UnityEngine.RenderSettings.ambientIntensity = 1f; // 🔴 Resources.GetBuiltinResource 로는 못 찾는다(콘솔 에러) — 빌트인 extra 리소스로 받아야 한다 UnityEngine.RenderSettings.skybox = UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Skybox.mat"); UnityEngine.RenderSettings.subtractiveShadowColor = new UnityEngine.Color(0.42f, 0.478f, 0.627f, 1f); // ── 4. 경계(반경 14 m) — 바위 띠를 촘촘히 + 나무 숲 + 아치 2개 var ring = new UnityEngine.GameObject("Arena_Ring"); int nStone = 56; for (int i = 0; i < nStone; i++) { float a = (float)i / nStone * 360f + Rf(-2.5f, 2.5f); if (UnityEngine.Mathf.Abs(UnityEngine.Mathf.DeltaAngle(a, 0f)) < 13f) continue; // 정문 if (UnityEngine.Mathf.Abs(UnityEngine.Mathf.DeltaAngle(a, 180f)) < 11f) continue; // 후문 if (NearPond(a)) continue; // 연못 쪽은 트인다 float r = Barrier + Rf(0.1f, 1.4f); float rad = a * UnityEngine.Mathf.Deg2Rad; bool big = rnd.NextDouble() < 0.6; float s = big ? Rf(1.0f, 1.8f) : Rf(1.1f, 2.0f); // 변형: y 회전 + 비균일 스케일 + x 음수 스케일(거울) — 데모 Stone_2 실측 기법 그대로 var sc = new UnityEngine.Vector3(s * Mirror() * Rf(0.8f, 1.3f), s * Rf(0.8f, 1.6f), s * Rf(0.85f, 1.2f)); Place(big ? "Stone_2" : "Stone_1", ring.transform, (big ? "Stone_2_" : "Stone_1_") + i, UnityEngine.Mathf.Cos(rad) * r, UnityEngine.Mathf.Sin(rad) * r, Rf(0f, 360f), sc, Rf(0.1f, 0.5f), false); } int nTree = 20; for (int i = 0; i < nTree; i++) { float a = (float)i / nTree * 360f + Rf(-6f, 6f); if (UnityEngine.Mathf.Abs(UnityEngine.Mathf.DeltaAngle(a, 0f)) < 18f) continue; if (NearPond(a)) continue; float r = Barrier + Rf(2.2f, 5.0f); float rad = a * UnityEngine.Mathf.Deg2Rad; float s = Rf(0.38f, 0.62f); Place("Tree", ring.transform, "Tree_Ring_" + i, UnityEngine.Mathf.Cos(rad) * r, UnityEngine.Mathf.Sin(rad) * r, Rf(0f, 360f), new UnityEngine.Vector3(s * Mirror(), s * Rf(0.85f, 1.25f), s), 0.05f, false); } Place("Arch_Large", ring.transform, "Gate_Arch_Large", Barrier + 0.6f, 0f, -90f, UnityEngine.Vector3.one * 0.34f, 0.05f, true); Place("Arch_Small", ring.transform, "Gate_Arch_Small", -(Barrier + 0.4f), 0f, 90f, UnityEngine.Vector3.one * 0.34f, 0.05f, true); // ── 5. 전투장 바닥 (Slab 포석 + 잔돌 · 중앙 반경 2 m 는 비운다) var floor = new UnityEngine.GameObject("Arena_Floor"); for (int i = 0; i < 14; i++) { float a = Rf(0f, 360f); float r = Rf(StartR + 1.2f, 12.2f); float rad = a * UnityEngine.Mathf.Deg2Rad; float s = Rf(0.26f, 0.50f); // Slab 원 크기 3.37×0.91×4.41 m → 0.9~2.2 m 포석 var g = Place("Slab", floor.transform, "Slab_" + i, UnityEngine.Mathf.Cos(rad) * r, UnityEngine.Mathf.Sin(rad) * r, Rf(0f, 360f), new UnityEngine.Vector3(s * Rf(0.85f, 1.25f), s * Rf(0.7f, 1.1f), s * Rf(0.85f, 1.25f)), Rf(0.12f, 0.26f), false); // 변형: z 180° 뒤집기 — 데모 Slab (2)(3) 실측 기법 if (g != null && rnd.NextDouble() < 0.35) g.transform.rotation = UnityEngine.Quaternion.Euler(0f, g.transform.eulerAngles.y, 180f); } for (int i = 0; i < 26; i++) // 잔돌 (Stone.mat · 새 머티리얼 0) { float a = Rf(0f, 360f); float r = Rf(StartR + 0.6f, 13.2f); float rad = a * UnityEngine.Mathf.Deg2Rad; float s = Rf(0.10f, 0.28f); Place((rnd.NextDouble() < 0.5) ? "Stone_1" : "Stone_2", floor.transform, "Pebble_" + i, UnityEngine.Mathf.Cos(rad) * r, UnityEngine.Mathf.Sin(rad) * r, Rf(0f, 360f), new UnityEngine.Vector3(s * Mirror() * Rf(0.8f, 1.4f), s * Rf(0.5f, 1.0f), s * Rf(0.8f, 1.4f)), Rf(0.01f, 0.06f), false); } // 가로등 2개 + 데모의 따뜻한 포인트 라이트 2개 float[] lpA = { 58f, 236f }; for (int i = 0; i < 2; i++) { float rad = lpA[i] * UnityEngine.Mathf.Deg2Rad; float px = UnityEngine.Mathf.Cos(rad) * 12.4f, pz = UnityEngine.Mathf.Sin(rad) * 12.4f; var lp = Place("Light_Post", floor.transform, "Light_Post_" + i, px, pz, lpA[i] + 180f, UnityEngine.Vector3.one * 0.22f, 0f, false); var pl = new UnityEngine.GameObject("Point Light " + i); pl.transform.SetParent(lightRoot.transform); pl.transform.position = new UnityEngine.Vector3(px, (lp != null ? Bounds(lp).max.y - 0.35f : 3.5f), pz); var plc = pl.AddComponent(); plc.type = UnityEngine.LightType.Point; plc.color = warm; plc.intensity = 0.54f; plc.range = 6.14f; plc.shadows = UnityEngine.LightShadows.None; } UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(lightRoot, scene); // ── 6. 연못 (Water_Plane · 데모의 흰 거품 가장자리) var pondRoot = new UnityEngine.GameObject("Pond"); var water = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(Pfx("Water_Plane"), pondRoot.transform); water.name = "Water_Plane"; water.transform.position = new UnityEngine.Vector3(PondC.x, WaterY, PondC.y); water.transform.localScale = new UnityEngine.Vector3(PondR * 2.15f / 10f, 1f, PondR * 1.9f / 10f); UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(pondRoot, scene); for (int i = 0; i < 7; i++) { float a = Rf(0f, 360f) * UnityEngine.Mathf.Deg2Rad; float s = Rf(0.6f, 1.3f); Place("Stone_1", pondRoot.transform, "Pond_Stone_" + i, PondC.x + UnityEngine.Mathf.Cos(a) * Rf(PondR * 1.0f, PondR * 1.35f), PondC.y + UnityEngine.Mathf.Sin(a) * Rf(PondR * 1.0f, PondR * 1.35f), Rf(0f, 360f), new UnityEngine.Vector3(s * Mirror(), s * Rf(0.7f, 1.1f), s), Rf(0.1f, 0.4f), false); } // ── 7. 배경 랜드마크 (원근감) var bg = new UnityEngine.GameObject("Background"); Place("Windmill", bg.transform, "Windmill", -20f, 28f, 143f, UnityEngine.Vector3.one * 3.2f, 0.1f, false); Place("House", bg.transform, "House_1", 25f, 23f, -142f, UnityEngine.Vector3.one * 4.6f, 0.1f, false); Place("House", bg.transform, "House_2", 31f, 16f, -118f, UnityEngine.Vector3.one * 4.0f, 0.1f, false); for (int i = 0; i < 14; i++) { float a = Rf(0f, 360f) * UnityEngine.Mathf.Deg2Rad; float r = Rf(22f, 36f); float s = Rf(0.5f, 0.9f); Place("Tree", bg.transform, "Tree_BG_" + i, UnityEngine.Mathf.Cos(a) * r, UnityEngine.Mathf.Sin(a) * r, Rf(0f, 360f), new UnityEngine.Vector3(s * Mirror(), s * Rf(0.85f, 1.25f), s), 0.05f, false); } UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(bg, scene); UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(ring, scene); UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(floor, scene); // ── 8. 캐릭터 (중앙 · 인게임과 같은 월드 높이 1.1912 m) float pcScale = 0.7f * 1.5038f; // ClassConfig f_Scale × WLCharacterSwapSettings.heightCompensation var pcSrc = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/PC/LH_M05.prefab"); var pc = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(pcSrc); pc.name = "PC_LH_M05"; pc.transform.localScale = UnityEngine.Vector3.one * pcScale; pc.transform.position = UnityEngine.Vector3.zero; pc.transform.rotation = UnityEngine.Quaternion.Euler(0f, 200f, 0f); var comp = pc.GetComponent("WL.Character.WLPcScaleCompensator") as UnityEngine.MonoBehaviour; if (comp != null) { // 씬 인스턴스 한정 오버라이드 — Play 때 스케일이 다시 곱해지지 않게(프리팹 수정 0) var so = new UnityEditor.SerializedObject(comp); var p = so.FindProperty("overrideCompensation"); if (p != null) { p.floatValue = 1f; so.ApplyModifiedProperties(); } } UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(pc, scene); log.AppendLine(PoseIdle(pc)); var pb = Bounds(pc); log.AppendLine(string.Format("PC scale={0:F5} worldHeight={1:F4} m (min.y={2:F4} max.y={3:F4})", pcScale, pb.size.y, pb.min.y, pb.max.y)); // ── 9. 카메라 (인게임 도트A 와 같은 각도·거리 · CameraFramingOverride 수식 실측) float H = 80f, D = 3f, Up = 3f, lookAtY = 1.06f; float rr = H * UnityEngine.Mathf.Deg2Rad; float camUp = Up + D * (UnityEngine.Mathf.Sin(rr) - UnityEngine.Mathf.Cos(rr)); float ss = UnityEngine.Mathf.Sin(rr) + UnityEngine.Mathf.Cos(rr); float camHoriz = D * UnityEngine.Mathf.Sqrt(1f + ss * ss); float yaw = 200f; var dir = new UnityEngine.Vector3(UnityEngine.Mathf.Sin(yaw * UnityEngine.Mathf.Deg2Rad), 0f, UnityEngine.Mathf.Cos(yaw * UnityEngine.Mathf.Deg2Rad)); var camGo = new UnityEngine.GameObject("Main Camera"); camGo.tag = "MainCamera"; var cam = camGo.AddComponent(); camGo.AddComponent(); camGo.transform.position = UnityEngine.Vector3.up * camUp - dir * camHoriz; camGo.transform.LookAt(UnityEngine.Vector3.up * lookAtY); cam.orthographic = true; cam.orthographicSize = 3.4f; cam.nearClipPlane = 0.3f; cam.farClipPlane = 500f; cam.clearFlags = UnityEngine.CameraClearFlags.Skybox; UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(camGo, scene); log.AppendLine(string.Format("Camera pos={0} euler={1} camUp={2:F4} camHoriz={3:F4} down={4:F2}deg orthoSize=3.4 · 지면 깊이={5:F2} m", camGo.transform.position, camGo.transform.eulerAngles, camUp, camHoriz, UnityEngine.Mathf.Atan2(camUp - lookAtY, camHoriz) * UnityEngine.Mathf.Rad2Deg, 6.8f / UnityEngine.Mathf.Sin(UnityEngine.Mathf.Atan2(camUp - lookAtY, camHoriz)))); UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, ScenePath); UnityEditor.AssetDatabase.SaveAssets(); UnityEditor.AssetDatabase.Refresh(); log.AppendLine("SAVED " + ScenePath); log.Append(WL814q_Stats.Measure()); return log.ToString(); } /// 데모의 지면 인스턴싱(풀·꽃·자갈)을 실측 설정 그대로 얹는다. 🔴 ExecuteAlways 가 아니라 Play 에서만 그려진다. static void AddTerrainInstancing(UnityEngine.GameObject terrGo) { var t = System.Type.GetType("Environment.Instancing.TerrainInstancesBehaviour, Assembly-CSharp"); if (t == null) { log.AppendLine("TerrainInstancesBehaviour type NOT FOUND — 생략"); return; } var c = terrGo.AddComponent(t) as UnityEngine.MonoBehaviour; var so = new UnityEditor.SerializedObject(c); // 데모 Demo.unity 실측값 SetLayer(so, "FirstLayer", 2.5f, new string[] { "1f298817fdd2a184480e8af5b21278bc", "dab2c84bf0b5ced4aaf0cffc4c491d55" }, new string[] { "a194cb4ce6027c246af528b83b728a15", "d50a40cb47ec86444b898d9121bbe0f7" }, new float[] { 100f, 1f }, new float[] { 0.35f, 0.35f }, new float[] { 0.04f, 0.04f }); SetLayer(so, "SecondLayer", 0.05f, new string[] { "7f1f11ac2c783374b876f8dc2bb7c7db" }, new string[] { "77d84437c100f274aad603219110357d" }, new float[] { 1f }, new float[] { 0.7f }, new float[] { 0.1f }); var pv = so.FindProperty("PositionVariance"); if (pv != null) pv.floatValue = 0.5f; var sv = so.FindProperty("ScaleVariance"); if (sv != null) sv.floatValue = 0.2f; so.ApplyModifiedProperties(); log.AppendLine("TerrainInstancesBehaviour 적용(데모 실측값) — 에디트 모드 캡처엔 안 나옴(ExecuteAlways 아님)"); } static void SetLayer(UnityEditor.SerializedObject so, string layer, float density, string[] meshGuid, string[] matGuid, float[] prob, float[] scale, float[] normOff) { var lp = so.FindProperty(layer); if (lp == null) { log.AppendLine("no prop " + layer); return; } lp.FindPropertyRelative("Density").floatValue = density; var arr = lp.FindPropertyRelative("Settings"); arr.arraySize = meshGuid.Length; for (int i = 0; i < meshGuid.Length; i++) { var e = arr.GetArrayElementAtIndex(i); e.FindPropertyRelative("Mesh").objectReferenceValue = FirstMesh(meshGuid[i]); e.FindPropertyRelative("Material").objectReferenceValue = UnityEditor.AssetDatabase.LoadAssetAtPath( UnityEditor.AssetDatabase.GUIDToAssetPath(matGuid[i])); e.FindPropertyRelative("Probability").floatValue = prob[i]; e.FindPropertyRelative("Scale").floatValue = scale[i]; e.FindPropertyRelative("NormalOffset").floatValue = normOff[i]; } } static UnityEngine.Mesh FirstMesh(string guid) { string p = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); foreach (var o in UnityEditor.AssetDatabase.LoadAllAssetsAtPath(p)) if (o is UnityEngine.Mesh) return (UnityEngine.Mesh)o; return null; } /// T 포즈를 피한다 — 컨트롤러의 idle 클립 1프레임을 씬 인스턴스에 굽는다(프리팹 수정 0). static string PoseIdle(UnityEngine.GameObject pc) { var an = pc.GetComponentInChildren(true); if (an == null) return "PoseIdle: Animator 없음"; UnityEngine.AnimationClip pick = null; if (an.runtimeAnimatorController != null) foreach (var cl in an.runtimeAnimatorController.animationClips) { if (cl == null) continue; string n = cl.name.ToLower(); if (n.Contains("idle") || n.Contains("stand")) { pick = cl; break; } if (pick == null) pick = cl; } if (pick == null) return "PoseIdle: 클립 없음(T 포즈 유지)"; bool prev = UnityEditor.AnimationMode.InAnimationMode(); if (!prev) UnityEditor.AnimationMode.StartAnimationMode(); UnityEditor.AnimationMode.BeginSampling(); UnityEditor.AnimationMode.SampleAnimationClip(an.gameObject, pick, 0.4f); UnityEditor.AnimationMode.EndSampling(); // 🔴 StopAnimationMode 가 트랜스폼을 되돌린다 — 샘플된 포즈를 먼저 떠 두고 나중에 다시 쓴다 var ts = pc.GetComponentsInChildren(true); var p = new UnityEngine.Vector3[ts.Length]; var q = new UnityEngine.Quaternion[ts.Length]; var s = new UnityEngine.Vector3[ts.Length]; for (int i = 0; i < ts.Length; i++) { p[i] = ts[i].localPosition; q[i] = ts[i].localRotation; s[i] = ts[i].localScale; } if (!prev) UnityEditor.AnimationMode.StopAnimationMode(); for (int i = 0; i < ts.Length; i++) { ts[i].localPosition = p[i]; ts[i].localRotation = q[i]; ts[i].localScale = s[i]; } return "PoseIdle: '" + pick.name + "' @0.4s 샘플(스냅샷 재적용)"; } } public static class WL814q_Stats { public static string Measure() { var sb = new System.Text.StringBuilder(); var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); var mats = new System.Collections.Generic.HashSet(); int tris = 0, rend = 0; foreach (var root in scene.GetRootGameObjects()) foreach (var r in root.GetComponentsInChildren(true)) { if (r is UnityEngine.ParticleSystemRenderer) continue; rend++; foreach (var m in r.sharedMaterials) if (m != null) mats.Add(m); var mf = r.GetComponent(); if (mf != null && mf.sharedMesh != null) tris += mf.sharedMesh.triangles.Length / 3; var smr = r as UnityEngine.SkinnedMeshRenderer; if (smr != null && smr.sharedMesh != null) tris += smr.sharedMesh.triangles.Length / 3; } int terrTris = 0; var t = UnityEngine.Object.FindFirstObjectByType(); if (t != null) { int q = t.terrainData.heightmapResolution - 1; terrTris = q * q * 2; sb.AppendLine(string.Format("Terrain hmRes={0} size={1} tris(full LOD)={2} mat={3}", t.terrainData.heightmapResolution, t.terrainData.size, terrTris, t.materialTemplate != null ? t.materialTemplate.name : "null")); } sb.AppendLine(string.Format("SCENE STATS: renderers={0} materialKinds={1}(+Terrain.mat) meshTris={2} +terrain={3} TOTAL={4}", rend, mats.Count, tris, terrTris, tris + terrTris)); var names = new System.Collections.Generic.List(); foreach (var m in mats) names.Add(m.name); names.Sort(); sb.AppendLine("MATERIALS: " + string.Join(" · ", names.ToArray())); return sb.ToString(); } }