Project_WL/Assets/WL/Look/Farm/WLDungeonGrassLook.cs

208 lines
14 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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<Material, Material> s_tuned = new Dictionary<Material, Material>();
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<DemoInstancer>(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<Terrain>(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<Light>(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<Light>(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;
/// <summary>#851 — 씬 안 수면(셰이더 이름에 `dungeonWaterShaderKey` 가 든 켜진 렌더러)의 xz 사각형 + 수면 높이.</summary>
static List<WLDungeonGrassInstancer.Pool> WaterPools(WLIslandLookSettings cfg, GameObject[] roots)
{
var key = cfg.dungeonWaterShaderKey;
if (string.IsNullOrEmpty(key)) return null;
var pools = new List<WLDungeonGrassInstancer.Pool>();
for (int r = 0; r < roots.Length; r++)
{
var rs = roots[r].GetComponentsInChildren<Renderer>(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;
}
/// <summary>섬의 `WLDemoTerrain.Tuned` 와 같은 규칙 — 구름 세기·크기·밝기 단계만 덮은 런타임 복사본(원본 재질 0줄).</summary>
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;
}
}
}