// WL-816o 진단 — 화면에 그려진 구름 띠 경계 ↔ CPU 가 계산한 띠 경계가 왜 어긋나는가. public static class WL816o_Diag { public static void Start() { var go = UnityEngine.GameObject.Find("~WL816oDiag"); if (go != null) UnityEngine.Object.DestroyImmediate(go); go = new UnityEngine.GameObject("~WL816oDiag"); go.AddComponent(); } } public class WL816o_DiagRunner : UnityEngine.MonoBehaviour { const int TW = 512; static System.Text.StringBuilder sb; void Start() { StartCoroutine(Co()); } System.Collections.IEnumerator Co() { sb = new System.Text.StringBuilder(); if (UnityEngine.SceneManagement.SceneManager.sceneCount < 2) { var op = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Level01", UnityEngine.SceneManagement.LoadSceneMode.Additive); while (op != null && !op.isDone) yield return null; } for (int i = 0; i < 6; i++) yield return new UnityEngine.WaitForSeconds(1f); var g = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); var cfg = WL.Look.Farm.WLIslandLookSettings.Instance; var mat = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial; // 실제로 화면에 보이는 섬 윗면 렌더러가 무슨 머티리얼을 쓰는가 (런타임 교체 후) var isls = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None); for (int i = 0; i < isls.Length && i < 3; i++) { var r = isls[i].GetComponent(); L("Island[" + i + "] pos=" + isls[i].transform.position + " mats=" + (r ? Names(r) : "-")); } var b = g.CalculateInstancesBounds(); float size = UnityEngine.Mathf.Max(b.size.x, b.size.z) * 0.5f; float cx = b.center.x, cz = b.center.z; float y = isls.Length > 0 ? isls[0].transform.position.y : b.center.y; L("bounds c=" + b.center + " size=" + b.size + " · ortho size=" + size + " · 윗면 y=" + y); bool was = g.enabled; g.enabled = false; var cam = MakeTop(cx, cz, size); var tex = GrabCam(cam, TW, TW); g.enabled = was; var px = tex.GetPixels32(); var f = new WL.Look.Farm.WLCloudShadowField(); if (!f.Configure(mat)) { L("Configure 실패 " + f.Why); Flush(); yield break; } L("field: " + f.Dump()); L("_Time.y(global)=" + UnityEngine.Shader.GetGlobalVector("_Time").y + " timeSinceLevelLoad=" + UnityEngine.Time.timeSinceLevelLoad); // 화면 밝기 계단 확인 (초록 픽셀만) var hist = new System.Collections.Generic.Dictionary(); for (int i = 0; i < px.Length; i++) { var p = px[i]; if (!(p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60)) continue; int l = Lum(p); if (!hist.ContainsKey(l)) hist[l] = 0; hist[l]++; } var keys = new System.Collections.Generic.List(hist.Keys); keys.Sort(); var top = new System.Collections.Generic.List(); for (int i = 0; i < keys.Count; i++) if (hist[keys[i]] > 2000) top.Add(keys[i] + "×" + hist[keys[i]]); L("초록 밝기 계단(2000픽셀 이상) = " + string.Join(" ", top.ToArray()) + " (고유값 " + keys.Count + ")"); // 가운데 줄 — 화면 경계 위치 vs CPU 띠 경계 위치 (월드 X) for (int r = 0; r < 3; r++) { int iy = 128 + r * 128; float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size; var scr = new System.Collections.Generic.List(); var cpu = new System.Collections.Generic.List(); int prevBand = int.MinValue; for (int ix = 1; ix < TW - 1; ix++) { float wx = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size; var A = px[iy * TW + ix - 1]; var B = px[iy * TW + ix]; if (Green(A) && Green(B)) { int d = Lum(B) - Lum(A); if (d < 0) d = -d; if (d >= 6 && d <= 45) scr.Add(wx.ToString("F2")); } int bd = f.Band(wx, y, wz); if (prevBand != int.MinValue && bd != prevBand) cpu.Add(wx.ToString("F2") + "(" + prevBand + "→" + bd + ")"); prevBand = bd; } L("row z=" + wz.ToString("F2") + " 화면경계X=[" + string.Join(" ", scr.ToArray()) + "]"); L(" CPU경계X=[" + string.Join(" ", cpu.ToArray()) + "] cloudiness(x=" + cx.ToString("F1") + ")=" + f.Cloudiness(cx, y, wz).ToString("F4")); } // 시간 오프셋 스캔 — 가장 잘 맞는 Δt 를 찾는다 float t0 = f.TimeValue; string best = ""; float bestScore = -1f, bestDt = 0f; for (float dt = -12f; dt <= 12.01f; dt += 0.5f) { f.TimeValue = t0 + dt; float s = Agree(px, f, cx, cz, size, y); if (s > bestScore) { bestScore = s; bestDt = dt; } if (System.Math.Abs(dt) < 0.01f) best = "Δt=0 일치율 " + (s * 100f).ToString("F1") + "%"; } f.TimeValue = t0; L(best + " · 최적 Δt=" + bestDt.ToString("F1") + " 일치율 " + (bestScore * 100f).ToString("F1") + "%"); UnityEngine.Object.DestroyImmediate(tex); Flush(); } static float Agree(UnityEngine.Color32[] px, WL.Look.Farm.WLCloudShadowField f, float cx, float cz, float size, float y) { int shown = 0, matched = 0; for (int iy = 8; iy < TW - 8; iy += 4) { float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size; for (int ix = 2; ix < TW - 3; ix += 2) { var A = px[iy * TW + ix]; var B = px[iy * TW + ix + 1]; if (!Green(A) || !Green(B)) continue; int d = Lum(B) - Lum(A); if (d < 0) d = -d; if (d < 6 || d > 45) continue; shown++; float w0 = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size; float w1 = cx + (((ix + 3.5f) / TW) - 0.5f) * 2f * size; float wm = cx + (((ix - 1.5f) / TW) - 0.5f) * 2f * size; int b0 = f.Band(wm, y, wz), b1 = f.Band(w1, y, wz); if (b0 != b1) matched++; _ = w0; } } return shown == 0 ? 0f : (float)matched / shown; } static bool Green(UnityEngine.Color32 p) { return p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60; } static int Lum(UnityEngine.Color32 p) { return (p.r * 77 + p.g * 150 + p.b * 29) >> 8; } static string Names(UnityEngine.Renderer r) { var m = r.sharedMaterials; var s = ""; for (int i = 0; i < m.Length; i++) s += (m[i] ? m[i].name : "-") + " "; return s; } static UnityEngine.Camera MakeTop(float cx, float cz, float size) { var go = UnityEngine.GameObject.Find("~816oTopD"); if (go == null) go = new UnityEngine.GameObject("~816oTopD"); var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.orthographic = true; c.orthographicSize = size; c.nearClipPlane = 0.1f; c.farClipPlane = 200f; c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f); c.transform.position = new UnityEngine.Vector3(cx, 40f, cz); c.enabled = false; return c; } static UnityEngine.Texture2D GrabCam(UnityEngine.Camera cam, int w, int h) { var rt = new UnityEngine.RenderTexture(w, h, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB); rt.Create(); cam.targetTexture = rt; cam.Render(); var prev = UnityEngine.RenderTexture.active; UnityEngine.RenderTexture.active = rt; var tex = new UnityEngine.Texture2D(w, h, UnityEngine.TextureFormat.RGB24, false); tex.ReadPixels(new UnityEngine.Rect(0, 0, w, h), 0, 0); tex.Apply(); UnityEngine.RenderTexture.active = prev; cam.targetTexture = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o"); System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/diag_top.png", UnityEngine.ImageConversion.EncodeToPNG(tex)); return tex; } static void L(string s) { sb.AppendLine(s); } static void Flush() { System.IO.File.WriteAllText("AgentScripts/WL816o_DIAG.txt", sb.ToString()); UnityEngine.Debug.Log("[816o diag]\n" + sb.ToString()); } }