// WL-816o — 구름 그림자 경계 배치: 전(816n) / 후 실측 + CPU 포팅 검증. // 에셋은 건드리지 않는다 (cfg 는 Instantiate 복제본 · 816f/816n 방식). public static class WL816o_Play { public static void Start() { var go = UnityEngine.GameObject.Find("~WL816oPlay"); if (go != null) UnityEngine.Object.DestroyImmediate(go); go = new UnityEngine.GameObject("~WL816oPlay"); go.AddComponent(); } } public class WL816o_PlayRunner : UnityEngine.MonoBehaviour { static System.Text.StringBuilder sb; const int W = 1080, H = 1920; const int TW = 512; 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 < 7; i++) yield return new UnityEngine.WaitForSeconds(1f); var g = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); if (g == null) { L("🔴 WLIslandGrass 없음 — 중단"); Flush(); yield break; } var src = WL.Look.Farm.WLIslandLookSettings.Instance; var rt = UnityEngine.Object.Instantiate(src); // 에셋 무변경 g.cfg = rt; var mat = rt.cloudEdgeSourceMaterial != null ? rt.cloudEdgeSourceMaterial : rt.islandTopMaterial; L("바닥 머티리얼 = " + (mat ? mat.name : "-") + " · 셰이더 " + (mat && mat.shader ? mat.shader.name : "-")); L("SO 값 : cell=" + rt.cloudEdgeCell + " maxCells=" + rt.cloudEdgeMaxCells + " dilate=" + rt.cloudEdgeDilate + " driftCover=" + rt.cloudEdgeDriftCover + " refresh=" + rt.cloudEdgeRefreshSeconds + "s densityScale=" + rt.cloudEdgeDensityScale); var cam = MakeCam(); L("카메라 " + cam.transform.position + " euler=" + cam.transform.eulerAngles + " fov=" + cam.fieldOfView + " (타깃까지 12 m · 45°)"); // ── BEFORE (816n 상태 = 균일 격자) ───────────────────────────── rt.cloudEdgeEnabled = 0; g.Rebuild(); yield return null; yield return null; L(Shot(cam, "a_before", "BEFORE 816n(균일 격자)")); L(" " + Stat() + " · " + Bench(cam, 60).ToString("F3") + " ms/frame"); int bInst = WL.Look.Farm.WLIslandGrass.Instances; long bTri = WL.Look.Farm.WLIslandGrass.Triangles; int bDraw = WL.Look.Farm.WLIslandGrass.DrawnConfigs; // ── AFTER (구름 경계 배치) ───────────────────────────────────── rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null; yield return null; L(Shot(cam, "b_after", "AFTER 816o(구름 경계 배치)")); L(" " + Stat() + " · " + Bench(cam, 60).ToString("F3") + " ms/frame"); int aInst = WL.Look.Farm.WLIslandGrass.Instances; long aTri = WL.Look.Farm.WLIslandGrass.Triangles; int aDraw = WL.Look.Farm.WLIslandGrass.DrawnConfigs; L(" 로그: " + WL.Look.Farm.WLIslandGrass.LastLog); // 교대 2회 (측정 노이즈 확인) rt.cloudEdgeEnabled = 0; g.Rebuild(); yield return null; float b2 = Bench(cam, 60); rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null; float a2 = Bench(cam, 60); L("[교대 2회] before=" + b2.ToString("F3") + " ms · after=" + a2.ToString("F3") + " ms"); L("[예산] 인스턴스 " + bInst + " → " + aInst + " (" + Pct(aInst, bInst) + ")" + " · 삼각형 " + bTri + " → " + aTri + " (" + Pct((int)aTri, (int)bTri) + ")" + " · 드로우콜 +" + bDraw + " → +" + aDraw + " · 후보 " + WL.Look.Farm.WLIslandGrass.Candidates); // 주기 갱신 비용 var sw = System.Diagnostics.Stopwatch.StartNew(); g.RefreshCloudEdges(); sw.Stop(); L("[주기 갱신] RefreshCloudEdges 1회 = " + sw.Elapsed.TotalMilliseconds.ToString("F2") + " ms" + " (굽기 " + WL.Look.Farm.WLIslandGrass.BakeMs.ToString("F2") + " · 고르기 " + WL.Look.Farm.WLIslandGrass.FilterMs.ToString("F2") + ") → 인스턴스 " + WL.Look.Farm.WLIslandGrass.Instances); // ── CPU 포팅 검증 (셰이더가 그린 띠 경계 ↔ CPU 가 계산한 띠 경계) ── L(Verify(g, rt, mat)); // 최종 상태 = 구름 경계 ON rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null; Shot(cam, "b_after", "final"); TopShot(g, "t_after"); rt.cloudEdgeEnabled = 0; g.Rebuild(); yield return null; TopShot(g, "t_before"); rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null; Flush(); } // ─────────────────────────────────────────── 검증 static string Verify(WL.Look.Farm.WLIslandGrass g, WL.Look.Farm.WLIslandLookSettings rt, UnityEngine.Material mat) { var f = new WL.Look.Farm.WLCloudShadowField(); if (!f.Configure(mat)) return "[검증] 실패 — " + f.Why; 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, y = b.center.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(); // 픽셀 ↔ 월드 (ortho · Euler(90,0,0) → screen +x = world +x, screen +y = world +z) var band = new int[TW * TW]; var ok = new bool[TW * TW]; for (int iy = 0; iy < TW; iy++) { float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size; for (int ix = 0; ix < TW; ix++) { float wx = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size; int i = iy * TW + ix; band[i] = f.Band(wx, y, wz); var p = px[i]; ok[i] = (p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60); // 풀밭 초록만 } } // CPU 가 「여기가 경계」라고 한 곳에 화면에도 색 계단이 있는가 (이 방향이 포팅 검증이다) int cpuEdges = 0, cpuHit = 0, flatTot = 0, flatHit = 0; for (int iy = 1; iy < TW - 1; iy++) { for (int ix = 3; ix < TW - 4; ix++) { int i = iy * TW + ix; if (!ok[i - 3] || !ok[i + 4]) continue; if (band[i] != band[i + 1]) { cpuEdges++; if (Stepy(px, i - 3, 7)) cpuHit++; } else if (band[i - 3] == band[i] && band[i] == band[i + 4]) { flatTot++; // CPU 가 평평하다고 한 곳 if (Stepy(px, i - 3, 7)) flatHit++; // 그런데 화면에 계단이 있으면 오차 } } } UnityEngine.Object.DestroyImmediate(tex); string s = "[검증] CPU 가 띠 경계라 한 " + cpuEdges + "곳 중 화면에도 색 계단이 있는 것 = " + (cpuEdges > 0 ? (100f * cpuHit / cpuEdges).ToString("F1") : "0") + "%" + " · CPU 가 평평하다 한 " + flatTot + "곳 중 화면에 계단이 있는 것 = " + (flatTot > 0 ? (100f * flatHit / flatTot).ToString("F1") : "0") + "% (낮을수록 좋다 · 타일·소품 경계 포함)"; s += "\n (_Time.y=" + UnityEngine.Shader.GetGlobalVector("_Time").y.ToString("F3") + " · timeSinceLevelLoad=" + UnityEngine.Time.timeSinceLevelLoad.ToString("F3") + " · 띠비율=" + (WL.Look.Farm.WLIslandGrass.EdgeFraction * 100f).ToString("F1") + "%" + " · 칸=" + WL.Look.Farm.WLIslandGrass.EdgeCellSize.ToString("F2") + "m×" + WL.Look.Farm.WLIslandGrass.EdgeCells + " · 노이즈=" + WL.Look.Farm.WLIslandGrass.EdgeSampleCell.ToString("F2") + "m×" + WL.Look.Farm.WLIslandGrass.EdgeSamples + " · 넓힘=" + WL.Look.Farm.WLIslandGrass.EdgeDilate + ")"; return s; } static int Lum(UnityEngine.Color32 p) { return (p.r * 77 + p.g * 150 + p.b * 29) >> 8; } /// i 부터 n 픽셀 안에 6~45 크기의 밝기 계단이 있는가. static bool Stepy(UnityEngine.Color32[] px, int i, int n) { for (int k = 0; k < n - 1; k++) { int d = Lum(px[i + k + 1]) - Lum(px[i + k]); if (d < 0) d = -d; if (d >= 6 && d <= 45) return true; } return false; } static string Pct(int a, int b) { return b <= 0 ? "-" : ((100f * a / b).ToString("F1") + "%"); } static string Stat() { return "인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 삼각형=" + WL.Look.Farm.WLIslandGrass.Triangles + " 드로우콜+" + WL.Look.Farm.WLIslandGrass.DrawnConfigs + " 타일=" + WL.Look.Farm.WLIslandGrass.Tiles; } // ─────────────────────────────────────────── helpers static UnityEngine.Camera MakeCam() { UnityEngine.Vector3 tgt = UnityEngine.Vector3.zero; var pc = UnityEngine.GameObject.FindGameObjectWithTag("Player"); if (pc != null) tgt = pc.transform.position; var go = UnityEngine.GameObject.Find("~816oCam"); if (go == null) go = new UnityEngine.GameObject("~816oCam"); var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.fieldOfView = 60f; c.orthographic = false; c.nearClipPlane = 0.1f; c.farClipPlane = 300f; c.transform.rotation = UnityEngine.Quaternion.Euler(45f, 45f, 0f); c.transform.position = tgt - c.transform.forward * 12f; c.enabled = false; return c; } static UnityEngine.Camera MakeTop(float cx, float cz, float size) { var go = UnityEngine.GameObject.Find("~816oTop"); if (go == null) go = new UnityEngine.GameObject("~816oTop"); 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); return tex; } static void TopShot(WL.Look.Farm.WLIslandGrass g, string name) { var b = g.CalculateInstancesBounds(); float size = UnityEngine.Mathf.Max(b.size.x, b.size.z) * 0.5f; var cam = MakeTop(b.center.x, b.center.z, size); var tex = GrabCam(cam, 768, 768); System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o"); System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/" + name + ".png", UnityEngine.ImageConversion.EncodeToPNG(tex)); UnityEngine.Object.DestroyImmediate(tex); } static string Shot(UnityEngine.Camera cam, string name, string tag) { var tex = GrabCam(cam, W, H); System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o"); System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/" + name + ".png", UnityEngine.ImageConversion.EncodeToPNG(tex)); var px = tex.GetPixels32(); long r = 0, gg = 0, bb = 0; int n = 0; 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) { r += p.r; gg += p.g; bb += p.b; n++; } } UnityEngine.Object.DestroyImmediate(tex); if (n == 0) return tag + " : 초록 픽셀 0"; return string.Format("{0} : 풀밭 초록 픽셀 {1} ({2:F1}%) 평균 #{3:X2}{4:X2}{5:X2}", tag, n, 100f * n / px.Length, r / n, gg / n, bb / n); } static float Bench(UnityEngine.Camera cam, int frames) { var rt = new UnityEngine.RenderTexture(W, H, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB); rt.Create(); 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 = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); return (float)sw.Elapsed.TotalMilliseconds / frames; } static void L(string s) { sb.AppendLine(s); } static void Flush() { System.IO.File.WriteAllText("AgentScripts/WL816o_PLAY.txt", sb.ToString()); UnityEngine.Debug.Log("[816o play]\n" + sb.ToString()); } }