Merge branch 'wl/gameplay/WL-816zd-grass-round-outline'
This commit is contained in:
commit
a0db722896
|
|
@ -113,6 +113,10 @@ MonoBehaviour:
|
|||
farmEdgeRadius: 1
|
||||
farmEdgeNoise: 0.4
|
||||
farmEdgeNoiseCell: 0.5
|
||||
outlineRoundEnabled: 1
|
||||
outlineRoundRadius: 1.5
|
||||
outlineOverhang: 0.3
|
||||
outlineRoundCell: 0.25
|
||||
propMargin: 0.05
|
||||
propMaxSize: 7.5
|
||||
maxInstances: 40000
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ namespace WL.Look.Farm
|
|||
public static bool CloudEdgeOn;
|
||||
public static string CloudWhy = "";
|
||||
|
||||
// 816zd — 둥근 윤곽 배치 진단
|
||||
public static bool OutlineRoundOn;
|
||||
public static int OutlineCells;
|
||||
public static float OutlineRadius, OutlineOverhang, OutlineBakeMs;
|
||||
public static string OutlineWhy = "";
|
||||
|
||||
public WLIslandLookSettings cfg;
|
||||
|
||||
Bounds _bounds = new Bounds(Vector3.zero, Vector3.one * 16f);
|
||||
|
|
@ -135,6 +141,8 @@ namespace WL.Look.Farm
|
|||
Instances = 0; Tiles = 0; Excluded = 0; Triangles = 0; DrawnConfigs = 0; UsedDensity = 0f;
|
||||
Candidates = 0; EdgeFraction = 1f; EdgeCells = 0; EdgeCellSize = 0f; EdgeDilate = 0;
|
||||
BakeMs = 0f; FilterMs = 0f; CloudEdgeOn = false; CloudWhy = ""; _edgeOn = false;
|
||||
OutlineRoundOn = false; OutlineCells = 0; OutlineRadius = 0f; OutlineOverhang = 0f;
|
||||
OutlineBakeMs = 0f; OutlineWhy = ""; _outOn = false;
|
||||
|
||||
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.grassEnabled == 0) { LastLog = "꺼짐"; return null; }
|
||||
|
|
@ -159,14 +167,18 @@ namespace WL.Look.Farm
|
|||
|
||||
BuildBlockers(tiles);
|
||||
|
||||
// 816zd — 풀을 깔 넓이를 「섬 footprint 를 열고(침식→팽창) 바깥으로 넓힌」 모양으로 미리 굽는다
|
||||
BuildOutlineField(tiles);
|
||||
|
||||
// 경계 박스 — 구름 격자를 굽기 전에 정해져야 한다
|
||||
var bmin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
var bmax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
float outPad = _outOn ? Mathf.Max(0f, cfg.outlineOverhang) : 0f; // 816zd — 절벽 위로 넘친 풀까지 덮는다
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var t = tiles[i];
|
||||
bmin = Vector3.Min(bmin, new Vector3(t.center.x - 4f, t.center.y - 1f, t.center.z - 4f));
|
||||
bmax = Vector3.Max(bmax, new Vector3(t.center.x + 4f, t.center.y + 3f, t.center.z + 4f));
|
||||
bmin = Vector3.Min(bmin, new Vector3(t.center.x - 4f - outPad, t.center.y - 1f, t.center.z - 4f - outPad));
|
||||
bmax = Vector3.Max(bmax, new Vector3(t.center.x + 4f + outPad, t.center.y + 3f, t.center.z + 4f + outPad));
|
||||
}
|
||||
_bounds = new Bounds((bmin + bmax) * 0.5f, bmax - bmin);
|
||||
_cacheY = tiles[0].center.y;
|
||||
|
|
@ -255,18 +267,25 @@ namespace WL.Look.Farm
|
|||
float pv = cfg.positionVariance;
|
||||
float sv = cfg.scaleVariance;
|
||||
|
||||
// 816zd — 둥근 윤곽은 타일 밖으로 `overhang` 만큼 넘친다. 흔들기 폭까지 더해 칸 범위를 넓힌다.
|
||||
// (이만큼 넓히면 마스크가 받아들일 수 있는 점의 칸이 하나도 빠지지 않는다 — 아래 OwnerTile 주석)
|
||||
float pad = _outOn ? Mathf.Max(0f, cfg.outlineOverhang) + pv * step : 0f;
|
||||
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var t = tiles[i];
|
||||
int ix0 = Mathf.CeilToInt((t.center.x - 4f) / step - 0.5f);
|
||||
int ix1 = Mathf.FloorToInt((t.center.x + 4f) / step - 0.5f);
|
||||
int iz0 = Mathf.CeilToInt((t.center.z - 4f) / step - 0.5f);
|
||||
int iz1 = Mathf.FloorToInt((t.center.z + 4f) / step - 0.5f);
|
||||
int ix0 = Mathf.CeilToInt((t.center.x - 4f - pad) / step - 0.5f);
|
||||
int ix1 = Mathf.FloorToInt((t.center.x + 4f + pad) / step - 0.5f);
|
||||
int iz0 = Mathf.CeilToInt((t.center.z - 4f - pad) / step - 0.5f);
|
||||
int iz1 = Mathf.FloorToInt((t.center.z + 4f + pad) / step - 0.5f);
|
||||
|
||||
for (int ix = ix0; ix <= ix1; ix++)
|
||||
{
|
||||
for (int iz = iz0; iz <= iz1; iz++)
|
||||
{
|
||||
// 816zd — 넓힌 범위는 이웃 타일과 겹친다. 칸 하나는 **한 타일만** 만든다(중복 0).
|
||||
if (_outOn && OwnerTile(tiles, (ix + 0.5f) * step, (iz + 0.5f) * step) != i) continue;
|
||||
|
||||
// 같은 칸이면 몇 번 다시 깔아도 같은 결과(확장 때 풀이 튀지 않는다)
|
||||
uint h = Hash((uint)(ix * 73856093) ^ (uint)(iz * 19349663) ^ (uint)(L * 83492791));
|
||||
float jx = (Frac(h, 0) - 0.5f) * 2f * pv;
|
||||
|
|
@ -274,7 +293,13 @@ namespace WL.Look.Farm
|
|||
float wx = (ix + 0.5f + jx) * step;
|
||||
float wz = (iz + 0.5f + jz) * step;
|
||||
|
||||
if (!OnTile(t, wx, wz)) { Excluded++; continue; }
|
||||
if (_outOn)
|
||||
{
|
||||
// 윗면 마스크 + edgeInset 대신 둥글린 윤곽으로 자른다. 길 제외는 그대로.
|
||||
if (!InRoundOutline(wx, wz)) { Excluded++; continue; }
|
||||
if (OnRoad(t, wx, wz)) { Excluded++; continue; }
|
||||
}
|
||||
else if (!OnTile(t, wx, wz)) { Excluded++; continue; }
|
||||
if (Blocked(wx, wz)) { Excluded++; continue; }
|
||||
|
||||
// 어느 정의가 걸리나(가중 추첨 · 결정적)
|
||||
|
|
@ -473,6 +498,11 @@ namespace WL.Look.Farm
|
|||
+ " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded
|
||||
+ " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)"
|
||||
+ (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "")
|
||||
+ (_outOn
|
||||
? " · 둥근윤곽 R" + OutlineRadius.ToString("F2") + "m·넘침 " + OutlineOverhang.ToString("F2")
|
||||
+ "m(칸 " + _outCell.ToString("F2") + "m×" + OutlineCells + " · 굽기 "
|
||||
+ OutlineBakeMs.ToString("F1") + "ms)"
|
||||
: " · 둥근윤곽 off" + (OutlineWhy.Length > 0 ? "(" + OutlineWhy + ")" : ""))
|
||||
+ (_viewOn
|
||||
? " · 가시범위 절두체(여유 " + cfg.viewMargin.ToString("F1") + "m) · 범위밖 제외 " + ViewCulled
|
||||
: " · 가시범위 off")
|
||||
|
|
@ -554,16 +584,153 @@ namespace WL.Look.Farm
|
|||
!t.mask.At(l.x, l.z + e) || !t.mask.At(l.x, l.z - e)) return false;
|
||||
}
|
||||
|
||||
// 길 제외
|
||||
if (t.roadMask != null && t.roadTf != null)
|
||||
return !OnRoad(t, wx, wz);
|
||||
}
|
||||
|
||||
/// <summary>길(Road 메시) 위인가 — 여백 포함. 816zd 가 둥근 윤곽 모드에서도 그대로 쓴다.</summary>
|
||||
bool OnRoad(TileRef t, float wx, float wz)
|
||||
{
|
||||
if (t.roadMask == null || t.roadTf == null) return false;
|
||||
var lr = t.roadTf.InverseTransformPoint(new Vector3(wx, t.center.y, wz));
|
||||
float m = cfg.roadMargin;
|
||||
if (t.roadMask.At(lr.x, lr.z)) return true;
|
||||
if (m > 0f && (t.roadMask.At(lr.x + m, lr.z) || t.roadMask.At(lr.x - m, lr.z) ||
|
||||
t.roadMask.At(lr.x, lr.z + m) || t.roadMask.At(lr.x, lr.z - m))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 816zd — 둥근 바깥 윤곽 (땅은 각진 그대로 · 풀 카펫만)
|
||||
//
|
||||
// 섬 윗면 footprint 를 반지름 R 로 **열고**(morphological opening = 침식 후 팽창)
|
||||
// 바깥으로 overhang 만큼 팽창한 넓이에만 풀을 깐다.
|
||||
// · 열기 → 볼록 모서리가 반지름 R 의 원호로 깎인다(모서리엔 풀이 빈다).
|
||||
// 오목한 곳·안쪽 넓은 면은 그대로라 풀 카펫의 **바깥선만** 둥글게 읽힌다.
|
||||
// · 팽창 → 가장자리 풀이 절벽 위로 살짝 넘친다.
|
||||
// 거리 정의는 816j2 `WLShoreFoam.Chamfer` 를 그대로 쓴다(SOT 1곳).
|
||||
// 팽창은 결합적이라 (E⊕B_R)⊕B_over = E⊕B_(R+over) — 거리장 한 장이면 충분하다.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
float[] _outDist; // 침식된 집합까지의 거리(m)
|
||||
int _outNx, _outNz;
|
||||
float _outMinX, _outMinZ, _outCell, _outLimit;
|
||||
bool _outOn;
|
||||
|
||||
void BuildOutlineField(List<TileRef> tiles)
|
||||
{
|
||||
_outOn = false;
|
||||
_outDist = null;
|
||||
if (cfg.outlineRoundEnabled == 0) { OutlineWhy = "꺼짐"; return; }
|
||||
|
||||
float R = Mathf.Max(0f, cfg.outlineRoundRadius);
|
||||
float over = Mathf.Max(0f, cfg.outlineOverhang);
|
||||
if (R <= 0f && over <= 0f) { OutlineWhy = "반지름·넘침 둘 다 0 = 현행과 같음"; return; }
|
||||
if (tiles.Count == 0) { OutlineWhy = "타일 0"; return; }
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
float cell = Mathf.Max(0.05f, cfg.outlineRoundCell);
|
||||
float half = cell * 0.5f;
|
||||
float pad = R + over + cell * 2f;
|
||||
|
||||
float minX = float.MaxValue, minZ = float.MaxValue, maxX = float.MinValue, maxZ = float.MinValue;
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var lr = t.roadTf.InverseTransformPoint(w);
|
||||
float m = cfg.roadMargin;
|
||||
if (t.roadMask.At(lr.x, lr.z)) return false;
|
||||
if (m > 0f && (t.roadMask.At(lr.x + m, lr.z) || t.roadMask.At(lr.x - m, lr.z) ||
|
||||
t.roadMask.At(lr.x, lr.z + m) || t.roadMask.At(lr.x, lr.z - m))) return false;
|
||||
var t = tiles[i];
|
||||
float h = t.mask != null ? t.mask.half : 4f;
|
||||
minX = Mathf.Min(minX, t.center.x - h); maxX = Mathf.Max(maxX, t.center.x + h);
|
||||
minZ = Mathf.Min(minZ, t.center.z - h); maxZ = Mathf.Max(maxZ, t.center.z + h);
|
||||
}
|
||||
return true;
|
||||
minX -= pad; minZ -= pad; maxX += pad; maxZ += pad;
|
||||
int nx = Mathf.Clamp(Mathf.CeilToInt((maxX - minX) / cell) + 1, 2, 1024);
|
||||
int nz = Mathf.Clamp(Mathf.CeilToInt((maxZ - minZ) / cell) + 1, 2, 1024);
|
||||
|
||||
// ① 섬 윗면 footprint(모든 타일 마스크의 합집합)
|
||||
var inside = new bool[nx * nz];
|
||||
for (int j = 0; j < nz; j++)
|
||||
{
|
||||
float z = minZ + j * cell;
|
||||
for (int i = 0; i < nx; i++) inside[j * nx + i] = OnAnyTileMask(tiles, minX + i * cell, z);
|
||||
}
|
||||
|
||||
// ② 침식 — 「지금의 바깥선(edgeInset)」에서 다시 R 이상 안쪽인 칸만 남긴다.
|
||||
// 침식은 결합적이라 (F⊖B_inset)⊖B_R = F⊖B_(inset+R) — 한 번에 깎는다.
|
||||
// 이 기준 덕에 `outlineOverhang = edgeInset` 이면 바깥선이 지금과 같고(모서리만 둥글어짐),
|
||||
// 그보다 크면 차이만큼 절벽 위로 넘친다.
|
||||
float inset = Mathf.Max(0f, cfg.edgeInset);
|
||||
var dIn = WLShoreFoam.Chamfer(inside, nx, nz, cell, true); // 안 → 가장 가까운 밖까지
|
||||
var eroded = new bool[nx * nz];
|
||||
int nEroded = 0;
|
||||
for (int k = 0; k < eroded.Length; k++)
|
||||
{
|
||||
bool v = inside[k] && dIn[k] >= inset + R + half;
|
||||
eroded[k] = v;
|
||||
if (v) nEroded++;
|
||||
}
|
||||
sw.Stop();
|
||||
if (nEroded == 0)
|
||||
{
|
||||
OutlineWhy = "섬이 반지름보다 얇다(침식 후 0칸) → 현행 배치 유지";
|
||||
cfg.Log("둥근 윤곽 — " + OutlineWhy);
|
||||
return;
|
||||
}
|
||||
|
||||
// ③ 팽창 — 침식된 집합에서 (R + overhang) 안쪽이면 풀을 깐다
|
||||
sw.Start();
|
||||
_outDist = WLShoreFoam.Chamfer(eroded, nx, nz, cell, false);
|
||||
sw.Stop();
|
||||
_outNx = nx; _outNz = nz; _outMinX = minX; _outMinZ = minZ; _outCell = cell;
|
||||
_outLimit = R + over + half;
|
||||
_outOn = true;
|
||||
|
||||
OutlineRoundOn = true; OutlineCells = nx * nz;
|
||||
OutlineRadius = R; OutlineOverhang = over;
|
||||
OutlineBakeMs = (float)sw.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>둥글린 윤곽 안인가 — 거리장을 이중선형으로 읽어 경계를 칸 크기보다 매끄럽게 만든다.</summary>
|
||||
bool InRoundOutline(float wx, float wz)
|
||||
{
|
||||
if (!_outOn) return true;
|
||||
float fx = (wx - _outMinX) / _outCell;
|
||||
float fz = (wz - _outMinZ) / _outCell;
|
||||
int i0 = Mathf.FloorToInt(fx), j0 = Mathf.FloorToInt(fz);
|
||||
if (i0 < 0 || j0 < 0 || i0 >= _outNx - 1 || j0 >= _outNz - 1) return false;
|
||||
float tx = fx - i0, tz = fz - j0;
|
||||
int k = j0 * _outNx + i0;
|
||||
float a = _outDist[k], b = _outDist[k + 1];
|
||||
float c = _outDist[k + _outNx], d = _outDist[k + _outNx + 1];
|
||||
return Mathf.Lerp(Mathf.Lerp(a, b, tx), Mathf.Lerp(c, d, tx), tz) <= _outLimit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 칸 하나를 어느 타일 몫으로 볼지 — 중심까지의 체비쇼프 거리가 가장 가까운 타일(동률이면 앞 번호).
|
||||
/// 마스크가 받아들이는 점은 어떤 타일 중심에서든 `4 + overhang` 안이고, 칸 중심은 거기서 흔들기 폭
|
||||
/// 안에 있다 → 그 칸은 **주인 타일의 넓힌 범위 안**이므로 정확히 한 번 만들어진다(빠짐도 중복도 0).
|
||||
/// </summary>
|
||||
static int OwnerTile(List<TileRef> tiles, float cx, float cz)
|
||||
{
|
||||
int best = -1; float bd = float.MaxValue;
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var c = tiles[i].center;
|
||||
float d = Mathf.Max(Mathf.Abs(cx - c.x), Mathf.Abs(cz - c.z));
|
||||
if (d < bd - 1e-4f) { bd = d; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>어느 타일이든 윗면이면 true (816j2 `WLShoreFoam.OnAnyTile` 과 같은 판정).</summary>
|
||||
static bool OnAnyTileMask(List<TileRef> tiles, float wx, float wz)
|
||||
{
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var t = tiles[i];
|
||||
if (t.mask == null) continue;
|
||||
float r = t.mask.half * 1.4143f; // 회전해도 안전한 바깥 반지름
|
||||
if (Mathf.Abs(wx - t.center.x) > r || Mathf.Abs(wz - t.center.z) > r) continue;
|
||||
var l = t.tf.InverseTransformPoint(new Vector3(wx, t.center.y, wz));
|
||||
if (t.mask.At(l.x, l.z)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -276,6 +276,23 @@ namespace WL.Look.Farm
|
|||
[Tooltip("경계 노이즈의 셀 크기(m). 작을수록 잘게 들쭉날쭉해진다.")]
|
||||
public float farmEdgeNoiseCell = 0.5f;
|
||||
|
||||
// ── 816zd: 땅은 각진 그대로 두고 **풀 카펫의 바깥 윤곽만** 둥글게 (PD 2026-09-15)
|
||||
[Header("§1-816zd — 섬 바깥 윤곽 둥근 풀 배치 (땅은 무변경)")]
|
||||
[Tooltip("1 이면 풀을 깔 넓이를 「섬 윗면 footprint 를 반지름만큼 **열고**(침식 후 팽창) " +
|
||||
"바깥으로 넘침폭만큼 넓힌」 모양으로 정한다. 볼록 모서리가 반지름만큼 둥글어져 " +
|
||||
"각진 타일 위에서도 풀 카펫의 바깥선만 둥글게 읽힌다. 0 이면 예전처럼 타일 마스크 + edgeInset.")]
|
||||
public int outlineRoundEnabled = 1;
|
||||
|
||||
[Tooltip("모서리를 둥글게 깎는 반지름(m). 클수록 모서리에 풀이 더 비고 윤곽이 더 둥글다.")]
|
||||
public float outlineRoundRadius = 1.5f;
|
||||
|
||||
[Tooltip("둥글린 윤곽을 바깥으로 넓히는 폭(m). 기준선은 위 edgeInset 이다 — 같은 값이면 바깥선이 " +
|
||||
"지금과 똑같고(모서리만 둥글어짐), 그보다 크면 차이만큼 가장자리 풀이 절벽 위로 넘친다.")]
|
||||
public float outlineOverhang = 0.3f;
|
||||
|
||||
[Tooltip("윤곽 계산 격자 칸 크기(m). 작을수록 윤곽선이 매끄럽지만 다시 깔 때 더 오래 걸린다.")]
|
||||
public float outlineRoundCell = 0.25f;
|
||||
|
||||
[Tooltip("건물·소품 콜라이더에서 이만큼(m) 떨어뜨린다.")]
|
||||
public float propMargin = 0.05f;
|
||||
|
||||
|
|
|
|||
|
|
@ -232,8 +232,11 @@ namespace WL.Look.Farm
|
|||
return cfg.shoreFoamFallbackY;
|
||||
}
|
||||
|
||||
/// <summary>챔퍼 거리 변환 — seed 가 true 인 칸에서의 거리(m).</summary>
|
||||
static float[] Chamfer(bool[] inside, int nx, int nz, float cell, bool seedOutside)
|
||||
/// <summary>
|
||||
/// 챔퍼 거리 변환 — seed 가 true 인 칸에서의 거리(m).
|
||||
/// 816zd 가 풀 배치의 둥근 윤곽(침식→팽창)에도 그대로 쓴다 — 거리 정의를 한 곳에만 둔다.
|
||||
/// </summary>
|
||||
public static float[] Chamfer(bool[] inside, int nx, int nz, float cell, bool seedOutside)
|
||||
{
|
||||
const float BIG = 1e9f;
|
||||
var d = new float[nx * nz];
|
||||
|
|
|
|||
Loading…
Reference in New Issue