diff --git a/Assets/WL/Look/Farm/Materials/Farm_Grass_Demo3.mat b/Assets/WL/Look/Farm/Materials/Farm_Grass_Demo3.mat index 4cd0ce006..e68cbbf57 100644 --- a/Assets/WL/Look/Farm/Materials/Farm_Grass_Demo3.mat +++ b/Assets/WL/Look/Farm/Materials/Farm_Grass_Demo3.mat @@ -138,7 +138,7 @@ Material: - _Color: {r: 1, g: 1, b: 1, a: 1} - _DiffuseColor: {r: 0.62, g: 0.82, b: 0.485, a: 0} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - - _ShadowDiffuseColor: {r: 0.62, g: 0.82, b: 0.485, a: 0} + - _ShadowDiffuseColor: {r: 0.55, g: 0.75, b: 0.47, a: 0} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} - _WindMovement: {r: 6, g: 0, b: 0, a: 0} m_BuildTextureStacks: [] diff --git a/Assets/WL/Look/Farm/WLCloudShadow.cs b/Assets/WL/Look/Farm/WLCloudShadow.cs index 3ad86873b..2ddc12a95 100644 --- a/Assets/WL/Look/Farm/WLCloudShadow.cs +++ b/Assets/WL/Look/Farm/WLCloudShadow.cs @@ -134,6 +134,12 @@ namespace WL.Look.Farm /// 시간만 다시 읽는다(주기 갱신용 — 머티리얼·태양은 그대로). public void Tick() { ResolveSun(); ResolveTime(); } + /// 시간만 다시 읽는다(816zh 띠 갱신용 — 태양·머티리얼은 그대로 · FindObjectsByType 0회). + public void TickTime() { ResolveTime(); } + + /// 머티리얼 `_Cloud_Cover` — 816zh 띠의 중심선(구름 높이가 이 값을 지나는 곳 = 그림자 얼룩의 가장자리). + public float Cover { get { return _cover; } } + /// 셰이더가 쓰는 `_Time.y`. 진단·검증에서 직접 넣어 볼 수 있다. public float TimeValue { get { return _time; } set { _time = value; } } @@ -321,6 +327,72 @@ namespace WL.Look.Farm return _edge[cz * _nx + cx] != 0; } + // ───────────────────────────────────────────────────────────── + // ③-b 816zh — 구름 「높이」 표본 격자 (강도를 걷어 낸 값 · 경계 = cover 를 지나는 선) + // + // Cloudiness = (cover + 0.5·noise) × strength. 그림자 얼룩의 「가장자리」는 노이즈가 0 을 지나는 선 + // = 강도를 걷어 낸 값이 cover 와 같아지는 선이다. 🔴 강도(0.5)를 곱한 값에 |c − cover| 를 재면 + // c 가 cover 에 닿지 못해(최대 ≈ 0.43) 띠가 아예 없어진다 — 그래서 판정은 강도를 걷어 낸 값으로 한다. + // 비싼 6 옥타브 노이즈는 성긴 격자에서만 계산하고(816o 와 같은 원칙) 사이는 겹선형 보간. + // ───────────────────────────────────────────────────────────── + float[] _lv; + int _lx, _lz; + float _l0x, _l0z, _linv; + public int LevelSamples; // 노이즈를 실제로 계산한 점 수(비용은 여기서만 난다) + public float LevelCell; + + /// 범위의 XZ 평면(높이 y)에 구름 높이를 성긴 격자로 굽는다. 칸 수가 상한을 넘으면 간격을 1.25 배씩 키운다. + public void BakeLevels(Bounds b, float y, float sampleCell, int maxSamples) + { + LevelSamples = 0; + if (!Ready) { _lv = null; return; } + if (sampleCell < 0.05f) sampleCell = 0.05f; + float w = Mathf.Max(b.size.x, 0.1f), h = Mathf.Max(b.size.z, 0.1f); + int sx = 2, sz = 2; + for (int guard = 0; guard < 24; guard++) + { + sx = Mathf.CeilToInt(w / sampleCell) + 3; // 한 칸 바깥에서 시작 + 끝 보간 여유 + sz = Mathf.CeilToInt(h / sampleCell) + 3; + if (maxSamples <= 0 || (long)sx * sz <= maxSamples) break; + sampleCell *= 1.25f; + } + _lx = sx; _lz = sz; _linv = 1f / sampleCell; LevelCell = sampleCell; + _l0x = b.min.x - sampleCell; _l0z = b.min.z - sampleCell; + if (_lv == null || _lv.Length < sx * sz) _lv = new float[sx * sz]; + float inv = _strength > 1e-6f ? 1f / _strength : 1f; + for (int j = 0; j < sz; j++) + { + float wz = _l0z + j * sampleCell; + int row = j * sx; + for (int i = 0; i < sx; i++) _lv[row + i] = Cloudiness(_l0x + i * sampleCell, y, wz) * inv; + } + LevelSamples = sx * sz; + } + + /// 표본 격자를 겹선형으로 읽은 구름 높이(= cover + 0.5·noise). 격자 밖은 가장자리 값으로 고정. + public float LevelAt(float wx, float wz) + { + if (_lv == null || LevelSamples == 0) return _cover; + float fx = (wx - _l0x) * _linv, fz = (wz - _l0z) * _linv; + int jx = (int)fx; if (jx < 0) jx = 0; else if (jx > _lx - 2) jx = _lx - 2; + int jz = (int)fz; if (jz < 0) jz = 0; else if (jz > _lz - 2) jz = _lz - 2; + float tx = fx - jx; if (tx < 0f) tx = 0f; else if (tx > 1f) tx = 1f; + float tz = fz - jz; if (tz < 0f) tz = 0f; else if (tz > 1f) tz = 1f; + int r0 = jz * _lx + jx, r1 = r0 + _lx; + float a = _lv[r0] + (_lv[r0 + 1] - _lv[r0]) * tx; + float c = _lv[r1] + (_lv[r1 + 1] - _lv[r1]) * tx; + return a + (c - a) * tz; + } + + /// 띠 가중치 w = smoothstep(clamp01(1 − |높이 − cover| / width)) — 그림자 경계에서 1, 안쪽·바깥쪽은 0. + public float BandWeight(float wx, float wz, float width) + { + float t = 1f - Mathf.Abs(LevelAt(wx, wz) - _cover) / (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줄 수정) // ───────────────────────────────────────────────────────────── diff --git a/Assets/WL/Look/Farm/WLIslandGrass.cs b/Assets/WL/Look/Farm/WLIslandGrass.cs index 61d9969c3..f74f9748e 100644 --- a/Assets/WL/Look/Farm/WLIslandGrass.cs +++ b/Assets/WL/Look/Farm/WLIslandGrass.cs @@ -56,6 +56,12 @@ namespace WL.Look.Farm public static float OutlineRadius, OutlineOverhang, OutlineBakeMs; public static string OutlineWhy = ""; + // 816zh — 구름 그림자 경계 띠(부드러운 성장) 진단 + public static bool CloudBandOn; + public static int BandCandidates, BandCompacted, BandRefreshes, BandSamples; + public static float BandMs, BandSampleCell; + public static string CloudBandWhy = ""; + public WLIslandLookSettings cfg; Bounds _bounds = new Bounds(Vector3.zero, Vector3.one * 16f); @@ -66,7 +72,8 @@ namespace WL.Look.Farm bool _softDirt; // ── 816o 캐시 — 한 번 뽑은 후보를 들고 있다가, 구름이 흐르면 「고르기」만 다시 한다 ── - struct Cand { public float x, z, y; public int def; public float sc; public float yaw; } + // 816zh — band: 1 = 물리 경계 띠 안(항상 배율 1) · 0 = 들판(구름 경계 가중치 w 가 배율을 정한다 · 0~1) + struct Cand { public float x, z, y; public int def; public float sc; public float yaw; public byte band; public float w; } Cand[] _cand; int _candCount; InstancingSettings[] _cset; @@ -77,6 +84,13 @@ namespace WL.Look.Farm bool _fastRefresh; readonly WLCloudShadowField _field = new WLCloudShadowField(); + // ── 816zh — 구름 그림자 경계 띠(부드러운 성장) ── + bool _bandOn; // 이번 캐시가 띠 가중치를 쓰는가(바닥 재질에서 구름을 읽었을 때만) + bool _bandRefresh; // 지금 토글이 「띠 목표 재계산」 갱신인가(816o 굽기·로그는 건너뛴다) + float _bandNext; // 다음 갱신 시각(unscaled) + int _bandPendingFrame = -1; // 프레임 끝 토글을 예약한 프레임(같은 프레임에 두 번 예약 방지 · 코루틴이 죽어도 다음 프레임에 복구) + static readonly WaitForEndOfFrame s_eof = new WaitForEndOfFrame(); + public override Bounds CalculateInstancesBounds() { return _bounds; } /// 다시 깐다(섬이 확장됐을 때). 멱등 — 몇 번 불러도 안전. @@ -132,6 +146,43 @@ namespace WL.Look.Farm CloudRefreshes++; } + /// + /// 816zh — 구름이 흐른 만큼 풀잎 **배율 목표만** 다시 계산해 버퍼를 다시 채운다(좌표·후보 불변 · 팝핑 0). + /// 갱신 주기 = `cloudBandRefreshSeconds`(LateUpdate 가 스스로 돈다). 프로브가 직접 불러도 안전. + /// + public void RefreshCloudBand() + { + if (!_cacheValid || !isActiveAndEnabled || !_bandOn) return; + _fastRefresh = true; _bandRefresh = true; + enabled = false; + enabled = true; + _fastRefresh = false; _bandRefresh = false; + BandRefreshes++; + } + + // 🔴 기반 클래스(InstancesBehaviour)에는 Update·OnEnable·OnDisable 만 있다(private) — LateUpdate 는 여기 정의해도 가려지는 것이 없다. + // OnEnable/OnDisable/Update 를 이 클래스에 정의하면 기반 것이 가려져 버퍼가 안 만들어진다 — 절대 정의하지 말 것. + void LateUpdate() + { + if (!_bandOn || !_cacheValid || cfg == null) return; + if (_bandPendingFrame == Time.frameCount) return; + if (Time.unscaledTime < _bandNext) return; + _bandPendingFrame = Time.frameCount; + StartCoroutine(RefreshCloudBandAtEndOfFrame()); + } + + /// + /// 토글(OnDisable → 버퍼 해제 → OnEnable → 재생성)은 **프레임 끝**에 한다 — 기반 Update 가 이번 프레임 드로우에 이미 넘긴 + /// 버퍼를 렌더링 전에 해제하면 그 프레임의 풀이 통째로 빠져 5 Hz 깜빡임이 된다. + /// + System.Collections.IEnumerator RefreshCloudBandAtEndOfFrame() + { + yield return s_eof; + if (cfg == null) yield break; + _bandNext = Time.unscaledTime + Mathf.Max(0.02f, cfg.cloudBandRefreshSeconds); + RefreshCloudBand(); + } + // ───────────────────────────────────────────────────────────────── // 점 뽑기 — 데모 `TerrainInstancesBehaviour.GetInstanceData` 와 같은 절차 // ───────────────────────────────────────────────────────────────── @@ -145,6 +196,8 @@ namespace WL.Look.Farm BakeMs = 0f; FilterMs = 0f; CloudEdgeOn = false; CloudWhy = ""; _edgeOn = false; OutlineRoundOn = false; OutlineCells = 0; OutlineRadius = 0f; OutlineOverhang = 0f; OutlineBakeMs = 0f; OutlineWhy = ""; _outOn = false; + CloudBandOn = false; CloudBandWhy = ""; _bandOn = false; + BandCandidates = 0; BandCompacted = 0; BandSamples = 0; BandMs = 0f; BandSampleCell = 0f; if (cfg == null) cfg = WLIslandLookSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.grassEnabled == 0) { LastLog = "꺼짐"; return null; } @@ -185,9 +238,20 @@ namespace WL.Look.Farm _bounds = new Bounds((bmin + bmax) * 0.5f, bmax - bmin); _cacheY = tiles[0].center.y; + // ── 816zh — 구름 그림자 경계 띠(부드러운 성장): 바닥 재질의 구름 식을 읽는다(816o 와 같은 필드 · 같은 재질) ── + // 못 읽으면(구름 꺼짐 등) 물리 경계 띠만 = 816ze 그대로. + if (cfg.cloudBandEnabled != 0) + { + var bsrc = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial; + if (_field.Configure(bsrc)) { _bandOn = true; CloudBandOn = true; } + else CloudBandWhy = _field.Why; + } + // ── 816o — 바닥과 **같은 식**으로 구름 그림자 색 띠의 경계를 굽는다 ── + // 816zh 띠가 켜져 있으면 816o(하드 컷 = 순간이동)는 쓰지 않는다 — 둘을 겹치면 팝핑이 되살아난다. float coverage = 1f; - if (cfg.cloudEdgeEnabled != 0) + if (cfg.cloudEdgeEnabled != 0 && _bandOn) CloudWhy = "816zh 구름 띠가 대신한다"; + else if (cfg.cloudEdgeEnabled != 0) { var src = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial; if (_field.Configure(src)) @@ -314,13 +378,25 @@ namespace WL.Look.Farm if (r <= acc) { pick = d; break; } } - // 2026-09-15 PD 「그라데이션과 무관하게 풀이 얼룩덜룩」 — 경계 전용 정의는 경계 띠 밖(들판 한가운데)에는 깔지 않는다. - if (cfg.grassEdgeBand > 0f && defs[pick].edgeOnly_ != 0 && !NearBoundary(tiles, wx, wz, cfg.grassEdgeBand)) { Excluded++; continue; } + // 2026-09-15 PD 「그라데이션과 무관하게 풀이 얼룩덜룩」 — 경계 전용 정의는 물리 경계 띠 밖(들판 한가운데)에는 깔지 않는다. + // 816zh(PD 「그림자를 기준으로」) — 구름 띠가 켜져 있으면 들판에도 후보를 두되 band = 0 으로 표시한다: + // 배율 = 구름 경계 가중치 w(주기마다 목표 재계산 · lerp 로 자라고 줄어듦). 물리 띠 안(band = 1)은 w 와 무관하게 항상 1. + byte band = 1; + if (defs[pick].edgeOnly_ != 0) + { + bool physical = cfg.grassEdgeBand > 0f; + bool near = physical && NearBoundary(tiles, wx, wz, cfg.grassEdgeBand); + if (!near) + { + if (_bandOn) { band = 0; BandCandidates++; } + else if (physical) { Excluded++; continue; } + } + } float sc = 1f + (Frac(h, 3) - 0.5f) * 2f * sv; float yaw = cfg.randomYaw != 0 ? Frac(h, 4) * 360f : 0f; - cands.Add(new Cand { x = wx, z = wz, y = t.center.y, def = baseIdx + pick, sc = sc, yaw = yaw }); + cands.Add(new Cand { x = wx, z = wz, y = t.center.y, def = baseIdx + pick, sc = sc, yaw = yaw, band = band, w = 1f }); } } } @@ -442,15 +518,42 @@ namespace WL.Look.Farm EdgeFraction = _field.EdgeFraction; EdgeDilate = dil; } + /// + /// 816zh — 구름 「높이」(강도를 걷어 낸 cover + 0.5·noise)를 성긴 격자에서 계산해 두고(6 옥타브 노이즈 비용은 여기서만 · + /// 간격·상한은 816o 의 `cloudEdgeSampleCell`/`cloudEdgeMaxSamples` 재사용), 들판 후보마다 목표 배율 + /// w = smoothstep(1 − |높이 − cover| / cloudBandWidth) 을 구한다 — 그림자 경계에서 1, 안쪽·바깥쪽은 0. + /// follow = true 면 현재값이 목표로 `cloudBandLerp` 만큼 다가간다(자라고 줄어듦) · false 면 목표값을 그대로 쓴다(첫 굽기). + /// + void BakeBand(bool follow) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + _field.TickTime(); + _field.BakeLevels(_bounds, _cacheY, cfg.cloudEdgeSampleCell, cfg.cloudEdgeMaxSamples); + float width = Mathf.Max(1e-4f, cfg.cloudBandWidth); + float k = follow ? Mathf.Clamp01(cfg.cloudBandLerp) : 1f; + for (int i = 0; i < _candCount; i++) + { + if (_cand[i].band != 0) continue; + float target = _field.BandWeight(_cand[i].x, _cand[i].z, width); + _cand[i].w += (target - _cand[i].w) * k; + } + sw.Stop(); + BandMs = (float)sw.Elapsed.TotalMilliseconds; + BandSamples = _field.LevelSamples; BandSampleCell = _field.LevelCell; + _bandNext = Time.unscaledTime + Mathf.Max(0.02f, cfg.cloudBandRefreshSeconds); + } + Dictionary> BuildFromCache(bool refresh) { - if (refresh) + if (refresh && !_bandRefresh) { _field.Tick(); BakeField(); } + // 816zh — 첫 굽기는 목표값 그대로(로드 때 자라나지 않는다) · 띠 갱신은 현재값이 목표로 lerp(자라고 줄어듦) · 뷰 갱신은 손대지 않는다 + if (_bandOn && (!refresh || _bandRefresh)) BakeBand(refresh); - Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0; ViewCulled = 0; + Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0; ViewCulled = 0; BandCompacted = 0; // 816q — 카메라가 보는 범위(+여유 폭)만 남긴다. 좌표는 손대지 않는다(순간이동 0). _viewOn = false; @@ -467,6 +570,7 @@ namespace WL.Look.Farm Vector3 bc = _bounds.center; bool edge = _edgeOn; + float minW = Mathf.Max(0f, cfg.cloudBandMinScale); ulong key = 0UL; for (int i = 0; i < _candCount; i++) { @@ -474,8 +578,15 @@ namespace WL.Look.Farm if (_viewOn && !InView(c.x, c.z)) { ViewCulled++; continue; } if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; } + float sc = c.sc; + if (c.band == 0) + { + // 816zh — 들판 풀잎은 구름 경계 가중치가 배율. 거의 0 이면 버퍼에서 뺀다(정점 0 · 압축). + if (c.w < minW) { BandCompacted++; continue; } + sc *= c.w; + } var rot = c.yaw != 0f ? Quaternion.Euler(0f, c.yaw, 0f) : Quaternion.identity; - var trs = Matrix4x4.TRS(new Vector3(c.x, c.y, c.z) - bc, rot, Vector3.one * c.sc); + var trs = Matrix4x4.TRS(new Vector3(c.x, c.y, c.z) - bc, rot, Vector3.one * sc); var def = _cdef[c.def]; trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity, new Vector3(def.scale, def.scale, def.scale)); @@ -516,8 +627,13 @@ namespace WL.Look.Farm + "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2") + "m×" + EdgeSamples + ") · 띠밖 제외 " + CloudExcluded + " · 굽기 " + BakeMs.ToString("F1") + "ms · 고르기 " + FilterMs.ToString("F1") + "ms" - : " · 구름띠 off" + (CloudWhy.Length > 0 ? "(" + CloudWhy + ")" : "")); - cfg.Log(LastLog); + : " · 구름띠 off" + (CloudWhy.Length > 0 ? "(" + CloudWhy + ")" : "")) + + (_bandOn + ? " · 구름경계띠(816zh) 폭 " + cfg.cloudBandWidth.ToString("F2") + " · 들판후보 " + BandCandidates + + " · 압축 " + BandCompacted + " · 표본 " + BandSampleCell.ToString("F2") + "m×" + BandSamples + + " · 굽기 " + BandMs.ToString("F1") + "ms · 갱신 " + BandRefreshes + : " · 구름경계띠(816zh) off" + (CloudBandWhy.Length > 0 ? "(" + CloudBandWhy + ")" : "")); + if (!_bandRefresh) cfg.Log(LastLog); // 816zh — 주기 갱신마다 로그를 찍지 않는다(프로브는 LastLog 를 읽는다) return result.Count == 0 ? null : result; } diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index 6262f70f9..db9085af6 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -370,8 +370,31 @@ namespace WL.Look.Farm // ───────────────────────────────── §1 풀밭 [Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")] [Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " + - "2026-09-15 PD 「그라데이션과 무관하게 풀이 얼룩덜룩」 — 들판 안쪽 풀잎은 같은 색이라도 미세한 음영 차로 얼룩이 되므로 경계 실루엣만 남긴다.")] - public float grassEdgeBand = 1.2f; + "2026-09-15 PD 「그라데이션과 무관하게 풀이 얼룩덜룩」 — 들판 안쪽 풀잎은 같은 색이라도 미세한 음영 차로 얼룩이 되므로 경계 실루엣만 남긴다. " + + "816zh — 구름 경계 띠(`cloudBandEnabled 1`)가 켜지면 들판은 구름 그림자 경계가 맡으므로 이 물리 띠는 0.6 으로 낮춰 병행한다(띠 안은 항상 배율 1).")] + public float grassEdgeBand = 0.6f; + + // ── 816zh: 풀잎을 「구름 그림자(그라데이션) 경계」에 — 크기로 부드럽게 자라고 줄어든다 (PD 2026-09-16 「그림자를 기준으로」) + [Header("§1-816zh — 풀잎 = 구름 그림자 경계 띠 (부드러운 성장 · 팝핑 0)")] + [Tooltip("🔴 되돌리기 — 0 = 지금처럼 `edgeOnly_` 풀잎을 물리 경계 띠(grassEdgeBand)에만. " + + "1 = 들판 전체에 후보를 두고, 구름 그림자 경계(구름 높이가 바닥 재질 `_Cloud_Cover` 를 지나는 선) 근처만 배율을 키워 그린다. " + + "816o(하드 컷 = 구름이 흐르면 순간이동)와 달리 껐다 켜는 게 아니라 **자라고 줄어든다**. 켜지면 816o `cloudEdgeEnabled` 는 무시된다. " + + "구름 식은 `cloudEdgeSourceMaterial`(비우면 `islandTopMaterial`)에서 읽는다 — 바닥과 같은 식.")] + public int cloudBandEnabled = 1; + + [Tooltip("띠 반폭(구름 높이 단위 · m 아님) — 강도를 걷어 낸 구름 높이(cover + 0.5·noise)가 cover 에서 이만큼 벗어나면 배율 0, 경계선 위는 1(smoothstep). " + + "🔴 현 바닥 재질(_Shades 7 · _Brightness 0.25 · 강도 0.5)의 보이는 색 계단은 cover ± 0.143 에 있다 — 0.12 는 그 사이 그라데이션 중심 띠. 계단선까지 덮으려면 0.15~0.18.")] + public float cloudBandWidth = 0.12f; + + [Tooltip("띠 목표 배율을 다시 계산하고 인스턴스 버퍼를 다시 채우는 주기(초). 구름은 초당 약 1.4 m 흐른다(_Cloud_Movement 1,1). " + + "비용 = 노이즈 표본(cloudEdgeSampleCell 간격 · 6 옥타브) + 후보 전수 겹선형 읽기 + 버퍼 재생성, 이 주기마다 1회.")] + public float cloudBandRefreshSeconds = 0.2f; + + [Tooltip("갱신 때마다 현재 배율이 목표로 다가가는 비율(0~1). 작을수록 천천히 자라고 줄어든다 — 0.15 × 0.2 s ≈ 시간상수 1.2 초(구름 이동 약 1.7 m 뒤따름). 1 = 즉시(=팝핑).")] + [Range(0.01f, 1f)] public float cloudBandLerp = 0.15f; + + [Tooltip("이 배율 미만인 들판 풀잎은 버퍼에서 뺀다(정점 0 · 압축). 너무 크면 사라질 때 톡 끊긴다.")] + public float cloudBandMinScale = 0.02f; [Tooltip("0 이면 풀을 깔지 않는다.")] public int grassEnabled = 1; @@ -460,10 +483,10 @@ namespace WL.Look.Farm [Tooltip("🔴 비용이 나는 유일한 곳 — 구름 노이즈(6 옥타브)를 실제로 계산하는 간격(m). " + "구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 면 충분하다. " + - "사이 값은 겹선형 보간으로 채운다.")] + "사이 값은 겹선형 보간으로 채운다. 816zh 구름 경계 띠도 이 간격으로 표본을 뜬다(0.2 s 마다 · 키우면 비용이 제곱으로 준다).")] public float cloudEdgeSampleCell = 1f; - [Tooltip("노이즈를 계산하는 점의 개수 상한. 섬이 커지면 간격을 자동으로 늘려 비용을 묶어 둔다.")] + [Tooltip("노이즈를 계산하는 점의 개수 상한. 섬이 커지면 간격을 자동으로 늘려 비용을 묶어 둔다(816zh 띠도 같은 상한).")] public int cloudEdgeMaxSamples = 4096; [Tooltip("경계에서 몇 칸 더 넓힐 것인가. 띠 폭 ≈ (1 + 2 × 이 값) × 칸(m). " +