Project_WL/Assets/WL/Look/Farm/WLShoreFoam.cs

273 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}