From da6854461d2e9662422a93a7ea5ac7a86f278bb6 Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 16 Sep 2026 22:35:36 +0900 Subject: [PATCH] =?UTF-8?q?[WL]=20=EC=84=AC=20=EA=B0=80=EC=9E=A5=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=81=EC=A7=90=C2=B7=EA=B2=BD=EA=B3=84=20?= =?UTF-8?q?=EC=99=84=ED=99=94=20=E2=80=94=20=EA=B1=B0=ED=92=88=20=EB=9D=A0?= =?UTF-8?q?=20v=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A0=9C=EA=B1=B0(=EA=B3=84?= =?UTF-8?q?=EB=8B=A8=C2=B7=EC=82=AC=EC=84=A0=20=EC=9B=90=EC=9D=B8)=20?= =?UTF-8?q?=C2=B7=20=EB=AC=BC=EA=B0=80=EC=84=A0=20=EC=9C=A0=ED=81=B4?= =?UTF-8?q?=EB=A6=AC=EB=93=9C=20=EA=B1=B0=EB=A6=AC=20=EB=9D=A0=20=C2=B7=20?= =?UTF-8?q?=ED=8E=84=EB=A6=B0=20=EB=85=B8=EC=9D=B4=EC=A6=88(=EA=B0=92=20?= =?UTF-8?q?=EB=85=B8=EC=9D=B4=EC=A6=88=20=EA=B2=A9=EC=9E=90=20=EB=AC=B4?= =?UTF-8?q?=EB=8A=AC=20=EC=A0=9C=EA=B1=B0)=20=C2=B7=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=202=20m=20=EC=95=88=EC=AA=BD=2030=20%=20+=20=EC=9E=94?= =?UTF-8?q?=EA=B5=BD=EC=9D=B4=20=C2=B7=20SSAO=20=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=ED=94=84=20=EC=84=B8=EC=85=98=20=ED=91=9C=EC=8B=9D=20(#816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WLShoreFoam: 물가선 모드 = 높이 차 → 수평 유클리드 거리(물가선 교점 414) · 런타임 텍스처 알파 제곱 감쇠·최대 0.75 · 격자 0.25 · **v 방향 스크롤 0**(이분 실측: 이것이 「각진 부분 사이의 사선」) - WLDemoTerrain: GradNoise(펄린 · 16방향 표) 로 굽이·경사 폭·굴곡·둔덕 전부 교체 · 물가선 ±0.05~0.4 m 굴곡 0(notch) · 바다 바닥 굴곡 0 · 페이드 안쪽 비율 · 잔굽이 옥타브 · 픽셀 오차 값(0.05 허용) - WLSkyScope: SSAO 끔 표식을 SessionState 에 두어 Play 중 컴파일(도메인 리로드) 뒤에도 복원 · EnteredEditMode 복원 - 값: terrainBlend 2 · terrainBlendInward 0.3 · terrainEdgeNoise 0.35/1 · terrainSlopeVar 0.25/7 · terrainBumpAmp 0.18/3 · terrainSeabedBumpAmp 0 · shoreFoamUp 0.3 · shoreFoamDown 2 · shoreFoamCell 0.25 · softDirtNoise 0.6 · softDirtFeather 2 Co-Authored-By: Claude Fable 5.1 --- .../Resources/WL/WLIslandLookSettings.asset | 25 ++++-- Assets/WL/Look/Farm/WLDemoTerrain.cs | 83 +++++++++++++++---- Assets/WL/Look/Farm/WLIslandLookSettings.cs | 51 ++++++++---- Assets/WL/Look/Farm/WLShoreFoam.cs | 59 +++++++++++-- Assets/WL/Look/Farm/WLSkyScope.cs | 49 +++++++++-- 5 files changed, 209 insertions(+), 58 deletions(-) diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index e1cb91117..cf3b9332e 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -75,12 +75,12 @@ MonoBehaviour: waterFrom: {fileID: 2100000, guid: ce0f8fd131f36964dbc918b8c63f838b, type: 2} waterTo: {fileID: 2100000, guid: 7c41d0e5b2a94f6d8e3517c0aa96b41d, type: 2} shoreFoamEnabled: 1 - shoreFoamUp: 0.08 - shoreFoamDown: 0.25 + shoreFoamUp: 0.3 + shoreFoamDown: 2 shoreFoamMaterial: {fileID: 2100000, guid: d24a4c5ce16646c409631c112b57f2cf, type: 2} shoreFoamWidth: 1.6 shoreFoamInner: 0.5 - shoreFoamCell: 0.5 + shoreFoamCell: 0.25 shoreFoamY: 0.02 shoreFoamFallbackY: -1 shoreFoamTileLength: 6 @@ -118,6 +118,8 @@ MonoBehaviour: grassEdgeBand: 0 cloudBandEnabled: 0 softDirtColor: {r: 0.71, g: 0.706, b: 0.549, a: 1} + softDirtNoise: 0.6 + softDirtFeather: 2 seabedDepth: 0.6 seabedColor: {r: 0.86, g: 0.86, b: 0.72, a: 1} skirtGrassBand: 0.4 @@ -408,14 +410,19 @@ MonoBehaviour: terrainAlphaRes: 256 terrainMargin: 8 terrainMaxSize: 128 - terrainBlend: 1 + terrainBlend: 2 + terrainBlendInward: 0.3 + terrainEdgeNoiseAmp: 0.35 + terrainEdgeNoiseCell: 1 + terrainPixelError: 1 + terrainBumpWaterNotch: 0.4 terrainBottomY: -1.8 terrainSlope: 5 - terrainSlopeVar: 0.35 - terrainSlopeVarCell: 5 - terrainBumpAmp: 0.25 - terrainBumpCell: 2.5 - terrainSeabedBumpAmp: 0.12 + terrainSlopeVar: 0.25 + terrainSlopeVarCell: 7 + terrainBumpAmp: 0.18 + terrainBumpCell: 3 + terrainSeabedBumpAmp: 0 terrainSeabedBumpCell: 3.5 terrainReliefAmp: 0.5 terrainReliefCell: 4.5 diff --git a/Assets/WL/Look/Farm/WLDemoTerrain.cs b/Assets/WL/Look/Farm/WLDemoTerrain.cs index 3c0d943e0..dcbb27474 100644 --- a/Assets/WL/Look/Farm/WLDemoTerrain.cs +++ b/Assets/WL/Look/Farm/WLDemoTerrain.cs @@ -127,7 +127,8 @@ namespace WL.Look.Farm + cfg.terrainSlopeVar * 127f + cfg.terrainSlopeVarCell * 131f + cfg.terrainBumpAmp * 137f + cfg.terrainBumpCell * 139f + cfg.terrainSeabedBumpAmp * 149f + cfg.terrainSeabedBumpCell * 151f + cfg.terrainReliefAmp * 157f + cfg.terrainReliefCell * 163f + cfg.terrainReliefEdgeFade * 167f + cfg.terrainReliefFlatRadius * 173f - + cfg.terrainReliefFeather * 179f + cfg.terrainReliefCollider * 181f + CollectFlatSpots(cfg) * 191f; + + cfg.terrainReliefFeather * 179f + cfg.terrainReliefCollider * 181f + CollectFlatSpots(cfg) * 191f + + cfg.terrainBlendInward * 193f + cfg.terrainEdgeNoiseAmp * 197f + cfg.terrainEdgeNoiseCell * 199f + cfg.terrainBumpWaterNotch * 211f + WaterY() * 223f; bool same = !_dirty && _data != null && _child != null && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f; @@ -154,7 +155,8 @@ namespace WL.Look.Farm + " · 윗면 y " + topY.ToString("F2") + (fixedTop ? "(고정)" : "(실측)") + " → 바닥 " + cfg.terrainBottomY.ToString("F2") + " · 경사 " + cfg.terrainSlope.ToString("F1") + " m(폭 ±" + (cfg.terrainSlopeVar * 100f).ToString("F0") + " % · 굴곡 ±" + cfg.terrainBumpAmp.ToString("F2") + " m · 바닥 굴곡 ±" + cfg.terrainSeabedBumpAmp.ToString("F2") + " m)" + " · 둔덕 0~" + cfg.terrainReliefAmp.ToString("F2") + " m(격자 " + cfg.terrainReliefCell.ToString("F1") + " · 평평 자리 " + _flatX.Count + " · 콜라이더 " + (LastReliefTris > 0 ? LastReliefTris + "△" : "없음") + ")" - + " · 잔디 경계 R " + cfg.skirtGrassRound.ToString("F1") + " 굽이 ±" + cfg.skirtWaveAmp.ToString("F1") + + " · 잔디 경계 R " + cfg.skirtGrassRound.ToString("F1") + " 굽이 ±" + cfg.skirtWaveAmp.ToString("F1") + "+잔굽이 ±" + cfg.terrainEdgeNoiseAmp.ToString("F2") + + " · 페이드 " + cfg.terrainBlend.ToString("F1") + " m(안쪽 " + (cfg.terrainBlendInward * 100f).ToString("F0") + " %) · 픽셀 오차 " + cfg.terrainPixelError.ToString("F0") + " · 밭 " + (zoneValid ? (zone.size.x).ToString("F1") + "×" + (zone.size.z).ToString("F1") + " m" : "미확정") + " · 굽기 " + LastBakeMs.ToString("F1") + " ms · 인스턴서 " + LastInstancerMs.ToString("F1") + " ms(" + instLog + ")" + " · 드로우콜 터레인 1(+그림자 패스) + 인다이렉트 3 · 타일 윗면 숨김 " + _hiddenTops.Count; @@ -421,22 +423,58 @@ namespace WL.Look.Farm return (h & 0xffffu) / 65535f; } - /// 값 노이즈 −1~+1(격자 cell · 겹선형 · 결정적). seed 0 = 경계 굽이(WLShoreSkirt 와 같은 식) · 다른 seed 는 격자를 옮겨 독립된 무늬. - static float ValueNoise(float x, float z, float cell, int seed) + /// + /// 그라디언트(펄린) 노이즈 −1~+1(격자 cell · 5차 보간 · 결정적). seed 마다 격자를 옮겨 독립된 무늬. + /// 🔴 2026-09-16 PD 「가장자리가 각졌다」 실측: 값 노이즈(격자 값 겹선형)는 등고선이 **격자에 맞춘 둥근 네모**라 토온 단계·물 깊이 색이 + /// 그 등고선을 그대로 드러내 계단·다각형이 됐다(거품·그림자·굴곡 끔 A/B 로 배제). 펄린은 등고선이 유기적 곡선. + /// + static float GradNoise(float x, float z, float cell, int seed) { if (cell <= 0.1f) return 0f; float qx = x / cell, qz = z / cell; - int gx = Mathf.FloorToInt(qx) + seed * 1013, gz = Mathf.FloorToInt(qz) + seed * 2027; float fx = qx - Mathf.Floor(qx), fz = qz - Mathf.Floor(qz); - fx = fx * fx * (3f - 2f * fx); fz = fz * fz * (3f - 2f * fz); - float v00 = Hash01(gx, gz), v10 = Hash01(gx + 1, gz), v01 = Hash01(gx, gz + 1), v11 = Hash01(gx + 1, gz + 1); - return (Mathf.Lerp(Mathf.Lerp(v00, v10, fx), Mathf.Lerp(v01, v11, fx), fz) - 0.5f) * 2f; + int gx = Mathf.FloorToInt(qx), gz = Mathf.FloorToInt(qz); + float fx = qx - gx, fz = qz - gz; + int ox = seed * 1013, oz = seed * 2027; + float n00 = Grad(gx + ox, gz + oz, fx, fz), n10 = Grad(gx + 1 + ox, gz + oz, fx - 1f, fz); + float n01 = Grad(gx + ox, gz + 1 + oz, fx, fz - 1f), n11 = Grad(gx + 1 + ox, gz + 1 + oz, fx - 1f, fz - 1f); + float ux = fx * fx * fx * (fx * (fx * 6f - 15f) + 10f), uz = fz * fz * fz * (fz * (fz * 6f - 15f) + 10f); + float v = Mathf.Lerp(Mathf.Lerp(n00, n10, ux), Mathf.Lerp(n01, n11, ux), uz); + return Mathf.Clamp(v * 1.6f, -1f, 1f); // 2D 펄린 범위 ≈ ±0.7 → ±1 근처로 } - /// 경계 굽이 — 값 노이즈(격자 skirtWaveCell) × 진폭. 직선 변을 파도처럼 굽이치게 한다(WLShoreSkirt 와 같은 식). + // 16방향 단위 벡터 표(해시로 고른다 · cos/sin 호출 없음 — 굽기 133 → ≈100 ms) + static readonly float[] s_gx = { 1f, 0.9239f, 0.7071f, 0.3827f, 0f, -0.3827f, -0.7071f, -0.9239f, -1f, -0.9239f, -0.7071f, -0.3827f, 0f, 0.3827f, 0.7071f, 0.9239f }; + static readonly float[] s_gz = { 0f, 0.3827f, 0.7071f, 0.9239f, 1f, 0.9239f, 0.7071f, 0.3827f, 0f, -0.3827f, -0.7071f, -0.9239f, -1f, -0.9239f, -0.7071f, -0.3827f }; + static float Grad(int ix, int iz, float dx, float dz) + { + int k = (int)(Hash01(ix, iz) * 15.999f); + return s_gx[k] * dx + s_gz[k] * dz; + } + + /// 경계 굽이 — 큰 굽이(격자 skirtWaveCell × skirtWaveAmp · WLShoreSkirt 와 같은 식) + 잔 굽이(terrainEdgeNoiseCell × Amp · 붓 자국). 직선 변을 굽이치게 한다. float Wave(float x, float z) { - if (cfg.skirtWaveAmp <= 0f || cfg.skirtWaveCell <= 0.1f) return 0f; - return ValueNoise(x, z, cfg.skirtWaveCell, 0) * cfg.skirtWaveAmp; + float w = 0f; + if (cfg.skirtWaveAmp > 0f && cfg.skirtWaveCell > 0.1f) w += GradNoise(x, z, cfg.skirtWaveCell, 0) * cfg.skirtWaveAmp; + if (cfg.terrainEdgeNoiseAmp > 0f && cfg.terrainEdgeNoiseCell > 0.1f) w += GradNoise(x, z, cfg.terrainEdgeNoiseCell, 5) * cfg.terrainEdgeNoiseAmp; + return w; + } + + /// 물 평면 높이 — 섬 씬의 `Water`(루트/자식) · 없으면 shoreFoamFallbackY. + float WaterY() + { + var scene = gameObject.scene; + if (scene.IsValid()) + { + var roots = scene.GetRootGameObjects(); + for (int i = 0; i < roots.Length; i++) + { + if (roots[i].name == "Water") return roots[i].transform.position.y; + var t = roots[i].transform.Find("Water"); + if (t != null) return t.position.y; + } + } + return cfg.shoreFoamFallbackY; } /// @@ -539,7 +577,9 @@ namespace WL.Look.Farm BuildEdges(size, half); float round = Mathf.Max(0f, cfg.skirtGrassRound); float cellA = Mathf.Max(w / aRes, l / aRes); - float far = slope * (1f + slopeVar) + seaFade + blend + Mathf.Max(0f, cfg.skirtWaveAmp) + cellA * 2f; // 이보다 먼 바깥은 둥글림이 높이·스플랫에 영향 없음 + float far = slope * (1f + slopeVar) + seaFade + blend + Mathf.Max(0f, cfg.skirtWaveAmp) + Mathf.Max(0f, cfg.terrainEdgeNoiseAmp) + cellA * 2f; // 이보다 먼 바깥은 둥글림이 높이·스플랫에 영향 없음 + float blendIn = blend * Mathf.Clamp(cfg.terrainBlendInward, 0f, 0.5f); // 페이드가 잔디 경계 안쪽으로 들어오는 길이 + float waterY = WaterY(), notch = Mathf.Max(0f, cfg.terrainBumpWaterNotch); // 🔴 2026-09-16 Lead 실측: 링 0.125 m 간격(632점) × 스플랫 65k 텍셀 = 굽기 851 ms → 상인 설치 때 화면이 멎어 「지형이 사라졌다 보임」. // 링은 0.5 m 간격(≈1/4)으로, 경계 거리 s 는 높이맵 격자(0.25 m)에서 한 번만 계산해 스플랫은 겹선형으로 읽는다(blend 1 m 에 충분). GatherRing(size, half, minX, minZ, maxX, maxZ, Mathf.Max(0.5f, Mathf.Min(w / aRes, l / aRes)), round); @@ -585,26 +625,32 @@ namespace WL.Look.Farm if (reliefAmp > 0f) { float env = ReliefEnv(x, z, s, reliefFade, reliefFeather, zoneValid, zcx, zcz, zhx, zhz, zrad, znoise, zcell); - if (env > 0f) y += env * reliefAmp * (ValueNoise(x, z, reliefCell, 4) * 0.5f + 0.5f); + if (env > 0f) y += env * reliefAmp * (GradNoise(x, z, reliefCell, 4) * 0.5f + 0.5f); } } else { // 경사 폭을 자리마다 ±slopeVar 로 흔든다 → 해안이 넓어졌다 좁아졌다 한다(굽이와 별개의 무늬). - float slopeL = slope * (1f + slopeVar * ValueNoise(x, z, varCell, 1)); + float slopeL = slope * (1f + slopeVar * GradNoise(x, z, varCell, 1)); if (slopeL < 0.5f) slopeL = 0.5f; float t = s / slopeL; if (t < 1f) { y = Mathf.Lerp(topY, bottom, SmoothStep01(t)); // 능선·골 — sin(πt) 로 양 끝(윗면·바닥)에서 0 → 걷는 면 위로 솟지 않고 바닥과도 이어진다. - if (bumpAmp > 0f) y += Mathf.Sin(t * Mathf.PI) * bumpAmp * ValueNoise(x, z, bumpCell, 2); + // 🔴 물가선(y = 물 높이) 근처에서는 0 — 굴곡이 물가선에 걸치면 선이 지그재그·계단이 된다(2026-09-16 PD 「각졌다」 실측). + if (bumpAmp > 0f) + { + float env = Mathf.Sin(t * Mathf.PI); + if (notch > 0f) env *= SmoothStep01((Mathf.Abs(y - waterY) - 0.05f) / notch); + y += env * bumpAmp * GradNoise(x, z, bumpCell, 2); + } } else { y = bottom; // 바다 바닥 — 경사 끝에서 seaFade 에 걸쳐 굴곡이 자란다(끝점 연속). - if (seaAmp > 0f) y += SmoothStep01((s - slopeL) / seaFade) * seaAmp * ValueNoise(x, z, seaCell, 3); + if (seaAmp > 0f) y += SmoothStep01((s - slopeL) / seaFade) * seaAmp * GradNoise(x, z, seaCell, 3); } } heights[j, i] = Mathf.Clamp01((y - baseY) / span); @@ -624,7 +670,7 @@ namespace WL.Look.Farm int gi = (int)gx, gj = (int)gz; float tx = gx - gi, tz = gz - gj; float s00 = sGrid[gj * hRes + gi], s10 = sGrid[gj * hRes + gi + 1], s01 = sGrid[(gj + 1) * hRes + gi], s11 = sGrid[(gj + 1) * hRes + gi + 1]; float s = Mathf.Lerp(Mathf.Lerp(s00, s10, tx), Mathf.Lerp(s01, s11, tx), tz); - float grass = 1f - SmoothStep01(s / blend); // s ≤ 0 잔디 1 → s ≥ blend 잔디 0(모래) + float grass = 1f - SmoothStep01((s + blendIn) / blend); // s ≤ −blendIn 잔디 1 → s ≥ blend − blendIn 잔디 0(모래) · 풀잎 선(50 %) = blend/2 − blendIn float dirt = 0f; if (zoneValid && Mathf.Abs(x - zcx) <= zhx + znoise + feather && Mathf.Abs(z - zcz) <= zhz + znoise + feather) { @@ -687,6 +733,9 @@ namespace WL.Look.Farm if (_terrain.materialTemplate != tmat) _terrain.materialTemplate = tmat; if (_terrain.drawInstanced) _terrain.drawInstanced = false; // Demo.unity 1228 m_DrawInstanced 0 · 그 외 Terrain 값은 데모 = 컴포넌트 기본값 if (_terrain.allowAutoConnect) _terrain.allowAutoConnect = false; + // 🔴 2026-09-16 PD 「가장자리가 각졌다」 — 데모(5)와 같은 픽셀 오차는 200 m 언덕용. 32 m 섬을 12 m 에서 보면 경사가 큰 삼각형으로 뭉개져 토온 음영이 각진 면이 된다 → 1. + float pe = Mathf.Clamp(cfg.terrainPixelError, 0.05f, 200f); // API 는 1 미만도 받는다(실측 0.05 반영) — 인스펙터 슬라이더 하한과 다름 + if (!Mathf.Approximately(_terrain.heightmapPixelError, pe)) _terrain.heightmapPixelError = pe; } // ───────────────────────────────────────────────────────────────── diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index f18602380..accc7affd 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -257,8 +257,8 @@ namespace WL.Look.Farm [Tooltip("섬 안쪽으로 겹쳐 들어가는 길이(m) — 물가에 틈이 생기지 않게. 섬 벽이 가려 준다.")] public float shoreFoamInner = 0.5f; - [Tooltip("띠 메시의 격자 간격(m). 크면 싸고 거칠다. 비용이 문제면 이것부터 키운다.")] - public float shoreFoamCell = 0.5f; + [Tooltip("띠 메시의 격자 간격(m). 크면 싸고 거칠다. 비용이 문제면 이것부터 키운다. 2026-09-16 물가선 모드: 0.5 → 0.25(경계 계단 완화 · 삼각형 ≈ 3천).")] + public float shoreFoamCell = 0.25f; [Tooltip("물 표면보다 얼마나 위에 깔 것인가(m). 너무 작으면 z-fighting.")] public float shoreFoamY = 0.02f; @@ -273,11 +273,13 @@ namespace WL.Look.Farm public float shoreFoamScroll = 0.06f; // ── 2026-09-16 PD 「물이 섬 경계에 닿을 때 자연스러운 물보라가 안 보인다」 — 데모 터레인 모드에서는 타일 발자국 대신 **터레인 물가선**(지형 높이 = 물 높이)을 따른다 - [Tooltip("데모 터레인 모드의 물가 띠 — 물가선에서 **모래 쪽**으로 지형이 물보다 이만큼(m · 높이) 높은 곳까지 거품이 올라온다(완만한 곳은 넓게 · 가파른 곳은 좁게 = 자연스러운 물보라).")] - public float shoreFoamUp = 0.08f; + [Tooltip("데모 터레인 모드의 물가 띠 — 물가선에서 **모래 쪽**으로 이만큼(m · 수평 거리)까지 거품이 올라온다. " + + "2026-09-16 PD 「가장자리가 각졌다」: 높이 차로 정하던 띠가 평평한 곳에서 흰 계단 덩어리가 됐다 → 수평 거리(어디서나 같은 폭)로 바꿈.")] + public float shoreFoamUp = 0.3f; - [Tooltip("데모 터레인 모드의 물가 띠 — 물가선에서 **바다 쪽**으로 수심 이만큼(m)까지 거품이 퍼진다.")] - public float shoreFoamDown = 0.25f; + [Tooltip("데모 터레인 모드의 물가 띠 — 물가선에서 **바다 쪽**으로 이만큼(m · 수평 거리)까지 메시가 뻗는다. 보이는 거품은 그 60 % 안(알파 제곱 감쇠)이고 끝은 투명 → 메시 경계가 안 보인다. " + + "2026-09-16 실측: 1.2 m 에 알파가 끝까지 남아 격자 칸 경계가 네모 계단으로 보였다 → 2 m.")] + public float shoreFoamDown = 2f; // ───────────────────────────────── §816zf 부드러운 흙 데칼 [Header("§816zf — 흙 자리를 데모처럼 「둥글게 번지게」 (PD 2026-09-15)")] @@ -440,8 +442,26 @@ namespace WL.Look.Farm [Tooltip("터레인 한 변 상한(m). 발자국 + 여백이 이보다 크면 발자국 중심 기준으로 자른다.")] public float terrainMaxSize = 128f; - [Tooltip("스플랫 잔디→모래 페이드 폭(m). 잔디 경계(둥글림+굽이)에서 바깥으로 이만큼에 걸쳐 1→0.")] - public float terrainBlend = 1.0f; + [Tooltip("스플랫 잔디→모래 페이드 폭(m). 잔디 경계(둥글림+굽이)에서 이만큼에 걸쳐 1→0. " + + "2026-09-16 PD 「흙·풀 경계가 너무 드로잉된 느낌」 → 1 → 2(데모의 붓으로 칠한 듯한 넓은 경계).")] + public float terrainBlend = 2.0f; + + [Tooltip("페이드 폭 중 잔디 경계 **안쪽**으로 들어오는 비율(0~0.5). 0.3 = 안쪽 0.6 m 부터 모래색이 스미기 시작 · 풀잎이 멈추는 선(잔디 50 %)은 경계 바깥 0.4 m 로 거의 그대로. " + + "0 = 예전처럼 경계 바깥으로만 페이드(풀잎 선이 폭/2 만큼 밖으로 밀린다).")] + [Range(0f, 0.5f)] public float terrainBlendInward = 0.3f; + + [Tooltip("잔디 경계에 얹는 **잔 굽이**(m · 값 노이즈 2번째 옥타브). 큰 굽이(skirtWaveAmp/Cell)만 있으면 매끈한 곡선이라 「그려 넣은 선」처럼 보인다 → 붓 자국처럼 잘게 흔든다. 0 = 없음.")] + public float terrainEdgeNoiseAmp = 0.35f; + + [Tooltip("잔 굽이 노이즈 격자(m).")] + public float terrainEdgeNoiseCell = 1.0f; + + [Tooltip("지형 렌더 LOD 허용 오차(픽셀 · Unity Terrain heightmapPixelError · 기본 5). 2026-09-16 PD 「가장자리가 각졌다」: 5 면 12 m 카메라에서 해변 경사가 큰 삼각형으로 뭉개져 토온 음영이 각진 면으로 보였다 → 1.")] + public float terrainPixelError = 1f; + + [Tooltip("경사면 굴곡(terrainBumpAmp)을 **물가선 근처에서는 0** 으로 죽이는 높이 폭(m). 물 높이 ±0.05 m 에서 0 → 이 값에서 100 %. " + + "굴곡이 물가선에 걸치면 물가선이 지그재그·계단이 되고 거품 띠가 덩어리진다(2026-09-16 실측). 0 = 안 죽임.")] + public float terrainBumpWaterNotch = 0.4f; [Tooltip("경사 바깥 끝 = 바다 바닥 높이(월드 y · m). 물(−1.0) 아래.")] public float terrainBottomY = -1.8f; @@ -452,19 +472,20 @@ namespace WL.Look.Farm // ── 2026-09-16 PD 「섬 가장자리와 바다로 이어지는 부분을 자연스러운 경사면으로 · 굴곡이 보이게」 [Tooltip("경사 폭을 자리마다 이 비율(±)로 흔든다(값 노이즈 · 격자 terrainSlopeVarCell). 0.35 = 폭 ×0.65~1.35 → 해안이 넓어졌다 좁아졌다 한다. 0 = 균일.")] - [Range(0f, 0.8f)] public float terrainSlopeVar = 0.35f; + [Range(0f, 0.8f)] public float terrainSlopeVar = 0.25f; // 2026-09-16 「각짐」 완화: 0.35/5 m → 0.25/7 m(옆으로 꺾이는 주름이 완만) [Tooltip("경사 폭 흔들기 노이즈 격자(m).")] - public float terrainSlopeVarCell = 5f; + public float terrainSlopeVarCell = 7f; - [Tooltip("경사면 위에 얹는 능선·골 굴곡 진폭(m · 경사 양 끝에서 0 이라 윗면·바닥과 이어진다). 데모 지형처럼 법선 음영이 굴곡을 드러낸다. 0 = 매끈한 smoothstep.")] - public float terrainBumpAmp = 0.25f; + [Tooltip("경사면 위에 얹는 능선·골 굴곡 진폭(m · 경사 양 끝·물가선에서 0). 데모 지형처럼 법선 음영이 굴곡을 드러낸다. 0 = 매끈한 smoothstep.")] + public float terrainBumpAmp = 0.18f; [Tooltip("굴곡 노이즈 격자(m).")] - public float terrainBumpCell = 2.5f; + public float terrainBumpCell = 3f; - [Tooltip("경사 밖 바다 바닥의 완만한 굴곡 진폭(m · 경사 끝에서 2 m 에 걸쳐 서서히 커진다). 물이 깊이로 색을 정하므로 얕은 곳/깊은 곳 얼룩으로 보인다. 0 = 평평.")] - public float terrainSeabedBumpAmp = 0.12f; + [Tooltip("경사 밖 바다 바닥의 완만한 굴곡 진폭(m · 경사 끝에서 2 m 에 걸쳐 서서히 커진다). " + + "🔴 2026-09-16 실측: 물 셰이더가 깊이 색을 계단으로 양자화해 바닥 굴곡의 등고선이 흰 계단 덩어리로 보였다 → 0(평평). 켜려면 0.05 이하 권장.")] + public float terrainSeabedBumpAmp = 0f; [Tooltip("바다 바닥 굴곡 격자(m).")] public float terrainSeabedBumpCell = 3.5f; diff --git a/Assets/WL/Look/Farm/WLShoreFoam.cs b/Assets/WL/Look/Farm/WLShoreFoam.cs index 44380b3a4..5d39085cc 100644 --- a/Assets/WL/Look/Farm/WLShoreFoam.cs +++ b/Assets/WL/Look/Farm/WLShoreFoam.cs @@ -77,7 +77,10 @@ namespace WL.Look.Farm if (sp == 0f) return; _scroll += Time.deltaTime * sp; if (_scroll > 1f) _scroll -= 1f; - _inst.SetTextureOffset(IdBase, new Vector2(_scroll, _scroll * 0.37f)); + // 🔴 2026-09-16 PD 「각진 부분 사이의 사선」 이분 실측: v(띠 가로) 방향 스크롤이 물가선 알파 프로파일을 띠 밖으로 밀어 모래 위에 계단·사선 + // 모양의 흰 덩어리를 만들었다(스크롤 0 이면 즉시 매끈). 물가선 모드에서는 **해안 방향(u)만** 흐른다. 네모 띠(816j2)는 예전 그대로. + bool waterline = cfg.demoTerrainEnabled != 0; + _inst.SetTextureOffset(IdBase, new Vector2(_scroll, waterline ? 0f : _scroll * 0.37f)); } Material _inst; static readonly int IdBase = Shader.PropertyToID("_BaseMap"); @@ -202,7 +205,7 @@ namespace WL.Look.Farm // 띠 = f ∈ [−shoreFoamUp, +shoreFoamDown] 에 걸치는 칸. 정점 높이 = max(지형, 물) + shoreFoamY → 모래 위에서는 모래를 덮고 물 위에서는 물을 덮는다. // UV v = 0(모래 끝) → 1(바다 끝) — 기존 띠 텍스처와 같은 방향. u = 해안 방향 반복(shoreFoamTileLength). // ───────────────────────────────────────────────────────────────── - public static int TerrainNodes; + public static int TerrainNodes, WaterlinePoints; void BuildFromTerrain(Terrain terrain) { @@ -222,7 +225,7 @@ namespace WL.Look.Farm float stepX = sz.x / (nx - 1), stepZ = sz.z / (nz - 1); var hgt = new float[nx * nz]; - var f = new float[nx * nz]; + var under = new bool[nx * nz]; var probe = new Vector3(); for (int j = 0; j < nz; j++) { @@ -232,10 +235,46 @@ namespace WL.Look.Farm probe.x = tp.x + i * stepX; float h = terrain.SampleHeight(probe) + tp.y; int k = j * nx + i; - hgt[k] = h; f[k] = waterY - h; + hgt[k] = h; under[k] = h < waterY; } } TerrainNodes = nx * nz; + // 🔴 2026-09-16 PD 「섬 가장자리가 각졌다」 실측: 높이 차(f)로 띠를 정하면 물 높이 근처가 평평한 곳에서 띠가 통째로 넓어져 + // 흰 계단 덩어리가 됐다. → 물가선까지의 **수평 거리**(격자 챔퍼 · + 물 쪽 / − 모래 쪽)로 띠를 정한다 = 어디서나 같은 폭. + var dToWater = Chamfer(under, nx, nz, cell, false); // 모래 노드 → 가장 가까운 물속 노드까지(m · 거친 선별용) + var dToSand = Chamfer(under, nx, nz, cell, true); // 물속 노드 → 가장 가까운 모래 노드까지(m · 거친 선별용) + // 🔴 2026-09-16 PD 「각진 부분 사이의 사선」 실측: 챔퍼 거리의 등고선은 축 방향 + 45° 사선의 **팔각형**이라 띠 가장자리가 그 모양 그대로 + // 계단·사선으로 보였다(물 셰이더 교체·높이맵 257 로도 불변 → 거품 띠가 원인). 물가선 교점(격자 변에서 물 높이를 가로지르는 점)을 모아 + // 띠 근처 노드만 **진짜 유클리드 거리**로 다시 잰다 → 등고선이 매끈한 곡선. + var wlx = new List(1024); var wlz = new List(1024); + for (int j = 0; j < nz; j++) + for (int i = 0; i < nx; i++) + { + int k = j * nx + i; + float x = tp.x + i * stepX, z = tp.z + j * stepZ; + if (i + 1 < nx && under[k] != under[k + 1]) + { float t = Mathf.Clamp01((waterY - hgt[k]) / (hgt[k + 1] - hgt[k])); wlx.Add(x + t * stepX); wlz.Add(z); } + if (j + 1 < nz && under[k] != under[k + nx]) + { float t = Mathf.Clamp01((waterY - hgt[k]) / (hgt[k + nx] - hgt[k])); wlx.Add(x); wlz.Add(z + t * stepZ); } + } + WaterlinePoints = wlx.Count; + float reach = Mathf.Max(up, down) + cell * 2f; + var f = new float[nx * nz]; + for (int j = 0; j < nz; j++) + for (int i = 0; i < nx; i++) + { + int k = j * nx + i; + float dc = under[k] ? dToSand[k] : dToWater[k]; + if (dc > reach || wlx.Count == 0) { f[k] = under[k] ? reach + 1f : -(reach + 1f); continue; } + float x = tp.x + i * stepX, z = tp.z + j * stepZ, best = float.MaxValue; + for (int q = 0; q < wlx.Count; q++) + { + float dx = x - wlx[q], dz = z - wlz[q]; float d2 = dx * dx + dz * dz; + if (d2 < best) best = d2; + } + float d = Mathf.Sqrt(best); + f[k] = under[k] ? d : -d; + } var vmap = new int[nx * nz]; for (int k = 0; k < vmap.Length; k++) vmap[k] = -1; @@ -278,8 +317,8 @@ namespace WL.Look.Farm _mr.enabled = true; Rebuilds++; Verts = verts.Count; Tris = tris.Count / 3; Tiles = 0; - LastLog = "터레인 물가선 띠 — 삼각형 " + Tris + " · 정점 " + Verts + " · 물 y " + waterY.ToString("F2") - + " · 모래 쪽 " + up.ToString("F2") + " m ↑ · 바다 쪽 수심 " + down.ToString("F2") + " m · 격자 " + cell.ToString("F2") + " m · 드로우콜 +1"; + LastLog = "터레인 물가선 띠 — 삼각형 " + Tris + " · 정점 " + Verts + " · 물 y " + waterY.ToString("F2") + " · 물가선 점 " + WaterlinePoints + + " · 모래 쪽 " + up.ToString("F2") + " m · 바다 쪽 " + down.ToString("F2") + " m(유클리드 수평 거리) · 격자 " + cell.ToString("F2") + " m · 드로우콜 +1"; cfg.Log("둘레 거품 띠 — " + LastLog); } @@ -298,16 +337,18 @@ namespace WL.Look.Farm s_foamTex = new Texture2D(W, H, TextureFormat.RGBA32, false) { name = "WL_ShoreFoam(runtime)", hideFlags = HideFlags.DontSave, wrapMode = TextureWrapMode.Repeat, filterMode = FilterMode.Bilinear }; var px = new Color32[W * H]; + // 🔴 2026-09-16 실측(남서 물가): 바다 쪽 끝까지 알파가 남아 메시 경계(격자 칸)가 네모 계단으로 보였다 → 바다 쪽은 제곱으로 빨리 꺼지고 + // (띠 폭의 60 % 지점에서 ≈ 5 %) 물가선 최대 알파도 0.75 로 낮춘다(데모의 옅은 물보라). 메시 끝(v=1)은 0. for (int y = 0; y < H; y++) { float v = (y + 0.5f) / H; - float a = v < vWl ? Mathf.Pow(Mathf.Clamp01(v / vWl), 2f) : 1f - SmoothStep01((v - vWl) / (1f - vWl)); + float a = v < vWl ? Mathf.Pow(Mathf.Clamp01(v / vWl), 2f) : Mathf.Pow(1f - SmoothStep01((v - vWl) / (1f - vWl)), 2.2f); for (int x = 0; x < W; x++) { float u = (float)x / W; float n = 0.5f + 0.5f * Mathf.Sin(u * 6.2832f * 3f + Mathf.Sin(u * 6.2832f * 7f + v * 3f) * 1.3f); - float edge = v > vWl ? Mathf.Clamp01(1f - (v - vWl) / (1f - vWl) * (0.6f + 0.8f * n)) : 1f; - float alpha = Mathf.Clamp01(a * edge * (0.75f + 0.25f * n)); + float edge = v > vWl ? Mathf.Clamp01(1f - (v - vWl) / (1f - vWl) * (0.8f + 0.9f * n)) : 1f; + float alpha = Mathf.Clamp01(a * edge * (0.7f + 0.3f * n)) * 0.75f; px[y * W + x] = new Color32(255, 255, 255, (byte)(alpha * 255f)); } } diff --git a/Assets/WL/Look/Farm/WLSkyScope.cs b/Assets/WL/Look/Farm/WLSkyScope.cs index 49eef2422..d5ae1bc1c 100644 --- a/Assets/WL/Look/Farm/WLSkyScope.cs +++ b/Assets/WL/Look/Farm/WLSkyScope.cs @@ -71,8 +71,13 @@ namespace WL.Look.Farm SceneManager.sceneUnloaded += OnSceneUnloaded; SceneManager.activeSceneChanged += OnActiveSceneChanged; #if UNITY_EDITOR - // 🔴 SSAO 는 렌더러 **에셋**의 값이라 섬에서 Play 를 끄면 「꺼진 채」 메모리에 남는다 → Play 를 나갈 때 반드시 되돌린다. - UnityEditor.EditorApplication.playModeStateChanged += st => { if (st == UnityEditor.PlayModeStateChange.ExitingPlayMode) RestoreSsao(); }; + // 🔴 SSAO 는 렌더러 **에셋**의 값이라 섬에서 Play 를 끄면 「꺼진 채」 메모리에 남는다 → Play 를 나갈 때·편집 모드로 돌아올 때 반드시 되돌린다. + UnityEditor.EditorApplication.playModeStateChanged += st => + { + if (st == UnityEditor.PlayModeStateChange.ExitingPlayMode || st == UnityEditor.PlayModeStateChange.EnteredEditMode) RestoreSsao(); + }; + // 도메인 리로드(Play 중 컴파일) 뒤 — 이전 도메인이 끈 채 남긴 SSAO 를 먼저 되돌리고, 아직 섬이면 아래에서 다시 끈다. + RestoreSsao(); #endif // 🔴 아직 아무 맵도 안 붙은 지금이 「활성 씬의 원래 하늘」이다 — 여기서 한 번만 뜬다. @@ -169,6 +174,13 @@ namespace WL.Look.Farm return false; } + const string SsaoSessionKey = "WL_SSAO_OFF_BY_ISLAND"; // 에디터 도메인 리로드(Play 중 컴파일)에도 살아남는 표식 — 정적 목록은 리로드에 지워진다(2026-09-16 실측: 섬에서 리컴파일 → SSAO 가 꺼진 채 남음) + + static bool IsSsaoFeature(UnityEngine.Rendering.Universal.ScriptableRendererFeature f) + { + return f != null && (f.GetType().Name == "ScreenSpaceAmbientOcclusion" || f.name == "SSAO"); + } + /// 섬에 들어갈 때 — 켜져 있는 SSAO 피처(이름 · 형식명으로 판별 · URP 클래스는 internal)를 끄고 기억한다. public static void AcquireSsao(WLIslandLookSettings cfg, string sceneName) { @@ -177,25 +189,46 @@ namespace WL.Look.Farm for (int i = 0; i < feats.Length; i++) { var f = feats[i]; - if (f == null || !f.isActive) continue; - if (f.GetType().Name != "ScreenSpaceAmbientOcclusion" && f.name != "SSAO") continue; + if (!IsSsaoFeature(f) || !f.isActive) continue; if (s_ssaoOff.Contains(f)) continue; f.SetActive(false); s_ssaoOff.Add(f); SsaoOffCount++; } SsaoHeld = s_ssaoOff.Count > 0; +#if UNITY_EDITOR + if (SsaoHeld) UnityEditor.SessionState.SetBool(SsaoSessionKey, true); +#endif if (SsaoHeld) cfg.Log("SSAO 끔 — " + sceneName + " 동안 · 피처 " + s_ssaoOff.Count + "개(나갈 때 복원)"); } - /// 섬을 나갈 때(또는 에디터 Play 종료) — 우리가 끈 피처만 다시 켠다. + /// + /// 섬을 나갈 때(또는 에디터 Play 종료·편집 모드 복귀) — 우리가 끈 피처를 다시 켠다. + /// 정적 목록이 비어 있어도 세션 표식이 남아 있으면(도메인 리로드로 목록만 잃은 경우) SSAO 형식 피처를 전부 켠다(에셋 원래 값 = 켜짐). + /// public static void RestoreSsao() { - if (s_ssaoOff.Count == 0) { SsaoHeld = false; return; } - for (int i = 0; i < s_ssaoOff.Count; i++) if (s_ssaoOff[i] != null) s_ssaoOff[i].SetActive(true); - SsaoRestores += s_ssaoOff.Count; + bool flagged = false; +#if UNITY_EDITOR + flagged = UnityEditor.SessionState.GetBool(SsaoSessionKey, false); +#endif + if (s_ssaoOff.Count == 0 && !flagged) { SsaoHeld = false; return; } + int n = 0; + if (s_ssaoOff.Count > 0) + { + for (int i = 0; i < s_ssaoOff.Count; i++) if (s_ssaoOff[i] != null && !s_ssaoOff[i].isActive) { s_ssaoOff[i].SetActive(true); n++; } + } + else + { + var feats = Resources.FindObjectsOfTypeAll(); + for (int i = 0; i < feats.Length; i++) if (IsSsaoFeature(feats[i]) && !feats[i].isActive) { feats[i].SetActive(true); n++; } + } + SsaoRestores += n; s_ssaoOff.Clear(); SsaoHeld = false; +#if UNITY_EDITOR + UnityEditor.SessionState.EraseBool(SsaoSessionKey); +#endif } // ─────────────────────────────────────────────────────────────────