[WL-816zg/zh] 풀 구역 모서리 둥글림(스커트 열기 연산 R3 · 타일 위 +1 cm 모래) · 풀잎 띠 = 보이는 색 계단선(ContourWeight · cloudBandMode 1) · grassEdgeBand 0 (#816)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-16 09:11:22 +09:00
parent 1362c01540
commit 53bba8b5bd
5 changed files with 69 additions and 4 deletions

View File

@ -107,6 +107,7 @@ MonoBehaviour:
density: 0.05 density: 0.05
positionVariance: 0.5 positionVariance: 0.5
scaleVariance: 0.2 scaleVariance: 0.2
grassEdgeBand: 0
softDirtColor: {r: 0.71, g: 0.706, b: 0.549, a: 1} softDirtColor: {r: 0.71, g: 0.706, b: 0.549, a: 1}
seabedDepth: 0.6 seabedDepth: 0.6
seabedColor: {r: 0.86, g: 0.86, b: 0.72, a: 1} seabedColor: {r: 0.86, g: 0.86, b: 0.72, a: 1}

View File

@ -393,6 +393,32 @@ namespace WL.Look.Farm
return t * t * (3f - 2f * t); return t * t * (3f - 2f * t);
} }
/// <summary>
/// 2026-09-16 PD 「데모는 그림자 그라데이션 **경계 지점**에 풀이 보인다 — 지금은 그림자와의 거리만으로 보인다」.
/// 화면에 실제로 보이는 경계는 ToonRamp 색 띠가 바뀌는 **계단선**(`Band` 가 바뀌는 곳)이다. 노이즈 0선(`BandWeight`)은
/// 계단 사이 그라데이션의 중심이라 그림자 가장자리에서 떨어진 곳에 풀이 생겼다.
/// 계단선의 높이 문턱 T_m = (1 (m brightness)/shades)/strength (m = 1..top1 · top 과 0 으로 뭉치는 칸은 경계가 없다)
/// 까지의 거리로 가중치를 준다: 가장 가까운 계단선 위 1 → width 만큼 떨어지면 0(smoothstep).
/// </summary>
public float ContourWeight(float wx, float wz, float width)
{
if (_strength <= 1e-6f) return 0f;
float lvl = LevelAt(wx, wz);
int top = Mathf.CeilToInt(_shades);
float best = float.MaxValue;
for (int m = 1; m < top; m++)
{
float T = (1f - (m - _brightness) / _shades) / _strength;
float d = Mathf.Abs(lvl - T);
if (d < best) best = d;
}
if (best == float.MaxValue) return 0f;
float t = 1f - best / (width > 1e-4f ? width : 1e-4f);
if (t <= 0f) return 0f;
if (t >= 1f) return 1f;
return t * t * (3f - 2f * t);
}
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// ④ SimplexNoise3D.hlsl `snoise` 이식 (Ashima Arts · MIT · 원본 파일 0줄 수정) // ④ SimplexNoise3D.hlsl `snoise` 이식 (Ashima Arts · MIT · 원본 파일 0줄 수정)
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────

View File

@ -534,7 +534,9 @@ namespace WL.Look.Farm
for (int i = 0; i < _candCount; i++) for (int i = 0; i < _candCount; i++)
{ {
if (_cand[i].band != 0) continue; if (_cand[i].band != 0) continue;
float target = _field.BandWeight(_cand[i].x, _cand[i].z, width); // 2026-09-16 PD — 기본은 화면에 보이는 색 계단선(ContourWeight) · 0 이면 노이즈 0선(BandWeight)
float target = cfg.cloudBandMode != 0 ? _field.ContourWeight(_cand[i].x, _cand[i].z, width)
: _field.BandWeight(_cand[i].x, _cand[i].z, width);
_cand[i].w += (target - _cand[i].w) * k; _cand[i].w += (target - _cand[i].w) * k;
} }
sw.Stop(); sw.Stop();

View File

@ -367,6 +367,10 @@ namespace WL.Look.Farm
[Tooltip("Toon 모드에서 풀/모래 경계선을 칸마다 흔드는 폭(m) — 직선이 아니라 들쭉날쭉한 물가. 0 = 직선.")] [Tooltip("Toon 모드에서 풀/모래 경계선을 칸마다 흔드는 폭(m) — 직선이 아니라 들쭉날쭉한 물가. 0 = 직선.")]
public float skirtEdgeJitter = 0.6f; public float skirtEdgeJitter = 0.6f;
[Tooltip("풀 구역 모서리 둥글림 반지름(m). 모래가 타일 모서리를 이 반지름으로 파고들어(열기 연산) 각진 타일 윤곽을 감춘다 — " +
"타일 윗면 위(+1 cm)에 모래를 얹는다. 풀 인스턴서 `outlineRoundRadius`(3)와 맞추면 풀잎도 같은 윤곽 안에만 깔린다. 0 = 각진 그대로(2026-09-16 PD 「각진 지형이 남는다」).")]
public float skirtGrassRound = 3f;
// ───────────────────────────────── §1 풀밭 // ───────────────────────────────── §1 풀밭
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")] [Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
[Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " + [Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " +
@ -386,6 +390,10 @@ namespace WL.Look.Farm
"🔴 현 바닥 재질(_Shades 7 · _Brightness 0.25 · 강도 0.5)의 보이는 색 계단은 cover ± 0.143 에 있다 — 0.12 는 그 사이 그라데이션 중심 띠. 계단선까지 덮으려면 0.15~0.18.")] "🔴 현 바닥 재질(_Shades 7 · _Brightness 0.25 · 강도 0.5)의 보이는 색 계단은 cover ± 0.143 에 있다 — 0.12 는 그 사이 그라데이션 중심 띠. 계단선까지 덮으려면 0.15~0.18.")]
public float cloudBandWidth = 0.12f; public float cloudBandWidth = 0.12f;
[Tooltip("띠를 어디에 둘 것인가. 1 = **화면에 보이는 색 계단선**(ToonRamp 띠가 바뀌는 문턱 · 데모처럼 그림자 가장자리에 풀 · 2026-09-16 PD) · " +
"0 = 노이즈 0선(cover · 계단 사이 그라데이션 중심 — 그림자에서 떨어져 보인다).")]
public int cloudBandMode = 1;
[Tooltip("띠 목표 배율을 다시 계산하고 인스턴스 버퍼를 다시 채우는 주기(초). 구름은 초당 약 1.4 m 흐른다(_Cloud_Movement 1,1). " + [Tooltip("띠 목표 배율을 다시 계산하고 인스턴스 버퍼를 다시 채우는 주기(초). 구름은 초당 약 1.4 m 흐른다(_Cloud_Movement 1,1). " +
"비용 = 노이즈 표본(cloudEdgeSampleCell 간격 · 6 옥타브) + 후보 전수 겹선형 읽기 + 버퍼 재생성, 이 주기마다 1회.")] "비용 = 노이즈 표본(cloudEdgeSampleCell 간격 · 6 옥타브) + 후보 전수 겹선형 읽기 + 버퍼 재생성, 이 주기마다 1회.")]
public float cloudBandRefreshSeconds = 0.2f; public float cloudBandRefreshSeconds = 0.2f;

View File

@ -127,7 +127,7 @@ namespace WL.Look.Farm
+ cfg.skirtCell * 5.9f + cfg.skirtInnerKeep * 11.9f + cfg.skirtTopDrop * 17.7f + cfg.skirtUnderwaterMul * 31.7f + 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 + 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; + cfg.skirtToonMode * 53f + cfg.skirtEdgeJitter * 59f + cfg.skirtSandShadowMul * 61f + cfg.skirtGrassRound * 67f;
bool same = !_dirty && (_inst != null || _sandInst != 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 (same) { _mr.enabled = true; return; }
@ -359,6 +359,33 @@ namespace WL.Look.Farm
for (int i = 0; i < nx; i++) dist[j * nx + i] = SignedDist(x0 + i * cell, z, half); for (int i = 0; i < nx; i++) dist[j * nx + i] = SignedDist(x0 + i * cell, z, half);
} }
// 2026-09-16 PD 「각진 지형이 남는다」 — 풀 구역 모서리를 R 로 둥글린다(열기 연산 = 침식 E{d ≤ R} 뒤 팽창):
// distR = (E 까지의 거리) R. 직선 변에서는 d 와 같고, 볼록 모서리에서는 반지름 R 원호가 된다.
// E 의 테두리 띠(d ∈ [R 1.5칸, R]) 점만 모아 최소 거리를 재므로 비용은 (노드 수 × 띠 점 수).
float round = Mathf.Max(0f, cfg.skirtGrassRound);
var distR = dist;
if (round > 0.01f)
{
distR = new float[nx * nz];
var ringX = new List<float>(512); var ringZ = new List<float>(512);
float lo = -round - cell * 1.5f, hi = -round;
for (int j = 0; j < nz; j++) for (int i = 0; i < nx; i++) { float dv = dist[j * nx + i]; if (dv <= hi && dv >= lo) { ringX.Add(x0 + i * cell); ringZ.Add(z0 + j * cell); } }
for (int j = 0; j < nz; j++)
{
float z = z0 + j * cell;
for (int i = 0; i < nx; i++)
{
int n = j * nx + i; float dv = dist[n];
if (ringX.Count == 0) { distR[n] = dv; continue; } // 섬이 2R 보다 작으면 둥글림 없음
if (dv <= hi) { distR[n] = dv + round; continue; } // E 안쪽
float x = x0 + i * cell; float best = float.MaxValue;
for (int k = 0; k < ringX.Count; k++) { float dx = x - ringX[k], dz = z - ringZ[k]; float d2 = dx * dx + dz * dz; if (d2 < best) best = d2; }
distR[n] = Mathf.Sqrt(best) - round;
}
}
keep = Mathf.Max(keep, round * 0.3f + cell); // 모서리가 파고드는 깊이 R(11/√2) ≈ 0.29R 까지 안쪽 노드 필요
}
_verts.Clear(); _cols.Clear(); _tris.Clear(); _trisSand.Clear(); _verts.Clear(); _cols.Clear(); _tris.Clear(); _trisSand.Clear();
float jitter = Mathf.Max(0f, cfg.skirtEdgeJitter); float jitter = Mathf.Max(0f, cfg.skirtEdgeJitter);
float outerKeep = width + cell * 2f; // 사각형 네 귀가 전부 있어야 하므로 바깥으로 두 칸 여유 float outerKeep = width + cell * 2f; // 사각형 네 귀가 전부 있어야 하므로 바깥으로 두 칸 여유
@ -372,7 +399,8 @@ namespace WL.Look.Farm
float t = Mathf.Clamp01(d / width); float t = Mathf.Clamp01(d / width);
t = t * t * (3f - 2f * t); // smoothstep — 윗면에서도 바닥에서도 접선이 수평(완만) t = t * t * (3f - 2f * t); // smoothstep — 윗면에서도 바닥에서도 접선이 수평(완만)
float y = top + (bottom - top) * t; // 타일 안(d<0)은 윗면 **위** 1 cm — 풀 서브메시는 타일 재질 그대로라 보이지 않고, 모래 서브메시가 모서리 위에 얹힌다(홈·립도 덮는다)
float y = (_toon && d < 0f) ? topY + 0.01f : top + (bottom - top) * t;
index[n] = _verts.Count; index[n] = _verts.Count;
_verts.Add(new Vector3(x0 + i * cell, y, z0 + j * cell)); _verts.Add(new Vector3(x0 + i * cell, y, z0 + j * cell));
@ -402,7 +430,7 @@ namespace WL.Look.Farm
// Toon 모드 — 풀/모래는 서브메시로 가른다. 문턱 = 풀 띠 + 칸별 흔들림(들쭉날쭉한 물가 · 결정적) // Toon 모드 — 풀/모래는 서브메시로 가른다. 문턱 = 풀 띠 + 칸별 흔들림(들쭉날쭉한 물가 · 결정적)
uint h = (uint)(i * 73856093) ^ (uint)(j * 19349663); h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15; 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 jit = ((h & 0xffffu) / 65535f - 0.5f) * jitter;
float dq = (dist[n] + dist[n + 1] + dist[n + nx] + dist[n + nx + 1]) * 0.25f; float dq = (distR[n] + distR[n + 1] + distR[n + nx] + distR[n + nx + 1]) * 0.25f; // 둥글린 거리로 가른다
if (dq >= band + jit) list = _trisSand; if (dq >= band + jit) list = _trisSand;
} }
list.Add(ia); list.Add(ic); list.Add(id); list.Add(ia); list.Add(ic); list.Add(id);