Merge branch 'wl/systems/WL-816zf-soft-dirt'
This commit is contained in:
commit
6b3040366d
|
|
@ -300,17 +300,24 @@ namespace WL.Island
|
|||
var rends = f.GetComponentsInChildren<Renderer>(true);
|
||||
for (int r = 0; r < rends.Length; r++)
|
||||
{
|
||||
if (rends[r] == null || !rends[r].enabled) continue;
|
||||
var rr = rends[r];
|
||||
if (rr == null) continue;
|
||||
// 🔴 WL-816zf — 구역 바운즈는 **이미 꺼 둔 렌더러까지 센다**.
|
||||
// 3 s 재훑기에서는 밭 렌더러가 전부 꺼져 있어, 켜진 것만 세면 두 번째 훑기부터
|
||||
// 구역을 통째로 잃는다(ZoneBoundsValid = false → 흙 데칼·발판 자리 소실).
|
||||
// 끈 렌더러도 `bounds` 는 그대로 유효하다.
|
||||
if (visible)
|
||||
{
|
||||
if (!boundsSet) { b = rends[r].bounds; boundsSet = true; }
|
||||
else b.Encapsulate(rends[r].bounds);
|
||||
if (!zoneSet) { zone = rends[r].bounds; zoneSet = true; } // WL-816u — 구역 전체
|
||||
else zone.Encapsulate(rends[r].bounds);
|
||||
if (!boundsSet) { b = rr.bounds; boundsSet = true; }
|
||||
else b.Encapsulate(rr.bounds);
|
||||
if (!zoneSet) { zone = rr.bounds; zoneSet = true; } // WL-816u — 구역 전체
|
||||
else zone.Encapsulate(rr.bounds);
|
||||
}
|
||||
if (!rr.enabled) continue;
|
||||
// 🔴 WL-816w ③ — 흙칸(Soil)은 남긴다(밭 자리가 흙으로 보이게). 콜라이더는 아래에서 끈다.
|
||||
if (IsSoilRenderer(cfg, rends[r])) { SoilRenderersKept++; continue; }
|
||||
rends[r].enabled = false; s_hiddenRends.Add(rends[r]); RenderersHidden++;
|
||||
// 816zf 데칼이 켜져 있으면 `IsSoilRenderer` 가 false 라 흙칸도 여기서 꺼진다(배타).
|
||||
if (IsSoilRenderer(cfg, rr)) { SoilRenderersKept++; continue; }
|
||||
rr.enabled = false; s_hiddenRends.Add(rr); RenderersHidden++;
|
||||
}
|
||||
|
||||
var cols = f.GetComponentsInChildren<Collider>(true);
|
||||
|
|
@ -368,10 +375,15 @@ namespace WL.Island
|
|||
/// <summary>WL-816w 진단 — 남겨 둔 흙칸 렌더러 수 · 끈 울타리 렌더러 수.</summary>
|
||||
public static int SoilRenderersKept, FenceRenderersHidden;
|
||||
|
||||
/// <summary>WL-816w ③ — 이 렌더러가 흙칸(FI `Soil`)의 것인가(자기 자신 또는 부모).</summary>
|
||||
/// <summary>
|
||||
/// WL-816w ③ — 이 렌더러가 흙칸(FI `Soil`)의 것인가(자기 자신 또는 부모).
|
||||
/// 🔴 WL-816zf — 부드러운 흙 데칼(`WLSoftDirt`)이 켜져 있으면 **false 를 돌려 흙 타일을 숨긴다**.
|
||||
/// 각진 1×1 타일과 둥근 데칼이 겹치면 데칼 가장자리 밖으로 타일 모서리가 삐져나온다(배타).
|
||||
/// </summary>
|
||||
static bool IsSoilRenderer(WLIslandSettings cfg, Renderer r)
|
||||
{
|
||||
if (cfg.showFarmSoil == 0 || r == null) return false;
|
||||
if (global::WL.Look.Farm.WLSoftDirt.Active) return false;
|
||||
return r.GetComponentInParent<FISoil>(true) != null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ namespace WL.Look.Farm
|
|||
readonly List<Rect> _blockers = new List<Rect>(128);
|
||||
// 816za — 밭 자리만 따로. 반듯한 사각형이 아니라 「모서리 둥글게 + 경계 노이즈」로 판정한다.
|
||||
readonly List<Rect> _farmRounds = new List<Rect>(8);
|
||||
// 816zf — 흙 데칼이 살아 있으면 밭 제외는 위 두 목록 대신 `WLSoftDirt` 마스크가 판정한다.
|
||||
bool _softDirt;
|
||||
|
||||
// ── 816o 캐시 — 한 번 뽑은 후보를 들고 있다가, 구름이 흐르면 「고르기」만 다시 한다 ──
|
||||
struct Cand { public float x, z, y; public int def; public float sc; public float yaw; }
|
||||
|
|
@ -741,11 +743,13 @@ namespace WL.Look.Farm
|
|||
_blockers.Clear();
|
||||
_farmRounds.Clear();
|
||||
bool round = cfg.farmEdgeRoundEnabled != 0 && cfg.farmEdgeRadius > 0f;
|
||||
// 816zf — 흙 데칼이 경계를 쥐고 있으면 밭 제외는 **데칼 마스크**가 정한다(경계 정의 1개).
|
||||
_softDirt = WLSoftDirt.Ready;
|
||||
var scene = tiles.Count > 0 ? tiles[0].tf.gameObject.scene : default(UnityEngine.SceneManagement.Scene);
|
||||
|
||||
// ① 농사 — Farm 이 Awake 에서 만드는 BoxCollider(size = (length,2,width))가 밭의 정확한 넓이다.
|
||||
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < farms.Length; i++)
|
||||
for (int i = 0; i < farms.Length && !_softDirt; i++)
|
||||
{
|
||||
var f = farms[i];
|
||||
if (f == null || f.gameObject.scene != scene) continue;
|
||||
|
|
@ -758,8 +762,9 @@ namespace WL.Look.Farm
|
|||
|
||||
// ② 농사 타일 하나하나(1×1) — Farm 이 런타임에 만든다. 이중 안전.
|
||||
// 라운드 모드에서는 ① 안에 든 타일을 빼야 한다. 안 그러면 반듯한 1×1 사각형들이 깎아낸 모서리를 도로 메운다.
|
||||
// 816zf 데칼 모드에서는 1×1 사각형이 **둥근 경계를 도로 각지게** 만들므로 아예 넣지 않는다.
|
||||
var soils = Object.FindObjectsByType<FISoil>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < soils.Length; i++)
|
||||
for (int i = 0; i < soils.Length && !_softDirt; i++)
|
||||
{
|
||||
var s = soils[i];
|
||||
if (s == null || s.gameObject.scene != scene) continue;
|
||||
|
|
@ -801,6 +806,9 @@ namespace WL.Look.Farm
|
|||
var r = _blockers[i];
|
||||
if (x >= r.xMin && x <= r.xMax && z >= r.yMin && z <= r.yMax) return true;
|
||||
}
|
||||
// 816zf — 흙 데칼과 **같은 경계**를 쓴다. 데칼 가장자리보다 `softDirtGrassBite` 안쪽만 막으므로
|
||||
// 그 폭만큼 풀 톱니가 흙 안으로 파고든다(두 군데서 따로 계산하지 않는다).
|
||||
if (_softDirt) return WLSoftDirt.Blocks(x, z);
|
||||
for (int i = 0; i < _farmRounds.Count; i++)
|
||||
if (InRoundedFarm(_farmRounds[i], x, z)) return true;
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ namespace WL.Look.Farm
|
|||
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
|
||||
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
|
||||
|
||||
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
|
||||
if (cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
|
||||
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
|
||||
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
|
||||
|
||||
|
|
@ -141,6 +143,7 @@ namespace WL.Look.Farm
|
|||
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
|
||||
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
|
||||
+ " · 풀 " + WLIslandGrass.LastLog
|
||||
+ " · 흙데칼 " + WLSoftDirt.LastLog
|
||||
+ " · 둘레거품 " + WLShoreFoam.LastLog);
|
||||
|
||||
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
|
||||
|
|
@ -159,6 +162,18 @@ namespace WL.Look.Farm
|
|||
// 그래서 조명은 재훑기에서 **다시 걸지 않는다** — 다시 걸면 `WLReferenceLook` 의
|
||||
// 무드(Flat)를 Skybox 로 되돌려 오히려 룩이 바뀐다(실측으로 확인).
|
||||
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
|
||||
// 🔴 816zf — 밭 구역(`WLIslandGateFlow.ZoneBounds`)은 게이트 플로우가 밭을 훑은 뒤에야 정해진다.
|
||||
// 재훑기에서 구역이 처음 잡히거나 바뀌면 데칼을 굽고, **그때만** 풀도 다시 깐다(경계가 같아지게).
|
||||
if (cfg.softDirtEnabled != 0)
|
||||
{
|
||||
int before = WLSoftDirt.Rebuilds;
|
||||
SpawnSoftDirt(cfg, scene);
|
||||
if (WLSoftDirt.Rebuilds != before && cfg.grassEnabled != 0)
|
||||
{
|
||||
var g2 = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||||
if (g2 != null) { g2.Rebuild(); GrassRebuilds++; }
|
||||
}
|
||||
}
|
||||
HookIslands(cfg, scene);
|
||||
}
|
||||
}
|
||||
|
|
@ -249,6 +264,13 @@ namespace WL.Look.Farm
|
|||
|
||||
static void RequestGrassRebuild(WLIslandLookSettings cfg)
|
||||
{
|
||||
// 🔴 816zf — 흙 데칼을 **먼저** 다시 굽는다. 섬 확장·밭 재구성으로 구역이 바뀌면
|
||||
// 풀의 밭 제외도 그 새 경계를 읽어야 흙과 풀이 같은 선에서 만난다.
|
||||
if (cfg.softDirtEnabled != 0)
|
||||
{
|
||||
var sd = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
|
||||
if (sd != null) { sd.cfg = cfg; sd.Rebuild(); }
|
||||
}
|
||||
if (cfg.grassEnabled != 0)
|
||||
{
|
||||
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||||
|
|
@ -264,6 +286,24 @@ namespace WL.Look.Farm
|
|||
|
||||
public static int ShoreFoamRebuilds;
|
||||
public static bool ShoreFoamSpawned;
|
||||
public static bool SoftDirtSpawned;
|
||||
|
||||
/// <summary>816zf — 밭 자리 부드러운 흙 데칼. 풀·거품과 같은 자리에서 만들고 같은 훅으로 다시 만든다.</summary>
|
||||
public static void SpawnSoftDirt(WLIslandLookSettings cfg, Scene scene)
|
||||
{
|
||||
var d = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
|
||||
if (d == null)
|
||||
{
|
||||
var go = new GameObject(WLSoftDirt.ObjectName);
|
||||
SceneManager.MoveGameObjectToScene(go, scene);
|
||||
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||||
d = go.AddComponent<WLSoftDirt>();
|
||||
d.cfg = cfg;
|
||||
d.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
|
||||
}
|
||||
else { d.cfg = cfg; d.Rebuild(); }
|
||||
SoftDirtSpawned = true;
|
||||
}
|
||||
|
||||
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
|
||||
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
|
||||
|
|
|
|||
|
|
@ -241,6 +241,57 @@ namespace WL.Look.Farm
|
|||
[Tooltip("거품이 흐르는 속도(1 = 초당 무늬 1칸). 0 이면 정지.")]
|
||||
public float shoreFoamScroll = 0.06f;
|
||||
|
||||
// ───────────────────────────────── §816zf 부드러운 흙 데칼
|
||||
[Header("§816zf — 흙 자리를 데모처럼 「둥글게 번지게」 (PD 2026-09-15)")]
|
||||
[Tooltip("0 = 지금(각진 1×1 흙 타일 · 100 % 복귀) · 1 = 밭 구역을 덮는 **부드러운 흙 데칼 1장**" +
|
||||
"(드로우콜 +1)으로 바꾸고 흙 타일 렌더러는 숨긴다(`WLIslandSettings.showFarmSoil` 과 배타). " +
|
||||
"816za 풀 제외도 이 데칼과 **같은 경계**를 쓰게 된다.")]
|
||||
public int softDirtEnabled = 1;
|
||||
|
||||
[Tooltip("데칼에 쓸 머티리얼. **비워 두는 것이 기본** — 비우면 816j2 거품 띠와 같은 URP Unlit 셰이더로 " +
|
||||
"런타임에 반투명 머티리얼을 만든다(에셋 0줄). 채우면 그 머티리얼의 복사본을 쓴다.")]
|
||||
public Material softDirtMaterial;
|
||||
|
||||
[Tooltip("흙 색(리니어) — 816r 데모 흙 톤 `#A4A88B` 를 리니어로 옮긴 값.")]
|
||||
public Color softDirtColor = new Color(0.371f, 0.392f, 0.258f, 1f);
|
||||
|
||||
[Tooltip("데칼 전체의 불투명도. 1 = 흙색 그대로 · 낮추면 밑의 잔디가 비쳐 흙이 옅어진다.")]
|
||||
[Range(0f, 1f)] public float softDirtOpacity = 1f;
|
||||
|
||||
[Tooltip("흙칸 윗면보다 얼마나 위에 깔 것인가(m). 너무 작으면 z-fighting, 너무 크면 떠 보인다.")]
|
||||
public float softDirtY = 0.02f;
|
||||
|
||||
[Tooltip("밭 구역 바운즈를 사방으로 이만큼(m) 넓혀 흙으로 친다. 데칼 가장자리가 페이드로 " +
|
||||
"물러나는 만큼을 메워, 흙 자리가 지금보다 좁아 보이지 않게 한다.")]
|
||||
public float softDirtExpand = 0.2f;
|
||||
|
||||
[Tooltip("흙 사각형의 네 모서리를 깎는 반지름(m). 클수록 둥글게 번진 모양으로 읽힌다.")]
|
||||
public float softDirtCornerRadius = 1.4f;
|
||||
|
||||
[Tooltip("흙 경계를 흔드는 진폭(m). 이만큼 흙이 풀 쪽으로 밀려나거나 물러나 경계가 들쭉날쭉해진다.")]
|
||||
public float softDirtNoise = 0.45f;
|
||||
|
||||
[Tooltip("경계 노이즈의 셀 크기(m). 작을수록 잘게 들쭉날쭉하고, 클수록 크게 굽이친다.")]
|
||||
public float softDirtNoiseCell = 0.9f;
|
||||
|
||||
[Tooltip("가장자리가 흙 → 풀로 사라지는 폭(m). 데모의 스플랫 번짐에 해당한다(0.8~1.2 권장).")]
|
||||
public float softDirtFeather = 1.0f;
|
||||
|
||||
[Tooltip("풀을 데칼 가장자리보다 이만큼(m) **안쪽까지** 들여보낸다 — 흙 가장자리에 풀 톱니가 " +
|
||||
"파고든 것처럼 보이게 하는 값. 0 이면 풀이 흙 경계에서 딱 끊긴다.")]
|
||||
public float softDirtGrassBite = 0.3f;
|
||||
|
||||
[Tooltip("데칼 사각형을 구역 바깥으로 더 두는 여유 폭(m). 페이드+노이즈가 잘리지 않을 만큼 " +
|
||||
"자동으로 커진다 — 보통 손댈 일이 없다.")]
|
||||
public float softDirtPad = 1.5f;
|
||||
|
||||
[Tooltip("마스크 텍스처 한 변의 픽셀 수. 256 = 256 KB · 굽는 데 한 번 몇 ms. " +
|
||||
"경계가 뭉개져 보이면 512 로 올린다.")]
|
||||
public int softDirtTexRes = 256;
|
||||
|
||||
[Tooltip("흙 얼룩의 세기(0~1). 한 덩어리 단색으로 보이지 않게 밝기를 이만큼 흔든다.")]
|
||||
[Range(0f, 1f)] public float softDirtTexNoise = 0.08f;
|
||||
|
||||
// ───────────────────────────────── §1 풀밭
|
||||
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
|
||||
[Tooltip("0 이면 풀을 깔지 않는다.")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,358 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSoftDirt.cs — 밭(흙) 자리를 데모처럼 **둥글게 번지는 한 장**으로 (WL-816zf · #816)
|
||||
//
|
||||
// PD 지시(2026-09-15) 「데모씬을 보면 자연스럽게 흙과 잔디가 라운딩 된 것처럼 배치되어 있어.
|
||||
// 데모씬과 유사하게 맞출 방법을 찾아서 최대한 맞춰.」
|
||||
//
|
||||
// ■ 왜 각져 보이는가
|
||||
// 데모는 **터레인 스플랫**이라 흙이 잔디 속으로 부드럽게 번진다.
|
||||
// 우리 섬은 FI 의 **1×1 흙 타일(Soil)** 묶음이라 경계가 계단처럼 각진다.
|
||||
// 타일을 아무리 골라 숨겨도 최소 단위가 1 m 라 「둥글게」가 안 나온다.
|
||||
//
|
||||
// ■ 어떻게 바꾸는가
|
||||
// 밭 구역(`WLIslandGateFlow.ZoneBounds`)을 덮는 **평면 메시 1장**(4정점 · 2삼각형)에
|
||||
// 런타임에 구운 **마스크 텍스처**(모서리를 깎은 사각형 + 경계 노이즈 + 가장자리 페이드)를 씌운다.
|
||||
// 모양은 전부 텍스처의 알파가 만들므로 메시는 사각형 하나면 된다 = **드로우콜 +1**.
|
||||
// 켜지면 각진 흙 타일 렌더러는 숨긴다(`WLIslandGateFlow.IsSoilRenderer` 가 배타 처리).
|
||||
//
|
||||
// ■ 풀과 흙이 같은 선을 쓴다
|
||||
// 816za 의 풀 제외(밭 자리)는 이 파일의 `SignedDist` 를 그대로 쓴다.
|
||||
// 풀은 데칼 가장자리보다 `softDirtGrassBite`(m) **안쪽까지** 들어와, 흙 가장자리에
|
||||
// 풀 톱니가 파고든 것처럼 읽힌다 — 경계를 두 군데서 따로 계산하지 않는다.
|
||||
//
|
||||
// ■ 되돌리기 — `softDirtEnabled = 0` 이면 흙 타일이 그대로 보이고 풀 제외도 816za 방식으로 돌아간다.
|
||||
//
|
||||
// 🔴 FI 코드 0줄 · 씬 파일 0줄 · 에셋 0줄. 메시·텍스처·머티리얼 전부 런타임 생성이다.
|
||||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using UnityEngine;
|
||||
using FISoil = CryingSnow.FarmingIsland.Soil;
|
||||
|
||||
namespace WL.Look.Farm
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class WLSoftDirt : MonoBehaviour
|
||||
{
|
||||
public const string ObjectName = "~WL_SoftDirt";
|
||||
|
||||
// ── 진단(프로브·보고가 읽는다)
|
||||
public static int Rebuilds;
|
||||
public static string LastLog = "꺼짐";
|
||||
|
||||
public WLIslandLookSettings cfg;
|
||||
|
||||
MeshFilter _mf;
|
||||
MeshRenderer _mr;
|
||||
Mesh _mesh;
|
||||
Texture2D _tex;
|
||||
Material _inst;
|
||||
|
||||
static readonly int IdBaseMap = Shader.PropertyToID("_BaseMap");
|
||||
static readonly int IdMainTex = Shader.PropertyToID("_MainTex");
|
||||
static readonly int IdBaseColor = Shader.PropertyToID("_BaseColor");
|
||||
static readonly int IdColor = Shader.PropertyToID("_Color");
|
||||
static readonly int IdSurface = Shader.PropertyToID("_Surface");
|
||||
static readonly int IdBlend = Shader.PropertyToID("_Blend");
|
||||
static readonly int IdSrcBlend = Shader.PropertyToID("_SrcBlend");
|
||||
static readonly int IdDstBlend = Shader.PropertyToID("_DstBlend");
|
||||
static readonly int IdZWrite = Shader.PropertyToID("_ZWrite");
|
||||
static readonly int IdCull = Shader.PropertyToID("_Cull");
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 마스크 — 흙 경계의 **단일 정의**. 데칼 텍스처와 풀 제외가 같은 값을 읽는다.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
static bool s_ready;
|
||||
static float s_cx, s_cz, s_hx, s_hz, s_rad, s_noise, s_cell, s_bite;
|
||||
|
||||
/// <summary>설정이 데칼을 쓰기로 했는가 — 흙 타일 숨김(배타)을 가르는 값. 구역을 몰라도 참이다.</summary>
|
||||
public static bool Active
|
||||
{
|
||||
get
|
||||
{
|
||||
var c = WLIslandLookSettings.Instance;
|
||||
return c != null && c.enabled_ != 0 && c.softDirtEnabled != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>마스크를 실제로 계산해 두었는가(밭 구역을 알아낸 뒤). 풀 제외는 이것이 참일 때만 바뀐다.</summary>
|
||||
public static bool Ready { get { return s_ready && Active; } }
|
||||
|
||||
/// <summary>
|
||||
/// 흙 경계까지의 부호 있는 거리(m). **음수 = 흙 안쪽 · 양수 = 바깥(풀)**.
|
||||
/// 모서리를 `softDirtCornerRadius` 만큼 깎은 사각형에 셀 노이즈로 경계를 흔든 모양이다.
|
||||
/// </summary>
|
||||
public static float SignedDist(float x, float z)
|
||||
{
|
||||
float dx = Mathf.Abs(x - s_cx) - (s_hx - s_rad);
|
||||
float dz = Mathf.Abs(z - s_cz) - (s_hz - s_rad);
|
||||
float ox = Mathf.Max(dx, 0f), oz = Mathf.Max(dz, 0f);
|
||||
float d = Mathf.Min(Mathf.Max(dx, dz), 0f) + Mathf.Sqrt(ox * ox + oz * oz) - s_rad;
|
||||
if (s_noise > 0f && s_cell > 0.0001f)
|
||||
d -= (CellNoise(x / s_cell, z / s_cell) * 2f - 1f) * s_noise;
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>여기에 풀을 깔지 말 것인가 — 데칼 가장자리보다 `softDirtGrassBite` 만큼 **더 안쪽**만 막는다.</summary>
|
||||
public static bool Blocks(float x, float z)
|
||||
{
|
||||
if (!s_ready) return false;
|
||||
// 싸게 먼저 거른다 — 구역 밖이면 노이즈를 계산하지 않는다.
|
||||
if (Mathf.Abs(x - s_cx) > s_hx + s_noise || Mathf.Abs(z - s_cz) > s_hz + s_noise) return false;
|
||||
return SignedDist(x, z) < -s_bite;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
void OnEnable() { Ensure(); _dirty = true; Rebuild(); }
|
||||
void OnDisable() { if (_mr != null) _mr.enabled = false; s_ready = false; }
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_inst != null) Destroy(_inst);
|
||||
if (_tex != null) Destroy(_tex);
|
||||
if (_mesh != null) Destroy(_mesh);
|
||||
s_ready = 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_SoftDirtMesh" };
|
||||
_mesh.MarkDynamic();
|
||||
_mesh.hideFlags = HideFlags.DontSave;
|
||||
}
|
||||
_mf.sharedMesh = _mesh;
|
||||
}
|
||||
|
||||
// 마지막으로 구운 조건 — 같으면 다시 굽지 않는다(1초 폴링에서 공짜로 돌게).
|
||||
bool _dirty = true;
|
||||
float _lastCx, _lastCz, _lastHx, _lastHz, _lastY, _lastSig = float.NaN;
|
||||
|
||||
/// <summary>다시 만든다(섬 확장·밭 재구성). 멱등 — 조건이 그대로면 아무것도 하지 않는다.</summary>
|
||||
public void Rebuild()
|
||||
{
|
||||
Ensure();
|
||||
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.softDirtEnabled == 0)
|
||||
{ _mr.enabled = false; s_ready = false; LastLog = "꺼짐"; return; }
|
||||
|
||||
if (!global::WL.Island.WLIslandGateFlow.ZoneBoundsValid)
|
||||
{ _mr.enabled = false; s_ready = false; LastLog = "밭 구역 미확정 — 대기"; return; }
|
||||
|
||||
var zone = global::WL.Island.WLIslandGateFlow.ZoneBounds;
|
||||
float hx = zone.extents.x + cfg.softDirtExpand;
|
||||
float hz = zone.extents.z + cfg.softDirtExpand;
|
||||
if (hx <= 0.01f || hz <= 0.01f)
|
||||
{ _mr.enabled = false; s_ready = false; LastLog = "밭 구역 넓이 0"; return; }
|
||||
|
||||
float y = SoilTopY(zone) + cfg.softDirtY;
|
||||
|
||||
// 값이 바뀌면 다시 굽는다(인스펙터에서 만져도 다음 폴링에 반영된다).
|
||||
float sig = cfg.softDirtCornerRadius * 7.1f + cfg.softDirtNoise * 13.3f + cfg.softDirtNoiseCell * 3.7f
|
||||
+ cfg.softDirtFeather * 29.3f + cfg.softDirtGrassBite * 5.9f + cfg.softDirtPad * 11.9f
|
||||
+ cfg.softDirtTexRes * 0.017f + cfg.softDirtTexNoise * 23.1f + cfg.softDirtOpacity * 17.7f
|
||||
+ cfg.softDirtColor.r * 101f + cfg.softDirtColor.g * 103f + cfg.softDirtColor.b * 107f;
|
||||
|
||||
bool same = !_dirty && _tex != null && _inst != null
|
||||
&& Mathf.Abs(_lastCx - zone.center.x) < 0.001f && Mathf.Abs(_lastCz - zone.center.z) < 0.001f
|
||||
&& Mathf.Abs(_lastHx - hx) < 0.001f && Mathf.Abs(_lastHz - hz) < 0.001f
|
||||
&& Mathf.Abs(_lastY - y) < 0.001f && Mathf.Abs(_lastSig - sig) < 0.0001f;
|
||||
|
||||
// 마스크 값은 매번 갱신한다 — 풀(816za)이 읽는 단일 출처다.
|
||||
s_cx = zone.center.x; s_cz = zone.center.z;
|
||||
s_hx = hx; s_hz = hz;
|
||||
s_rad = Mathf.Clamp(cfg.softDirtCornerRadius, 0f, Mathf.Min(hx, hz));
|
||||
s_noise = Mathf.Max(0f, cfg.softDirtNoise);
|
||||
s_cell = Mathf.Max(0.05f, cfg.softDirtNoiseCell);
|
||||
s_bite = Mathf.Max(0f, cfg.softDirtGrassBite);
|
||||
s_ready = true;
|
||||
|
||||
if (same) { _mr.enabled = true; return; }
|
||||
|
||||
if (!EnsureMaterial()) { _mr.enabled = false; s_ready = false; LastLog = "🔴 URP Unlit 셰이더를 못 찾음"; return; }
|
||||
|
||||
float feather = Mathf.Max(0.05f, cfg.softDirtFeather);
|
||||
float pad = Mathf.Max(cfg.softDirtPad, feather + s_noise + 0.25f);
|
||||
int res = Mathf.Clamp(cfg.softDirtTexRes, 32, 1024);
|
||||
|
||||
BakeMask(hx + pad, hz + pad, res, feather);
|
||||
BuildQuad(hx + pad, hz + pad, y);
|
||||
|
||||
_inst.SetTexture(IdBaseMap, _tex);
|
||||
if (_inst.HasProperty(IdMainTex)) _inst.SetTexture(IdMainTex, _tex);
|
||||
var col = cfg.softDirtColor; col.a = Mathf.Clamp01(cfg.softDirtOpacity);
|
||||
_inst.SetColor(IdBaseColor, col);
|
||||
if (_inst.HasProperty(IdColor)) _inst.SetColor(IdColor, col);
|
||||
_mr.sharedMaterial = _inst;
|
||||
_mr.enabled = true;
|
||||
|
||||
_lastCx = s_cx; _lastCz = s_cz; _lastHx = hx; _lastHz = hz; _lastY = y; _lastSig = sig;
|
||||
_dirty = false;
|
||||
Rebuilds++;
|
||||
|
||||
LastLog = "흙 데칼 — 구역 " + (hx * 2f).ToString("F1") + "×" + (hz * 2f).ToString("F1")
|
||||
+ " m · 모서리 R " + s_rad.ToString("F2") + " m · 노이즈 ±" + s_noise.ToString("F2")
|
||||
+ " m(셀 " + s_cell.ToString("F2") + ") · 페이드 " + feather.ToString("F2")
|
||||
+ " m · 풀 파고듦 " + s_bite.ToString("F2") + " m · 텍스처 " + res + "² · 드로우콜 +1";
|
||||
cfg.Log(LastLog);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 흙칸(Soil) 윗면 높이 — 데칼을 여기 바로 위에 깐다(밭 바닥이 곧 걸어다니는 면).
|
||||
/// 렌더러를 꺼 두어도 `bounds` 는 유효하다. 구역 안에 흙칸이 하나도 없으면 구역 바닥을 쓴다.
|
||||
/// </summary>
|
||||
static float SoilTopY(Bounds zone)
|
||||
{
|
||||
float y = float.MinValue;
|
||||
var soils = Object.FindObjectsByType<FISoil>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < soils.Length; i++)
|
||||
{
|
||||
var s = soils[i];
|
||||
if (s == null) continue;
|
||||
var p = s.transform.position;
|
||||
if (Mathf.Abs(p.x - zone.center.x) > zone.extents.x + 1f) continue; // 다른 섬의 밭은 뺀다
|
||||
if (Mathf.Abs(p.z - zone.center.z) > zone.extents.z + 1f) continue;
|
||||
var r = s.GetComponentInChildren<Renderer>(true);
|
||||
float t = r != null ? r.bounds.max.y : p.y;
|
||||
if (t > y) y = t;
|
||||
}
|
||||
return y > -1e8f ? y : zone.min.y;
|
||||
}
|
||||
|
||||
/// <summary>URP Unlit 반투명 런타임 머티리얼(816j2 `Farm_ShoreFoam.mat` 과 같은 구성).</summary>
|
||||
bool EnsureMaterial()
|
||||
{
|
||||
if (cfg.softDirtMaterial != null)
|
||||
{
|
||||
if (_inst == null || _inst.shader != cfg.softDirtMaterial.shader)
|
||||
{
|
||||
if (_inst != null) Destroy(_inst);
|
||||
_inst = new Material(cfg.softDirtMaterial) { name = "WL_SoftDirt(runtime)", hideFlags = HideFlags.DontSave };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_inst != null) return true;
|
||||
|
||||
// 에셋을 새로 만들지 않는다 — 816j2 거품 띠가 쓰는 URP Unlit 셰이더를 그대로 빌려 쓴다.
|
||||
Shader sh = cfg.shoreFoamMaterial != null ? cfg.shoreFoamMaterial.shader : null;
|
||||
if (sh == null) sh = Shader.Find("Universal Render Pipeline/Unlit");
|
||||
if (sh == null) sh = Shader.Find("Unlit/Transparent");
|
||||
if (sh == null) return false;
|
||||
|
||||
_inst = new Material(sh) { name = "WL_SoftDirt(runtime)", hideFlags = HideFlags.DontSave };
|
||||
// URP Unlit 을 반투명으로 — Farm_ShoreFoam.mat 실측값과 같다(_Surface 1 · SrcAlpha/OneMinusSrcAlpha · ZWrite 0).
|
||||
if (_inst.HasProperty(IdSurface)) _inst.SetFloat(IdSurface, 1f);
|
||||
if (_inst.HasProperty(IdBlend)) _inst.SetFloat(IdBlend, 0f);
|
||||
if (_inst.HasProperty(IdSrcBlend)) _inst.SetFloat(IdSrcBlend, (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
if (_inst.HasProperty(IdDstBlend)) _inst.SetFloat(IdDstBlend, (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
if (_inst.HasProperty(IdZWrite)) _inst.SetFloat(IdZWrite, 0f);
|
||||
if (_inst.HasProperty(IdCull)) _inst.SetFloat(IdCull, (float)UnityEngine.Rendering.CullMode.Off);
|
||||
_inst.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
|
||||
_inst.SetShaderPassEnabled("ShadowCaster", false);
|
||||
_inst.SetShaderPassEnabled("DepthOnly", false);
|
||||
_inst.SetShaderPassEnabled("MOTIONVECTORS", false);
|
||||
_inst.SetOverrideTag("RenderType", "Transparent");
|
||||
// 거품 띠(3050)보다 먼저 — 땅에 붙는 데칼이 먼저 깔리고 그 위로 물가 거품이 온다.
|
||||
_inst.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>마스크를 굽는다 — 알파 = 흙 덮임(1) → 풀(0) 으로 `feather`(m) 폭에서 부드럽게 넘어간다.</summary>
|
||||
void BakeMask(float halfX, float halfZ, int res, float feather)
|
||||
{
|
||||
// 구운 뒤 CPU 사본을 버리므로(아래 `Apply(false, true)`) 다시 구울 때는 새로 만든다.
|
||||
if (_tex != null) Destroy(_tex);
|
||||
// linear:true — RGB 값을 그대로 곱하게 한다(sRGB 변환이 끼면 얼룩 세기가 뜻대로 안 나온다).
|
||||
_tex = new Texture2D(res, res, TextureFormat.RGBA32, false, true)
|
||||
{ name = "WL_SoftDirtMask", hideFlags = HideFlags.DontSave, wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear };
|
||||
|
||||
float texNoise = Mathf.Clamp01(cfg.softDirtTexNoise);
|
||||
float half = feather * 0.5f;
|
||||
var px = new Color32[res * res];
|
||||
float stepX = halfX * 2f / res, stepZ = halfZ * 2f / res;
|
||||
|
||||
for (int j = 0; j < res; j++)
|
||||
{
|
||||
float wz = s_cz - halfZ + (j + 0.5f) * stepZ;
|
||||
for (int i = 0; i < res; i++)
|
||||
{
|
||||
float wx = s_cx - halfX + (i + 0.5f) * stepX;
|
||||
float d = SignedDist(wx, wz);
|
||||
// 1 = 흙 · 0 = 풀. 가장자리에서 feather(m) 폭으로 부드럽게 사라진다.
|
||||
float a = Mathf.Clamp01((half - d) / Mathf.Max(0.0001f, feather));
|
||||
a = a * a * (3f - 2f * a); // smoothstep — 띠가 선처럼 보이지 않게
|
||||
|
||||
float v = 1f;
|
||||
if (texNoise > 0f) // 흙 얼룩 — 한 덩어리 단색으로 보이지 않게
|
||||
v = 1f - texNoise * CellNoise(wx * 1.7f, wz * 1.7f);
|
||||
|
||||
byte c = (byte)Mathf.RoundToInt(Mathf.Clamp01(v) * 255f);
|
||||
px[j * res + i] = new Color32(c, c, c, (byte)Mathf.RoundToInt(a * 255f));
|
||||
}
|
||||
}
|
||||
|
||||
_tex.SetPixels32(px);
|
||||
_tex.Apply(false, true); // 밉 없음 + CPU 사본 해제
|
||||
}
|
||||
|
||||
/// <summary>구역을 덮는 사각형 한 장(4정점 · 2삼각형). 모양은 전부 마스크가 만든다.</summary>
|
||||
void BuildQuad(float halfX, float halfZ, float y)
|
||||
{
|
||||
float x0 = s_cx - halfX, x1 = s_cx + halfX;
|
||||
float z0 = s_cz - halfZ, z1 = s_cz + halfZ;
|
||||
|
||||
_mesh.Clear();
|
||||
_mesh.SetVertices(new System.Collections.Generic.List<Vector3>(4)
|
||||
{
|
||||
new Vector3(x0, y, z0), new Vector3(x1, y, z0),
|
||||
new Vector3(x0, y, z1), new Vector3(x1, y, z1)
|
||||
});
|
||||
_mesh.SetUVs(0, new System.Collections.Generic.List<Vector2>(4)
|
||||
{
|
||||
new Vector2(0f, 0f), new Vector2(1f, 0f),
|
||||
new Vector2(0f, 1f), new Vector2(1f, 1f)
|
||||
});
|
||||
_mesh.SetTriangles(new int[] { 0, 2, 1, 1, 2, 3 }, 0);
|
||||
_mesh.RecalculateBounds();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 816za 와 **같은 식**의 셀 격자 값 노이즈 [0,1] — 경계를 들쭉날쭉하게 만든다.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
static float CellNoise(float fx, float fz)
|
||||
{
|
||||
int x0 = Mathf.FloorToInt(fx), z0 = Mathf.FloorToInt(fz);
|
||||
float tx = fx - x0, tz = fz - z0;
|
||||
tx = tx * tx * (3f - 2f * tx);
|
||||
tz = tz * tz * (3f - 2f * tz);
|
||||
float a = CellRand(x0, z0), b = CellRand(x0 + 1, z0);
|
||||
float c = CellRand(x0, z0 + 1), d = CellRand(x0 + 1, z0 + 1);
|
||||
return Mathf.Lerp(Mathf.Lerp(a, b, tx), Mathf.Lerp(c, d, tx), tz);
|
||||
}
|
||||
|
||||
static float CellRand(int x, int z)
|
||||
{
|
||||
uint h = Hash(unchecked((uint)x * 73856093U) ^ unchecked((uint)z * 19349663U) ^ 0x9E3779B9U);
|
||||
return (h & 0xFFFFFF) / 16777215f;
|
||||
}
|
||||
|
||||
static uint Hash(uint x)
|
||||
{
|
||||
x ^= x >> 16; x *= 0x7feb352dU;
|
||||
x ^= x >> 15; x *= 0x846ca68bU;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue