273 lines
13 KiB
C#
273 lines
13 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(); Rebuild(); }
|
||
void OnDisable() { if (_mr != null) _mr.enabled = false; }
|
||
|
||
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;
|
||
_inst.SetTextureOffset(IdBase, new Vector2(_scroll, _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;
|
||
|
||
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);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
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;
|
||
}
|
||
}
|
||
}
|