diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index 91184f687..70b603569 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -110,7 +110,7 @@ MonoBehaviour: softDirtColor: {r: 0.71, g: 0.706, b: 0.549, a: 1} seabedDepth: 0.6 seabedColor: {r: 0.86, g: 0.86, b: 0.72, a: 1} - skirtGrassBand: 0 + skirtGrassBand: 0.4 skirtInnerKeep: 0.5 skirtTopDrop: 0.01 randomYaw: 0 diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index 0fbc83ee0..6262f70f9 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -356,6 +356,17 @@ namespace WL.Look.Farm [Tooltip("거리장·메시 격자 정점 수 상한. 넘으면 칸을 두 배씩 키운다(섬이 아무리 커져도 비용이 묶인다).")] public int skirtMaxGridNodes = 400000; + [Tooltip("1 = 스커트를 타일과 **같은 Toon 재질**로 그린다(풀 = islandTopMaterial 그대로 · 모래 = 그 복사본에 모래색) → " + + "구름 그림자·조명이 타일과 똑같이 떨어져 이음새가 뜨지 않는다(2026-09-16 PD 「가장자리 경계가 붕 뜬다」 실측: 언릿 스커트가 구름 아래 타일보다 최대 15 % 밝았다). " + + "0 = 전용 언릿 정점색 셰이더(그라데이션은 부드럽지만 구름을 못 받는다).")] + public int skirtToonMode = 1; + + [Tooltip("Toon 모드 모래 재질의 그늘색 배율(모래색 × 이 값) — 경사의 그늘진 면.")] + public float skirtSandShadowMul = 0.72f; + + [Tooltip("Toon 모드에서 풀/모래 경계선을 칸마다 흔드는 폭(m) — 직선이 아니라 들쭉날쭉한 물가. 0 = 직선.")] + public float skirtEdgeJitter = 0.6f; + // ───────────────────────────────── §1 풀밭 [Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")] [Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " + diff --git a/Assets/WL/Look/Farm/WLShoreSkirt.cs b/Assets/WL/Look/Farm/WLShoreSkirt.cs index 76769a80f..47562eebe 100644 --- a/Assets/WL/Look/Farm/WLShoreSkirt.cs +++ b/Assets/WL/Look/Farm/WLShoreSkirt.cs @@ -59,6 +59,10 @@ namespace WL.Look.Farm readonly List _verts = new List(); readonly List _cols = new List(); readonly List _tris = new List(); + readonly List _trisSand = new List(); // Toon 모드 — 모래 서브메시 + static readonly int IdShadowDiffuseColor = Shader.PropertyToID("_ShadowDiffuseColor"); + Material _sandInst; // Toon 모드 — 타일 재질 복사본(모래색) + bool _toon; // 마지막으로 만든 조건 — 같으면 다시 만들지 않는다(1초 폴링·재훑기에서 공짜로 돌게). bool _dirty = true; @@ -71,6 +75,7 @@ namespace WL.Look.Farm void OnDestroy() { if (_inst != null) Destroy(_inst); + if (_sandInst != null) Destroy(_sandInst); if (_mesh != null) Destroy(_mesh); } @@ -121,9 +126,10 @@ namespace WL.Look.Farm float valSig = cfg.skirtWidth * 7.1f + cfg.skirtBottomY * 13.3f + cfg.skirtGrassBand * 3.7f + cfg.skirtSandBlend * 29.3f + cfg.skirtCell * 5.9f + cfg.skirtInnerKeep * 11.9f + cfg.skirtTopDrop * 17.7f + cfg.skirtUnderwaterMul * 31.7f + cfg.skirtMaxGridNodes * 0.001f + topY * 37.3f + waterY * 41.1f - + grass.r * 101f + grass.g * 103f + grass.b * 107f + sand.r * 109f + sand.g * 113f + sand.b * 127f; + + grass.r * 101f + grass.g * 103f + grass.b * 107f + sand.r * 109f + sand.g * 113f + sand.b * 127f + + cfg.skirtToonMode * 53f + cfg.skirtEdgeJitter * 59f + cfg.skirtSandShadowMul * 61f; - bool same = !_dirty && _inst != null && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f; + bool same = !_dirty && (_inst != null || _sandInst != null) && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f; if (same) { _mr.enabled = true; return; } if (!EnsureMaterial()) { _mr.enabled = false; LastLog = "🔴 셰이더를 못 찾음(WL/Shore Skirt · URP Unlit 둘 다)"; return; } @@ -131,7 +137,8 @@ namespace WL.Look.Farm BuildEdges(size, half); float cell = BuildMesh(half, topY, waterY, grass, sand); - _mr.sharedMaterial = _inst; + if (_toon) _mr.sharedMaterials = new[] { cfg.islandTopMaterial, _sandInst }; // 풀 = 타일 재질 그대로 · 모래 = 복사본 + else _mr.sharedMaterial = _inst; _mr.enabled = true; _lastTileSig = tileSig; _lastValSig = valSig; _dirty = false; Rebuilds++; @@ -142,8 +149,8 @@ namespace WL.Look.Farm + (fixedTop ? "(고정 · 실측 " + (float.IsNaN(topMeasured) ? "없음" : topMeasured.ToString("F2")) : "(실측 · 고정값 " + cfg.skirtTopY.ToString("F2")) + ")" + " → 바닥 " + cfg.skirtBottomY.ToString("F2") + " · 물 " + waterY.ToString("F2") + (waterFound ? "" : "(미발견·대체값)") - + " · 격자 " + cell.ToString("F2") + " m · 셰이더 " + (_vertexColor ? ShaderName : "대체 URP Unlit(정점색 없음 · 모래 단색)") - + " · 드로우콜 +1"; + + " · 격자 " + cell.ToString("F2") + " m · 셰이더 " + (_toon ? "Toon 2재질(풀=타일 · 모래=복사본 · 구름 동일)" : _vertexColor ? ShaderName : "대체 URP Unlit(정점색 없음 · 모래 단색)") + + " · 드로우콜 +" + (_toon ? "2" : "1"); cfg.Log(LastLog); } @@ -222,6 +229,20 @@ namespace WL.Look.Farm /// 전용 셰이더(정점색) → 없으면 URP Unlit 모래 단색 대체. 에셋은 만들지 않는다. bool EnsureMaterial() { + // 2026-09-16 Lead — Toon 모드: 타일과 같은 재질 계열이라 구름 그림자·조명이 똑같이 떨어진다(이음새 0). + if (cfg.skirtToonMode != 0 && cfg.islandTopMaterial != null) + { + _toon = true; _vertexColor = false; + if (_sandInst == null) + _sandInst = new Material(cfg.islandTopMaterial) { name = "WL_ShoreSkirt_Sand(runtime)", hideFlags = HideFlags.DontSave }; + var sc = cfg.softDirtColor; sc.a = 1f; + float sm = Mathf.Clamp01(cfg.skirtSandShadowMul); + if (_sandInst.HasProperty(IdDiffuseColor)) _sandInst.SetColor(IdDiffuseColor, sc); + if (_sandInst.HasProperty(IdShadowDiffuseColor)) _sandInst.SetColor(IdShadowDiffuseColor, new Color(sc.r * sm, sc.g * sm, sc.b * sm, 1f)); + if (_sandInst.HasProperty(IdBaseColor)) _sandInst.SetColor(IdBaseColor, sc); + return true; + } + _toon = false; if (_inst != null) return true; Shader sh = cfg.shoreSkirtMaterial != null ? cfg.shoreSkirtMaterial.shader : null; @@ -338,7 +359,8 @@ namespace WL.Look.Farm for (int i = 0; i < nx; i++) dist[j * nx + i] = SignedDist(x0 + i * cell, z, half); } - _verts.Clear(); _cols.Clear(); _tris.Clear(); + _verts.Clear(); _cols.Clear(); _tris.Clear(); _trisSand.Clear(); + float jitter = Mathf.Max(0f, cfg.skirtEdgeJitter); float outerKeep = width + cell * 2f; // 사각형 네 귀가 전부 있어야 하므로 바깥으로 두 칸 여유 for (int j = 0; j < nz; j++) { @@ -374,8 +396,17 @@ namespace WL.Look.Farm float dm = Mathf.Min(Mathf.Min(dist[n], dist[n + 1]), Mathf.Min(dist[n + nx], dist[n + nx + 1])); if (dm >= width) continue; // 바닥판(SpawnSeabed)과 같은 감김 — (x0,z0)→(x0,z1)→(x1,z1) 이 위를 향한다 - _tris.Add(ia); _tris.Add(ic); _tris.Add(id); - _tris.Add(ia); _tris.Add(id); _tris.Add(ib); + var list = _tris; + if (_toon) + { + // Toon 모드 — 풀/모래는 서브메시로 가른다. 문턱 = 풀 띠 + 칸별 흔들림(들쭉날쭉한 물가 · 결정적) + uint h = (uint)(i * 73856093) ^ (uint)(j * 19349663); h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15; + float jit = ((h & 0xffffu) / 65535f - 0.5f) * jitter; + float dq = (dist[n] + dist[n + 1] + dist[n + nx] + dist[n + nx + 1]) * 0.25f; + if (dq >= band + jit) list = _trisSand; + } + list.Add(ia); list.Add(ic); list.Add(id); + list.Add(ia); list.Add(id); list.Add(ib); } } @@ -383,12 +414,14 @@ namespace WL.Look.Farm _mesh.indexFormat = _verts.Count > 65000 ? IndexFormat.UInt32 : IndexFormat.UInt16; _mesh.SetVertices(_verts); _mesh.SetColors(_cols); + _mesh.subMeshCount = _toon ? 2 : 1; _mesh.SetTriangles(_tris, 0); - _mesh.RecalculateNormals(); // DepthNormals 패스용 + if (_toon) _mesh.SetTriangles(_trisSand, 1); + _mesh.RecalculateNormals(); // DepthNormals 패스용 · Toon 조명용 _mesh.RecalculateBounds(); LastVertices = _verts.Count; - LastTriangles = _tris.Count / 3; + LastTriangles = (_tris.Count + _trisSand.Count) / 3; return cell; } }