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

739 lines
41 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandLook.cs — **실제 게임이 로드하는 섬 씬**(FarmingIsland/Scenes/Level01)에
// 데모 룩과 풀밭을 런타임으로 얹는다 (WL-816e · #816)
//
// ■ 왜 필요한가(발주서 §0)
// 816a 는 복사본 씬 `Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity` 에만 룩을 저장했다.
// 게임은 `Level01` 을 로드하므로 PD 캡처의 섬은 여전히 **원본 색**(쨍한 파란 하늘·원색 초록)이었다.
// → 원본 씬을 고치지 않고(`Assets/FarmingIsland/**` 0줄) **로드 직후** 얹는다.
//
// ■ 걸리는 지점
// `[RuntimeInitializeOnLoadMethod]` + `SceneManager.sceneLoaded` — 기존 파일 **0줄 수정**.
// (816d 의 `WLIslandBridge` 를 고치지 않는다 = 다른 worktree 와 충돌 0)
//
// ■ C8 되돌리기 — `WLIslandLookSettings.enabled_ = 0` → 아무것도 얹지 않는다.
// 씬·머티리얼·프리팹 **파일을 쓰지 않으므로** 복원 작업 자체가 없다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using CryingSnow.FarmingIsland;
using FIIsland = CryingSnow.FarmingIsland.Island;
using FISoil = CryingSnow.FarmingIsland.Soil;
using FIFarm = CryingSnow.FarmingIsland.Farm;
namespace WL.Look.Farm
{
/// <summary>섬 씬에 룩·풀밭을 얹는 정적 진입점.</summary>
public static class WLIslandLook
{
public const string RunnerName = "~WLIslandLookRunner";
public const string ReferenceLookName = "WL_ReferenceLook";
// ── 진단 ────────────────────────────────────────────────────────
public static int SwappedRenderers, SwappedSlots, Rescans, LightingApplied, GrassRebuilds;
public static bool LookApplied, ReferenceLookApplied, GrassSpawned;
public static string LastLog = "";
static bool s_installed;
static WLIslandLookRunner s_runner;
static readonly HashSet<Renderer> s_done = new HashSet<Renderer>();
static readonly HashSet<FIIsland> s_hooked = new HashSet<FIIsland>();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
public static void Install()
{
if (s_installed) return;
var cfg = WLIslandLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
s_installed = true;
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var sc = SceneManager.GetSceneAt(i);
if (sc.isLoaded) OnSceneLoaded(sc, LoadSceneMode.Additive);
}
}
/// <summary>에디터 프로브가 Play 중에 다시 걸 때 쓴다.</summary>
public static void ForceApply(Scene scene)
{
var cfg = WLIslandLookSettings.Instance;
if (cfg == null) return;
s_done.Clear();
EnsureRunner();
s_runner.StartCoroutine(Co_Apply(cfg, scene));
}
static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
var cfg = WLIslandLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
if (!IsIslandScene(cfg, scene)) return;
EnsureRunner();
s_runner.StartCoroutine(Co_Apply(cfg, scene));
}
static void OnSceneUnloaded(Scene scene)
{
s_done.Clear();
s_hooked.Clear();
s_tonedFarms.Clear();
LookApplied = false; GrassSpawned = false; ReferenceLookApplied = false;
}
static bool IsIslandScene(WLIslandLookSettings cfg, Scene scene)
{
if (!scene.IsValid() || !scene.isLoaded) return false;
if (cfg.islandSceneNames != null)
for (int i = 0; i < cfg.islandSceneNames.Length; i++)
if (scene.name == cfg.islandSceneNames[i]) return true;
// 이름을 몰라도 IslandManager 가 있으면 섬이다.
var roots = scene.GetRootGameObjects();
for (int i = 0; i < roots.Length; i++)
if (roots[i].GetComponentInChildren<IslandManager>(true) != null) return true;
return false;
}
static void EnsureRunner()
{
if (s_runner != null) return;
var go = new GameObject(RunnerName);
go.hideFlags = HideFlags.DontSave;
Object.DontDestroyOnLoad(go);
s_runner = go.AddComponent<WLIslandLookRunner>();
}
// ─────────────────────────────────────────────────────────────────
// 본체
// ─────────────────────────────────────────────────────────────────
static IEnumerator Co_Apply(WLIslandLookSettings cfg, Scene scene)
{
yield return null; // FI 매니저들의 Awake 를 기다린다
if (cfg.disableFiGraphicsManager != 0) DisableGraphicsManager(scene);
if (cfg.applyLook != 0)
{
SwapMaterials(cfg, scene);
ApplyIslandOutline(cfg);
LookApplied = true;
}
if (cfg.applyLighting != 0) ApplyLighting(cfg, scene);
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
// 🔴 816zj — 데모 터레인 모드(`demoTerrainEnabled 1`)에서는 흙 데칼·바닥판·스커트·풀(WLIslandGrass)을 만들지 않는다(값으로 상호 배타).
// 바닥·경사·잔디/모래 경계·풀 전부를 데모 부품(Terrain + Terrain.mat + TerrainInstancesBehaviour)이 맡는다 — 본체는 `WLDemoTerrain`.
bool demo = cfg.demoTerrainEnabled != 0;
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
if (!demo && cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
if (!demo && cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
if (!demo && cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
if (demo) SpawnDemoTerrain(cfg, scene);
if (!demo && cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
HookIslands(cfg, scene);
cfg.Log("섬 룩 적용 — 렌더러 " + SwappedRenderers + " · 슬롯 " + SwappedSlots
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
+ " · 데모터레인 " + WLDemoTerrain.LastLog
+ " · 풀 " + WLIslandGrass.LastLog
+ " · 흙데칼 " + WLSoftDirt.LastLog
+ " · 물가스커트 " + WLShoreSkirt.LastLog
+ " · 둘레거품 " + WLShoreFoam.LastLog);
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
if (cfg.rescanAtSeconds != null)
{
float prev = 0f;
for (int i = 0; i < cfg.rescanAtSeconds.Length; i++)
{
float w = cfg.rescanAtSeconds[i] - prev;
prev = cfg.rescanAtSeconds[i];
if (w > 0f) yield return new WaitForSeconds(w);
if (!scene.isLoaded) yield break;
if (cfg.applyLook != 0) { SwapMaterials(cfg, scene); Rescans++; }
// 🔴 816f 실측 — PD 실행 경로(Additive)에서 `RenderSettings` 는 **활성 씬(InGame)**
// 소유이고, 그 씬에는 조명을 덮어쓰는 코드(`MapData`)가 없다(실측).
// 그래서 조명은 재훑기에서 **다시 걸지 않는다** — 다시 걸면 `WLReferenceLook` 의
// 무드(Flat)를 Skybox 로 되돌려 오히려 룩이 바뀐다(실측으로 확인).
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
// 🔴 816zf — 밭 구역(`WLIslandGateFlow.ZoneBounds`)은 게이트 플로우가 밭을 훑은 뒤에야 정해진다.
// 재훑기에서 구역이 처음 잡히거나 바뀌면 데칼을 굽고, **그때만** 풀도 다시 깐다(경계가 같아지게).
if (!demo && 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++; }
}
}
// 🔴 816zg — 물가 스커트도 같은 재훑기에서 다시 만든다(발자국이 그대로면 멱등 = 공짜).
if (!demo && cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
// 🔴 816zj — 데모 터레인: 밭 구역이 재훑기에서 처음 잡히면 스플랫(밭 자리 모래)을 다시 굽는다(구역·발자국이 그대로면 멱등 = 공짜).
if (demo) SpawnDemoTerrain(cfg, scene);
HookIslands(cfg, scene);
}
}
// 섬 확장 감시 — 새로 열린 타일에도 자동으로 풀이 깔린다
if (cfg.rebuildPollSeconds > 0f)
{
int last = CountUnlocked(scene);
float nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
float nextView = Time.time + Mathf.Max(0f, cfg.viewPollSeconds);
float nextPoll = Time.time + cfg.rebuildPollSeconds;
while (scene.isLoaded)
{
// 816q — 카메라 감시는 확장 감시(1 s)보다 촘촘해야 해서 따로 돈다
float wait = cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f
? Mathf.Min(cfg.rebuildPollSeconds, cfg.viewPollSeconds) : cfg.rebuildPollSeconds;
yield return new WaitForSeconds(wait);
if (!scene.isLoaded) yield break;
if (cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f && Time.time >= nextView)
{
nextView = Time.time + cfg.viewPollSeconds;
var gv = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (gv != null && gv.ViewMovedEnough()) gv.RefreshView();
}
if (Time.time < nextPoll) continue;
nextPoll = Time.time + cfg.rebuildPollSeconds;
HookIslands(cfg, scene);
int now = CountUnlocked(scene);
if (now != last)
{
last = now;
if (cfg.applyLook != 0) SwapMaterials(cfg, scene);
RequestGrassRebuild(cfg);
nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
continue;
}
// 816o — 구름이 흐른 만큼 띠를 다시 고른다(타일·제외 사각형은 다시 훑지 않는다)
if (cfg.cloudEdgeEnabled != 0 && cfg.cloudEdgeRefreshSeconds > 0f && Time.time >= nextCloud)
{
nextCloud = Time.time + cfg.cloudEdgeRefreshSeconds;
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g != null) g.RefreshCloudEdges();
}
}
}
}
static int CountUnlocked(Scene scene)
{
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
int n = 0;
for (int i = 0; i < islands.Length; i++)
if (islands[i] != null && islands[i].IsUnlocked && islands[i].gameObject.activeInHierarchy) n++;
return n;
}
/// <summary>FI `FIIsland.OnActivated`(public event)에 붙는다 — FI 코드 0줄.</summary>
static void HookIslands(WLIslandLookSettings cfg, Scene scene)
{
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
for (int i = 0; i < islands.Length; i++)
{
var isl = islands[i];
if (isl == null || isl.gameObject.scene != scene) continue;
if (s_hooked.Contains(isl)) continue;
s_hooked.Add(isl);
isl.OnActivated += () => OnIslandActivated(cfg, isl);
}
}
static void OnIslandActivated(WLIslandLookSettings cfg, FIIsland isl)
{
if (cfg == null || cfg.enabled_ == 0) return;
EnsureRunner();
s_runner.StartCoroutine(Co_AfterActivate(cfg, isl));
}
static IEnumerator Co_AfterActivate(WLIslandLookSettings cfg, FIIsland isl)
{
// 확장 애니메이션(DOTween DOScale 1초) + 소품 배치가 끝나기를 기다린다
yield return new WaitForSeconds(cfg.rebuildDelaySeconds);
if (isl == null) yield break;
if (cfg.applyLook != 0) SwapMaterials(cfg, isl.gameObject.scene);
RequestGrassRebuild(cfg);
}
static void RequestGrassRebuild(WLIslandLookSettings cfg)
{
bool demo = cfg.demoTerrainEnabled != 0;
// 🔴 816zj — 데모 터레인 모드: 섬이 확장되면 높이맵·스플랫만 다시 굽고 인스턴서를 껐다 켠다(멱등). 데칼·풀·스커트는 이 모드에 없다.
if (demo)
{
var dt = Object.FindFirstObjectByType<WLDemoTerrain>(FindObjectsInactive.Include);
if (dt != null) { dt.cfg = cfg; dt.Rebuild(); DemoTerrainRebuilds++; }
}
// 🔴 816zf — 흙 데칼을 **먼저** 다시 굽는다. 섬 확장·밭 재구성으로 구역이 바뀌면
// 풀의 밭 제외도 그 새 경계를 읽어야 흙과 풀이 같은 선에서 만난다.
if (!demo && cfg.softDirtEnabled != 0)
{
var sd = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
if (sd != null) { sd.cfg = cfg; sd.Rebuild(); }
}
if (!demo && cfg.grassEnabled != 0)
{
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g != null) { g.Rebuild(); GrassRebuilds++; }
}
// 🔴 816j2 — 섬이 확장되면 **둘레 거품 띠도 같은 훅으로** 다시 만든다(새 가장자리 자동).
if (cfg.shoreFoamEnabled != 0)
{
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
}
// 🔴 816zg — 섬이 확장되면 물가 스커트도 같은 훅으로 다시 만든다(발자국 변화 → 거리장 재계산).
if (!demo && cfg.shoreSkirtEnabled != 0)
{
var sk = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
if (sk != null) { sk.cfg = cfg; sk.Rebuild(); ShoreSkirtRebuilds++; }
}
}
public static bool DemoTerrainSpawned;
public static int DemoTerrainRebuilds;
/// <summary>
/// 816zj — 섬 바닥을 데모와 같은 부품으로(런타임 Terrain + 데모 Terrain.mat + 데모 TerrainInstancesBehaviour).
/// 데칼·바닥판·스커트·풀과 같은 자리에서 만들고 같은 훅(재훑기·확장)으로 다시 굽는다 — 본체는 <see cref="WLDemoTerrain"/>.
/// </summary>
public static void SpawnDemoTerrain(WLIslandLookSettings cfg, Scene scene)
{
var t = Object.FindFirstObjectByType<WLDemoTerrain>(FindObjectsInactive.Include);
if (t == null)
{
var go = new GameObject(WLDemoTerrain.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
t = go.AddComponent<WLDemoTerrain>();
t.cfg = cfg;
t.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
}
else { t.cfg = cfg; t.Rebuild(); }
DemoTerrainSpawned = true;
}
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;
}
public static bool SeabedSpawned;
public static string SeabedLog = "";
/// <summary>
/// 2026-09-15 Lead 실측 — 데모 물 셰이더(`Farm_Water_Demo3` = 데모 `Water.mat`)는 **깊이**로 색·거품을 정한다.
/// 데모 연못은 바닥이 1 m 안팎이라 청록 + 흰 물가가 나오지만, 우리 바다는 밑에 아무것도 없어 카메라 배경색이 비쳐
/// 진파랑으로 찍혔다(PD 「데모와 느낌이 다르다」의 큰 몫). 물 아래 `seabedDepth` 에 모래색 언릿 판 1장(삼각형 2 · 드로우콜 1)을 깐다.
/// 🔴 모바일 빌드: `Universal Render Pipeline/Unlit` 이 빌드에 포함되지 않으면 분홍 → Always Included 확인.
/// </summary>
public static void SpawnSeabed(WLIslandLookSettings cfg, Scene scene)
{
const string Name = "~WL_Seabed";
float waterY = -1f; bool found = false;
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" || (scene.IsValid() && r.gameObject.scene != scene)) continue;
waterY = r.transform.position.y; found = true; break;
}
GameObject go = null;
for (int i = 0; i < mrs.Length; i++) if (mrs[i] != null && mrs[i].gameObject.name == Name) { go = mrs[i].gameObject; break; }
if (go == null)
{
go = new GameObject(Name);
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(go, scene);
var mf = go.AddComponent<MeshFilter>();
var mesh = new Mesh { name = "WL_SeabedQuad" };
mesh.vertices = new[] { new Vector3(-0.5f, 0f, -0.5f), new Vector3(-0.5f, 0f, 0.5f), new Vector3(0.5f, 0f, 0.5f), new Vector3(0.5f, 0f, -0.5f) };
mesh.triangles = new[] { 0, 1, 2, 0, 2, 3 };
mesh.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
mesh.uv = new[] { Vector2.zero, Vector2.up, Vector2.one, Vector2.right };
mesh.RecalculateBounds();
mf.sharedMesh = mesh;
var mr = go.AddComponent<MeshRenderer>();
var sh = Shader.Find("Universal Render Pipeline/Unlit");
var mat = new Material(sh != null ? sh : Shader.Find("Unlit/Color")) { name = "WL_Seabed(runtime)" };
mr.sharedMaterial = mat;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
}
var m = go.GetComponent<MeshRenderer>().sharedMaterial;
if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", cfg.seabedColor);
if (m.HasProperty("_Color")) m.SetColor("_Color", cfg.seabedColor);
go.transform.position = new Vector3(0f, waterY - Mathf.Max(0.05f, cfg.seabedDepth), 0f);
go.transform.rotation = Quaternion.identity;
go.transform.localScale = new Vector3(Mathf.Max(1f, cfg.seabedSize), 1f, Mathf.Max(1f, cfg.seabedSize));
go.SetActive(cfg.seabedEnabled != 0);
SeabedSpawned = true;
SeabedLog = "바다 바닥판 y=" + go.transform.position.y.ToString("F2") + (found ? "(물 " + waterY.ToString("F2") + ")" : "(물 미발견 · 기본 -1)") + " · 깊이 " + cfg.seabedDepth + " m";
}
public static bool ShoreSkirtSpawned;
public static int ShoreSkirtRebuilds;
/// <summary>
/// 816zg — 섬 둘레 완만한 모래 경사(물가 스커트). 바닥판·거품과 같은 자리에서 만들고 같은 훅으로 다시 만든다.
/// 열린 타일 발자국의 거리장으로 띠 메시 1장(드로우콜 +1)을 굽는다 — 본체는 <see cref="WLShoreSkirt"/>.
/// </summary>
public static void SpawnShoreSkirt(WLIslandLookSettings cfg, Scene scene)
{
var s = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
if (s == null)
{
var go = new GameObject(WLShoreSkirt.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
s = go.AddComponent<WLShoreSkirt>();
s.cfg = cfg;
s.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
}
else { s.cfg = cfg; s.Rebuild(); }
ShoreSkirtSpawned = true;
}
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
{
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
if (f == null)
{
var go = new GameObject(WLShoreFoam.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
f = go.AddComponent<WLShoreFoam>();
f.cfg = cfg;
f.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
}
else { f.cfg = cfg; f.Rebuild(); }
ShoreFoamSpawned = true;
}
// ─────────────────────────────────────────────────────────────────
// ① 머티리얼 — 원본을 **읽기만** 하고 렌더러의 sharedMaterials 만 바꾼다
// ─────────────────────────────────────────────────────────────────
public static void SwapMaterials(WLIslandLookSettings cfg, Scene scene)
{
if (cfg.materialRemap == null || cfg.materialRemap.Length == 0) return;
var rends = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
for (int i = 0; i < rends.Length; i++)
{
var r = rends[i];
if (r == null || r.gameObject.scene != scene) continue;
if (s_done.Contains(r)) continue;
if (r is ParticleSystemRenderer) continue;
// 🔴 농사 타일은 코드가 `material.color` 로 직접 색을 칠한다(Soil.Initialize).
// Toon 셰이더에는 그 프로퍼티가 없어 갈아끼우면 밭 색이 죽는다 → 원본 유지.
if (cfg.skipSoilRenderers != 0 && r.GetComponent<FISoil>() != null) { s_done.Add(r); continue; }
bool isIslandBody = r.GetComponent<FIIsland>() != null;
var mats = r.sharedMaterials;
bool changed = false;
for (int m = 0; m < mats.Length; m++)
{
var src = mats[m];
if (src == null) continue;
Material dst = null;
if (isIslandBody && cfg.islandTopMaterial != null) dst = cfg.islandTopMaterial;
else dst = Lookup(cfg, src);
if (dst == null || dst == src) continue;
mats[m] = dst; changed = true; SwappedSlots++;
}
if (changed) { r.sharedMaterials = mats; SwappedRenderers++; }
s_done.Add(r);
}
}
static readonly int OutlineProp = Shader.PropertyToID("_OUTLINESENABLED");
/// <summary>
/// 816ze — 섬 타일 셰이더의 외곽선 패스 스위치.
/// 데모 `Terrain.mat` 에는 외곽선 프로퍼티가 없으므로 전사본(`Farm_IslandTop_Demo3`)은
/// `_OUTLINESENABLED 0` 으로 구워져 있다 = 기본값(0)에서는 **아무 것도 하지 않는다**
/// (공유 에셋을 런타임에 건드리지 않는다 = 에디터에서 에셋이 더러워지지 않는다).
/// 1 로 두었을 때만 되돌리기용으로 키워드를 다시 켠다.
/// </summary>
static void ApplyIslandOutline(WLIslandLookSettings cfg)
{
if (cfg.islandOutlineEnabled == 0) return; // 기본 경로 = 무동작
var m = cfg.islandTopMaterial;
if (m == null || !m.HasProperty(OutlineProp)) return;
m.SetFloat(OutlineProp, 1f);
m.EnableKeyword("_OUTLINESENABLED");
}
/// <summary>
/// 🔴 816h — 섬 둘레의 바다(`Water` · 1000×1000 평면)를 **WL 워터 셰이더**로.
/// `Assets/FarmingIsland/**` 는 0줄 — 렌더러의 `sharedMaterials` 만 런타임에 바꾼다.
/// `waterMode = 0` 이면 이 판정 자체를 건너뛰어 지금(FarmingIsland 물)으로 100 % 돌아간다.
/// </summary>
static bool IsWater(WLIslandLookSettings cfg, Material src)
{
if (src == null) return false;
if (cfg.waterFrom != null && src == cfg.waterFrom) return true;
if (src.shader != null && src.shader.name == "Shader Graphs/Simple Water") return true;
// 이미 우리 복사본(Farm_SimpleWater_Demo)으로 바뀐 뒤 다시 훑는 경우
return src.name.StartsWith("Farm_SimpleWater");
}
public static int WaterSwapped;
static Material Lookup(WLIslandLookSettings cfg, Material src)
{
if (cfg.waterMode != 0 && cfg.waterTo != null && IsWater(cfg, src))
{
if (src != cfg.waterTo) WaterSwapped++;
return cfg.waterTo;
}
for (int i = 0; i < cfg.materialRemap.Length; i++)
{
var e = cfg.materialRemap[i];
if (e == null || e.enabled_ == 0 || e.from == null || e.to == null) continue;
if (e.from == src) return e.to;
// 런타임 인스턴스("Palette (Instance)")도 잡는다
if (src.name.StartsWith(e.from.name) && src.shader == e.from.shader) return e.to;
}
return null;
}
// ─────────────────────────────────────────────────────────────────
// ② 조명 · 앰비언트 · 스카이박스 (816a 실측 = 데모/아레나 값)
// ─────────────────────────────────────────────────────────────────
public static void ApplyLighting(WLIslandLookSettings cfg, Scene scene)
{
// 🔴 816h — 하늘(앰비언트·스카이박스·안개·반사)은 **활성 씬(InGame)** 소유라
// 여기서 그냥 쓰면 섬에서 나간 뒤 전투 맵·로비까지 그대로 남는다.
// 그래서 `WLSkyScope` 가 **스냅샷 → 적용 → 나갈 때 원복** 을 맡는다.
// (값은 그대로 이 SO 것 = 아레나/데모 값. 새 값 0)
if (cfg.skyScopeEnabled != 0) WLSkyScope.Acquire(cfg, scene.name);
else
{
RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientSkyColor = cfg.ambientSky;
RenderSettings.ambientEquatorColor = cfg.ambientEquator;
RenderSettings.ambientGroundColor = cfg.ambientGround;
RenderSettings.ambientLight = cfg.ambientSky;
RenderSettings.ambientIntensity = cfg.ambientIntensity;
if (cfg.fogOff != 0) RenderSettings.fog = false;
if (cfg.skybox != null) RenderSettings.skybox = cfg.skybox;
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
}
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
int suns = 0;
for (int i = 0; i < lights.Length; i++)
{
var l = lights[i];
if (l == null || l.gameObject.scene != scene) continue;
if (l.type != LightType.Directional || !l.enabled) continue;
ApplySun(cfg, l);
suns++;
}
// 🔴 2026-09-16 PD 「자연스러운 광원(태양)을 배치」 — 섬 씬에 켜진 태양이 없을 때만 하나 만든다(있으면 그 값만 맞춘다 · 씬 파일 0줄).
if (suns == 0 && cfg.dirLightEnsure != 0)
{
var go = new GameObject("~WL_Sun");
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(go, scene);
go.transform.rotation = Quaternion.Euler(50f, 330f, 0f); // 데모 Demo.unity 의 태양 회전
var l = go.AddComponent<Light>();
l.type = LightType.Directional;
l.shadows = LightShadows.Soft;
ApplySun(cfg, l);
SunCreated++;
}
}
public static int SunCreated;
static void ApplySun(WLIslandLookSettings cfg, Light l)
{
l.color = cfg.dirLightColor;
l.intensity = cfg.dirLightIntensity;
if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft;
if (cfg.dirLightShadowStrength >= 0f) l.shadowStrength = Mathf.Clamp01(cfg.dirLightShadowStrength);
RenderSettings.sun = l;
LightingApplied++;
}
// ─────────────────────────────────────────────────────────────────
// ②-b 밭(Soil) 칸 표시 — **기능은 그대로 두고 색만** 데모 톤으로 (816f · PD 지시)
// FI `Farm` 의 4색(마름1·마름2·젖음1·젖음2)을 런타임에 낮추고
// `Soil.Initialize(farm)`(public) 를 다시 불러 칠만 갱신한다.
// → 물주기 피드백(마름↔젖음)·칸 구분은 전부 살아 있다. `Assets/FarmingIsland/**` 0줄.
// ─────────────────────────────────────────────────────────────────
static readonly string[] s_soilFields = { "soilDryColor1", "soilDryColor2", "soilWetColor1", "soilWetColor2" };
static readonly HashSet<FIFarm> s_tonedFarms = new HashSet<FIFarm>();
public static int SoilFarmsToned, SoilTilesRepainted;
public static void ToneSoil(WLIslandLookSettings cfg, Scene scene)
{
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
var t = typeof(FIFarm);
for (int i = 0; i < farms.Length; i++)
{
var farm = farms[i];
if (farm == null || farm.gameObject.scene != scene) continue;
if (s_tonedFarms.Contains(farm)) continue;
// 1) 마름1/마름2 · 젖음1/젖음2 각각의 평균으로 모으고(대비 축소) 틴트를 곱한다
var vals = new Color[4];
var fis = new System.Reflection.FieldInfo[4];
bool ok = true;
for (int k = 0; k < 4; k++)
{
fis[k] = t.GetField(s_soilFields[k],
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (fis[k] == null) { ok = false; break; }
vals[k] = (Color)fis[k].GetValue(farm);
}
if (!ok) continue;
// 「마름」 중점 밝기 = 기준. 젖음은 이 비를 그대로 유지한다(물주기 피드백 보존).
float dryLum = SoilLum((vals[0] + vals[1]) * 0.5f);
Apply(cfg, fis, farm, vals, 0, 1, dryLum); // 마름 한 쌍
Apply(cfg, fis, farm, vals, 2, 3, dryLum); // 젖음 한 쌍
s_tonedFarms.Add(farm);
SoilFarmsToned++;
// 2) 이미 칠해진 타일을 다시 칠한다 — FI 의 public 진입점 그대로
var soils = farm.GetComponentsInChildren<FISoil>(true);
for (int s = 0; s < soils.Length; s++)
{
if (soils[s] == null) continue;
try { soils[s].Initialize(farm); SoilTilesRepainted++; } catch { }
}
}
}
static void Apply(WLIslandLookSettings cfg, System.Reflection.FieldInfo[] fis, FIFarm farm, Color[] v, int a, int b, float dryLum)
{
var mid = (v[a] + v[b]) * 0.5f;
float c = cfg.soilCheckerContrast;
var ca = Mul(Color.Lerp(mid, v[a], c), cfg.soilTint);
var cb = Mul(Color.Lerp(mid, v[b], c), cfg.soilTint);
// 816r — 목표 흙색으로 통째로 옮긴다. 칸 차이(camid, cbmid)와 마름↔젖음 밝기비는 보존.
if (cfg.soilTargetEnabled != 0 && dryLum > 0.0001f)
{
var baseMid = Mul(mid, cfg.soilTint);
float k = SoilLum(mid) / dryLum; // 마름 = 1 · 젖음 = 젖음/마름
var t = cfg.soilTargetColor * k;
ca = new Color(t.r + (ca.r - baseMid.r), t.g + (ca.g - baseMid.g), t.b + (ca.b - baseMid.b), ca.a);
cb = new Color(t.r + (cb.r - baseMid.r), t.g + (cb.g - baseMid.g), t.b + (cb.b - baseMid.b), cb.a);
}
fis[a].SetValue(farm, ca);
fis[b].SetValue(farm, cb);
}
static Color Mul(Color a, Color b) { return new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a); }
static float SoilLum(Color c) { return 0.299f * c.r + 0.587f * c.g + 0.114f * c.b; }
// ─────────────────────────────────────────────────────────────────
// ③ 아레나와 같은 외곽선·대비·무드 (씬 비의존 — 816a 실측)
// ─────────────────────────────────────────────────────────────────
public static void ApplyReferenceLook(WLIslandLookSettings cfg, Scene scene)
{
var existing = Object.FindFirstObjectByType<WL.Look.Arena.WLReferenceLook>(FindObjectsInactive.Include);
if (existing != null)
{
if (!existing.gameObject.activeSelf) existing.gameObject.SetActive(true);
ReferenceLookApplied = true;
return;
}
var go = new GameObject(ReferenceLookName);
SceneManager.MoveGameObjectToScene(go, scene);
go.AddComponent<WL.Look.Arena.WLReferenceLook>(); // OnEnable 이 적용한다
ReferenceLookApplied = true;
}
// ─────────────────────────────────────────────────────────────────
// ④ 풀밭
// ─────────────────────────────────────────────────────────────────
public static void SpawnGrass(WLIslandLookSettings cfg, Scene scene)
{
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g == null)
{
var go = new GameObject(WLIslandGrass.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
g = go.AddComponent<WLIslandGrass>(); // OnEnable 이 만든다
g.cfg = cfg;
}
else
{
g.cfg = cfg;
g.Rebuild();
}
GrassSpawned = true;
}
// ─────────────────────────────────────────────────────────────────
// ⑤ FI GraphicsManager — Play 중 FI 의 URP 에셋 **파일**을 고친다(816a 실측)
// 우리 활성 URP 가 아니라 화면 영향 0. `Assets/FarmingIsland/**` 0줄을 지키려면 꺼야 한다.
// ─────────────────────────────────────────────────────────────────
static void DisableGraphicsManager(Scene scene)
{
var roots = scene.GetRootGameObjects();
for (int i = 0; i < roots.Length; i++)
{
var comps = roots[i].GetComponentsInChildren<MonoBehaviour>(true);
for (int c = 0; c < comps.Length; c++)
{
if (comps[c] == null) continue;
if (comps[c].GetType().Name != "GraphicsManager") continue;
comps[c].enabled = false;
comps[c].gameObject.SetActive(false);
}
}
}
}
/// <summary>코루틴 숙주. 씬을 넘어 살아남는다.</summary>
public sealed class WLIslandLookRunner : MonoBehaviour { }
}