461 lines
25 KiB
C#
461 lines
25 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLShoreFoam.cs — 섬 **둘레**를 따라 흰 거품 띠를 두른다 (WL-816j2 · #816)
|
||
//
|
||
// 왜 필요한가 (816j 실측)
|
||
// 셰이더의 깊이 기반 Foam 은 **카메라가 보는 쪽 가장자리에만** 거품이 생긴다
|
||
// (섬이 수직 벽 덩어리라 시선이 물을 뚫고 잠긴 벽에 닿는 쪽만 「얕은 물」로 읽힌다).
|
||
// PD 가 「둘레에 띠를 둘러줘」로 결정 → 섬 윗면 footprint 의 **바깥 테두리**를 따라
|
||
// 메시 띠를 하나 만들어 물 위에 깐다. 사방 전부에 생기고, 카메라 방향과 무관하다.
|
||
//
|
||
// 비용
|
||
// 메시 1장 · 머티리얼 1개 · **드로우콜 +1**. 섬 타일 4칸 기준 삼각형 1천 대.
|
||
// 비싸면 `shoreFoamCell`(격자 간격)을 키우거나 `shoreFoamWidth`(폭)를 줄인다.
|
||
//
|
||
// 섬 확장
|
||
// `WLIslandLook.RequestIslandRebuild` 가 풀과 함께 이 띠도 다시 만든다
|
||
// (816e 확장 훅 = `FIIsland.OnActivated` + 잠금 해제 수 폴링) → 새 가장자리 자동.
|
||
//
|
||
// 🔴 FI 코드 0줄 · 씬 파일 0줄 · 원본 셰이더 0줄. 런타임에 메시만 만든다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||
|
||
namespace WL.Look.Farm
|
||
{
|
||
[DisallowMultipleComponent]
|
||
public sealed class WLShoreFoam : MonoBehaviour
|
||
{
|
||
public const string ObjectName = "~WL_ShoreFoam";
|
||
|
||
public static int Rebuilds, Verts, Tris, Tiles;
|
||
public static string LastLog = "";
|
||
|
||
public WLIslandLookSettings cfg;
|
||
|
||
// 검증용 런타임 오버라이드 — 0/음수면 설정값을 쓴다. 🔴 에셋을 고치지 않고 3단 비교를 하기 위한 것.
|
||
[System.NonSerialized] public float widthOverride = 0f;
|
||
[System.NonSerialized] public float innerOverride = -1f;
|
||
[System.NonSerialized] public float cellOverride = 0f;
|
||
|
||
MeshFilter _mf;
|
||
MeshRenderer _mr;
|
||
Mesh _mesh;
|
||
float _scroll;
|
||
|
||
void OnEnable() { Ensure(); WLDemoTerrain.Baked += OnTerrainBaked; Rebuild(); }
|
||
void OnDisable() { WLDemoTerrain.Baked -= OnTerrainBaked; if (_mr != null) _mr.enabled = false; }
|
||
|
||
/// <summary>데모 터레인이 실제로 다시 구워졌을 때(확장·밭 변화) 물가선을 다시 딴다.</summary>
|
||
void OnTerrainBaked() { if (isActiveAndEnabled) Rebuild(); }
|
||
|
||
void Ensure()
|
||
{
|
||
if (_mf == null) _mf = GetComponent<MeshFilter>();
|
||
if (_mf == null) _mf = gameObject.AddComponent<MeshFilter>();
|
||
if (_mr == null) _mr = GetComponent<MeshRenderer>();
|
||
if (_mr == null) _mr = gameObject.AddComponent<MeshRenderer>();
|
||
_mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
_mr.receiveShadows = false;
|
||
_mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||
_mr.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
|
||
_mr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
|
||
if (_mesh == null)
|
||
{
|
||
_mesh = new Mesh { name = "WL_ShoreFoamMesh" };
|
||
_mesh.MarkDynamic();
|
||
_mesh.hideFlags = HideFlags.DontSave;
|
||
}
|
||
_mf.sharedMesh = _mesh;
|
||
}
|
||
|
||
/// <summary>거품이 해안을 따라 천천히 흐른다. 🔴 에셋이 아니라 **런타임 복사본**의 오프셋만 만진다.</summary>
|
||
void Update()
|
||
{
|
||
if (cfg == null || _mr == null || _inst == null) return;
|
||
float sp = cfg.shoreFoamScroll;
|
||
if (sp == 0f) return;
|
||
_scroll += Time.deltaTime * sp;
|
||
if (_scroll > 1f) _scroll -= 1f;
|
||
// 🔴 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");
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public void Rebuild()
|
||
{
|
||
Ensure();
|
||
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0 || cfg.shoreFoamEnabled == 0)
|
||
{ _mr.enabled = false; LastLog = "꺼짐"; return; }
|
||
|
||
if (cfg.shoreFoamMaterial == null) { _mr.enabled = false; LastLog = "🔴 머티리얼 없음"; return; }
|
||
if (_inst == null || _inst.shader != cfg.shoreFoamMaterial.shader)
|
||
{
|
||
if (_inst != null) Destroy(_inst);
|
||
_inst = new Material(cfg.shoreFoamMaterial) { name = "WL_ShoreFoam(runtime)", hideFlags = HideFlags.DontSave };
|
||
}
|
||
_mr.sharedMaterial = _inst;
|
||
|
||
// 🔴 2026-09-16 PD 「물이 섬 경계에 닿을 때 자연스러운 물보라」 — 데모 터레인 모드면 타일 발자국(네모) 대신
|
||
// **터레인 물가선**(지형 높이 = 물 높이)을 따라 띠를 두른다. 경사가 완만한 곳은 넓고 가파른 곳은 좁아 자연스럽다.
|
||
if (cfg.demoTerrainEnabled != 0)
|
||
{
|
||
var dt = Object.FindFirstObjectByType<WLDemoTerrain>(FindObjectsInactive.Include);
|
||
var terrain = dt != null ? dt.TerrainComponent : null;
|
||
if (terrain == null) { _mesh.Clear(); _mr.enabled = false; LastLog = "터레인 대기"; return; }
|
||
BuildFromTerrain(terrain);
|
||
return;
|
||
}
|
||
|
||
var tiles = CollectTiles();
|
||
Tiles = tiles.Count;
|
||
if (tiles.Count == 0) { _mesh.Clear(); _mr.enabled = false; LastLog = "타일 0"; return; }
|
||
|
||
float cell = Mathf.Max(0.15f, cellOverride > 0f ? cellOverride : cfg.shoreFoamCell);
|
||
float w = Mathf.Max(0.1f, widthOverride > 0f ? widthOverride : cfg.shoreFoamWidth);
|
||
float inner = Mathf.Max(0f, innerOverride >= 0f ? innerOverride : cfg.shoreFoamInner);
|
||
float pad = w + inner + cell * 2f;
|
||
|
||
// 섬 전체를 덮는 격자
|
||
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]; float h = t.half;
|
||
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);
|
||
}
|
||
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);
|
||
|
||
// ① 안/밖
|
||
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++)
|
||
{
|
||
float x = minX + i * cell;
|
||
inside[j * nx + i] = OnAnyTile(tiles, x, z);
|
||
}
|
||
}
|
||
|
||
// ② 부호 있는 거리(챔퍼 2패스) — 밖은 +, 안은 −
|
||
var d = Chamfer(inside, nx, nz, cell, false); // 밖 → 가장 가까운 안까지
|
||
var e = Chamfer(inside, nx, nz, cell, true); // 안 → 가장 가까운 밖까지
|
||
var sd = new float[nx * nz];
|
||
for (int k = 0; k < sd.Length; k++) sd[k] = inside[k] ? -e[k] : d[k];
|
||
|
||
// ③ 띠 구간([-inner, w])에 걸치는 칸만 삼각형으로
|
||
float y = WaterY(tiles) + cfg.shoreFoamY;
|
||
float tileLen = Mathf.Max(0.5f, cfg.shoreFoamTileLength);
|
||
var vmap = new int[nx * nz];
|
||
for (int k = 0; k < vmap.Length; k++) vmap[k] = -1;
|
||
var verts = new List<Vector3>(4096);
|
||
var uvs = new List<Vector2>(4096);
|
||
var tris = new List<int>(8192);
|
||
|
||
for (int j = 0; j < nz - 1; j++)
|
||
for (int i = 0; i < nx - 1; i++)
|
||
{
|
||
int a = j * nx + i, b = a + 1, c = a + nx, dd = c + 1;
|
||
float lo = Mathf.Min(Mathf.Min(sd[a], sd[b]), Mathf.Min(sd[c], sd[dd]));
|
||
float hi = Mathf.Max(Mathf.Max(sd[a], sd[b]), Mathf.Max(sd[c], sd[dd]));
|
||
if (lo > w || hi < -inner) continue;
|
||
int va = V(a), vb = V(b), vc = V(c), vd = V(dd);
|
||
tris.Add(va); tris.Add(vc); tris.Add(vb);
|
||
tris.Add(vb); tris.Add(vc); tris.Add(vd);
|
||
}
|
||
|
||
int V(int k)
|
||
{
|
||
if (vmap[k] >= 0) return vmap[k];
|
||
int i = k % nx, j = k / nx;
|
||
float x = minX + i * cell, z = minZ + j * cell;
|
||
verts.Add(new Vector3(x, y, z));
|
||
float v = Mathf.Clamp01((sd[k] + inner) / (w + inner));
|
||
uvs.Add(new Vector2((x * 0.7071f + z * 0.7071f) / tileLen, v));
|
||
vmap[k] = verts.Count - 1;
|
||
return vmap[k];
|
||
}
|
||
|
||
_mesh.Clear();
|
||
if (verts.Count == 0 || tris.Count == 0) { _mr.enabled = false; LastLog = "띠 0"; return; }
|
||
_mesh.indexFormat = verts.Count > 65000
|
||
? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||
_mesh.SetVertices(verts);
|
||
_mesh.SetUVs(0, uvs);
|
||
_mesh.SetTriangles(tris, 0);
|
||
_mesh.RecalculateBounds();
|
||
_mr.enabled = true;
|
||
|
||
Rebuilds++; Verts = verts.Count; Tris = tris.Count / 3;
|
||
LastLog = "타일 " + Tiles + " · 띠 삼각형 " + Tris + " · 정점 " + Verts
|
||
+ " · 폭 " + w.ToString("F2") + " m · 격자 " + cell.ToString("F2") + " m · 드로우콜 +1";
|
||
cfg.Log("둘레 거품 띠 — " + LastLog);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 데모 터레인 물가선 띠 — 격자마다 f = 물 높이 − 지형 높이(+ = 물속 깊이 · − = 모래 위 높이).
|
||
// 띠 = f ∈ [−shoreFoamUp, +shoreFoamDown] 에 걸치는 칸. 정점 높이 = max(지형, 물) + shoreFoamY → 모래 위에서는 모래를 덮고 물 위에서는 물을 덮는다.
|
||
// UV v = 0(모래 끝) → 1(바다 끝) — 기존 띠 텍스처와 같은 방향. u = 해안 방향 반복(shoreFoamTileLength).
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static int TerrainNodes, WaterlinePoints;
|
||
|
||
void BuildFromTerrain(Terrain terrain)
|
||
{
|
||
float waterY = WaterYOfScene(terrain.gameObject.scene);
|
||
float up = Mathf.Max(0.01f, cfg.shoreFoamUp), down = Mathf.Max(0.01f, cfg.shoreFoamDown);
|
||
float cell = Mathf.Max(0.15f, cellOverride > 0f ? cellOverride : cfg.shoreFoamCell);
|
||
float lift = cfg.shoreFoamY;
|
||
float tileLen = Mathf.Max(0.5f, cfg.shoreFoamTileLength);
|
||
var tp = terrain.transform.position;
|
||
var sz = terrain.terrainData.size;
|
||
// 띠 텍스처 = 런타임 생성(물가선에서 진하고 모래 쪽은 짧게 · 바다 쪽은 길게 들쭉날쭉 사라진다) — 에셋 텍스처는 네모 띠(816j2)용이라 가장자리가 딱 끊긴다
|
||
var tex = FoamTexture(up / (up + down));
|
||
_inst.SetTexture(IdBase, tex);
|
||
if (_inst.HasProperty("_MainTex")) _inst.SetTexture("_MainTex", tex);
|
||
int nx = Mathf.Clamp(Mathf.CeilToInt(sz.x / cell) + 1, 2, 1024);
|
||
int nz = Mathf.Clamp(Mathf.CeilToInt(sz.z / cell) + 1, 2, 1024);
|
||
float stepX = sz.x / (nx - 1), stepZ = sz.z / (nz - 1);
|
||
|
||
var hgt = new float[nx * nz];
|
||
var under = new bool[nx * nz];
|
||
var probe = new Vector3();
|
||
for (int j = 0; j < nz; j++)
|
||
{
|
||
probe.z = tp.z + j * stepZ;
|
||
for (int i = 0; i < nx; i++)
|
||
{
|
||
probe.x = tp.x + i * stepX;
|
||
float h = terrain.SampleHeight(probe) + tp.y;
|
||
int k = j * nx + i;
|
||
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;
|
||
var verts = new List<Vector3>(2048);
|
||
var uvs = new List<Vector2>(2048);
|
||
var tris = new List<int>(4096);
|
||
|
||
for (int j = 0; j < nz - 1; j++)
|
||
for (int i = 0; i < nx - 1; i++)
|
||
{
|
||
int a = j * nx + i, b = a + 1, c = a + nx, dd = c + 1;
|
||
float lo = Mathf.Min(Mathf.Min(f[a], f[b]), Mathf.Min(f[c], f[dd]));
|
||
float hi = Mathf.Max(Mathf.Max(f[a], f[b]), Mathf.Max(f[c], f[dd]));
|
||
if (lo > down || hi < -up) continue;
|
||
int va = V(a), vb = V(b), vc = V(c), vd = V(dd);
|
||
tris.Add(va); tris.Add(vc); tris.Add(vb);
|
||
tris.Add(vb); tris.Add(vc); tris.Add(vd);
|
||
}
|
||
|
||
int V(int k)
|
||
{
|
||
if (vmap[k] >= 0) return vmap[k];
|
||
int i = k % nx, j = k / nx;
|
||
float x = tp.x + i * stepX, z = tp.z + j * stepZ;
|
||
verts.Add(new Vector3(x, Mathf.Max(hgt[k], waterY) + lift, z));
|
||
float v = Mathf.Clamp01((f[k] + up) / (down + up));
|
||
uvs.Add(new Vector2((x * 0.7071f + z * 0.7071f) / tileLen, v));
|
||
vmap[k] = verts.Count - 1;
|
||
return vmap[k];
|
||
}
|
||
|
||
_mesh.Clear();
|
||
if (verts.Count == 0 || tris.Count == 0) { _mr.enabled = false; LastLog = "물가선 띠 0(물 높이 " + waterY.ToString("F2") + ")"; return; }
|
||
_mesh.indexFormat = verts.Count > 65000
|
||
? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||
_mesh.SetVertices(verts);
|
||
_mesh.SetUVs(0, uvs);
|
||
_mesh.SetTriangles(tris, 0);
|
||
_mesh.RecalculateBounds();
|
||
_mr.enabled = true;
|
||
|
||
Rebuilds++; Verts = verts.Count; Tris = tris.Count / 3; Tiles = 0;
|
||
LastLog = "터레인 물가선 띠 — 삼각형 " + Tris + " · 정점 " + Verts + " · 물 y " + waterY.ToString("F2") + " · 물가선 점 " + WaterlinePoints
|
||
+ " · 모래 쪽 " + up.ToString("F2") + " m · 바다 쪽 " + down.ToString("F2") + " m(유클리드 수평 거리) · 격자 " + cell.ToString("F2") + " m · 드로우콜 +1";
|
||
cfg.Log("둘레 거품 띠 — " + LastLog);
|
||
}
|
||
|
||
static float SmoothStep01(float t) { t = Mathf.Clamp01(t); return t * t * (3f - 2f * t); }
|
||
|
||
// 물가선 띠 텍스처(런타임 · 에셋 0줄): u = 해안 방향(주기 1 · 반복) · v = 모래 끝(0) → 물가선(vWl) → 바다 끝(1).
|
||
// 알파 = 물가선 1 · 모래 쪽으로 짧게(제곱) · 바다 쪽으로 길게, 바다 쪽 끝은 해안 방향 파형으로 들쭉날쭉.
|
||
static Texture2D s_foamTex;
|
||
static float s_foamTexKey = float.NaN;
|
||
static Texture2D FoamTexture(float vWl)
|
||
{
|
||
vWl = Mathf.Clamp(vWl, 0.05f, 0.95f);
|
||
if (s_foamTex != null && Mathf.Abs(s_foamTexKey - vWl) < 0.001f) return s_foamTex;
|
||
const int W = 256, H = 64;
|
||
if (s_foamTex == null)
|
||
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) : 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.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));
|
||
}
|
||
}
|
||
s_foamTex.SetPixels32(px);
|
||
s_foamTex.Apply(false, false);
|
||
s_foamTexKey = vWl;
|
||
return s_foamTex;
|
||
}
|
||
|
||
/// <summary>물 평면의 높이(씬 기준) — `Water` 루트/자식을 찾고 없으면 설정값.</summary>
|
||
float WaterYOfScene(UnityEngine.SceneManagement.Scene scene)
|
||
{
|
||
if (!scene.IsValid()) return cfg.shoreFoamFallbackY;
|
||
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;
|
||
}
|
||
|
||
struct TileRef { public Transform tf; public Vector3 center; public WLTileMask mask; public float half; }
|
||
|
||
readonly List<TileRef> _tiles = new List<TileRef>(96);
|
||
|
||
List<TileRef> CollectTiles()
|
||
{
|
||
_tiles.Clear();
|
||
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
for (int i = 0; i < islands.Length; i++)
|
||
{
|
||
var isl = islands[i];
|
||
if (isl == null || !isl.IsUnlocked) continue;
|
||
if (!isl.gameObject.activeInHierarchy) continue;
|
||
if (isl.transform.localScale.x < 0.99f) continue; // 확장 애니메이션 중
|
||
var mf = isl.GetComponent<MeshFilter>();
|
||
if (mf == null || mf.sharedMesh == null) continue;
|
||
var mask = cfg.FindTileMask(mf.sharedMesh.name);
|
||
if (mask == null) continue;
|
||
_tiles.Add(new TileRef { tf = isl.transform, center = isl.transform.position, mask = mask, half = mask.half });
|
||
}
|
||
return _tiles;
|
||
}
|
||
|
||
static bool OnAnyTile(List<TileRef> tiles, float wx, float wz)
|
||
{
|
||
for (int i = 0; i < tiles.Count; i++)
|
||
{
|
||
var t = tiles[i];
|
||
var l = t.tf.InverseTransformPoint(new Vector3(wx, t.center.y, wz));
|
||
if (t.mask.At(l.x, l.z)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>물 평면의 높이 — 섬 씬의 `Water` 를 찾고 없으면 설정값.</summary>
|
||
float WaterY(List<TileRef> tiles)
|
||
{
|
||
var scene = tiles.Count > 0 ? tiles[0].tf.gameObject.scene : gameObject.scene;
|
||
var roots = scene.GetRootGameObjects();
|
||
for (int i = 0; i < roots.Length; i++)
|
||
{
|
||
var t = roots[i].transform.Find("Water");
|
||
if (t != null) return t.position.y;
|
||
if (roots[i].name == "Water") return roots[i].transform.position.y;
|
||
}
|
||
return cfg.shoreFoamFallbackY;
|
||
}
|
||
|
||
/// <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];
|
||
for (int k = 0; k < d.Length; k++)
|
||
{
|
||
bool seed = seedOutside ? !inside[k] : inside[k];
|
||
d[k] = seed ? 0f : BIG;
|
||
}
|
||
float a = cell, b = cell * 1.41421356f;
|
||
for (int j = 0; j < nz; j++)
|
||
for (int i = 0; i < nx; i++)
|
||
{
|
||
int k = j * nx + i; float m = d[k];
|
||
if (i > 0) m = Mathf.Min(m, d[k - 1] + a);
|
||
if (j > 0) m = Mathf.Min(m, d[k - nx] + a);
|
||
if (i > 0 && j > 0) m = Mathf.Min(m, d[k - nx - 1] + b);
|
||
if (i < nx - 1 && j > 0) m = Mathf.Min(m, d[k - nx + 1] + b);
|
||
d[k] = m;
|
||
}
|
||
for (int j = nz - 1; j >= 0; j--)
|
||
for (int i = nx - 1; i >= 0; i--)
|
||
{
|
||
int k = j * nx + i; float m = d[k];
|
||
if (i < nx - 1) m = Mathf.Min(m, d[k + 1] + a);
|
||
if (j < nz - 1) m = Mathf.Min(m, d[k + nx] + a);
|
||
if (i < nx - 1 && j < nz - 1) m = Mathf.Min(m, d[k + nx + 1] + b);
|
||
if (i > 0 && j < nz - 1) m = Mathf.Min(m, d[k + nx - 1] + b);
|
||
d[k] = m;
|
||
}
|
||
return d;
|
||
}
|
||
}
|
||
}
|