[WL] 섬 가장자리 각짐·경계 완화 — 거품 띠 v스크롤 제거(계단·사선 원인) · 물가선 유클리드 거리 띠 · 펄린 노이즈(값 노이즈 격자 무늬 제거) · 페이드 2 m 안쪽 30 % + 잔굽이 · SSAO 스코프 세션 표식 (#816)

- 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 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-16 22:35:36 +09:00
parent e66bb838f6
commit da6854461d
5 changed files with 209 additions and 58 deletions

View File

@ -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

View File

@ -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;
}
/// <summary>값 노이즈 1~+1(격자 cell · 겹선형 · 결정적). seed 0 = 경계 굽이(WLShoreSkirt 와 같은 식) · 다른 seed 는 격자를 옮겨 독립된 무늬.</summary>
static float ValueNoise(float x, float z, float cell, int seed)
/// <summary>
/// 그라디언트(펄린) 노이즈 1~+1(격자 cell · 5차 보간 · 결정적). seed 마다 격자를 옮겨 독립된 무늬.
/// 🔴 2026-09-16 PD 「가장자리가 각졌다」 실측: 값 노이즈(격자 값 겹선형)는 등고선이 **격자에 맞춘 둥근 네모**라 토온 단계·물 깊이 색이
/// 그 등고선을 그대로 드러내 계단·다각형이 됐다(거품·그림자·굴곡 끔 A/B 로 배제). 펄린은 등고선이 유기적 곡선.
/// </summary>
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 근처로
}
/// <summary>경계 굽이 — 값 노이즈(격자 skirtWaveCell) × 진폭. 직선 변을 파도처럼 굽이치게 한다(WLShoreSkirt 와 같은 식).</summary>
// 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;
}
/// <summary>경계 굽이 — 큰 굽이(격자 skirtWaveCell × skirtWaveAmp · WLShoreSkirt 와 같은 식) + 잔 굽이(terrainEdgeNoiseCell × Amp · 붓 자국). 직선 변을 굽이치게 한다.</summary>
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;
}
/// <summary>물 평면 높이 — 섬 씬의 `Water`(루트/자식) · 없으면 shoreFoamFallbackY.</summary>
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;
}
/// <summary>
@ -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;
}
// ─────────────────────────────────────────────────────────────────

View File

@ -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;

View File

@ -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<float>(1024); var wlz = new List<float>(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));
}
}

View File

@ -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");
}
/// <summary>섬에 들어갈 때 — 켜져 있는 SSAO 피처(이름 · 형식명으로 판별 · URP 클래스는 internal)를 끄고 기억한다.</summary>
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 + "개(나갈 때 복원)");
}
/// <summary>섬을 나갈 때(또는 에디터 Play 종료) — 우리가 끈 피처만 다시 켠다.</summary>
/// <summary>
/// 섬을 나갈 때(또는 에디터 Play 종료·편집 모드 복귀) — 우리가 끈 피처를 다시 켠다.
/// 정적 목록이 비어 있어도 세션 표식이 남아 있으면(도메인 리로드로 목록만 잃은 경우) SSAO 형식 피처를 전부 켠다(에셋 원래 값 = 켜짐).
/// </summary>
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<UnityEngine.Rendering.Universal.ScriptableRendererFeature>();
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
}
// ─────────────────────────────────────────────────────────────────