// ───────────────────────────────────────────────────────────────────────────── // WLDungeonGrassLook.cs — 던전(인게임) 풀밭을 섬과 같은 느낌으로 맞춘다 // // 2026-09-17 PD 「인게임의 풀밭의 느낌도 섬과 동일하게 맞춰줘」. // // ■ 실측(WL_Dungeon01 · 데모 씬 복사본)으로 본 차이 // 항목 섬 던전(원래) // 풀/꽃 크기 0.22 / 0.26 0.35 / 0.35 // 구름 그늘 세기 0.5 1.0 // 구름 그늘 크기 0.025 0.010 // 토온 밝기 단계 0.12 0.25 // 태양 그림자 0.45 1.0 // SSAO 끔 켬(풀잎마다 발밑 그늘 = 얼룩) // ■ 무엇을 하나(씬 파일·원본 재질 0줄 — 전부 런타임) // 던전 씬의 데모 인스턴서(`TerrainInstancesBehaviour`) 항목을 섬 값으로 바꾸고(크기 · 재질은 구름/밝기만 덮은 런타임 복사본) // 인스턴서를 껐다 켜 다시 심는다. 터레인 재질도 같은 복사본으로, 디렉셔널 라이트는 섬의 색·세기·그림자 진하기로 맞춘다. // SSAO 는 `WLIslandLookSettings.ssaoOffScenes` 에 던전 씬을 넣어 `WLSkyScope` 가 끈다(나갈 때 복원). // ■ 값 = 전부 `WLIslandLookSettings`(섬과 같은 칸을 읽는다 → 섬을 고치면 던전도 따라온다) · `dungeonGrassMatchIsland 0` = 아무 것도 안 한다. // ■ #851(2026-10-12 PD 「원형 지역에 패턴처럼 듬성듬성」) — 크기만 섬 값이던 것을 **밀도도 섬 값**(`demoFirstLayerDensity`)으로 맞추고, // 수면이 있으면 `WLDungeonGrassInstancer`(물 밑 빼기판)로 바꿔 연못 바닥 풀잎을 뺀다. // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using DemoInstancer = global::Environment.Instancing.TerrainInstancesBehaviour; using DemoLayer = global::Environment.Instancing.TerrainInstancesBehaviour.TerrainInstancingInput; namespace WL.Look.Farm { public static class WLDungeonGrassLook { // ── 진단(프로브가 읽는다) public static int Instancers, EntriesScaled, MaterialsTuned, TerrainsTuned, LightsMatched; public static string LastLog = ""; public static float TerrainDarknessMatched = -1f; static float GrassDarkness(DemoLayer layer, Mesh grassMesh) { if (layer == null || layer.Settings == null) return -1f; for (int i = 0; i < layer.Settings.Length; i++) { var e = layer.Settings[i]; if (e == null || e.Material == null || !e.Material.HasProperty("_MinimumDarkness")) continue; if (grassMesh == null || e.Mesh == grassMesh) return e.Material.GetFloat("_MinimumDarkness"); } return -1f; } static readonly Dictionary s_tuned = new Dictionary(); public static void Apply(Scene scene) { var cfg = WLIslandLookSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.dungeonGrassMatchIsland == 0 || !scene.IsValid() || !scene.isLoaded) return; Mesh grassMesh = ScatterMesh(cfg, cfg.demoGrass), flowerMesh = ScatterMesh(cfg, cfg.demoFlower); int insts = 0, scaled = 0, terrains = 0, lights = 0, densityMatched = 0, replaced = 0, lampsOff = 0; int tunedBefore = s_tuned.Count; var roots = scene.GetRootGameObjects(); var pools = cfg.dungeonGrassSkipUnderwater != 0 ? WaterPools(cfg, roots) : null; float grassMd = -1f; for (int r = 0; r < roots.Length; r++) { var list = roots[r].GetComponentsInChildren(true); for (int i = 0; i < list.Length; i++) { var inst = list[i]; if (inst == null || inst is WLDungeonGrassInstancer) continue; bool was = inst.enabled; if (was) inst.enabled = false; // OnDisable → 버퍼 해제 scaled += TuneLayer(cfg, inst.FirstLayer, grassMesh, flowerMesh); scaled += TuneLayer(cfg, inst.SecondLayer, grassMesh, flowerMesh); scaled += TuneLayer(cfg, inst.ThirdLayer, grassMesh, flowerMesh); scaled += TuneLayer(cfg, inst.FourthLayer, grassMesh, flowerMesh); // #851 크기만 섬 값이고 밀도는 데모 값(2.5)이라 풀잎이 듬성듬성 격자로 보였다 → 밀도도 섬 값으로 if (cfg.dungeonGrassDensityMatchIsland != 0 && inst.FirstLayer != null && cfg.demoFirstLayerDensity > 0f && !Mathf.Approximately(inst.FirstLayer.Density, cfg.demoFirstLayerDensity)) { inst.FirstLayer.Density = cfg.demoFirstLayerDensity; densityMatched++; } // #851 연못 바닥 풀 빼기 — 수면이 있는 씬만 「물 밑 빼기」판으로 바꿔 심는다(원본은 꺼 둔 채) WLDungeonGrassInstancer rep = null; if (was && pools != null && pools.Count > 0) rep = WLDungeonGrassInstancer.Replace(inst, pools, cfg.dungeonGrassUnderwaterMargin); if (rep != null) replaced++; else if (was) inst.enabled = true; // OnEnable → 새 값으로 다시 심는다 insts++; if (grassMd < 0f) grassMd = GrassDarkness(inst.FirstLayer, grassMesh); } } DensityMatched = densityMatched; WaterPoolCount = pools != null ? pools.Count : 0; Replaced = replaced; // 🔴 2026-09-17 PD 「전투맵의 풀이 여전히 자글자글(섬에서 지적한 현상과 동일)」 실측(풀만 있는 구역의 밝기 기울기): // SSAO·후처리·그림자·풀 외곽선은 무관. 814t 대비 복제가 **지형** 그늘 최소 밝기를 0.12 로 낮춰 풀잎(0.2)보다 그늘이 어두워져 // 구름 그늘·명암 단계마다 풀잎이 하나씩 밝은 점으로 도드라졌다(0.57). 지형을 풀잎과 같은 값으로 맞추면 0.11(섬과 같은 매끈함). for (int r = 0; r < roots.Length; r++) { var ts = roots[r].GetComponentsInChildren(true); for (int i = 0; i < ts.Length; i++) { var t = ts[i]; if (t == null || t.materialTemplate == null) continue; var m = Tuned(cfg, t.materialTemplate); if (cfg.dungeonTerrainMatchGrassDarkness != 0 && grassMd >= 0f && m.HasProperty("_MinimumDarkness")) { if (m == t.materialTemplate && !s_tuned.ContainsValue(m)) { var c = new Material(m) { name = m.name + " (WL dungeon=island)" }; s_tuned[m] = c; m = c; } m.SetFloat("_MinimumDarkness", grassMd); TerrainDarknessMatched = grassMd; } if (m != t.materialTemplate) { t.materialTemplate = m; terrains++; } } // 2026-10-15 PD 「던전 일부 지역의 풀이 여전히 듬성듬성」 실측: 가로등 점광·스폿이 비추는 원 안에서만 풀잎이 지형과 갈라져 도드라졌다 // (구름 그늘 0 · 지형 LOD 1 로 바꿔도 그대로 · 가로등 4개를 끄면 사라짐) → 던전에서는 가로등 조명을 끈다. if (cfg.dungeonLampLightsOff != 0) { var ls = roots[r].GetComponentsInChildren(true); for (int i = 0; i < ls.Length; i++) { var l = ls[i]; if (l == null || l.type == LightType.Directional || !l.enabled) continue; l.enabled = false; lampsOff++; } } if (cfg.dungeonLightMatchIsland != 0) { var ls = roots[r].GetComponentsInChildren(true); for (int i = 0; i < ls.Length; i++) { var l = ls[i]; if (l == null || l.type != LightType.Directional) continue; l.color = cfg.dirLightColor; l.intensity = cfg.dirLightIntensity; if (cfg.dirLightShadowStrength >= 0f) l.shadowStrength = Mathf.Clamp01(cfg.dirLightShadowStrength); if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft; lights++; } } } Instancers = insts; EntriesScaled = scaled; TerrainsTuned = terrains; LightsMatched = lights; MaterialsTuned = s_tuned.Count; LastLog = "던전 풀밭 = 섬 값 — 인스턴서 " + insts + " · 크기 바꾼 항목 " + scaled + " · 밀도 맞춤 " + densityMatched + "(" + cfg.demoFirstLayerDensity.ToString("F1") + ")" + " · 수면 " + WaterPoolCount + "(물 밑 뺀 풀 " + (replaced > 0 ? WLDungeonGrassInstancer.LastDropped : 0) + " / 남은 " + (replaced > 0 ? WLDungeonGrassInstancer.LastKept : -1) + ")" + " · 재질 복사본 " + s_tuned.Count + "(새로 " + (s_tuned.Count - tunedBefore) + ")" + " · 터레인 " + terrains + "(그늘 최소 밝기 = 풀잎 " + TerrainDarknessMatched.ToString("F2") + ") · 태양 " + lights + " · 가로등 끔 " + lampsOff + " @" + scene.name; cfg.Log(LastLog); } public static int DensityMatched, WaterPoolCount, Replaced; /// #851 — 씬 안 수면(셰이더 이름에 `dungeonWaterShaderKey` 가 든 켜진 렌더러)의 xz 사각형 + 수면 높이. static List WaterPools(WLIslandLookSettings cfg, GameObject[] roots) { var key = cfg.dungeonWaterShaderKey; if (string.IsNullOrEmpty(key)) return null; var pools = new List(); for (int r = 0; r < roots.Length; r++) { var rs = roots[r].GetComponentsInChildren(false); for (int i = 0; i < rs.Length; i++) { var rd = rs[i]; if (rd == null || !rd.enabled) continue; var m = rd.sharedMaterial; if (m == null || m.shader == null || m.shader.name.IndexOf(key, System.StringComparison.Ordinal) < 0) continue; var b = rd.bounds; pools.Add(new WLDungeonGrassInstancer.Pool { minX = b.min.x, maxX = b.max.x, minZ = b.min.z, maxZ = b.max.z, surfaceY = b.max.y }); } } return pools; } static int TuneLayer(WLIslandLookSettings cfg, DemoLayer layer, Mesh grassMesh, Mesh flowerMesh) { if (layer == null || layer.Settings == null) return 0; int n = 0; for (int i = 0; i < layer.Settings.Length; i++) { var e = layer.Settings[i]; if (e == null) continue; if (e.Material != null) e.Material = Tuned(cfg, e.Material); if (grassMesh != null && e.Mesh == grassMesh && cfg.demoGrass != null && cfg.demoGrass.scale > 0f && !Mathf.Approximately(e.Scale, cfg.demoGrass.scale)) { e.Scale = cfg.demoGrass.scale; n++; } else if (flowerMesh != null && e.Mesh == flowerMesh && cfg.demoFlower != null && cfg.demoFlower.scale > 0f && !Mathf.Approximately(e.Scale, cfg.demoFlower.scale)) { e.Scale = cfg.demoFlower.scale; n++; } } return n; } static Mesh ScatterMesh(WLIslandLookSettings cfg, WLDemoScatter d) { var sc = cfg.scatter; if (d == null || sc == null || d.scatterIndex < 0 || d.scatterIndex >= sc.Length || sc[d.scatterIndex] == null) return null; return sc[d.scatterIndex].mesh; } /// 섬의 `WLDemoTerrain.Tuned` 와 같은 규칙 — 구름 세기·크기·밝기 단계만 덮은 런타임 복사본(원본 재질 0줄). static Material Tuned(WLIslandLookSettings cfg, Material src) { if (src == null) return null; Material m; if (s_tuned.TryGetValue(src, out m) && m != null) return m; foreach (var kv in s_tuned) if (kv.Value == src) return src; // 이미 우리 복사본 bool strength = cfg.demoCloudStrength > 0f && src.HasProperty("_Cloud_Strength"); bool density = cfg.demoCloudDensity > 0f && src.HasProperty("_Cloud_Density"); bool bright = cfg.demoTerrainBrightness > 0f && src.HasProperty("_Brightness") && src.GetFloat("_Brightness") < 0.99f; // 자갈(1.0)은 그대로 if (!strength && !density && !bright) return src; m = new Material(src) { name = src.name + " (WL dungeon=island)" }; if (strength) m.SetFloat("_Cloud_Strength", cfg.demoCloudStrength); if (density) m.SetFloat("_Cloud_Density", cfg.demoCloudDensity); if (bright) m.SetFloat("_Brightness", cfg.demoTerrainBrightness); s_tuned[src] = m; return m; } } }