[WL-816zj] 섬 바닥 = 데모 터레인 조립(Terrain + Terrain.mat + TerrainInstancesBehaviour) (#816)

- 새 WLDemoTerrain.cs: 섬 씬 로드 시 런타임 TerrainData+Terrain(자식 오브젝트 · 콜라이더 없음)
  · 범위 = 열린 타일 발자국 + terrainMargin 8(최대 128) · 높이맵 129² (안 = 윗면 y0 → 둥글린·굽이친 경계에서
  3 m smoothstep → −1.8 평평) · 스플랫 256² 2층(데모 Demo_Grass/Demo_Dirt · 잔디 경계 blend 1.0 · 밭 구역 =
  WLSoftDirt 와 같은 둥근+노이즈 식) · materialTemplate = 데모 Terrain.mat 그대로 · drawInstanced 0
- 풀 = 데모 TerrainInstancesBehaviour AddComponent(Demo.unity 1281~1309 값: 1층 2.5 [풀 100/0.35/0.04 · 꽃 1/0.35/0.04]
  · 2층 0.05 [자갈 1/0.7/0.1] · 3·4층 빈 배열 · 분산 0.5/0.2) · 메시 = scatter[] · 재질 = 데모 원본(§0-d 필드)
- WLIslandLook.Apply: demoTerrainEnabled 1 이면 SoftDirt·Seabed·ShoreSkirt·WLIslandGrass 생략(값 배타) ·
  재훑기·확장 훅에서 높이맵/스플랫만 재굽기(멱등) + 인스턴서 enabled 토글 · 타일 윗면 렌더러 숨김(콜라이더 유지)
- WLSoftDirt: Active 에 데모 모드 포함(흙 타일 숨김 배타 유지) · ShapeDist 정적 분리(같은 식 공유)
- WLIslandLookSettings §0-d + 에셋 값(데모 에셋 guid 참조 6종 · 값 그대로) · 되돌리기 demoTerrainEnabled 0

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-16 14:45:47 +09:00
parent 55c17ba871
commit 461960168b
5 changed files with 723 additions and 16 deletions

View File

@ -393,3 +393,38 @@ MonoBehaviour:
islandSceneNames:
- Level01
- WL_FarmLook
demoTerrainEnabled: 1
terrainHeightRes: 129
terrainAlphaRes: 256
terrainMargin: 8
terrainMaxSize: 128
terrainBlend: 1
terrainBottomY: -1.8
terrainSlope: 3
terrainHeightSpan: 10
terrainRoundHeight: 1
demoTerrainMaterial: {fileID: 2100000, guid: 719df871080742445b86937d1b9977c9, type: 2}
demoGrassLayer: {fileID: 8574412962073106934, guid: fb43b2e3f8791ff4891972af8083fc0a, type: 2}
demoDirtLayer: {fileID: 8574412962073106934, guid: 710e14cf7c696034aadf2619338640ab, type: 2}
demoGrassMaterial: {fileID: 2100000, guid: a194cb4ce6027c246af528b83b728a15, type: 2}
demoFlowerMaterial: {fileID: 2100000, guid: d50a40cb47ec86444b898d9121bbe0f7, type: 2}
demoGravelMaterial: {fileID: 2100000, guid: 77d84437c100f274aad603219110357d, type: 2}
demoGrass:
scatterIndex: 0
probability: 100
scale: 0.35
normalOffset: 0.04
demoFlower:
scatterIndex: 1
probability: 1
scale: 0.35
normalOffset: 0.04
demoGravel:
scatterIndex: 2
probability: 1
scale: 0.7
normalOffset: 0.1
demoFirstLayerDensity: 2.5
demoSecondLayerDensity: 0.05
demoPositionVariance: 0.5
demoScaleVariance: 0.2

View File

@ -0,0 +1,531 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLDemoTerrain.cs — 섬 바닥을 데모와 「똑같은」 부품으로 조립 (WL-816zj · #816)
//
// PD(2026-09-16) 「데모와 가깝게가 아니라 **똑같은** 결과를 원한다」.
// 같은 카메라로 찍은 데모와 우리 섬의 남은 차이 = ① 지형 셰이더(데모 `Terrain.mat` 스플랫·지형 법선 음영)
// ② 풀 배치 스크립트. 값 흉내를 그만두고 **데모 부품 그대로** 조립한다.
//
// ■ 만드는 것 (전부 런타임 · 씬 파일 0줄 · 원본 에셋 0줄 · FI 코드 0줄)
// `~WL_DemoTerrain`(이 컴포넌트) ─ 자식 `Terrain(데모 부품)`:
// · `TerrainData` — 높이맵(`terrainHeightRes 129`) + 스플랫(`terrainAlphaRes 256` · 2층 = 데모 `Demo_Grass`·`Demo_Dirt` TerrainLayer)
// · `Terrain` — `materialTemplate` = 데모 `Terrain.mat` **그대로**(값 무수정) · 콜라이더 없음(걷기·밭·수확은 FI 콜라이더 그대로)
// · `TerrainInstancesBehaviour`(데모 스크립트) — Demo.unity 1281~1309 값 그대로(풀 100 : 꽃 1 · 밀도 2.5 · 자갈 밀도 0.05 …)
// 범위 = 열린 타일 발자국 + 바깥 `terrainMargin`(한 변 최대 `terrainMaxSize`).
// 높이 = 발자국 안 타일 윗면(y 0) → 둥글린 경계(`skirtGrassRound` 열기 연산 + `skirtWaveAmp/Cell` 굽이)에서
// 바깥 `terrainSlope` m 동안 smoothstep 으로 `terrainBottomY`(1.8) → 그 밖은 평평(바다 바닥 겸함).
// 스플랫 = 층0 잔디(둥글린 발자국 안 · 경계 `terrainBlend` 에 걸쳐 1→0) · 층1 모래(나머지 + 밭 구역 = WLSoftDirt 와 같은 둥근+노이즈 경계).
// 타일 윗면 렌더러(FI Island 본체)는 이 모드에서 숨긴다(렌더러만 · 콜라이더 유지).
//
// ■ 상호 배타 (값 · `WLIslandLook.Apply` 분기)
// `demoTerrainEnabled 1` 이면 `WLIslandGrass`·`WLShoreSkirt`·`WLSoftDirt`·`SpawnSeabed` 를 만들지 않는다.
// `demoTerrainEnabled 0` = 지금 구조 100 % 복귀(이 파일은 아무것도 만들지 않는다).
//
// ■ 멱등 — 발자국 서명·밭 구역·값이 그대로면 다시 굽지 않는다(재훑기·확장 훅에서 공짜). 바뀌면 높이맵·스플랫만 다시 굽고
// 인스턴서는 `enabled` 를 껐다 켜서 재생성한다(데모 스크립트의 OnDisable/OnEnable = 버퍼 해제/재생성).
//
// ■ 거리장·둥글림·굽이 = `WLShoreSkirt` 와 **같은 식**(발자국 바깥 테두리 선분 유클리드 거리 · 열기 연산 링 · 값 노이즈 굽이).
// 그 파일은 Lead 가 손보는 중이라(816zg/ze) 정적 분리 대신 같은 식을 옮겨 적었다 — 공통화는 후속 부채로 남긴다.
// 두 곳만 다르다: ① 침식 집합 E 안쪽(d ≤ R)의 값을 `d` 그대로 둔다(스커트는 `d + R` → E 경계에서 R 만큼 뛴다 · 여기서는 연속인 쪽).
// ② 링 점까지의 거리에 그 점의 깊이를 더한다(`dist + d_q` · 링 점이 E 경계에서 얼마나 안쪽인지 보정) — 링을 타일 가장자리에 맞춘
// 격자에서 모으므로 R 이 격자 칸의 배수(3 = 0.5 × 6)면 직선 변에서 오차 0, 아니어도 격자 위상에 따른 계통 오차(최대 한 칸)가 사라진다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업). 데모 스크립트가 LogError 를 내는 조건
// (Settings null · Mesh/Material null · Scale ≤ 0)은 **켜기 전에** 걸러 낸다.
// 🔴 asmdef 금지(Assembly-CSharp 단일) · `Environment.Instancing` 은 `global::` 로 고정(WL 네임스페이스와 이름 충돌 방지).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using FIIsland = CryingSnow.FarmingIsland.Island;
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
using DemoInstancer = global::Environment.Instancing.TerrainInstancesBehaviour;
using DemoLayer = global::Environment.Instancing.TerrainInstancesBehaviour.TerrainInstancingInput;
using DemoEntry = global::Environment.Instancing.InstancingSettings;
namespace WL.Look.Farm
{
[DisallowMultipleComponent]
public sealed class WLDemoTerrain : MonoBehaviour
{
public const string ObjectName = "~WL_DemoTerrain";
public const string TerrainChildName = "Terrain(데모 부품)";
// ── 진단(프로브·보고가 읽는다)
public static int Rebuilds, Bakes, LastTiles, LastEdges, LastRingPoints, LastInstancerRestarts;
public static float LastBakeMs, LastInstancerMs, LastSizeX, LastSizeZ;
public static string LastLog = "꺼짐";
public WLIslandLookSettings cfg;
GameObject _child;
Terrain _terrain;
TerrainData _data;
DemoInstancer _inst;
int _dataHeightRes, _dataAlphaRes;
struct Seg { public float ax, az, bx, bz; }
readonly List<Vector3> _tiles = new List<Vector3>(); // x·z = 타일 중심 · y = 렌더러 윗면 실측
readonly List<Renderer> _tileTops = new List<Renderer>(); // 열린 타일 본체 렌더러(숨김 대상)
readonly List<Renderer> _hiddenTops = new List<Renderer>();
readonly List<Seg> _edges = new List<Seg>(); // 발자국 바깥 테두리 선분
readonly List<float> _ringX = new List<float>(512), _ringZ = new List<float>(512), _ringD = new List<float>(512); // 열기 연산용 침식 집합 테두리 띠(위치 · 원거리 d)
// 마지막으로 구운 조건 — 같으면 다시 굽지 않는다(재훑기·확장 훅에서 공짜로 돌게).
bool _dirty = true;
long _lastTileSig;
float _lastValSig = float.NaN;
void OnEnable() { _dirty = true; Rebuild(); }
void OnDisable() { if (_child != null) _child.SetActive(false); RestoreTops(); }
void OnDestroy()
{
RestoreTops();
if (_data != null) Destroy(_data); // 자식 Terrain 도 같은 파괴에 사라진다(Destroy 는 프레임 끝에 실행)
}
/// <summary>다시 만든다(섬 확장·밭 재구성·재훑기). 멱등 — 발자국·구역·값이 그대로면 아무것도 하지 않는다.</summary>
public void Rebuild()
{
if (cfg == null) cfg = WLIslandLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0 || cfg.demoTerrainEnabled == 0) { Hide("꺼짐"); return; }
float size = TileSize();
if (size <= 0.01f) { Hide("타일 크기 미확인(IslandManager 없음) — 대기"); return; }
float half = size * 0.5f;
long tileSig = CollectTiles(out float topMeasured);
LastTiles = _tiles.Count;
if (_tiles.Count == 0) { Hide("열린 타일 0 — 대기"); return; }
if (cfg.demoTerrainMaterial == null || cfg.demoGrassLayer == null || cfg.demoDirtLayer == null)
{ Hide("🔴 데모 에셋 참조 비어 있음(demoTerrainMaterial · demoGrassLayer · demoDirtLayer) — WLIslandLookSettings 확인"); return; }
bool fixedTop = cfg.skirtTopMode != 0 || float.IsNaN(topMeasured);
float topY = fixedTop ? cfg.skirtTopY : topMeasured;
bool zoneValid = global::WL.Island.WLIslandGateFlow.ZoneBoundsValid;
var zone = zoneValid ? global::WL.Island.WLIslandGateFlow.ZoneBounds : new Bounds();
// 값이 바뀌면 다시 굽는다(인스펙터에서 만져도 다음 훅에 반영된다).
float valSig = cfg.terrainHeightRes * 0.013f + cfg.terrainAlphaRes * 0.007f + cfg.terrainMargin * 7.1f + cfg.terrainMaxSize * 0.11f
+ cfg.terrainBlend * 13.3f + cfg.terrainBottomY * 17.7f + cfg.terrainSlope * 19.3f + cfg.terrainHeightSpan * 2.3f
+ cfg.terrainRoundHeight * 23f + cfg.skirtGrassRound * 29.3f + cfg.skirtWaveAmp * 31.7f + cfg.skirtWaveCell * 37.3f
+ topY * 41.1f + (zoneValid ? 43f : 0f)
+ zone.center.x * 47f + zone.center.z * 53f + zone.extents.x * 59f + zone.extents.z * 61f
+ cfg.softDirtExpand * 67f + cfg.softDirtCornerRadius * 71f + cfg.softDirtNoise * 73f + cfg.softDirtNoiseCell * 79f + cfg.softDirtFeather * 83f
+ cfg.demoFirstLayerDensity * 89f + cfg.demoSecondLayerDensity * 97f + cfg.demoPositionVariance * 101f + cfg.demoScaleVariance * 103f
+ ScatterSig(cfg.demoGrass) * 107f + ScatterSig(cfg.demoFlower) * 109f + ScatterSig(cfg.demoGravel) * 113f;
bool same = !_dirty && _data != null && _child != null && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f;
HideTops(); // 새로 열린 타일 윗면도 숨긴다(멱등)
if (same) { _child.SetActive(true); return; }
var sw = System.Diagnostics.Stopwatch.StartNew();
Bake(size, half, topY, zoneValid, zone);
LastBakeMs = (float)sw.Elapsed.TotalMilliseconds;
sw.Restart();
string instLog = SetupInstancer();
_child.SetActive(true); // → 인스턴서 OnEnable(SetAlphamaps 뒤) · Terrain 표시
LastInstancerMs = (float)sw.Elapsed.TotalMilliseconds;
_lastTileSig = tileSig; _lastValSig = valSig; _dirty = false;
Rebuilds++;
LastLog = "데모 터레인 — 타일 " + _tiles.Count + " · 테두리 변 " + LastEdges + " · 링 " + LastRingPoints
+ " · 범위 " + LastSizeX.ToString("F0") + "×" + LastSizeZ.ToString("F0") + " m(원점 "
+ _child.transform.position.x.ToString("F1") + ", " + _child.transform.position.z.ToString("F1") + ")"
+ " · 높이맵 " + _dataHeightRes + "² · 스플랫 " + _dataAlphaRes + "²"
+ " · 윗면 y " + topY.ToString("F2") + (fixedTop ? "(고정)" : "(실측)") + " → 바닥 " + cfg.terrainBottomY.ToString("F2")
+ " · 경사 " + cfg.terrainSlope.ToString("F1") + " m · 잔디 경계 R " + cfg.skirtGrassRound.ToString("F1") + " 굽이 ±" + cfg.skirtWaveAmp.ToString("F1")
+ " · 밭 " + (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;
cfg.Log(LastLog);
}
static float ScatterSig(WLDemoScatter d)
{
return d == null ? 0f : d.scatterIndex * 1.7f + d.probability * 0.31f + d.scale * 3.1f + d.normalOffset * 5.3f;
}
void Hide(string reason)
{
if (_child != null) _child.SetActive(false); // 인스턴서 OnDisable → 버퍼 해제 · Terrain 숨김
RestoreTops();
LastLog = reason;
}
// ─────────────────────────────────────────────────────────────────
// 입력 수집 — WLShoreSkirt 와 같은 판별
// ─────────────────────────────────────────────────────────────────
static float TileSize()
{
var im = FIIslandManager.Instance;
return im != null ? im.IslandSize : 0f;
}
/// <summary>열린 타일 중심·본체 렌더러를 모은다. 반환 = 발자국 서명(순서 무관) — 확장으로 바뀌면 값이 바뀐다.</summary>
long CollectTiles(out float topMeasured)
{
_tiles.Clear(); _tileTops.Clear();
topMeasured = float.NaN;
var scene = gameObject.scene;
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
long sig = 17;
for (int i = 0; i < islands.Length; i++)
{
var isl = islands[i];
if (isl == null || !isl.IsUnlocked || !isl.gameObject.activeInHierarchy) continue;
if (scene.IsValid() && isl.gameObject.scene != scene) continue;
var p = isl.transform.position;
float top = p.y;
var r = isl.GetComponent<Renderer>(); // 섬 본체 렌더러(FIIsland 와 같은 오브젝트 · SwapMaterials 와 같은 판별)
if (r != null) { top = r.bounds.max.y; _tileTops.Add(r); }
if (float.IsNaN(topMeasured) || top > topMeasured) topMeasured = top;
_tiles.Add(new Vector3(p.x, top, p.z));
sig += ((long)Mathf.RoundToInt(p.x * 8f) * 73856093L) ^ ((long)Mathf.RoundToInt(p.z * 8f) * 19349663L);
}
return sig ^ ((long)_tiles.Count << 40);
}
/// <summary>타일 윗면(FI Island 본체 렌더러)을 숨긴다 — 터레인이 그 자리를 그린다. 콜라이더는 그대로.</summary>
void HideTops()
{
for (int i = 0; i < _tileTops.Count; i++)
{
var r = _tileTops[i];
if (r == null || !r.enabled) continue;
r.enabled = false;
_hiddenTops.Add(r);
}
}
void RestoreTops()
{
for (int i = 0; i < _hiddenTops.Count; i++) if (_hiddenTops[i] != null) _hiddenTops[i].enabled = true;
_hiddenTops.Clear();
}
// ─────────────────────────────────────────────────────────────────
// 거리장 — 발자국 바깥 테두리 선분까지의 유클리드 거리 · 부호 = 타일 안()/밖(+) (WLShoreSkirt 와 같은 식)
// ─────────────────────────────────────────────────────────────────
void BuildEdges(float size, float half)
{
_edges.Clear();
float tol = size * 0.25f; // 이웃 판정 허용 오차(타일 격자는 정수 배)
for (int i = 0; i < _tiles.Count; i++)
{
float cx = _tiles[i].x, cz = _tiles[i].z;
if (!HasTile(cx + size, cz, tol)) _edges.Add(new Seg { ax = cx + half, az = cz - half, bx = cx + half, bz = cz + half });
if (!HasTile(cx - size, cz, tol)) _edges.Add(new Seg { ax = cx - half, az = cz - half, bx = cx - half, bz = cz + half });
if (!HasTile(cx, cz + size, tol)) _edges.Add(new Seg { ax = cx - half, az = cz + half, bx = cx + half, bz = cz + half });
if (!HasTile(cx, cz - size, tol)) _edges.Add(new Seg { ax = cx - half, az = cz - half, bx = cx + half, bz = cz - half });
}
LastEdges = _edges.Count;
}
bool HasTile(float x, float z, float tol)
{
for (int i = 0; i < _tiles.Count; i++)
if (Mathf.Abs(_tiles[i].x - x) <= tol && Mathf.Abs(_tiles[i].z - z) <= tol) return true;
return false;
}
bool Inside(float x, float z, float half)
{
for (int i = 0; i < _tiles.Count; i++)
if (Mathf.Abs(x - _tiles[i].x) <= half && Mathf.Abs(z - _tiles[i].z) <= half) return true;
return false;
}
/// <summary>발자국 테두리까지의 부호 있는 거리(m). **음수 = 타일 안 · 양수 = 바깥(물 쪽)** · 테두리 선 위 = 0.</summary>
float RawDist(float x, float z, float half)
{
float best = float.MaxValue;
for (int i = 0; i < _edges.Count; i++)
{
var s = _edges[i];
float ex = s.bx - s.ax, ez = s.bz - s.az;
float t = Mathf.Clamp01(((x - s.ax) * ex + (z - s.az) * ez) / (ex * ex + ez * ez));
float dx = x - (s.ax + ex * t), dz = z - (s.az + ez * t);
float d2 = dx * dx + dz * dz;
if (d2 < best) best = d2;
}
float d = Mathf.Sqrt(best);
return Inside(x, z, half) ? -d : d;
}
/// <summary>격자 정수 좌표 → 0~1 결정적 해시(굽이 노이즈용 · WLShoreSkirt 와 같은 식).</summary>
static float Hash01(int x, int z)
{
uint h = (uint)(x * 73856093) ^ (uint)(z * 19349663) ^ 0x9E3779B9u; h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15;
return (h & 0xffffu) / 65535f;
}
/// <summary>경계 굽이 — 값 노이즈(격자 skirtWaveCell · 겹선형 · 결정적) × 진폭. 직선 변을 파도처럼 굽이치게 한다(WLShoreSkirt 와 같은 식).</summary>
float Wave(float x, float z)
{
if (cfg.skirtWaveAmp <= 0f || cfg.skirtWaveCell <= 0.1f) return 0f;
float qx = x / cfg.skirtWaveCell, qz = z / cfg.skirtWaveCell;
int gx = Mathf.FloorToInt(qx), gz = Mathf.FloorToInt(qz); float fx = qx - gx, fz = qz - gz;
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 * cfg.skirtWaveAmp;
}
/// <summary>
/// 열기 연산용 링 — 침식 집합 E{d ≤ R} 의 테두리 띠(d ∈ [R 1.5칸, R]) 점을 **타일 가장자리에 맞춘 격자**(칸 = 타일 한 변을 나누는 값 ≤ 스플랫 텍셀)에서 모은다.
/// 띠는 발자국 안에만 있으므로 발자국 바운즈만 훑는다.
/// </summary>
void GatherRing(float size, float half, float minX, float minZ, float maxX, float maxZ, float texel, float round)
{
_ringX.Clear(); _ringZ.Clear(); _ringD.Clear();
if (round <= 0.01f) return;
int n = Mathf.Max(1, Mathf.CeilToInt(size / Mathf.Max(0.01f, texel)));
float cell = size / n;
float lo = -round - cell * 1.5f, hi = -round;
int nx = Mathf.RoundToInt((maxX - minX) / cell) + 1, nz = Mathf.RoundToInt((maxZ - minZ) / cell) + 1;
for (int j = 0; j < nz; j++)
{
float z = minZ + j * cell;
for (int i = 0; i < nx; i++)
{
float x = minX + i * cell;
float d = RawDist(x, z, half);
if (d <= hi && d >= lo) { _ringX.Add(x); _ringZ.Add(z); _ringD.Add(d); }
}
}
LastRingPoints = _ringX.Count;
}
/// <summary>
/// 잔디 경계까지의 부호 있는 거리 s(m) — 둥글림(열기 연산 = E 까지의 거리 R ≈ min(링 점까지의 거리 + 그 점의 d)) 뒤 굽이를 뺀다.
/// 링이 비었거나(섬이 2R 보다 작음) E 안쪽·먼 바깥(`far` 너머 = 높이·스플랫 모두 상수)이면 d 그대로 쓴다(열기는 d 를 줄이지 않는다).
/// </summary>
float Boundary(float x, float z, float d, float round, float far)
{
float r = d;
if (round > 0.01f && _ringX.Count > 0 && d > -round && d < far)
{
float best = float.MaxValue;
for (int k = 0; k < _ringX.Count; k++)
{
float dx = x - _ringX[k], dz = z - _ringZ[k];
float e = Mathf.Sqrt(dx * dx + dz * dz) + _ringD[k];
if (e < best) best = e;
}
r = Mathf.Max(d, best);
}
return r - Wave(x, z);
}
static float SmoothStep01(float t) { t = Mathf.Clamp01(t); return t * t * (3f - 2f * t); }
/// <summary>높이맵 해상도는 2ⁿ+1(33~4097)만 유효 — 값 이하의 가장 큰 유효값.</summary>
static int HeightRes(int v) { int p = 32; while (p * 2 + 1 <= v && p < 4096) p *= 2; return p + 1; }
/// <summary>스플랫 해상도는 2ⁿ(16~4096) — 값 이하의 가장 큰 유효값.</summary>
static int AlphaRes(int v) { int p = 16; while (p * 2 <= v && p < 4096) p *= 2; return p; }
// ─────────────────────────────────────────────────────────────────
// 굽기 — TerrainData 범위·높이맵·스플랫
// ─────────────────────────────────────────────────────────────────
void Bake(float size, float half, float topY, bool zoneValid, Bounds zone)
{
// 1) 범위 = 발자국 + 바깥 여백(한 변 최대 terrainMaxSize · 넘치면 발자국 중심 기준으로 자른다)
float minX = float.MaxValue, minZ = float.MaxValue, maxX = float.MinValue, maxZ = float.MinValue;
for (int i = 0; i < _tiles.Count; i++)
{
var t = _tiles[i];
if (t.x - half < minX) minX = t.x - half;
if (t.x + half > maxX) maxX = t.x + half;
if (t.z - half < minZ) minZ = t.z - half;
if (t.z + half > maxZ) maxZ = t.z + half;
}
float margin = Mathf.Max(0f, cfg.terrainMargin);
float maxSize = Mathf.Max(size, cfg.terrainMaxSize);
float w = Mathf.Min((maxX - minX) + 2f * margin, maxSize);
float l = Mathf.Min((maxZ - minZ) + 2f * margin, maxSize);
float ox = (minX + maxX) * 0.5f - w * 0.5f, oz = (minZ + maxZ) * 0.5f - l * 0.5f;
float bottom = cfg.terrainBottomY;
float span = Mathf.Max(cfg.terrainHeightSpan, topY - bottom); // 높이맵 1.0 = bottom + span ≥ 윗면
if (span <= 0.001f) span = 1f;
float slope = Mathf.Max(0.01f, cfg.terrainSlope);
float blend = Mathf.Max(0.01f, cfg.terrainBlend);
int hRes = HeightRes(cfg.terrainHeightRes);
int aRes = AlphaRes(cfg.terrainAlphaRes);
LastSizeX = w; LastSizeZ = l;
// 2) TerrainData·Terrain — 해상도가 바뀌면 새로 만들고, 아니면 크기·위치만 옮긴다
EnsureData(hRes, aRes);
_data.size = new Vector3(w, span, l);
_child.transform.position = new Vector3(ox, bottom, oz);
// 3) 거리장 — 테두리 선분 · 열기 연산 링(타일 가장자리에 맞춘 격자 · 칸 ≤ 스플랫 텍셀)
BuildEdges(size, half);
float round = Mathf.Max(0f, cfg.skirtGrassRound);
float cellA = Mathf.Max(w / aRes, l / aRes);
float far = slope + blend + Mathf.Max(0f, cfg.skirtWaveAmp) + cellA * 2f; // 이보다 먼 바깥은 높이·스플랫 모두 상수
GatherRing(size, half, minX, minZ, maxX, maxZ, Mathf.Min(w / aRes, l / aRes), round);
// 4) 스플랫 — 층0 잔디 = 둥글린 발자국 안(경계 blend 에 걸쳐 1→0) · 층1 모래 = 나머지 + 밭 구역(WLSoftDirt 와 같은 둥근+노이즈 경계)
float zcx = 0f, zcz = 0f, zhx = 0f, zhz = 0f, zrad = 0f, znoise = 0f, zcell = 1f, feather = 1f;
if (zoneValid)
{
zcx = zone.center.x; zcz = zone.center.z;
zhx = zone.extents.x + cfg.softDirtExpand;
zhz = zone.extents.z + cfg.softDirtExpand;
zrad = Mathf.Clamp(cfg.softDirtCornerRadius, 0f, Mathf.Min(zhx, zhz));
znoise = Mathf.Max(0f, cfg.softDirtNoise);
zcell = Mathf.Max(0.05f, cfg.softDirtNoiseCell);
feather = Mathf.Max(0.05f, cfg.softDirtFeather);
if (zhx <= 0.01f || zhz <= 0.01f) zoneValid = false;
}
var alpha = new float[aRes, aRes, 2];
for (int j = 0; j < aRes; j++)
{
float z = oz + (j + 0.5f) * l / aRes;
for (int i = 0; i < aRes; i++)
{
float x = ox + (i + 0.5f) * w / aRes;
float s = Boundary(x, z, RawDist(x, z, half), round, far);
float grass = 1f - SmoothStep01(s / blend); // s ≤ 0 잔디 1 → s ≥ blend 잔디 0(모래)
float dirt = 0f;
if (zoneValid && Mathf.Abs(x - zcx) <= zhx + znoise + feather && Mathf.Abs(z - zcz) <= zhz + znoise + feather)
{
float dz = WLSoftDirt.ShapeDist(x, z, zcx, zcz, zhx, zhz, zrad, znoise, zcell);
dirt = SmoothStep01((feather * 0.5f - dz) / feather); // WLSoftDirt.BakeMask 와 같은 페이드(1 = 흙 · 0 = 풀)
}
float sand = Mathf.Max(1f - grass, dirt);
alpha[j, i, 1] = sand;
alpha[j, i, 0] = 1f - sand;
}
}
// 5) 높이맵 — 발자국 안 윗면 → 경계에서 바깥 slope 동안 smoothstep 으로 바닥 → 그 밖 평평
// terrainRoundHeight 1 = 둥글린·굽이친 경계에서 내려간다(발주서) · 0 = 타일 네모 가장자리에서 내려간다(콜라이더와 일치 · 둥글림은 스플랫만)
bool roundHeight = cfg.terrainRoundHeight != 0;
var heights = new float[hRes, hRes];
float stepX = w / (hRes - 1), stepZ = l / (hRes - 1);
for (int j = 0; j < hRes; j++)
{
float z = oz + j * stepZ;
for (int i = 0; i < hRes; i++)
{
float x = ox + i * stepX;
float d = RawDist(x, z, half);
float s = roundHeight ? Boundary(x, z, d, round, far) : d;
float y = s <= 0f ? topY : s >= slope ? bottom : Mathf.Lerp(topY, bottom, SmoothStep01(s / slope));
heights[j, i] = Mathf.Clamp01((y - bottom) / span);
}
}
_data.SetHeights(0, 0, heights);
_data.SetAlphamaps(0, 0, alpha);
Bakes++;
}
void EnsureChild()
{
if (_child == null)
{
var t = transform.Find(TerrainChildName);
if (t != null) _child = t.gameObject;
else
{
_child = new GameObject(TerrainChildName);
_child.SetActive(false); // 값을 다 채운 뒤에 켠다(데모 인스턴서 OnEnable 순서)
_child.transform.SetParent(transform, false);
}
}
if (_terrain == null) _terrain = _child.GetComponent<Terrain>();
if (_terrain == null) _terrain = _child.AddComponent<Terrain>();
if (_inst == null) _inst = _child.GetComponent<DemoInstancer>();
}
void EnsureData(int hRes, int aRes)
{
EnsureChild();
if (_data != null && (_dataHeightRes != hRes || _dataAlphaRes != aRes))
{
if (_inst != null) _inst.enabled = false; // 옛 스플랫 텍스처를 읽던 버퍼 해제
var old = _data; _data = null;
Destroy(old); // 아래에서 새 데이터를 먼저 물린다(Destroy 는 프레임 끝) → Terrain 이 빈 참조를 보는 순간 없음
}
if (_data == null)
{
_data = new TerrainData { name = "WL_DemoTerrainData", hideFlags = HideFlags.DontSave };
_data.heightmapResolution = hRes;
_data.alphamapResolution = aRes;
_dataHeightRes = hRes; _dataAlphaRes = aRes;
}
var layers = _data.terrainLayers;
if (layers == null || layers.Length != 2 || layers[0] != cfg.demoGrassLayer || layers[1] != cfg.demoDirtLayer)
_data.terrainLayers = new[] { cfg.demoGrassLayer, cfg.demoDirtLayer }; // 층0 잔디 · 층1 모래(데모 TerrainLayer 에셋 참조)
_terrain.terrainData = _data;
_terrain.materialTemplate = cfg.demoTerrainMaterial; // 데모 Terrain.mat 그대로(값 무수정)
_terrain.drawInstanced = false; // Demo.unity 1228 m_DrawInstanced 0 · 그 외 Terrain 값은 데모 = 컴포넌트 기본값
_terrain.allowAutoConnect = false;
}
// ─────────────────────────────────────────────────────────────────
// 풀 — 데모 스크립트 TerrainInstancesBehaviour 를 그대로(Demo.unity 1281~1309 값)
// ─────────────────────────────────────────────────────────────────
string SetupInstancer()
{
string why;
DemoLayer first, second;
if (!BuildLayers(out first, out second, out why))
{
if (_inst != null) _inst.enabled = false;
return "인스턴서 꺼짐 — " + why;
}
if (_inst == null)
{
// 🔴 데모 스크립트 OnEnable 은 Settings 가 비어 있으면 LogError 를 낸다 → 비활성 자식에 붙여 값을 채운 뒤 켠다(Rebuild 끝에서 켠다).
if (_child.activeSelf) _child.SetActive(false);
_inst = _child.AddComponent<DemoInstancer>();
}
else _inst.enabled = false; // OnDisable → 버퍼 해제(재생성 준비)
_inst.FirstLayer = first;
_inst.SecondLayer = second;
_inst.ThirdLayer = new DemoLayer { Density = 1f, Settings = new DemoEntry[0] }; // Demo.unity 1302~1307 — 빈 배열 · 밀도 1
_inst.FourthLayer = new DemoLayer { Density = 1f, Settings = new DemoEntry[0] };
_inst.PositionVariance = Mathf.Clamp(cfg.demoPositionVariance, 0f, 0.5f);
_inst.ScaleVariance = Mathf.Clamp(cfg.demoScaleVariance, 0f, 0.9f);
_inst.enabled = true; // 자식이 켜질 때(Rebuild 끝) OnEnable → 인스턴스 생성
LastInstancerRestarts++;
return "풀 " + first.Settings[0].Probability.ToString("F0") + ":꽃 " + first.Settings[1].Probability.ToString("F0")
+ " 밀도 " + first.Density.ToString("F2") + " · 자갈 밀도 " + second.Density.ToString("F2")
+ " · 크기 " + first.Settings[0].Scale.ToString("F2") + "/" + first.Settings[1].Scale.ToString("F2") + "/" + second.Settings[0].Scale.ToString("F2");
}
bool BuildLayers(out DemoLayer first, out DemoLayer second, out string why)
{
first = null; second = null; why = null;
var g = Entry(cfg.demoGrass, cfg.demoGrassMaterial, "풀", ref why);
var f = Entry(cfg.demoFlower, cfg.demoFlowerMaterial, "꽃", ref why);
var s = Entry(cfg.demoGravel, cfg.demoGravelMaterial, "자갈", ref why);
if (g == null || f == null || s == null) return false;
first = new DemoLayer { Density = Mathf.Max(0.01f, cfg.demoFirstLayerDensity), Settings = new[] { g, f } };
second = new DemoLayer { Density = Mathf.Max(0.01f, cfg.demoSecondLayerDensity), Settings = new[] { s } };
return true;
}
/// <summary>메시 = `scatter[scatterIndex]` 가 이미 참조하는 데모 메시 · 재질 = 데모 원본 재질 필드 · 값 = 데모 값(§0-d).</summary>
DemoEntry Entry(WLDemoScatter d, Material demoMat, string label, ref string why)
{
if (d == null) { why = label + " 정의 없음"; return null; }
var sc = cfg.scatter;
Mesh mesh = sc != null && d.scatterIndex >= 0 && d.scatterIndex < sc.Length && sc[d.scatterIndex] != null ? sc[d.scatterIndex].mesh : null;
if (mesh == null) { why = label + " 메시 없음(scatter[" + d.scatterIndex + "])"; return null; }
if (demoMat == null) { why = label + " 데모 재질 비어 있음"; return null; }
if (d.scale <= 0f) { why = label + " scale ≤ 0"; return null; }
return new DemoEntry { Mesh = mesh, Material = demoMat, Probability = d.probability, Scale = d.scale, NormalOffset = d.normalOffset };
}
}
}

View File

@ -132,11 +132,15 @@ namespace WL.Look.Farm
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
// 🔴 816zj — 데모 터레인 모드(`demoTerrainEnabled 1`)에서는 흙 데칼·바닥판·스커트·풀(WLIslandGrass)을 만들지 않는다(값으로 상호 배타).
// 바닥·경사·잔디/모래 경계·풀 전부를 데모 부품(Terrain + Terrain.mat + TerrainInstancesBehaviour)이 맡는다 — 본체는 `WLDemoTerrain`.
bool demo = cfg.demoTerrainEnabled != 0;
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
if (cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
if (cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
if (!demo && cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
if (!demo && cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
if (!demo && cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
if (demo) SpawnDemoTerrain(cfg, scene);
if (!demo && cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
HookIslands(cfg, scene);
@ -144,6 +148,7 @@ namespace WL.Look.Farm
cfg.Log("섬 룩 적용 — 렌더러 " + SwappedRenderers + " · 슬롯 " + SwappedSlots
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
+ " · 데모터레인 " + WLDemoTerrain.LastLog
+ " · 풀 " + WLIslandGrass.LastLog
+ " · 흙데칼 " + WLSoftDirt.LastLog
+ " · 물가스커트 " + WLShoreSkirt.LastLog
@ -167,7 +172,7 @@ namespace WL.Look.Farm
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
// 🔴 816zf — 밭 구역(`WLIslandGateFlow.ZoneBounds`)은 게이트 플로우가 밭을 훑은 뒤에야 정해진다.
// 재훑기에서 구역이 처음 잡히거나 바뀌면 데칼을 굽고, **그때만** 풀도 다시 깐다(경계가 같아지게).
if (cfg.softDirtEnabled != 0)
if (!demo && cfg.softDirtEnabled != 0)
{
int before = WLSoftDirt.Rebuilds;
SpawnSoftDirt(cfg, scene);
@ -178,7 +183,9 @@ namespace WL.Look.Farm
}
}
// 🔴 816zg — 물가 스커트도 같은 재훑기에서 다시 만든다(발자국이 그대로면 멱등 = 공짜).
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
if (!demo && cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
// 🔴 816zj — 데모 터레인: 밭 구역이 재훑기에서 처음 잡히면 스플랫(밭 자리 모래)을 다시 굽는다(구역·발자국이 그대로면 멱등 = 공짜).
if (demo) SpawnDemoTerrain(cfg, scene);
HookIslands(cfg, scene);
}
}
@ -269,14 +276,21 @@ namespace WL.Look.Farm
static void RequestGrassRebuild(WLIslandLookSettings cfg)
{
bool demo = cfg.demoTerrainEnabled != 0;
// 🔴 816zj — 데모 터레인 모드: 섬이 확장되면 높이맵·스플랫만 다시 굽고 인스턴서를 껐다 켠다(멱등). 데칼·풀·스커트는 이 모드에 없다.
if (demo)
{
var dt = Object.FindFirstObjectByType<WLDemoTerrain>(FindObjectsInactive.Include);
if (dt != null) { dt.cfg = cfg; dt.Rebuild(); DemoTerrainRebuilds++; }
}
// 🔴 816zf — 흙 데칼을 **먼저** 다시 굽는다. 섬 확장·밭 재구성으로 구역이 바뀌면
// 풀의 밭 제외도 그 새 경계를 읽어야 흙과 풀이 같은 선에서 만난다.
if (cfg.softDirtEnabled != 0)
if (!demo && cfg.softDirtEnabled != 0)
{
var sd = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
if (sd != null) { sd.cfg = cfg; sd.Rebuild(); }
}
if (cfg.grassEnabled != 0)
if (!demo && cfg.grassEnabled != 0)
{
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g != null) { g.Rebuild(); GrassRebuilds++; }
@ -288,13 +302,36 @@ namespace WL.Look.Farm
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
}
// 🔴 816zg — 섬이 확장되면 물가 스커트도 같은 훅으로 다시 만든다(발자국 변화 → 거리장 재계산).
if (cfg.shoreSkirtEnabled != 0)
if (!demo && cfg.shoreSkirtEnabled != 0)
{
var sk = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
if (sk != null) { sk.cfg = cfg; sk.Rebuild(); ShoreSkirtRebuilds++; }
}
}
public static bool DemoTerrainSpawned;
public static int DemoTerrainRebuilds;
/// <summary>
/// 816zj — 섬 바닥을 데모와 같은 부품으로(런타임 Terrain + 데모 Terrain.mat + 데모 TerrainInstancesBehaviour).
/// 데칼·바닥판·스커트·풀과 같은 자리에서 만들고 같은 훅(재훑기·확장)으로 다시 굽는다 — 본체는 <see cref="WLDemoTerrain"/>.
/// </summary>
public static void SpawnDemoTerrain(WLIslandLookSettings cfg, Scene scene)
{
var t = Object.FindFirstObjectByType<WLDemoTerrain>(FindObjectsInactive.Include);
if (t == null)
{
var go = new GameObject(WLDemoTerrain.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
t = go.AddComponent<WLDemoTerrain>();
t.cfg = cfg;
t.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
}
else { t.cfg = cfg; t.Rebuild(); }
DemoTerrainSpawned = true;
}
public static int ShoreFoamRebuilds;
public static bool ShoreFoamSpawned;
public static bool SoftDirtSpawned;

View File

@ -90,6 +90,22 @@ namespace WL.Look.Farm
public int edgeOnly_ = 0;
}
/// <summary>
/// 816zj — 데모 인스턴서(`TerrainInstancesBehaviour.InstancingSettings`) 한 줄의 값. Demo.unity 1284~1301 과 같은 구성.
/// 메시는 `scatter[scatterIndex]` 가 이미 참조하는 데모 메시를 쓰고, 재질은 §0-d 의 데모 원본 재질 필드를 쓴다.
/// </summary>
[Serializable]
public sealed class WLDemoScatter
{
[Tooltip("메시를 가져올 `scatter[]` (0). = 0 · 1 · 2.")] public int scatterIndex = 0;
[Tooltip("같은 층 안에서의 가중치. 데모 = 풀 100 : 꽃 1 · 자갈 1.")] public float probability = 1f;
[Tooltip("메시 배율. 데모 = 풀·꽃 0.35 · 자갈 0.7.")] public float scale = 0.35f;
[Tooltip("법선 방향으로 띄우는 거리(m). 데모 = 풀·꽃 0.04 · 자갈 0.1.")] public float normalOffset = 0.04f;
public WLDemoScatter() { }
public WLDemoScatter(int index, float prob, float scl, float offset) { scatterIndex = index; probability = prob; scale = scl; normalOffset = offset; }
}
[CreateAssetMenu(fileName = "WLIslandLookSettings", menuName = "WL/Island Look Settings", order = 39)]
public sealed class WLIslandLookSettings : ScriptableObject
{
@ -377,6 +393,81 @@ namespace WL.Look.Farm
[Tooltip("굽이의 크기(m) — 노이즈 격자 한 칸. 클수록 완만한 큰 굽이.")]
public float skirtWaveCell = 2.5f;
// ───────────────────────────────── §0-d 데모 터레인 조립 (WL-816zj · PD 「데모와 똑같은 결과」 2026-09-16)
[Header("§0-d — 섬 바닥 = 데모와 같은 부품(Unity Terrain + 데모 Terrain.mat + 데모 TerrainInstancesBehaviour)")]
[Tooltip("1 = 섬 씬 로드 시 런타임 Terrain(높이맵 = 열린 타일 발자국 · 스플랫 = 잔디/모래 · 콜라이더 없음)을 만들어 데모 `Terrain.mat` 과 " +
"데모 풀 인스턴서를 **그대로** 얹는다. 이때 §816zf 흙 데칼 · §0-b 바닥판 · §0-c 스커트 · §1 풀(WLIslandGrass)은 만들지 않고(값으로 상호 배타) " +
"타일 윗면 렌더러는 숨긴다(콜라이더 유지 = 걷기·밭·수확 그대로). 0 = 지금 구조 100 % 복귀.")]
public int demoTerrainEnabled = 1;
[Tooltip("높이맵 해상도(2ⁿ+1 · 33~4097). 129 = 한 변 128 m 일 때 1 m 간격, 작은 섬이면 더 촘촘해진다.")]
public int terrainHeightRes = 129;
[Tooltip("스플랫(잔디/모래 가중치) 해상도(2ⁿ · 16~4096). 256 = 한 변 128 m 일 때 0.5 m 텍셀.")]
public int terrainAlphaRes = 256;
[Tooltip("열린 타일 발자국 바깥으로 두는 여백(m). 경사(terrainSlope) 밖은 바닥 높이의 평평한 바다 바닥이 된다(이 모드에서는 §0-b 바닥판을 만들지 않는다).")]
public float terrainMargin = 8f;
[Tooltip("터레인 한 변 상한(m). 발자국 + 여백이 이보다 크면 발자국 중심 기준으로 자른다.")]
public float terrainMaxSize = 128f;
[Tooltip("스플랫 잔디→모래 페이드 폭(m). 잔디 경계(둥글림+굽이)에서 바깥으로 이만큼에 걸쳐 1→0.")]
public float terrainBlend = 1.0f;
[Tooltip("경사 바깥 끝 = 바다 바닥 높이(월드 y · m). 물(1.0) 아래.")]
public float terrainBottomY = -1.8f;
[Tooltip("잔디 경계에서 바닥까지 내려가는 경사의 폭(m). smoothstep — 윗면에서도 바닥에서도 접선이 수평.")]
public float terrainSlope = 3f;
[Tooltip("높이맵 size.y(m) — 바닥(terrainBottomY)에서 위로 이만큼까지 표현한다(높이값 1.0 = 바닥 + 이 값). 타일 윗면(0)이 들어가면 된다.")]
public float terrainHeightSpan = 10f;
[Tooltip("1 = 높이도 둥글린·굽이친 잔디 경계(skirtGrassRound · skirtWaveAmp)에서 내려간다(발주서 값). " +
"🔴 FI 타일 콜라이더는 8 m 네모 그대로라 볼록 모서리(최대 약 0.9 m)·굽이가 안쪽인 곳(최대 0.8 m)에서는 발이 경사 위 허공에 뜬다(최대 약 0.3~0.4 m). " +
"0 = 높이는 타일 네모 가장자리에서 내려가고(콜라이더와 일치) 둥글림·굽이는 스플랫(잔디/모래 색 경계)만 따른다.")]
public int terrainRoundHeight = 1;
[Tooltip("데모 `Assets/3DPixelArtEnvironment/Materials/Terrain.mat` — 값 무수정 · 참조만. Terrain.materialTemplate.")]
public Material demoTerrainMaterial;
[Tooltip("층0 잔디 = 데모 `Demo/Demo_Grass.terrainlayer`(참조만).")]
public TerrainLayer demoGrassLayer;
[Tooltip("층1 모래 = 데모 `Demo/Demo_Dirt.terrainlayer`(참조만).")]
public TerrainLayer demoDirtLayer;
[Tooltip("풀 재질 = 데모 원본 `Instanced_Grass.mat`(guid a194cb4c…). scatter[] WL .")]
public Material demoGrassMaterial;
[Tooltip("꽃 재질 = 데모 원본 `Instanced_Flower.mat`(guid d50a40cb…).")]
public Material demoFlowerMaterial;
[Tooltip("자갈 재질 = 데모 원본 `Instanced_Gravel.mat`(guid 77d84437…).")]
public Material demoGravelMaterial;
[Tooltip("1층(잔디 위) 풀 — Demo.unity 1284~1288: 확률 100 · 크기 0.35 · 오프셋 0.04 · 메시 = scatter[0](Grass_Leaf).")]
public WLDemoScatter demoGrass = new WLDemoScatter(0, 100f, 0.35f, 0.04f);
[Tooltip("1층(잔디 위) 꽃 — Demo.unity 1289~1293: 확률 1 · 크기 0.35 · 오프셋 0.04 · 메시 = scatter[1](Flower_Leaf).")]
public WLDemoScatter demoFlower = new WLDemoScatter(1, 1f, 0.35f, 0.04f);
[Tooltip("2층(모래 위) 자갈 — Demo.unity 1297~1301: 확률 1 · 크기 0.7 · 오프셋 0.1 · 메시 = scatter[2](Stone_1).")]
public WLDemoScatter demoGravel = new WLDemoScatter(2, 1f, 0.7f, 0.1f);
[Tooltip("1층 밀도(개/m 한 변 · 실제 = 값²). Demo.unity 1282 = 2.5(6.25개/㎡).")]
public float demoFirstLayerDensity = 2.5f;
[Tooltip("2층 밀도. Demo.unity 1295 = 0.05(20 m 격자).")]
public float demoSecondLayerDensity = 0.05f;
[Tooltip("격자 흔들기(칸 단위). Demo.unity 1308 = 0.5.")]
[Range(0f, 0.5f)] public float demoPositionVariance = 0.5f;
[Tooltip("크기 흔들기. Demo.unity 1309 = 0.2 → ×0.8~1.2.")]
[Range(0f, 0.9f)] public float demoScaleVariance = 0.2f;
// ───────────────────────────────── §1 풀밭
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
[Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " +

View File

@ -64,13 +64,17 @@ namespace WL.Look.Farm
static bool s_ready;
static float s_cx, s_cz, s_hx, s_hz, s_rad, s_noise, s_cell, s_bite;
/// <summary>설정이 데칼을 쓰기로 했는가 — 흙 타일 숨김(배타)을 가르는 값. 구역을 몰라도 참이다.</summary>
/// <summary>
/// 설정이 데칼을 쓰기로 했는가 — 흙 타일 숨김(배타)을 가르는 값. 구역을 몰라도 참이다.
/// 816zj — 데모 터레인 모드(`demoTerrainEnabled 1`)에서는 데칼 대신 터레인 스플랫이 밭 자리를 흙으로 칠하므로
/// 각진 흙 타일은 마찬가지로 숨겨야 한다 → 그 모드도 참으로 친다(`softDirtEnabled` 값과 무관).
/// </summary>
public static bool Active
{
get
{
var c = WLIslandLookSettings.Instance;
return c != null && c.enabled_ != 0 && c.softDirtEnabled != 0;
return c != null && c.enabled_ != 0 && (c.softDirtEnabled != 0 || c.demoTerrainEnabled != 0);
}
}
@ -83,12 +87,21 @@ namespace WL.Look.Farm
/// </summary>
public static float SignedDist(float x, float z)
{
float dx = Mathf.Abs(x - s_cx) - (s_hx - s_rad);
float dz = Mathf.Abs(z - s_cz) - (s_hz - s_rad);
return ShapeDist(x, z, s_cx, s_cz, s_hx, s_hz, s_rad, s_noise, s_cell);
}
/// <summary>
/// 흙 모양의 **단일 식** — 중심(cx,cz)·반폭(hx,hz)·모서리 반지름 rad 의 둥근 사각형에 셀 노이즈(진폭 noise · 셀 cell)로 경계를 흔든 부호 거리(m).
/// 816zj 데모 터레인 스플랫이 같은 식으로 밭 자리를 칠한다(정적 상태 없이 값만 받는다).
/// </summary>
public static float ShapeDist(float x, float z, float cx, float cz, float hx, float hz, float rad, float noise, float cell)
{
float dx = Mathf.Abs(x - cx) - (hx - rad);
float dz = Mathf.Abs(z - cz) - (hz - rad);
float ox = Mathf.Max(dx, 0f), oz = Mathf.Max(dz, 0f);
float d = Mathf.Min(Mathf.Max(dx, dz), 0f) + Mathf.Sqrt(ox * ox + oz * oz) - s_rad;
if (s_noise > 0f && s_cell > 0.0001f)
d -= (CellNoise(x / s_cell, z / s_cell) * 2f - 1f) * s_noise;
float d = Mathf.Min(Mathf.Max(dx, dz), 0f) + Mathf.Sqrt(ox * ox + oz * oz) - rad;
if (noise > 0f && cell > 0.0001f)
d -= (CellNoise(x / cell, z / cell) * 2f - 1f) * noise;
return d;
}