2026-09-15 15:01:24 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// WLShoreSkirt.cs — 섬 둘레 완만한 모래 경사 테두리(물가 스커트) (WL-816zg · #816)
|
|
|
|
|
|
//
|
|
|
|
|
|
// PD 결정(2026-09-15 「물은 진행」) — 데모 연못처럼 섬 가장자리가 완만한 모래 경사로 물에 잠기게.
|
|
|
|
|
|
// · 둥근 윤곽 : 열린 타일 발자국의 **유클리드 거리장**으로 띠를 만든다 → 볼록 모서리가 저절로 둥글다.
|
|
|
|
|
|
// · 흰 물가 띠 : 물 셰이더(Farm_Water_Demo3)가 **깊이**로 만든다. 경사가 물 아래까지 이어져 얕은 물이 생긴다.
|
|
|
|
|
|
// · 타일 옆면 가림 : 경사가 타일 가장자리(윗면 − skirtTopDrop)에서 시작해 수직 옆면을 덮는다.
|
|
|
|
|
|
//
|
|
|
|
|
|
// ■ 만드는 법
|
|
|
|
|
|
// 1) 열린 타일(FI `Island` · IsUnlocked && activeInHierarchy · 한 변 = IslandManager.IslandSize)의 중심을 모은다.
|
|
|
|
|
|
// 2) 발자국의 **바깥 테두리 선분**(이웃 타일이 없는 변)만 골라 둔다. 격자 정점마다 그 선분들까지의 최단 거리가
|
|
|
|
|
|
// 부호 있는 거리 d(안쪽 −, 바깥 +)다 — 타일별 사각형 SDF 의 min 은 타일 이음매에서 「안쪽인데 0 근처」로 틀려
|
|
|
|
|
|
// 윗면 위에 조각이 생기므로 쓰지 않는다(WLSoftDirt.SignedDist 의 「부호 있는 거리 → 띠」 방식은 그대로).
|
|
|
|
|
|
// 3) −skirtInnerKeep ≤ d ≤ skirtWidth 인 격자(셀 skirtCell)만 만들고, y = smoothstep(d / skirtWidth) 로 윗면 → skirtBottomY.
|
|
|
|
|
|
// 격자 원점을 타일 가장자리에 맞춰 정점이 가장자리 선 위에 정확히 놓인다(틈 0 · 윗면 z-fight 0).
|
|
|
|
|
|
// 4) 색 = 정점색. d 0~skirtGrassBand 는 풀색(islandTopMaterial `_DiffuseColor`) → 모래색(softDirtColor) · 물 아래 × skirtUnderwaterMul.
|
|
|
|
|
|
// 셰이더 `WL/Shore Skirt` = 언릿 정점색 · URP · DepthOnly/DepthNormals 패스 포함(물 셰이더가 깊이 텍스처에서 경사를 봐야 한다).
|
|
|
|
|
|
// 메시 1장 · 드로우콜 +1 · 삼각형 수 = LastTriangles(로그에도 남긴다).
|
|
|
|
|
|
//
|
|
|
|
|
|
// ■ 되돌리기 — `shoreSkirtEnabled = 0` 이면 만들지 않는다(지금 상태 100 %).
|
|
|
|
|
|
//
|
|
|
|
|
|
// 🔴 FI 코드 0줄 · 씬 파일 0줄 · 에셋 0줄. 메시·머티리얼은 런타임 생성이다.
|
|
|
|
|
|
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Rendering;
|
|
|
|
|
|
using FIIsland = CryingSnow.FarmingIsland.Island;
|
|
|
|
|
|
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
|
|
|
|
|
|
|
|
|
|
|
|
namespace WL.Look.Farm
|
|
|
|
|
|
{
|
|
|
|
|
|
[DisallowMultipleComponent]
|
|
|
|
|
|
public sealed class WLShoreSkirt : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public const string ObjectName = "~WL_ShoreSkirt";
|
|
|
|
|
|
public const string ShaderName = "WL/Shore Skirt";
|
|
|
|
|
|
|
|
|
|
|
|
// ── 진단(프로브·보고가 읽는다)
|
|
|
|
|
|
public static int Rebuilds;
|
|
|
|
|
|
public static int LastTiles, LastVertices, LastTriangles;
|
|
|
|
|
|
public static string LastLog = "꺼짐";
|
|
|
|
|
|
|
|
|
|
|
|
public WLIslandLookSettings cfg;
|
|
|
|
|
|
|
|
|
|
|
|
MeshFilter _mf;
|
|
|
|
|
|
MeshRenderer _mr;
|
|
|
|
|
|
Mesh _mesh;
|
|
|
|
|
|
Material _inst;
|
|
|
|
|
|
bool _vertexColor; // 전용 셰이더를 찾았는가(아니면 URP Unlit 모래 단색 대체)
|
|
|
|
|
|
|
|
|
|
|
|
static readonly int IdBaseColor = Shader.PropertyToID("_BaseColor");
|
|
|
|
|
|
static readonly int IdColor = Shader.PropertyToID("_Color");
|
|
|
|
|
|
static readonly int IdDiffuseColor = Shader.PropertyToID("_DiffuseColor");
|
|
|
|
|
|
|
|
|
|
|
|
struct Seg { public float ax, az, bx, bz; }
|
|
|
|
|
|
readonly List<Vector3> _tiles = new List<Vector3>(); // x·z = 타일 중심 · y = 렌더러 윗면 실측
|
|
|
|
|
|
readonly List<Seg> _edges = new List<Seg>(); // 발자국 바깥 테두리 선분
|
|
|
|
|
|
readonly List<Vector3> _verts = new List<Vector3>();
|
|
|
|
|
|
readonly List<Color32> _cols = new List<Color32>();
|
|
|
|
|
|
readonly List<int> _tris = new List<int>();
|
2026-09-15 15:19:47 +00:00
|
|
|
|
readonly List<int> _trisSand = new List<int>(); // Toon 모드 — 모래 서브메시
|
|
|
|
|
|
static readonly int IdShadowDiffuseColor = Shader.PropertyToID("_ShadowDiffuseColor");
|
|
|
|
|
|
Material _sandInst; // Toon 모드 — 타일 재질 복사본(모래색)
|
|
|
|
|
|
bool _toon;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
|
|
|
|
|
|
// 마지막으로 만든 조건 — 같으면 다시 만들지 않는다(1초 폴링·재훑기에서 공짜로 돌게).
|
|
|
|
|
|
bool _dirty = true;
|
|
|
|
|
|
long _lastTileSig;
|
|
|
|
|
|
float _lastValSig = float.NaN;
|
|
|
|
|
|
|
|
|
|
|
|
void OnEnable() { Ensure(); _dirty = true; Rebuild(); }
|
|
|
|
|
|
void OnDisable() { if (_mr != null) _mr.enabled = false; }
|
|
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_inst != null) Destroy(_inst);
|
2026-09-15 15:19:47 +00:00
|
|
|
|
if (_sandInst != null) Destroy(_sandInst);
|
2026-09-15 15:01:24 +00:00
|
|
|
|
if (_mesh != null) Destroy(_mesh);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 = ShadowCastingMode.Off;
|
|
|
|
|
|
_mr.receiveShadows = false;
|
|
|
|
|
|
_mr.lightProbeUsage = LightProbeUsage.Off;
|
|
|
|
|
|
_mr.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
|
|
|
|
|
_mr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
|
|
|
|
|
|
if (_mesh == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_mesh = new Mesh { name = "WL_ShoreSkirtMesh" };
|
|
|
|
|
|
_mesh.MarkDynamic();
|
|
|
|
|
|
_mesh.hideFlags = HideFlags.DontSave;
|
|
|
|
|
|
}
|
|
|
|
|
|
_mf.sharedMesh = _mesh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>다시 만든다(섬 확장·재훑기). 멱등 — 발자국·값이 그대로면 아무것도 하지 않는다.</summary>
|
|
|
|
|
|
public void Rebuild()
|
|
|
|
|
|
{
|
|
|
|
|
|
Ensure();
|
|
|
|
|
|
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
|
|
|
|
|
if (cfg == null || cfg.enabled_ == 0 || cfg.shoreSkirtEnabled == 0)
|
|
|
|
|
|
{ _mr.enabled = false; LastLog = "꺼짐"; return; }
|
|
|
|
|
|
|
|
|
|
|
|
float size = TileSize();
|
|
|
|
|
|
if (size <= 0.01f) { _mr.enabled = false; LastLog = "타일 크기 미확인(IslandManager 없음) — 대기"; return; }
|
|
|
|
|
|
float half = size * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
long tileSig = CollectTiles(out float topMeasured);
|
|
|
|
|
|
LastTiles = _tiles.Count;
|
|
|
|
|
|
if (_tiles.Count == 0) { _mr.enabled = false; LastLog = "열린 타일 0 — 대기"; return; }
|
|
|
|
|
|
|
|
|
|
|
|
bool fixedTop = cfg.skirtTopMode != 0 || float.IsNaN(topMeasured);
|
|
|
|
|
|
float topY = fixedTop ? cfg.skirtTopY : topMeasured;
|
|
|
|
|
|
float waterY = FindWaterY(out bool waterFound);
|
|
|
|
|
|
|
|
|
|
|
|
Color grass = ToVertexSpace(GrassColor());
|
|
|
|
|
|
Color sand = ToVertexSpace(cfg.softDirtColor);
|
|
|
|
|
|
|
|
|
|
|
|
// 값이 바뀌면 다시 만든다(인스펙터에서 만져도 다음 폴링에 반영된다).
|
|
|
|
|
|
float valSig = cfg.skirtWidth * 7.1f + cfg.skirtBottomY * 13.3f + cfg.skirtGrassBand * 3.7f + cfg.skirtSandBlend * 29.3f
|
|
|
|
|
|
+ cfg.skirtCell * 5.9f + cfg.skirtInnerKeep * 11.9f + cfg.skirtTopDrop * 17.7f + cfg.skirtUnderwaterMul * 31.7f
|
|
|
|
|
|
+ cfg.skirtMaxGridNodes * 0.001f + topY * 37.3f + waterY * 41.1f
|
2026-09-15 15:19:47 +00:00
|
|
|
|
+ grass.r * 101f + grass.g * 103f + grass.b * 107f + sand.r * 109f + sand.g * 113f + sand.b * 127f
|
2026-09-16 00:49:01 +00:00
|
|
|
|
+ cfg.skirtToonMode * 53f + cfg.skirtEdgeJitter * 59f + cfg.skirtSandShadowMul * 61f + cfg.skirtGrassRound * 67f
|
|
|
|
|
|
+ cfg.skirtWaveAmp * 71f + cfg.skirtWaveCell * 73f;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
|
2026-09-15 15:19:47 +00:00
|
|
|
|
bool same = !_dirty && (_inst != null || _sandInst != null) && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
if (same) { _mr.enabled = true; return; }
|
|
|
|
|
|
|
|
|
|
|
|
if (!EnsureMaterial()) { _mr.enabled = false; LastLog = "🔴 셰이더를 못 찾음(WL/Shore Skirt · URP Unlit 둘 다)"; return; }
|
|
|
|
|
|
|
|
|
|
|
|
BuildEdges(size, half);
|
|
|
|
|
|
float cell = BuildMesh(half, topY, waterY, grass, sand);
|
|
|
|
|
|
|
2026-09-15 15:19:47 +00:00
|
|
|
|
if (_toon) _mr.sharedMaterials = new[] { cfg.islandTopMaterial, _sandInst }; // 풀 = 타일 재질 그대로 · 모래 = 복사본
|
|
|
|
|
|
else _mr.sharedMaterial = _inst;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
_mr.enabled = true;
|
|
|
|
|
|
_lastTileSig = tileSig; _lastValSig = valSig; _dirty = false;
|
|
|
|
|
|
Rebuilds++;
|
|
|
|
|
|
|
|
|
|
|
|
LastLog = "물가 스커트 — 타일 " + _tiles.Count + " · 테두리 변 " + _edges.Count
|
|
|
|
|
|
+ " · 정점 " + LastVertices + " · 삼각형 " + LastTriangles
|
|
|
|
|
|
+ " · 폭 " + cfg.skirtWidth.ToString("F1") + " m · 윗면 y " + topY.ToString("F2")
|
|
|
|
|
|
+ (fixedTop ? "(고정 · 실측 " + (float.IsNaN(topMeasured) ? "없음" : topMeasured.ToString("F2"))
|
|
|
|
|
|
: "(실측 · 고정값 " + cfg.skirtTopY.ToString("F2")) + ")"
|
|
|
|
|
|
+ " → 바닥 " + cfg.skirtBottomY.ToString("F2") + " · 물 " + waterY.ToString("F2") + (waterFound ? "" : "(미발견·대체값)")
|
2026-09-15 15:19:47 +00:00
|
|
|
|
+ " · 격자 " + cell.ToString("F2") + " m · 셰이더 " + (_toon ? "Toon 2재질(풀=타일 · 모래=복사본 · 구름 동일)" : _vertexColor ? ShaderName : "대체 URP Unlit(정점색 없음 · 모래 단색)")
|
|
|
|
|
|
+ " · 드로우콜 +" + (_toon ? "2" : "1");
|
2026-09-15 15:01:24 +00:00
|
|
|
|
cfg.Log(LastLog);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 입력 수집
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static float TileSize()
|
|
|
|
|
|
{
|
|
|
|
|
|
var im = FIIslandManager.Instance;
|
|
|
|
|
|
return im != null ? im.IslandSize : 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>열린 타일 중심을 모은다. 반환 = 발자국 서명(순서 무관) — 확장으로 바뀌면 값이 바뀐다.</summary>
|
|
|
|
|
|
long CollectTiles(out float topMeasured)
|
|
|
|
|
|
{
|
|
|
|
|
|
_tiles.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;
|
|
|
|
|
|
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>물 높이 — SpawnSeabed 와 같은 판별(씬의 `Water` 렌더러). 없으면 거품 띠의 대체값.</summary>
|
|
|
|
|
|
float FindWaterY(out bool found)
|
|
|
|
|
|
{
|
|
|
|
|
|
var scene = gameObject.scene;
|
|
|
|
|
|
var mrs = Object.FindObjectsByType<MeshRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
|
|
|
|
for (int i = 0; i < mrs.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = mrs[i];
|
|
|
|
|
|
if (r == null || r.gameObject.name != "Water") continue;
|
|
|
|
|
|
if (scene.IsValid() && r.gameObject.scene != scene) continue;
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
return r.transform.position.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
found = false;
|
|
|
|
|
|
return cfg.shoreFoamFallbackY;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 00:49:01 +00:00
|
|
|
|
/// <summary>격자 정수 좌표 → 0~1 결정적 해시(굽이 노이즈용).</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 · 겹선형 · 결정적) × 진폭. 직선 변을 파도처럼 굽이치게 한다.</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 마칭 트라이앵글 — s 의 부호가 갈리는 삼각형을 등고선(s=0)으로 잘라 풀/모래 서브메시에 나눠 담는다 ──
|
|
|
|
|
|
float[] _s;
|
|
|
|
|
|
readonly Dictionary<long, int> _cut = new Dictionary<long, int>();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>노드 na/nb 사이 s=0 지점의 정점(위치·색 보간 · 변마다 한 번만 생성).</summary>
|
|
|
|
|
|
int CrossVertex(int va, int na, int vb, int nb)
|
|
|
|
|
|
{
|
|
|
|
|
|
long key = na < nb ? ((long)na << 32) | (uint)nb : ((long)nb << 32) | (uint)na;
|
|
|
|
|
|
int idx;
|
|
|
|
|
|
if (_cut.TryGetValue(key, out idx)) return idx;
|
|
|
|
|
|
float sa = _s[na], sb = _s[nb];
|
|
|
|
|
|
float t = Mathf.Clamp01(sa / (sa - sb));
|
|
|
|
|
|
idx = _verts.Count;
|
|
|
|
|
|
_verts.Add(Vector3.LerpUnclamped(_verts[va], _verts[vb], t));
|
|
|
|
|
|
_cols.Add(Color32.Lerp(_cols[va], _cols[vb], t));
|
|
|
|
|
|
_cut[key] = idx;
|
|
|
|
|
|
return idx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>삼각형(정점 va,vb,vc · 노드 na,nb,nc · 감김 유지)을 s 부호로 가른다.</summary>
|
|
|
|
|
|
void CutTri(int va, int na, int vb, int nb, int vc, int nc)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool ga = _s[na] < 0f, gb = _s[nb] < 0f, gc = _s[nc] < 0f;
|
|
|
|
|
|
if (ga == gb && gb == gc) { var l = ga ? _tris : _trisSand; l.Add(va); l.Add(vb); l.Add(vc); return; }
|
|
|
|
|
|
// 홀로 다른 꼭짓점 o 와 나머지 p,q (a→b→c 순환 순서 유지)
|
|
|
|
|
|
int vo, no, vp, np, vq, nq; bool go;
|
|
|
|
|
|
if (ga != gb && ga != gc) { vo = va; no = na; vp = vb; np = nb; vq = vc; nq = nc; go = ga; }
|
|
|
|
|
|
else if (gb != ga && gb != gc) { vo = vb; no = nb; vp = vc; np = nc; vq = va; nq = na; go = gb; }
|
|
|
|
|
|
else { vo = vc; no = nc; vp = va; np = na; vq = vb; nq = nb; go = gc; }
|
|
|
|
|
|
int m1 = CrossVertex(vo, no, vp, np);
|
|
|
|
|
|
int m2 = CrossVertex(vo, no, vq, nq);
|
|
|
|
|
|
var lo = go ? _tris : _trisSand; // o 쪽 조각(삼각형 1)
|
|
|
|
|
|
var lr = go ? _trisSand : _tris; // 나머지 조각(사각형 = 삼각형 2)
|
|
|
|
|
|
lo.Add(vo); lo.Add(m1); lo.Add(m2);
|
|
|
|
|
|
lr.Add(m1); lr.Add(vp); lr.Add(vq);
|
|
|
|
|
|
lr.Add(m1); lr.Add(vq); lr.Add(m2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-15 15:01:24 +00:00
|
|
|
|
/// <summary>풀색 — 타일 윗면 머티리얼(Farm_IslandTop_Demo3)의 `_DiffuseColor`. 없으면 `_BaseColor`·`_Color` → 설정 대체값.</summary>
|
|
|
|
|
|
Color GrassColor()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = cfg.islandTopMaterial;
|
|
|
|
|
|
if (m != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m.HasProperty(IdDiffuseColor)) return m.GetColor(IdDiffuseColor);
|
|
|
|
|
|
if (m.HasProperty(IdBaseColor)) return m.GetColor(IdBaseColor);
|
|
|
|
|
|
if (m.HasProperty(IdColor)) return m.GetColor(IdColor);
|
|
|
|
|
|
}
|
|
|
|
|
|
return cfg.skirtGrassFallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 설정·머티리얼의 색은 sRGB 로 적힌 값이다(URP Unlit `_BaseColor` 와 같은 해석 — 바닥판·흙 데칼과 같은 숫자면 같은 색).
|
|
|
|
|
|
/// 정점색은 변환 없이 셰이더로 들어가므로 리니어 파이프라인에서는 리니어로 옮겨 넣는다.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
static Color ToVertexSpace(Color c)
|
|
|
|
|
|
{
|
|
|
|
|
|
return QualitySettings.activeColorSpace == ColorSpace.Linear ? c.linear : c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>전용 셰이더(정점색) → 없으면 URP Unlit 모래 단색 대체. 에셋은 만들지 않는다.</summary>
|
|
|
|
|
|
bool EnsureMaterial()
|
|
|
|
|
|
{
|
2026-09-15 15:19:47 +00:00
|
|
|
|
// 2026-09-16 Lead — Toon 모드: 타일과 같은 재질 계열이라 구름 그림자·조명이 똑같이 떨어진다(이음새 0).
|
|
|
|
|
|
if (cfg.skirtToonMode != 0 && cfg.islandTopMaterial != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_toon = true; _vertexColor = false;
|
|
|
|
|
|
if (_sandInst == null)
|
|
|
|
|
|
_sandInst = new Material(cfg.islandTopMaterial) { name = "WL_ShoreSkirt_Sand(runtime)", hideFlags = HideFlags.DontSave };
|
|
|
|
|
|
var sc = cfg.softDirtColor; sc.a = 1f;
|
|
|
|
|
|
float sm = Mathf.Clamp01(cfg.skirtSandShadowMul);
|
|
|
|
|
|
if (_sandInst.HasProperty(IdDiffuseColor)) _sandInst.SetColor(IdDiffuseColor, sc);
|
|
|
|
|
|
if (_sandInst.HasProperty(IdShadowDiffuseColor)) _sandInst.SetColor(IdShadowDiffuseColor, new Color(sc.r * sm, sc.g * sm, sc.b * sm, 1f));
|
|
|
|
|
|
if (_sandInst.HasProperty(IdBaseColor)) _sandInst.SetColor(IdBaseColor, sc);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
_toon = false;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
if (_inst != null) return true;
|
|
|
|
|
|
|
|
|
|
|
|
Shader sh = cfg.shoreSkirtMaterial != null ? cfg.shoreSkirtMaterial.shader : null;
|
|
|
|
|
|
if (sh == null) sh = Shader.Find(ShaderName);
|
|
|
|
|
|
if (sh == null) sh = Shader.Find("Universal Render Pipeline/Unlit");
|
|
|
|
|
|
if (sh == null) sh = Shader.Find("Unlit/Color");
|
|
|
|
|
|
if (sh == null) return false;
|
|
|
|
|
|
_vertexColor = sh.name == ShaderName;
|
|
|
|
|
|
|
|
|
|
|
|
_inst = cfg.shoreSkirtMaterial != null && cfg.shoreSkirtMaterial.shader == sh
|
|
|
|
|
|
? new Material(cfg.shoreSkirtMaterial) : new Material(sh);
|
|
|
|
|
|
_inst.name = "WL_ShoreSkirt(runtime)";
|
|
|
|
|
|
_inst.hideFlags = HideFlags.DontSave;
|
|
|
|
|
|
if (!_vertexColor)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 대체 셰이더의 색 프로퍼티는 sRGB 해석 → 설정값을 그대로 넣는다(흙 데칼과 같은 색)
|
|
|
|
|
|
if (_inst.HasProperty(IdBaseColor)) _inst.SetColor(IdBaseColor, cfg.softDirtColor);
|
|
|
|
|
|
if (_inst.HasProperty(IdColor)) _inst.SetColor(IdColor, cfg.softDirtColor);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 거리장 — 발자국 바깥 테두리 선분까지의 유클리드 거리(정확) · 부호 = 타일 안(−)/밖(+)
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 SignedDist(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 메시 — 격자(셀 skirtCell) · 정점마다 d → 높이(smoothstep)·정점색
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
float BuildMesh(float half, float topY, float waterY, Color grass, Color sand)
|
|
|
|
|
|
{
|
|
|
|
|
|
float cell = Mathf.Max(0.1f, cfg.skirtCell);
|
|
|
|
|
|
float width = Mathf.Max(cell, cfg.skirtWidth);
|
|
|
|
|
|
float keep = Mathf.Max(0f, cfg.skirtInnerKeep);
|
|
|
|
|
|
float top = topY - Mathf.Max(0f, cfg.skirtTopDrop);
|
|
|
|
|
|
float bottom = cfg.skirtBottomY;
|
|
|
|
|
|
float band = Mathf.Max(0f, cfg.skirtGrassBand);
|
|
|
|
|
|
float blend = Mathf.Max(0.01f, cfg.skirtSandBlend);
|
|
|
|
|
|
float uw = Mathf.Clamp01(cfg.skirtUnderwaterMul);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 격자 정점 수 상한 — 넘으면 칸을 키운다(모바일 안전 · 섬이 아무리 커져도 비용이 묶인다)
|
|
|
|
|
|
int maxNodes = Mathf.Max(1000, cfg.skirtMaxGridNodes);
|
|
|
|
|
|
int mc, nx, nz;
|
|
|
|
|
|
while (true)
|
|
|
|
|
|
{
|
|
|
|
|
|
mc = Mathf.CeilToInt((width + cell * 2f) / cell);
|
|
|
|
|
|
nx = Mathf.CeilToInt((maxX - minX) / cell - 0.0001f) + 2 * mc + 1;
|
|
|
|
|
|
nz = Mathf.CeilToInt((maxZ - minZ) / cell - 0.0001f) + 2 * mc + 1;
|
|
|
|
|
|
if ((long)nx * nz <= maxNodes) break;
|
|
|
|
|
|
cell *= 2f;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 원점 = 발자국 최소 가장자리에서 정수 칸만큼 바깥 → 칸이 타일 한 변을 나누면 정점이 모든 타일 가장자리 선 위에 놓인다
|
|
|
|
|
|
float x0 = minX - mc * cell, z0 = minZ - mc * cell;
|
|
|
|
|
|
|
|
|
|
|
|
var dist = new float[nx * nz];
|
|
|
|
|
|
var index = new int[nx * nz];
|
|
|
|
|
|
for (int j = 0; j < nz; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
float z = z0 + j * cell;
|
|
|
|
|
|
for (int i = 0; i < nx; i++) dist[j * nx + i] = SignedDist(x0 + i * cell, z, half);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 00:11:22 +00:00
|
|
|
|
// 2026-09-16 PD 「각진 지형이 남는다」 — 풀 구역 모서리를 R 로 둥글린다(열기 연산 = 침식 E{d ≤ −R} 뒤 팽창):
|
|
|
|
|
|
// distR = (E 까지의 거리) − R. 직선 변에서는 d 와 같고, 볼록 모서리에서는 반지름 R 원호가 된다.
|
|
|
|
|
|
// E 의 테두리 띠(d ∈ [−R − 1.5칸, −R]) 점만 모아 최소 거리를 재므로 비용은 (노드 수 × 띠 점 수).
|
|
|
|
|
|
float round = Mathf.Max(0f, cfg.skirtGrassRound);
|
|
|
|
|
|
var distR = dist;
|
|
|
|
|
|
if (round > 0.01f)
|
|
|
|
|
|
{
|
|
|
|
|
|
distR = new float[nx * nz];
|
|
|
|
|
|
var ringX = new List<float>(512); var ringZ = new List<float>(512);
|
|
|
|
|
|
float lo = -round - cell * 1.5f, hi = -round;
|
|
|
|
|
|
for (int j = 0; j < nz; j++) for (int i = 0; i < nx; i++) { float dv = dist[j * nx + i]; if (dv <= hi && dv >= lo) { ringX.Add(x0 + i * cell); ringZ.Add(z0 + j * cell); } }
|
|
|
|
|
|
for (int j = 0; j < nz; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
float z = z0 + j * cell;
|
|
|
|
|
|
for (int i = 0; i < nx; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = j * nx + i; float dv = dist[n];
|
|
|
|
|
|
if (ringX.Count == 0) { distR[n] = dv; continue; } // 섬이 2R 보다 작으면 둥글림 없음
|
|
|
|
|
|
if (dv <= hi) { distR[n] = dv + round; continue; } // E 안쪽
|
|
|
|
|
|
float x = x0 + i * cell; float best = float.MaxValue;
|
|
|
|
|
|
for (int k = 0; k < ringX.Count; k++) { float dx = x - ringX[k], dz = z - ringZ[k]; float d2 = dx * dx + dz * dz; if (d2 < best) best = d2; }
|
|
|
|
|
|
distR[n] = Mathf.Sqrt(best) - round;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-16 00:49:01 +00:00
|
|
|
|
// 🔴 실측(2026-09-16 09:5x): 모서리 필렛은 변을 따라 R 까지 이어지는 곡선 삼각형(깊이 R) — keep 이 R 보다 작으면 직사각형으로 잘린다.
|
|
|
|
|
|
keep = Mathf.Max(keep, round + cell);
|
2026-09-16 00:11:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-15 15:19:47 +00:00
|
|
|
|
_verts.Clear(); _cols.Clear(); _tris.Clear(); _trisSand.Clear();
|
2026-09-16 00:49:01 +00:00
|
|
|
|
float jitter = Mathf.Max(0f, cfg.skirtEdgeJitter); // 노드 단위 미세 흔들림(m) — 등고선을 살짝 거칠게
|
2026-09-15 15:01:24 +00:00
|
|
|
|
float outerKeep = width + cell * 2f; // 사각형 네 귀가 전부 있어야 하므로 바깥으로 두 칸 여유
|
|
|
|
|
|
for (int j = 0; j < nz; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < nx; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = j * nx + i;
|
|
|
|
|
|
float d = dist[n];
|
|
|
|
|
|
if (d < -keep || d > outerKeep) { index[n] = -1; continue; }
|
|
|
|
|
|
|
|
|
|
|
|
float t = Mathf.Clamp01(d / width);
|
|
|
|
|
|
t = t * t * (3f - 2f * t); // smoothstep — 윗면에서도 바닥에서도 접선이 수평(완만)
|
2026-09-16 00:11:22 +00:00
|
|
|
|
// 타일 안(d<0)은 윗면 **위** 1 cm — 풀 서브메시는 타일 재질 그대로라 보이지 않고, 모래 서브메시가 모서리 위에 얹힌다(홈·립도 덮는다)
|
|
|
|
|
|
float y = (_toon && d < 0f) ? topY + 0.01f : top + (bottom - top) * t;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
index[n] = _verts.Count;
|
|
|
|
|
|
_verts.Add(new Vector3(x0 + i * cell, y, z0 + j * cell));
|
|
|
|
|
|
|
|
|
|
|
|
float k = Mathf.Clamp01((d - band) / blend); // 풀 띠(0~band) → blend 폭에서 모래로
|
|
|
|
|
|
k = k * k * (3f - 2f * k);
|
|
|
|
|
|
var c = Color.LerpUnclamped(grass, sand, k);
|
|
|
|
|
|
if (y < waterY) { c.r *= uw; c.g *= uw; c.b *= uw; }
|
|
|
|
|
|
c.a = 1f;
|
|
|
|
|
|
_cols.Add(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 00:49:01 +00:00
|
|
|
|
// 2026-09-16 PD 「각진 경계 · 완만한 해변」 — 풀/모래 경계를 **격자 칸이 아니라 등고선으로 자른다**(칸 단위 계단 제거).
|
|
|
|
|
|
// 노드 값 s = 둥글린 거리 − 굽이(값 노이즈) − 풀 띠 (+ 미세 흔들림). s<0 = 풀 · s≥0 = 모래. 삼각형이 두 쪽에 걸치면
|
|
|
|
|
|
// s=0 을 지나는 변 위에 정점을 보간해 만들고 세 조각으로 나눈다(마칭 트라이앵글).
|
|
|
|
|
|
if (_s == null || _s.Length < nx * nz) _s = new float[nx * nz];
|
|
|
|
|
|
_cut.Clear();
|
|
|
|
|
|
for (int j = 0; j < nz; j++)
|
|
|
|
|
|
for (int i = 0; i < nx; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = j * nx + i;
|
|
|
|
|
|
if (index[n] < 0) { _s[n] = 0f; continue; }
|
|
|
|
|
|
float x = x0 + i * cell, z = z0 + j * cell;
|
|
|
|
|
|
_s[n] = distR[n] - Wave(x, z) - band + (Hash01(i * 3 + 7, j * 5 + 11) - 0.5f) * jitter;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-15 15:01:24 +00:00
|
|
|
|
for (int j = 0; j < nz - 1; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < nx - 1; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = j * nx + i;
|
|
|
|
|
|
int ia = index[n], ib = index[n + 1], ic = index[n + nx], id = index[n + nx + 1];
|
|
|
|
|
|
if (ia < 0 || ib < 0 || ic < 0 || id < 0) continue;
|
|
|
|
|
|
// 네 귀가 전부 경사 바깥(d ≥ width)이면 바닥과 같은 높이의 평판 — 만들지 않는다
|
|
|
|
|
|
float dm = Mathf.Min(Mathf.Min(dist[n], dist[n + 1]), Mathf.Min(dist[n + nx], dist[n + nx + 1]));
|
|
|
|
|
|
if (dm >= width) continue;
|
|
|
|
|
|
// 바닥판(SpawnSeabed)과 같은 감김 — (x0,z0)→(x0,z1)→(x1,z1) 이 위를 향한다
|
2026-09-15 15:19:47 +00:00
|
|
|
|
if (_toon)
|
|
|
|
|
|
{
|
2026-09-16 00:49:01 +00:00
|
|
|
|
CutTri(ia, n, ic, n + nx, id, n + nx + 1);
|
|
|
|
|
|
CutTri(ia, n, id, n + nx + 1, ib, n + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_tris.Add(ia); _tris.Add(ic); _tris.Add(id);
|
|
|
|
|
|
_tris.Add(ia); _tris.Add(id); _tris.Add(ib);
|
2026-09-15 15:19:47 +00:00
|
|
|
|
}
|
2026-09-15 15:01:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_mesh.Clear();
|
|
|
|
|
|
_mesh.indexFormat = _verts.Count > 65000 ? IndexFormat.UInt32 : IndexFormat.UInt16;
|
|
|
|
|
|
_mesh.SetVertices(_verts);
|
|
|
|
|
|
_mesh.SetColors(_cols);
|
2026-09-15 15:19:47 +00:00
|
|
|
|
_mesh.subMeshCount = _toon ? 2 : 1;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
_mesh.SetTriangles(_tris, 0);
|
2026-09-15 15:19:47 +00:00
|
|
|
|
if (_toon) _mesh.SetTriangles(_trisSand, 1);
|
|
|
|
|
|
_mesh.RecalculateNormals(); // DepthNormals 패스용 · Toon 조명용
|
2026-09-15 15:01:24 +00:00
|
|
|
|
_mesh.RecalculateBounds();
|
|
|
|
|
|
|
|
|
|
|
|
LastVertices = _verts.Count;
|
2026-09-15 15:19:47 +00:00
|
|
|
|
LastTriangles = (_tris.Count + _trisSand.Count) / 3;
|
2026-09-15 15:01:24 +00:00
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|