// WL-816h — ④ PD 경로에서 적용 확인 · 물 3단 비교 · 성능 전/후 · 🔴 맵별 하늘 격리 // 전부 Play 중 실측. 🔴 Play 중에 에셋(SO·머티리얼)을 고치지 않는다(§9) — 렌더러만 바꾼다. using System.Collections; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.SceneManagement; public static class WL816h_Verify { public const string Dir = "Screenshots_WL/WL816h/"; public const int W = 1080, H = 1920; public static void Open() { UnityEditor.SceneManagement.EditorSceneManager.OpenScene( "Assets/Scenes/InGame.unity", UnityEditor.SceneManagement.OpenSceneMode.Single); Debug.Log("[816h] InGame 열림"); } public static void Start() { var go = GameObject.Find("~WL816hVerify"); if (go != null) Object.DestroyImmediate(go); go = new GameObject("~WL816hVerify"); go.AddComponent(); Debug.Log("[816h] verify 러너 시작"); } // ── 공용 ──────────────────────────────────────────────────────────── public static string Sky() { return "amb=" + RenderSettings.ambientMode + " light=" + F(RenderSettings.ambientLight) + " skySrc=" + F(RenderSettings.ambientSkyColor) + " I=" + RenderSettings.ambientIntensity + " skybox=" + (RenderSettings.skybox ? RenderSettings.skybox.name : "없음") + " fog=" + RenderSettings.fog + " refl=" + RenderSettings.defaultReflectionMode + "/" + RenderSettings.reflectionIntensity + " sun=" + (RenderSettings.sun ? RenderSettings.sun.name : "없음"); } public static string F(Color c) { return "(" + c.r.ToString("F3") + "," + c.g.ToString("F3") + "," + c.b.ToString("F3") + ")"; } public static Camera Cam(string name, Vector3 pos, Vector3 euler, float fov, CameraClearFlags clear) { var go = GameObject.Find(name); if (go == null) { go = new GameObject(name); go.hideFlags = HideFlags.DontSave; } var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.orthographic = false; c.fieldOfView = fov; c.nearClipPlane = 0.3f; c.farClipPlane = 2000f; c.clearFlags = clear; c.cullingMask = ~0; c.depth = -100f; go.transform.position = pos; go.transform.rotation = Quaternion.Euler(euler); var ac = go.GetComponent(); if (ac == null) ac = go.AddComponent(); ac.renderPostProcessing = true; // 아레나 무드가 걸린 화면 그대로 return c; } /// 섬 가장자리 바깥 — 바다가 화면의 절반을 차지한다. public static Camera WaterCam() { return Cam("~WL816hWaterCam", new Vector3(-16f, 5.5f, -16f), new Vector3(10f, 45f, 0f), 45f, CameraClearFlags.Skybox); } /// 수평선이 화면에 들어오는 각도 — 하늘과 바다가 함께 보인다. public static Camera HorizonCam() { return Cam("~WL816hHorizonCam", new Vector3(-14f, 8f, -14f), new Vector3(4f, 45f, 0f), 55f, CameraClearFlags.Skybox); } public static Camera SkyCam() { return Cam("~WL816hSkyCam3", new Vector3(0f, 40f, 0f), new Vector3(-16f, 30f, 0f), 50f, CameraClearFlags.Skybox); } public static Texture2D Grab(Camera cam) { var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.Create(); var pT = cam.targetTexture; var pA = RenderTexture.active; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; var tex = new Texture2D(W, H, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, W, H), 0, 0); tex.Apply(); RenderTexture.active = pA; cam.targetTexture = pT; rt.Release(); Object.DestroyImmediate(rt); return tex; } public static string Shot(Camera cam, string path) { if (cam == null) return "카메라 없음"; System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); var tex = Grab(cam); System.IO.File.WriteAllBytes(path, tex.EncodeToPNG()); string s = "상10%=" + Band(tex, 0.90f, 1.00f) + " 중=" + Band(tex, 0.45f, 0.55f) + " 하10%=" + Band(tex, 0f, 0.10f); Object.DestroyImmediate(tex); return s + " → " + path; } public static string Band(Texture2D t, float y0, float y1) { int a = Mathf.RoundToInt(y0 * (t.height - 1)), b = Mathf.RoundToInt(y1 * (t.height - 1)); var px = t.GetPixels32(); long r = 0, g = 0, bl = 0; int n = 0; for (int y = a; y < b; y += 3) for (int x = 0; x < t.width; x += 3) { var c = px[y * t.width + x]; r += c.r; g += c.g; bl += c.b; n++; } if (n == 0) return "-"; return "#" + ((int)(r / n)).ToString("X2") + ((int)(g / n)).ToString("X2") + ((int)(bl / n)).ToString("X2"); } /// 같은 카메라로 60프레임 오프스크린 렌더 — ms/frame. public static float Ms(Camera cam, int frames) { var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.Create(); var pT = cam.targetTexture; cam.targetTexture = rt; for (int i = 0; i < 10; i++) cam.Render(); // 워밍업 var sw = System.Diagnostics.Stopwatch.StartNew(); for (int i = 0; i < frames; i++) cam.Render(); sw.Stop(); cam.targetTexture = pT; rt.Release(); Object.DestroyImmediate(rt); return (float)sw.Elapsed.TotalMilliseconds / frames; } /// 좌/우 두 장을 한 장으로 (PD 판단용). public static void Pair(string left, string right, string outPath) { var a = Load(left); var b = Load(right); if (a == null || b == null) return; int w = a.width + b.width, h = Mathf.Max(a.height, b.height); var o = new Texture2D(w, h, TextureFormat.RGB24, false); var fill = new Color32[w * h]; for (int i = 0; i < fill.Length; i++) fill[i] = new Color32(24, 24, 28, 255); o.SetPixels32(fill); o.SetPixels32(0, 0, a.width, a.height, a.GetPixels32()); o.SetPixels32(a.width, 0, b.width, b.height, b.GetPixels32()); o.Apply(); System.IO.File.WriteAllBytes(outPath, o.EncodeToPNG()); Object.DestroyImmediate(o); Object.DestroyImmediate(a); Object.DestroyImmediate(b); } /// 여러 장을 가로로 이어 붙인다(값 3단 비교용). public static void Strip(string[] paths, string outPath) { var ts = new Texture2D[paths.Length]; int w = 0, h = 0; for (int i = 0; i < paths.Length; i++) { ts[i] = Load(paths[i]); if (ts[i] == null) return; w += ts[i].width; h = Mathf.Max(h, ts[i].height); } var o = new Texture2D(w, h, TextureFormat.RGB24, false); var fill = new Color32[w * h]; for (int i = 0; i < fill.Length; i++) fill[i] = new Color32(24, 24, 28, 255); o.SetPixels32(fill); int x = 0; for (int i = 0; i < ts.Length; i++) { o.SetPixels32(x, 0, ts[i].width, ts[i].height, ts[i].GetPixels32()); x += ts[i].width; } o.Apply(); System.IO.File.WriteAllBytes(outPath, o.EncodeToPNG()); Object.DestroyImmediate(o); for (int i = 0; i < ts.Length; i++) Object.DestroyImmediate(ts[i]); } static Texture2D Load(string p) { if (!System.IO.File.Exists(p)) return null; var t = new Texture2D(2, 2, TextureFormat.RGB24, false); t.LoadImage(System.IO.File.ReadAllBytes(p)); return t; } } public class WL816h_VerifyRunner : MonoBehaviour { static System.Text.StringBuilder sb; static void L(string s) { sb.AppendLine(s); } const string MatDir = "Assets/WL/Look/Farm/Materials/"; void Start() { StartCoroutine(Co()); } IEnumerator Co() { sb = new System.Text.StringBuilder(); L("=== WL-816h 적용 확인 (PD 경로 = InGame 활성 + Level01 Additive) ==="); // 활성 씬(InGame)의 **손 안 댄** 하늘 = 나중에 「원래대로」 판정 기준 string pristine = WL816h_Verify.Sky(); L("0) InGame 원래 하늘 : " + pristine); bool viaGame = false; try { if (InGameInfo.Ins != null) { InGameInfo.Ins.Load_Map(900); viaGame = true; } } catch (System.Exception) { } L(" Load_Map(900) = " + (viaGame ? "성공" : "🔴 로그인 없이는 못 탄다 → LoadSceneAsync(Level01, Additive) 로 대체(816f 와 같음)")); if (!viaGame) { var op = SceneManager.LoadSceneAsync("Level01", LoadSceneMode.Additive); while (op != null && !op.isDone) yield return null; } for (int i = 0; i < 7; i++) yield return new WaitForSeconds(1f); // ── 1) 훅이 돌았나 ──────────────────────────────────────────── L(""); L("1) 섬 진입 — 활성씬=" + SceneManager.GetActiveScene().name + " sceneCount=" + SceneManager.sceneCount); L(" IslandLook 렌더러 " + WL.Look.Farm.WLIslandLook.SwappedRenderers + " · 물 교체 " + WL.Look.Farm.WLIslandLook.WaterSwapped + " · refLook=" + WL.Look.Farm.WLIslandLook.ReferenceLookApplied); L(" 🔴 SkyScope held=" + WL.Look.Farm.WLSkyScope.Held + " by=" + WL.Look.Farm.WLSkyScope.HeldBy + " acquires=" + WL.Look.Farm.WLSkyScope.Acquires + " restores=" + WL.Look.Farm.WLSkyScope.Restores); L(" 섬 하늘 = " + WL816h_Verify.Sky()); Renderer water = null; foreach (var r in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) if (r != null && r.gameObject.scene.name == "Level01" && r.name == "Water") water = r; L(" 물 렌더러 = " + (water == null ? "🔴 없음" : water.name + " mat=" + water.sharedMaterial.name + " shader=" + water.sharedMaterial.shader.name + " queue=" + water.sharedMaterial.renderQueue)); Camera live = null; foreach (var c in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { if (!c.gameObject.activeInHierarchy || !c.enabled || c.targetTexture != null) continue; if (c.name.StartsWith("~WL816h")) continue; if (live == null || c.depth > live.depth) live = c; } L(" 보이는 카메라 = " + (live == null ? "없음" : live.name + "(" + live.gameObject.scene.name + ")")); // ── 2) 물 3단 비교 + 성능 (같은 카메라 · 같은 프레임 구간) ──── L(""); L("2) 물 값 3단 비교 + 성능 (같은 카메라 1080×1920 · 60프레임 오프스크린)"); string[] names = { null, "Farm_Water_WL_A", "Farm_Water_WL_B", "Farm_Water_WL_C" }; string[] labels = { "지금(FI SimpleWater)", "A=WL_Water_Ocean 원본", "B=데모 톤", "C=차분" }; string[] liveShots = new string[4]; string[] horizonShots = new string[4]; var fiMat = water != null ? water.sharedMaterial : null; // 지금 걸려 있는 것(= WL 워터 B) // 「지금」 = FarmingIsland 물(우리 복사본 Farm_SimpleWater_Demo) Material now = UnityEditor.AssetDatabase.LoadAssetAtPath(MatDir + "Farm_SimpleWater_Demo.mat"); for (int i = 0; i < names.Length; i++) { Material m = names[i] == null ? now : UnityEditor.AssetDatabase.LoadAssetAtPath(MatDir + names[i] + ".mat"); if (m == null) { L(" 🔴 머티리얼 없음 " + (names[i] ?? "Farm_SimpleWater_Demo")); continue; } if (water != null) water.sharedMaterial = m; yield return null; yield return null; yield return new WaitForSeconds(0.4f); string tag = names[i] == null ? "now" : names[i].Substring(names[i].Length - 1).ToLower(); liveShots[i] = WL816h_Verify.Dir + "w_" + i + "_" + tag + "_live.png"; horizonShots[i] = WL816h_Verify.Dir + "w_" + i + "_" + tag + "_horizon.png"; string s1 = WL816h_Verify.Shot(live, liveShots[i]); string s2 = WL816h_Verify.Shot(WL816h_Verify.HorizonCam(), horizonShots[i]); float ms = WL816h_Verify.Ms(live, 60); L(" [" + labels[i] + "] " + ms.ToString("F3") + " ms/frame"); L(" 게임화면 " + s1); L(" 수평선 " + s2); } // 채택값으로 되돌린다(SO 가 가리키는 것) var cfg = WL.Look.Farm.WLIslandLookSettings.Instance; if (water != null && cfg != null && cfg.waterTo != null) water.sharedMaterial = cfg.waterTo; yield return null; L(" → 채택 = " + (cfg != null && cfg.waterTo != null ? cfg.waterTo.name : "?")); // ── 2-b) 성능 전/후 — 순서 바꿔 두 번씩 (측정 순서 편향 제거) ── L(""); L("2-b) 성능 전/후 (같은 카메라 · 60프레임 × 2회 · 순서 교대)"); var adopted = cfg != null ? cfg.waterTo : null; for (int round = 0; round < 2; round++) { foreach (var pair in new[] { new object[] { "전(FI SimpleWater)", now }, new object[] { "후(" + (adopted ? adopted.name : "?") + ")", adopted } }) { var m = pair[1] as Material; if (m == null) continue; if (water != null) water.sharedMaterial = m; yield return null; yield return null; L(" " + round + "회차 " + (string)pair[0] + " = " + WL816h_Verify.Ms(live, 60).ToString("F3") + " ms/frame"); } } if (water != null && adopted != null) water.sharedMaterial = adopted; yield return null; WL816h_Verify.Strip(liveShots, WL816h_Verify.Dir + "e_water_steps.png"); WL816h_Verify.Strip(horizonShots, WL816h_Verify.Dir + "e_water_steps_horizon.png"); L(" 3단 비교 한 장 = " + WL816h_Verify.Dir + "e_water_steps.png · e_water_steps_horizon.png"); // ── 3) 적용 후 대표 캡처 ────────────────────────────────────── L(""); L("3) 적용 후"); L(" 섬 게임화면 = " + WL816h_Verify.Shot(live, WL816h_Verify.Dir + "b_after_island.png")); L(" 수평선 = " + WL816h_Verify.Shot(WL816h_Verify.HorizonCam(), WL816h_Verify.Dir + "b_after_horizon.png")); L(" 물 근접 = " + WL816h_Verify.Shot(WL816h_Verify.WaterCam(), WL816h_Verify.Dir + "b_after_water.png")); L(" 하늘만 = " + WL816h_Verify.Shot(WL816h_Verify.SkyCam(), WL816h_Verify.Dir + "b_after_sky.png")); WL816h_Verify.Pair(WL816h_Verify.Dir + "a_now_island.png", WL816h_Verify.Dir + "b_after_island.png", WL816h_Verify.Dir + "c_side_by_side.png"); L(" 좌우 비교 = " + WL816h_Verify.Dir + "c_side_by_side.png (좌 지금 / 우 적용 후)"); // ── 4) 🔴 맵별 하늘 격리 ────────────────────────────────────── L(""); L("4) 🔴 맵별 하늘 격리 — 섬 → 던전 → 전투 맵"); L(" [섬] " + WL816h_Verify.Sky()); string islandSky = WL816h_Verify.Shot(WL816h_Verify.SkyCam(), WL816h_Verify.Dir + "f1_island_sky.png"); L(" [섬] 하늘 = " + islandSky); // 섬 → 던전 (같은 하늘이어야 한다) var u1 = SceneManager.UnloadSceneAsync("Level01"); while (u1 != null && !u1.isDone) yield return null; var d1 = SceneManager.LoadSceneAsync("WL_Dungeon01", LoadSceneMode.Additive); while (d1 != null && !d1.isDone) yield return null; yield return new WaitForSeconds(2f); L(" [던전] SkyScope held=" + WL.Look.Farm.WLSkyScope.Held + " by=" + WL.Look.Farm.WLSkyScope.HeldBy + " acquires=" + WL.Look.Farm.WLSkyScope.Acquires + " restores=" + WL.Look.Farm.WLSkyScope.Restores); L(" [던전] " + WL816h_Verify.Sky()); L(" [던전] 하늘 = " + WL816h_Verify.Shot(WL816h_Verify.SkyCam(), WL816h_Verify.Dir + "f2_dungeon_sky.png")); // 던전 → 전투 맵 (원래대로여야 한다) var u2 = SceneManager.UnloadSceneAsync("WL_Dungeon01"); while (u2 != null && !u2.isDone) yield return null; var b1 = SceneManager.LoadSceneAsync("Map_C01", LoadSceneMode.Additive); while (b1 != null && !b1.isDone) yield return null; yield return new WaitForSeconds(2f); L(" [전투] SkyScope held=" + WL.Look.Farm.WLSkyScope.Held + " acquires=" + WL.Look.Farm.WLSkyScope.Acquires + " restores=" + WL.Look.Farm.WLSkyScope.Restores); string after = WL816h_Verify.Sky(); L(" [전투] " + after); L(" [전투] 하늘 = " + WL816h_Verify.Shot(WL816h_Verify.SkyCam(), WL816h_Verify.Dir + "f3_battle_sky.png")); L(" 🔴 원래대로인가 = " + (after == pristine ? "예 — 글자 하나까지 같다(오염 0)" : "🔴 아니오\n 전 : " + pristine + "\n 후 : " + after)); WL816h_Verify.Strip(new string[] { WL816h_Verify.Dir + "f1_island_sky.png", WL816h_Verify.Dir + "f2_dungeon_sky.png", WL816h_Verify.Dir + "f3_battle_sky.png" }, WL816h_Verify.Dir + "f_sky_isolation.png"); L(" 한 장 = " + WL816h_Verify.Dir + "f_sky_isolation.png (좌 섬 / 중 던전 / 우 전투 맵)"); System.IO.File.WriteAllText("AgentScripts/WL816h_VERIFY.txt", sb.ToString()); Debug.Log("[816h verify 완료]\n" + sb); } }