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

564 lines
36 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandLookSettings.cs — 실제 게임 섬(FarmingIsland Level01)에 **런타임으로** 얹는
// ① 데모 룩(머티리얼·조명) ② 데모 풀밭의 단일 출처 (WL-816e · #816)
//
// PD 지시(2026-09-13) 「섬 배경이 단순 타일로만 되어있어. 기본 타일의 디자인을
// 데모씬에서 사용한 풀밭처럼 꾸미고, 전체적인 비주얼을 맞춰줘」
//
// ■ 왜 런타임인가 — 816a 는 **복사본 씬**(WL_FarmLook)에만 룩을 저장했다.
// 게임이 실제로 로드하는 것은 `Assets/FarmingIsland/Scenes/Level01.unity` 라
// 그 씬은 아직 원본 색이다. 원본 씬을 고치지 않고(FI 0줄) 로드 직후 얹는다.
//
// ■ C8 되돌리기 — `enabled_ = 0` 이면 아무것도 얹지 않는다 = 지금 상태 100 %.
// 씬 파일·FI 에셋을 한 글자도 쓰지 않으므로 되돌리기에 복원 작업이 필요 없다.
//
// ■ C45 — 밀도·크기·제외 여백 같은 값은 전부 이 에셋에 있다(코드 상수 0).
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using System;
using UnityEngine;
namespace WL.Look.Farm
{
/// <summary>원본 머티리얼 → 데모 톤 복사본. 한 줄 = 머티리얼 하나.</summary>
[Serializable]
public sealed class WLMatRemap
{
[Tooltip("0 이면 이 줄은 없는 것으로 친다.")]
public int enabled_ = 1;
[Tooltip("FarmingIsland 원본 머티리얼(한 글자도 안 고친다 · 참조만).")]
public Material from;
[Tooltip("816e/816a 가 만든 데모 톤 복사본.")]
public Material to;
}
/// <summary>
/// 메시 하나의 **윗면(걸어다니는 면)** 을 XZ 격자 비트맵으로 구운 것.
/// FI 메시는 `isReadable = 0` 이라 런타임에 정점을 못 읽는다 → 에디터에서 구워 둔다.
/// (FI 에셋의 임포트 설정을 고치지 않기 위한 선택 — `Assets/FarmingIsland/**` 0줄)
/// </summary>
[Serializable]
public sealed class WLTileMask
{
public string meshName = "";
[Tooltip("격자 해상도(res × res).")] public int res = 64;
[Tooltip("마스크가 덮는 반폭(m). 섬 타일 8 m → 4.")] public float half = 4f;
[Tooltip("res*res 비트를 담은 패킹 배열.")] public byte[] bits;
public bool At(float lx, float lz)
{
if (bits == null || res <= 0) return false;
int cx = (int)((lx + half) / (half * 2f) * res);
int cz = (int)((lz + half) / (half * 2f) * res);
if (cx < 0 || cz < 0 || cx >= res || cz >= res) return false;
int i = cz * res + cx;
int b = i >> 3;
if (b >= bits.Length) return false;
return (bits[b] & (1 << (i & 7))) != 0;
}
public void Set(int cx, int cz, bool v)
{
if (bits == null) bits = new byte[(res * res + 7) / 8];
int i = cz * res + cx;
int b = i >> 3;
if (b >= bits.Length) return;
if (v) bits[b] |= (byte)(1 << (i & 7));
else bits[b] &= (byte)~(1 << (i & 7));
}
}
/// <summary>흩뿌릴 것 하나(풀 · 꽃 · 자갈). 데모 `InstancingSettings` 와 같은 값 구성.</summary>
[Serializable]
public sealed class WLScatterDef
{
public int enabled_ = 1;
[Tooltip("이름표(로그용).")] public string label = "풀";
public Mesh mesh;
public Material material;
[Tooltip("같은 층 안에서의 가중치. 데모 = 풀 100 : 꽃 1.")] public float probability = 100f;
[Tooltip("메시 배율. 데모 = 0.35.")] public float scale = 0.35f;
[Tooltip("법선 방향으로 띄우는 거리(m). 데모 = 0.04.")] public float normalOffset = 0.04f;
[Tooltip("이 층의 밀도(개/m 한 변). 실제 밀도 = 값². 데모 1층 = 2.5(6.25개/㎡).")]
public float density = 2.5f;
[Tooltip("1 = 이 정의는 경계(섬 윤곽·흙 경계·소품) 근처 `grassEdgeBand` 띠에만 깐다 — 들판 한가운데의 얼룩덜룩함 제거(2026-09-15 PD). 0 = 어디나.")]
public int edgeOnly_ = 0;
}
[CreateAssetMenu(fileName = "WLIslandLookSettings", menuName = "WL/Island Look Settings", order = 39)]
public sealed class WLIslandLookSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLIslandLookSettings";
static WLIslandLookSettings s_inst;
static bool s_tried;
public static WLIslandLookSettings Instance
{
get
{
if (s_inst == null && !s_tried) { s_tried = true; s_inst = Resources.Load<WLIslandLookSettings>(ResourcesPath); }
return s_inst;
}
}
public static void Invalidate() { s_inst = null; s_tried = false; }
// ───────────────────────────────── 마스터
[Header("되돌리기 (C8)")]
[Tooltip("0 이면 섬에 아무것도 얹지 않는다 = 지금(원본) 상태 100 %.")]
public int enabled_ = 1;
[Tooltip("1 이면 적용 결과를 Console 에 남긴다.")]
public int verboseLog = 1;
// ───────────────────────────────── §0 룩
[Header("§0 — 실게임 섬(Level01)에 데모 룩 적용")]
[Tooltip("1 이면 섬 씬 로드 직후 머티리얼을 데모 톤 복사본으로 바꾼다(씬 파일 무변경 · 런타임만).")]
public int applyLook = 1;
[Tooltip("1 이면 조명·앰비언트·스카이박스도 데모/아레나 값으로 맞춘다.")]
public int applyLighting = 1;
[Tooltip("1 이면 아레나와 같은 `WL_ReferenceLook`(외곽선·대비·무드)을 섬에도 얹는다. " +
"🔴 816ze — 데모 씬에는 이 보정이 없다. 데모와 같은 룩을 원하면 0.")]
public int applyReferenceLook = 0;
[Tooltip("섬 타일 셰이더 자체의 외곽선 패스(`_OUTLINESENABLED`). " +
"🔴 816ze — 데모 `Terrain.mat` 에는 외곽선 프로퍼티가 아예 없다 → 기본 0(= 전사본 에셋 상태 그대로). " +
"1 로 두면 `islandTopMaterial` 의 외곽선 키워드를 런타임에 **다시 켠다**(되돌리기용).")]
public int islandOutlineEnabled = 0;
[Tooltip("머티리얼 교체 표. 한 줄 = 원본 → 복사본.")]
public WLMatRemap[] materialRemap = new WLMatRemap[0];
[Tooltip("섬 타일 윗면(걸어다니는 면)에 쓸 머티리얼. 비우면 위 표의 Island01 줄을 쓴다. " +
"체크무늬 두 초록을 데모 잔디색 한 가지로 합친 것.")]
public Material islandTopMaterial;
[Tooltip("1 이면 농사 타일(Soil)은 원본 머티리얼 그대로 둔다(색을 코드가 직접 칠하는 곳이라 건드리면 안 된다).")]
public int skipSoilRenderers = 1;
// ── 816f: 밭(Soil) 체크무늬 **기능은 유지**하고 색만 데모 톤으로 (PD 2026-09-13)
[Header("§0-816f — 밭(Soil) 칸 표시 톤 (기능 유지 · 색만)")]
[Tooltip("1 이면 FI `Farm` 의 마른/젖은 흙 색 4개를 런타임에 데모 톤으로 낮춘다. " +
"칸 구분(마름/젖음 표시)은 그대로 남는다 — 색만 바뀐다. 0 이면 FI 원본 색.")]
public int soilToneEnabled = 1;
[Tooltip("흙 색에 곱하는 값(0~1). 낮출수록 주황이 죽는다.")]
public Color soilTint = new Color(0.84f, 0.86f, 0.90f, 1f);
[Tooltip("체크무늬 두 색의 차이를 이 비율로 줄인다(1 = FI 원본 차이 · 0 = 차이 없음=칸 안 보임). " +
"🔴 0 으로 두지 말 것 — 어디를 갈았는지 안 보인다.")]
[Range(0f, 1f)] public float soilCheckerContrast = 0.6f;
// ── 816r: 밭 흙색을 데모 흙 톤으로 「옮긴다」 (곱셈 틴트로는 못 가는 색)
[Tooltip("0 = 816f 상태(곱셈 틴트만) · 1 이면 아래 목표색으로 밭 바닥색을 옮긴다. " +
"칸 차이(어디를 갈았나)와 마름↔젖음 밝기비는 그대로 보존한다.")]
public int soilTargetEnabled = 1;
[Tooltip("밭 「마름」 바닥의 목표색(리니어). 816r 실측 — 데모 흙 화면색에 맞춘 값. " +
"젖은 흙은 이 색에 원본의 젖음/마름 밝기비를 곱해 자동으로 어두워진다.")]
public Color soilTargetColor = new Color(0.3049f, 0.4033f, 0.2961f, 1f);
[Header("§0 — 조명 값 (816a 실측 = 데모/아레나와 동일)")]
public Color ambientSky = new Color(0.212f, 0.227f, 0.259f, 1f);
public Color ambientEquator = new Color(0.114f, 0.125f, 0.133f, 1f);
public Color ambientGround = new Color(0.047f, 0.043f, 0.035f, 1f);
public float ambientIntensity = 1f;
[Tooltip("빌트인 Default-Skybox. 비우면 스카이박스는 안 건드린다.")]
public Material skybox;
public Color dirLightColor = Color.white;
public float dirLightIntensity = 1f;
[Tooltip("1 = Soft(데모) · 0 = 원본(Hard) 유지.")]
public int dirLightSoftShadows = 1;
[Tooltip("1 이면 안개를 끈다(데모 = off).")]
public int fogOff = 1;
[Tooltip("1 이면 FI `GraphicsManager` 를 끈다. 이 컴포넌트는 Play 중 FI 의 URP 에셋 파일에 " +
"renderScale 을 **직접 써 넣어** `Assets/FarmingIsland/**` 를 더럽힌다(816a 실측). " +
"우리 프로젝트의 활성 URP 에셋이 아니라 화면에는 영향 0.")]
public int disableFiGraphicsManager = 1;
[Tooltip("런타임에 뒤늦게 생기는 오브젝트(상인·작물·아이템)까지 잡기 위해 다시 훑는 시각(초).")]
public float[] rescanAtSeconds = new float[] { 0.5f, 2f, 5f };
// ───────────────────────────────── §816h-1 하늘 범위
[Header("§816h — 하늘(RenderSettings)은 「활성 씬」 소유다")]
[Tooltip("🔴 816f/816h 실측 — 섬·던전·전투 맵은 전부 `InGame` 위에 **Additive** 로 붙는다. " +
"그래서 앰비언트·스카이박스·안개는 붙는 씬이 아니라 **활성 씬(InGame)** 것이 쓰인다. " +
"1 이면 아래 `skyScenes` 에 들어간 맵에 들어갈 때만 활성 씬의 하늘을 **스냅샷 뜨고** 우리 값으로 바꾸고, " +
"그 맵에서 나가면 **스냅샷 그대로 되돌린다**(= 다른 맵 오염 0).")]
public int skyScopeEnabled = 1;
[Tooltip("하늘을 통일할 맵. 섬과 던전이 같은 하늘을 쓰게 한다. 전투 맵은 여기 넣지 않는다(원래대로).")]
public string[] skyScenes = new string[] { "Level01", "WL_FarmLook", "WL_Dungeon01", "WL_Dungeon02" };
[Tooltip("맵을 바꿀 때 `SceneInfo` 는 내리기와 올리기를 같은 프레임에 시작한다(실측). " +
"이만큼 기다린 뒤에도 범위 맵이 하나도 없을 때만 하늘을 반납한다 — 섬↔던전 이동에서 한 번 튀는 것을 막는다.")]
public float skyReleaseDelaySeconds = 1.5f;
// ───────────────────────────────── §816h-2 물
[Header("§816h — 섬의 물을 WL 워터 셰이더로 (PD 지시)")]
[Tooltip("0 = 지금(FarmingIsland 물 · 100 % 복귀) · 1 = WL 워터(`ToonWaterU`).")]
public int waterMode = 1;
[Tooltip("교체 대상 판별 — FarmingIsland 원본 물 머티리얼(참조만 · 0줄 수정).")]
public Material waterFrom;
[Tooltip("WL 워터 복사본(`Assets/WL/Materials/WL_Water_Ocean.mat` 기준). 비우면 교체하지 않는다.")]
public Material waterTo;
// ───────────────────────────────── §816j2 섬 둘레 거품 띠
[Header("§816j2 — 섬 둘레 흰 거품 띠 (PD 「둘레에 띠를 둘러줘」)")]
[Tooltip("0 = 띠 없음(816j 상태로 100 % 복귀) · 1 = 섬 둘레를 따라 메시 띠 1장(드로우콜 +1).")]
public int shoreFoamEnabled = 1;
[Tooltip("띠에 쓰는 머티리얼(URP Unlit · 반투명). 비우면 띠를 만들지 않는다.")]
public Material shoreFoamMaterial;
[Tooltip("띠가 물 쪽으로 뻗는 폭(m). 캐릭터 키 1.19 m · 섬 타일 8 m 기준.")]
public float shoreFoamWidth = 1.6f;
[Tooltip("섬 안쪽으로 겹쳐 들어가는 길이(m) — 물가에 틈이 생기지 않게. 섬 벽이 가려 준다.")]
public float shoreFoamInner = 0.5f;
[Tooltip("띠 메시의 격자 간격(m). 크면 싸고 거칠다. 비용이 문제면 이것부터 키운다.")]
public float shoreFoamCell = 0.5f;
[Tooltip("물 표면보다 얼마나 위에 깔 것인가(m). 너무 작으면 z-fighting.")]
public float shoreFoamY = 0.02f;
[Tooltip("씬에서 `Water` 를 못 찾았을 때 쓸 물 높이(m).")]
public float shoreFoamFallbackY = -1f;
[Tooltip("거품 무늬가 해안을 따라 반복되는 길이(m).")]
public float shoreFoamTileLength = 6f;
[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;
// ───────────────────────────────── §0-b 바다 바닥 (2026-09-15 Lead 실측)
[Header("§0-b — 물 밑 바닥판 (데모 연못은 바닥이 얕아 물이 청록 · 우리는 바닥이 없어 배경색이 비쳐 진파랑)")]
[Tooltip("1 = 물 표면 아래에 모래색 바닥판 1장을 깐다. 데모 물 셰이더는 깊이로 색·거품을 정하므로 바닥이 있어야 데모처럼 청록 물 + 흰 물가가 나온다. 0 = 없음(배경색이 비침).")]
public int seabedEnabled = 1;
[Tooltip("물 표면에서 바닥판까지 깊이(m). 얕을수록 청록·거품이 진해진다.")]
public float seabedDepth = 0.8f;
[Tooltip("바닥판 한 변 길이(m). 카메라가 보는 범위만 덮으면 된다.")]
public float seabedSize = 400f;
[Tooltip("바닥판 색(언릿 · sRGB). 기본 = 데모 모래색.")]
public Color seabedColor = new Color(0.71f, 0.706f, 0.549f, 1f);
// ───────────────────────────────── §0-c 물가 스커트 (WL-816zg · PD 「물은 진행」 2026-09-15)
[Header("§0-c — 섬 둘레 완만한 모래 경사(물가 스커트) — 데모 연못처럼 가장자리가 모래 경사로 물에 잠긴다")]
[Tooltip("1 = 열린 타일 발자국 바깥에 거리장 기반 모래 경사 메시 1장(드로우콜 +1)을 깐다 — 둥근 윤곽 · 타일 수직 옆면 가림 · " +
"흰 물가 띠는 물 셰이더가 깊이로 만든다. 0 = 없음(지금 상태 100 % 복귀).")]
public int shoreSkirtEnabled = 1;
[Tooltip("경사에 쓸 머티리얼(선택). 비우면 `WL/Shore Skirt` 셰이더로 런타임 머티리얼을 만든다(에셋 0줄). " +
"🔴 모바일 빌드에 이 셰이더가 들어가게 하려면 그 셰이더를 쓰는 머티리얼을 여기에 넣어 두는 것이 가장 확실하다(Always Included 대신).")]
public Material shoreSkirtMaterial;
[Tooltip("타일 가장자리에서 바깥(물 쪽)으로 뻗는 경사 폭(m).")]
public float skirtWidth = 3.0f;
[Tooltip("경사 바깥 끝의 높이(월드 y · m). 물(1.0) 아래 · 바닥판(물 seabedDepth = 1.8) 위 또는 같게.")]
public float skirtBottomY = -1.8f;
[Tooltip("타일 가장자리에서 이 거리(m)까지는 풀색(islandTopMaterial `_DiffuseColor`), 그 뒤 모래색(softDirtColor)으로 넘어간다.")]
public float skirtGrassBand = 0.6f;
[Tooltip("풀색 → 모래색으로 넘어가는 폭(m).")]
public float skirtSandBlend = 0.6f;
[Tooltip("메시 격자 칸(m). 🔴 타일 한 변(8 m)을 나누는 값이어야 정점이 타일 가장자리 선 위에 정확히 놓인다(0.5 · 0.25 · 1).")]
public float skirtCell = 0.5f;
[Tooltip("타일 안쪽으로 이 거리(m)까지만 정점을 둔다(가장자리 밑으로 살짝 들어가 틈을 막는다). 그 안쪽은 만들지 않는다(윗면 z-fight 방지).")]
public float skirtInnerKeep = 0.3f;
[Tooltip("타일 윗면보다 이만큼(m) 낮은 곳에서 경사를 시작한다 — 윗면과 같은 높이면 z-fight. 타일 옆면이 그만큼만 보인다.")]
public float skirtTopDrop = 0.04f;
[Tooltip("1 = 타일 윗면 높이를 아래 `skirtTopY` 고정값으로(Lead 실측 0 · 확장 애니메이션 중에도 흔들리지 않는다) · " +
"0 = 열린 타일 렌더러 bounds 윗면을 실측해 쓴다(로그에 두 값이 같이 찍힌다).")]
public int skirtTopMode = 1;
[Tooltip("타일 윗면 높이 고정값(월드 y · m). 실측(2026-09-15) = 0. 실측 모드에서 렌더러를 못 찾을 때의 대체값이기도 하다.")]
public float skirtTopY = 0f;
[Tooltip("물에 잠긴 정점(y < 물 높이)의 색 배율 — 젖은 모래·물속 어둡기.")]
public float skirtUnderwaterMul = 0.85f;
[Tooltip("풀색 대체값(sRGB). 기본은 `islandTopMaterial`(Farm_IslandTop_Demo3)의 `_DiffuseColor` 를 읽고, 없을 때만 이 값을 쓴다. " +
"기본 = 816ze 들판 실측 0.62/0.82/0.485.")]
public Color skirtGrassFallback = new Color(0.62f, 0.82f, 0.485f, 1f);
[Tooltip("거리장·메시 격자 정점 수 상한. 넘으면 칸을 두 배씩 키운다(섬이 아무리 커져도 비용이 묶인다).")]
public int skirtMaxGridNodes = 400000;
[Tooltip("1 = 스커트를 타일과 **같은 Toon 재질**로 그린다(풀 = islandTopMaterial 그대로 · 모래 = 그 복사본에 모래색) → " +
"구름 그림자·조명이 타일과 똑같이 떨어져 이음새가 뜨지 않는다(2026-09-16 PD 「가장자리 경계가 붕 뜬다」 실측: 언릿 스커트가 구름 아래 타일보다 최대 15 % 밝았다). " +
"0 = 전용 언릿 정점색 셰이더(그라데이션은 부드럽지만 구름을 못 받는다).")]
public int skirtToonMode = 1;
[Tooltip("Toon 모드 모래 재질의 그늘색 배율(모래색 × 이 값) — 경사의 그늘진 면.")]
public float skirtSandShadowMul = 0.72f;
[Tooltip("Toon 모드에서 풀/모래 경계선을 칸마다 흔드는 폭(m) — 직선이 아니라 들쭉날쭉한 물가. 0 = 직선.")]
public float skirtEdgeJitter = 0.6f;
// ───────────────────────────────── §1 풀밭
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
[Tooltip("`edgeOnly_ 1` 정의를 경계에서 이만큼(m) 안쪽까지만 깐다. 0 = 띠 제한 없음. " +
"2026-09-15 PD 「그라데이션과 무관하게 풀이 얼룩덜룩」 — 들판 안쪽 풀잎은 같은 색이라도 미세한 음영 차로 얼룩이 되므로 경계 실루엣만 남긴다. " +
"816zh — 구름 경계 띠(`cloudBandEnabled 1`)가 켜지면 들판은 구름 그림자 경계가 맡으므로 이 물리 띠는 0.6 으로 낮춰 병행한다(띠 안은 항상 배율 1).")]
public float grassEdgeBand = 0.6f;
// ── 816zh: 풀잎을 「구름 그림자(그라데이션) 경계」에 — 크기로 부드럽게 자라고 줄어든다 (PD 2026-09-16 「그림자를 기준으로」)
[Header("§1-816zh — 풀잎 = 구름 그림자 경계 띠 (부드러운 성장 · 팝핑 0)")]
[Tooltip("🔴 되돌리기 — 0 = 지금처럼 `edgeOnly_` 풀잎을 물리 경계 띠(grassEdgeBand)에만. " +
"1 = 들판 전체에 후보를 두고, 구름 그림자 경계(구름 높이가 바닥 재질 `_Cloud_Cover` 를 지나는 선) 근처만 배율을 키워 그린다. " +
"816o(하드 컷 = 구름이 흐르면 순간이동)와 달리 껐다 켜는 게 아니라 **자라고 줄어든다**. 켜지면 816o `cloudEdgeEnabled` 는 무시된다. " +
"구름 식은 `cloudEdgeSourceMaterial`(비우면 `islandTopMaterial`)에서 읽는다 — 바닥과 같은 식.")]
public int cloudBandEnabled = 1;
[Tooltip("띠 반폭(구름 높이 단위 · m 아님) — 강도를 걷어 낸 구름 높이(cover + 0.5·noise)가 cover 에서 이만큼 벗어나면 배율 0, 경계선 위는 1(smoothstep). " +
"🔴 현 바닥 재질(_Shades 7 · _Brightness 0.25 · 강도 0.5)의 보이는 색 계단은 cover ± 0.143 에 있다 — 0.12 는 그 사이 그라데이션 중심 띠. 계단선까지 덮으려면 0.15~0.18.")]
public float cloudBandWidth = 0.12f;
[Tooltip("띠 목표 배율을 다시 계산하고 인스턴스 버퍼를 다시 채우는 주기(초). 구름은 초당 약 1.4 m 흐른다(_Cloud_Movement 1,1). " +
"비용 = 노이즈 표본(cloudEdgeSampleCell 간격 · 6 옥타브) + 후보 전수 겹선형 읽기 + 버퍼 재생성, 이 주기마다 1회.")]
public float cloudBandRefreshSeconds = 0.2f;
[Tooltip("갱신 때마다 현재 배율이 목표로 다가가는 비율(0~1). 작을수록 천천히 자라고 줄어든다 — 0.15 × 0.2 s ≈ 시간상수 1.2 초(구름 이동 약 1.7 m 뒤따름). 1 = 즉시(=팝핑).")]
[Range(0.01f, 1f)] public float cloudBandLerp = 0.15f;
[Tooltip("이 배율 미만인 들판 풀잎은 버퍼에서 뺀다(정점 0 · 압축). 너무 크면 사라질 때 톡 끊긴다.")]
public float cloudBandMinScale = 0.02f;
[Tooltip("0 이면 풀을 깔지 않는다.")]
public int grassEnabled = 1;
[Tooltip("흩뿌릴 것들. 같은 density 를 가진 줄끼리 한 층으로 묶여 확률 분배된다(데모와 같은 방식).")]
public WLScatterDef[] scatter = new WLScatterDef[0];
[Tooltip("격자 흔들기(격자 칸 단위). 데모 = 0.5.")]
[Range(0f, 0.5f)] public float positionVariance = 0.5f;
[Tooltip("크기 흔들기. 데모 = 0.2 → ×0.8~1.2.")]
[Range(0f, 0.9f)] public float scaleVariance = 0.2f;
[Tooltip("1 이면 풀 하나하나를 Y 축으로 랜덤 회전한다(데모는 회전 0 = 빌보드라 불필요). " +
"빌보드 셰이더에는 영향이 없다.")]
public int randomYaw = 0;
[Header("§1 — 풀을 깔지 않는 곳 (제외 규칙)")]
[Tooltip("섬 타일 가장자리에서 이만큼(m) 안쪽까지만 깐다. 물 위로 삐져나오지 않게.")]
public float edgeInset = 0.15f;
[Tooltip("길(Road 메시)에서 이만큼(m) 떨어뜨린다.")]
public float roadMargin = 0.15f;
[Tooltip("농사 타일(Farm 트리거 · Soil 1×1)에서 이만큼(m) 떨어뜨린다.")]
public float farmMargin = 0.35f;
[Tooltip("1 이면 밭 자리 제외 사각형의 네 모서리를 깎고 경계를 노이즈로 흔들어 " +
"풀이 흙 가장자리로 파고들게 한다. 0 이면 예전처럼 반듯한 사각형.")]
public int farmEdgeRoundEnabled = 1;
[Tooltip("밭 자리 모서리를 깎는 반지름(m). 클수록 둥글게 읽힌다.")]
public float farmEdgeRadius = 1.0f;
[Tooltip("밭 자리 경계를 흔드는 진폭(m). 이만큼 풀이 파고들거나 물러난다.")]
public float farmEdgeNoise = 0.4f;
[Tooltip("경계 노이즈의 셀 크기(m). 작을수록 잘게 들쭉날쭉해진다.")]
public float farmEdgeNoiseCell = 0.5f;
// ── 816zd: 땅은 각진 그대로 두고 **풀 카펫의 바깥 윤곽만** 둥글게 (PD 2026-09-15)
[Header("§1-816zd — 섬 바깥 윤곽 둥근 풀 배치 (땅은 무변경)")]
[Tooltip("1 이면 풀을 깔 넓이를 「섬 윗면 footprint 를 반지름만큼 **열고**(침식 후 팽창) " +
"바깥으로 넘침폭만큼 넓힌」 모양으로 정한다. 볼록 모서리가 반지름만큼 둥글어져 " +
"각진 타일 위에서도 풀 카펫의 바깥선만 둥글게 읽힌다. 0 이면 예전처럼 타일 마스크 + edgeInset.")]
public int outlineRoundEnabled = 1;
[Tooltip("모서리를 둥글게 깎는 반지름(m). 클수록 모서리에 풀이 더 비고 윤곽이 더 둥글다.")]
public float outlineRoundRadius = 1.5f;
[Tooltip("둥글린 윤곽을 바깥으로 넓히는 폭(m). 기준선은 위 edgeInset 이다 — 같은 값이면 바깥선이 " +
"지금과 똑같고(모서리만 둥글어짐), 그보다 크면 차이만큼 가장자리 풀이 절벽 위로 넘친다.")]
public float outlineOverhang = 0.3f;
[Tooltip("윤곽 계산 격자 칸 크기(m). 작을수록 윤곽선이 매끄럽지만 다시 깔 때 더 오래 걸린다.")]
public float outlineRoundCell = 0.25f;
[Tooltip("건물·소품 콜라이더에서 이만큼(m) 떨어뜨린다.")]
public float propMargin = 0.05f;
[Tooltip("이 크기(m) 이상인 콜라이더는 섬 본체로 보고 제외 대상에서 뺀다.")]
public float propMaxSize = 7.5f;
[Header("§1 — 모바일 예산")]
[Tooltip("인스턴스 총개수 상한. 넘으면 밀도를 자동으로 낮춘다(0 = 무제한).")]
public int maxInstances = 40000;
[Tooltip("섬이 확장되면 다시 깐다 — 이 주기(초)로 잠긴 섬이 열렸는지 본다. 0 이면 감시 안 함.")]
public float rebuildPollSeconds = 1f;
[Tooltip("확장 애니메이션(DOTween 1초)이 끝나기를 기다리는 초.")]
public float rebuildDelaySeconds = 1.4f;
// ───────────────────────────────── §2 구름 경계 배치 (816o)
[Header("§2 — 구름 그림자 경계에만 풀을 심는다 (816o · PD 「모바일 부하 없게」)")]
[Tooltip("🔴 되돌리기 스위치 — 0 이면 816n 상태 100 % 복귀(섬 전체에 균일 격자). " +
"1 이면 바닥 색 띠가 바뀌는 곳(구름 그림자 경계) 근처에만 심는다. " +
"816n 실측: 보이는 풀의 88 % 가 이 띠에 몰려 있다 = 나머지는 삼각형만 먹는다.")]
public int cloudEdgeEnabled = 1;
[Tooltip("경계 판정 격자 칸(m). 띠 폭의 최소 단위 = 이 값. 여기는 노이즈를 계산하지 않는다(보간).")]
public float cloudEdgeCell = 0.3f;
[Tooltip("판정 격자 칸 수 상한. 섬이 커지면 칸을 자동으로 키운다(0 = 무제한).")]
public int cloudEdgeMaxCells = 120000;
[Tooltip("🔴 비용이 나는 유일한 곳 — 구름 노이즈(6 옥타브)를 실제로 계산하는 간격(m). " +
"구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 면 충분하다. " +
"사이 값은 겹선형 보간으로 채운다. 816zh 구름 경계 띠도 이 간격으로 표본을 뜬다(0.2 s 마다 · 키우면 비용이 제곱으로 준다).")]
public float cloudEdgeSampleCell = 1f;
[Tooltip("노이즈를 계산하는 점의 개수 상한. 섬이 커지면 간격을 자동으로 늘려 비용을 묶어 둔다(816zh 띠도 같은 상한).")]
public int cloudEdgeMaxSamples = 4096;
[Tooltip("경계에서 몇 칸 더 넓힐 것인가. 띠 폭 ≈ (1 + 2 × 이 값) × 칸(m). " +
"구름이 흐르는 만큼(`cloudEdgeDriftCover`)은 여기에 자동으로 더해진다.")]
public int cloudEdgeDilate = 1;
[Tooltip("1 이면 갱신 주기 동안 구름이 흐르는 거리만큼 띠를 자동으로 더 넓힌다 — " +
"띠가 움직여도 풀이 뒤따라오지 못해 생기는 빈틈을 막는다. 0 이면 위 값만 쓴다.")]
public int cloudEdgeDriftCover = 1;
[Tooltip("구름이 흐르므로 이 주기(초)마다 띠를 다시 고른다. 0 이면 한 번 굽고 고정. " +
"🔴 실제 주기는 `rebuildPollSeconds` 단위로 반올림된다.")]
public float cloudEdgeRefreshSeconds = 1f;
[Tooltip("띠 안의 밀도 배율. 면적이 줄어든 만큼 띠를 더 촘촘히 해 데모 느낌을 살린다. " +
"1 = 816n 과 같은 밀도(= 인스턴스가 띠 면적 비율만큼 그대로 줄어든다).")]
public float cloudEdgeDensityScale = 1.25f;
[Tooltip("구름 값을 읽어 올 머티리얼. 비우면 `islandTopMaterial`(바닥)을 쓴다 — " +
"바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")]
public Material cloudEdgeSourceMaterial;
// ───────────────────────────────── §3 가시 범위 배치 (816q)
[Header("§3 — 카메라가 보는 범위에만 인스턴스를 둔다 (816q · 배치가 아니라 범위로 최적화)")]
[Tooltip("🔴 되돌리기 스위치 — 0 이면 섬 전체에 인스턴스를 둔다(816n 상태). " +
"1 이면 카메라 시야 사각형 + 여유 폭 안에만 둔다. " +
"격자는 월드 고정이라 같은 자리의 풀은 항상 같은 자리다(순간이동 0) — " +
"늘고 주는 것은 **화면 밖 여유 폭 안에서만** 일어난다.")]
public int viewCullEnabled = 1;
[Tooltip("시야 사각형 바깥으로 더 두는 여유 폭(m). " +
"🔴 `viewRebuildStep` 보다 커야 한다 — 차이만큼이 「화면 밖에서만 바뀐다」는 보장 폭이다.")]
public float viewMargin = 4f;
[Tooltip("카메라가 이만큼(m) 움직였을 때만 다시 고른다. " +
"작을수록 자주 고르고, 클수록 여유 폭을 많이 먹는다.")]
public float viewRebuildStep = 1.5f;
[Tooltip("카메라가 이만큼(°) 돌았을 때도 다시 고른다. 회전은 시야를 통째로 돌리므로 위치보다 민감하다.")]
public float viewRebuildAngle = 3f;
[Tooltip("시야 사다리꼴의 먼 쪽을 이 거리(m)에서 끊는다. 하늘을 향하는 모서리도 여기로 자른다. " +
"PD 구도(12 m·45°)에서 화면 위끝의 바닥은 약 32 m 라 40 이면 남는다.")]
public float viewMaxDistance = 40f;
[Tooltip("시야 사각형을 다시 재는 주기(초). 0 이면 감시 안 함(한 번 고르고 고정).")]
public float viewPollSeconds = 0.25f;
// ───────────────────────────────── 구운 마스크
[Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")]
public WLTileMask[] tileMasks = new WLTileMask[0];
public WLTileMask[] roadMasks = new WLTileMask[0];
// ───────────────────────────────── 섬 씬 판별
[Header("어느 씬에 적용하는가")]
[Tooltip("이 이름의 씬에 적용한다(비워도 IslandManager 가 있으면 섬으로 본다).")]
public string[] islandSceneNames = new string[] { "Level01", "WL_FarmLook" };
public WLTileMask FindTileMask(string meshName) { return Find(tileMasks, meshName); }
public WLTileMask FindRoadMask(string meshName) { return Find(roadMasks, meshName); }
static WLTileMask Find(WLTileMask[] a, string n)
{
if (a == null || string.IsNullOrEmpty(n)) return null;
// `MeshFilter.mesh` 로 읽으면 이름 뒤에 " Instance" 가 붙는다 — 떼고 비교한다.
int k = n.IndexOf(" Instance");
if (k > 0) n = n.Substring(0, k);
for (int i = 0; i < a.Length; i++) if (a[i] != null && a[i].meshName == n) return a[i];
return null;
}
public void Log(string msg) { if (verboseLog != 0) Debug.Log("[WL-816e] " + msg); }
}
}