// WL-816q 최종 검증 — 전/후 · 예산 · 0/2/4 s 좌표 불변 · 가시 범위(절두체) 팝 0 증명 · 79타일 환산. // 에셋 무변경(런타임 복제본). Debug.LogError 금지. public static class WL816q_Final { public static void Start() { var go = UnityEngine.GameObject.Find("~WL816qFinal"); if (go != null) UnityEngine.Object.DestroyImmediate(go); go = new UnityEngine.GameObject("~WL816qFinal"); go.AddComponent(); } } public class WL816q_FinalRunner : UnityEngine.MonoBehaviour { static System.Text.StringBuilder sb; const int W = 1080, H = 1920, TS = 768; static string Dir = "Screenshots_WL/WL816q"; static readonly UnityEngine.Color BeforeDif = new UnityEngine.Color(0.60541177f, 0.82788235f, 0.5798824f, 1f); static readonly UnityEngine.Color BeforeShd = new UnityEngine.Color(0.5434118f, 0.744f, 0.5215294f, 1f); const float BeforeScale = 0.35f; void Start() { StartCoroutine(Co()); } System.Collections.IEnumerator Co() { sb = new System.Text.StringBuilder(); System.IO.Directory.CreateDirectory(Dir); 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); if (g == null) { L("grass 없음"); Flush(); yield break; } var rt = UnityEngine.Object.Instantiate(WL.Look.Farm.WLIslandLookSettings.Instance); var arr = new WL.Look.Farm.WLScatterDef[rt.scatter.Length]; for (int i = 0; i < arr.Length; i++) { var s = rt.scatter[i]; arr[i] = new WL.Look.Farm.WLScatterDef { enabled_ = s.enabled_, label = s.label, mesh = s.mesh, material = s.material, probability = s.probability, scale = s.scale, normalOffset = s.normalOffset, density = s.density }; } rt.scatter = arr; g.cfg = rt; float afterScale = arr[0].scale; var baseMat = arr[0].material; var afterDif = baseMat.GetColor("_DiffuseColor"); var afterShd = baseMat.GetColor("_ShadowDiffuseColor"); var clone = new UnityEngine.Material(baseMat); clone.name = baseMat.name + "_final"; for (int i = 0; i < arr.Length; i++) if (arr[i].material == baseMat) arr[i].material = clone; L("채택값 : 풀 Dif=" + afterDif + " Shd=" + afterShd + " scale=" + afterScale + " density=" + arr[0].density + " · 구름 Change=" + baseMat.GetFloat("_Cloud_Change") + " Move=" + baseMat.GetVector("_Cloud_Movement")); var b = g.CalculateInstancesBounds(); var camTop = MakeCam("~816qTop", true, b.center.x, b.center.z, 6f); var camPd = MakeCam("~816qPd", false, 0, 0, 0); UnityEngine.Time.timeScale = 0f; yield return null; // ── 전 (816n 색 · scale 0.35) ──────────────────────────────── Set(clone, BeforeDif, BeforeShd); SetScale(arr, BeforeScale); rt.viewCullEnabled = 0; g.viewCameraOverride = camTop; g.Rebuild(); yield return null; yield return null; L("[전 탑다운] " + Shot(camTop, TS, TS, "f_before_top")); g.viewCameraOverride = camPd; g.Rebuild(); yield return null; yield return null; L("[전 PD ] " + Shot(camPd, W, H, "f_before_pd") + " | " + Stat() + " | " + Bench(camPd, 60).ToString("F3") + " ms/frame"); // ── 후 ──────────────────────────────────────────────────────── Set(clone, afterDif, afterShd); SetScale(arr, afterScale); g.viewCameraOverride = camTop; g.Rebuild(); yield return null; yield return null; L("[후 탑다운] " + Shot(camTop, TS, TS, "f_after_top")); g.viewCameraOverride = camPd; g.Rebuild(); yield return null; yield return null; L("[후 PD 범위off] " + Shot(camPd, W, H, "f_after_pd_nocull") + " | " + Stat() + " | " + Bench(camPd, 60).ToString("F3") + " ms/frame"); rt.viewCullEnabled = 1; g.Rebuild(); yield return null; yield return null; L("[후 PD 범위on ] " + Shot(camPd, W, H, "f_after_pd") + " | " + Stat() + " | " + Bench(camPd, 60).ToString("F3") + " ms/frame"); L(" " + WL.Look.Farm.WLIslandGrass.LastLog); // ── 절두체 + 여유 폭이 덮는 바닥 넓이(몬테카를로) → 79타일 환산 ── float area = GroundArea(camPd, rt.viewMargin, b.center.y, rt.viewMaxDistance, 80f, 400000); float area0 = GroundArea(camPd, 0f, b.center.y, rt.viewMaxDistance, 80f, 400000); float d2 = arr[0].density * arr[0].density; L(string.Format("PD 절두체 바닥 넓이 = {0:F0} ㎡(여유 0) / {1:F0} ㎡(여유 {2:F1} m) · 밀도 {3:F2}개/㎡ → 큰 섬 환산 인스턴스 ≈ {4:F0}개 · 삼각형 ≈ {5:F0}", area0, area, rt.viewMargin, d2, area * d2 * 0.93f, area * d2 * 0.93f * 9f)); L(" (4타일 섬 = 16×16 m 라 절두체보다 작아 잘릴 것이 없다 — 실측 범위밖 제외 " + WL.Look.Farm.WLIslandGrass.ViewCulled + ")"); // ── 가시 범위 팝 0 증명 : P 에서 고른 집합이 P+보폭 에서도 그대로인가 ── var p0 = camPd.transform.position; g.RefreshView(); yield return null; yield return null; ulong kA = WL.Look.Farm.WLIslandGrass.InstanceKey; int nA = WL.Look.Farm.WLIslandGrass.Instances; var right = camPd.transform.right; camPd.transform.position = p0 + right * rt.viewRebuildStep; // 보폭만큼 이동(아직 안 고름) var I2 = Grab(camPd, W, H); g.RefreshView(); yield return null; yield return null; // 그 자리에서 다시 고름 var I3 = Grab(camPd, W, H); int diff = PixDiff(I2.GetPixels32(), I3.GetPixels32()); L(string.Format("팝 0 증명 : 보폭 {0:F1} m 이동 후 [다시 고르기 전] vs [다시 고른 뒤] 화면 차이 픽셀 = {1} ({2:F4}%) · 인스턴스 {3} → {4}", rt.viewRebuildStep, diff, 100.0 * diff / (W * H), nA, WL.Look.Farm.WLIslandGrass.Instances)); System.IO.File.WriteAllBytes(Dir + "/f_pop_before_refresh.png", UnityEngine.ImageConversion.EncodeToPNG(I2)); System.IO.File.WriteAllBytes(Dir + "/f_pop_after_refresh.png", UnityEngine.ImageConversion.EncodeToPNG(I3)); UnityEngine.Object.DestroyImmediate(I2); UnityEngine.Object.DestroyImmediate(I3); camPd.transform.position = p0; g.RefreshView(); yield return null; yield return null; L(" 제자리 복귀 후 지문 = " + WL.Look.Farm.WLIslandGrass.InstanceKey + (WL.Look.Farm.WLIslandGrass.InstanceKey == kA ? " → 처음과 동일(격자 월드 고정)" : " 🔴 다름")); // ── 0 / 2 / 4 s ─────────────────────────────────────────────── UnityEngine.Time.timeScale = 1f; ulong k0 = 0; int n0 = 0; for (int f = 0; f < 3; f++) { if (f > 0) { float t0 = UnityEngine.Time.realtimeSinceStartup; while (UnityEngine.Time.realtimeSinceStartup - t0 < 2f) yield return null; } g.RefreshView(); yield return null; yield return null; var tex = Grab(camPd, W, H); System.IO.File.WriteAllBytes(Dir + "/f_t" + (f * 2) + "s.png", UnityEngine.ImageConversion.EncodeToPNG(tex)); UnityEngine.Object.DestroyImmediate(tex); ulong k = WL.Look.Farm.WLIslandGrass.InstanceKey; int n = WL.Look.Farm.WLIslandGrass.Instances; if (f == 0) { k0 = k; n0 = n; } L("t=" + (f * 2) + "s 인스턴스=" + n + " 좌표지문=" + k + (f == 0 ? "" : (k == k0 && n == n0 ? " → 0 s 와 동일(생성·소멸·이동 0)" : " 🔴 달라졌다"))); } g.viewCameraOverride = null; Flush(); } static void Set(UnityEngine.Material m, UnityEngine.Color d, UnityEngine.Color s) { m.SetColor("_DiffuseColor", d); m.SetColor("_ShadowDiffuseColor", s); } static void SetScale(WL.Look.Farm.WLScatterDef[] arr, float s) { for (int i = 0; i < arr.Length; i++) if (arr[i].label != null && arr[i].label.Contains("풀")) arr[i].scale = s; } // 게임 코드(TryViewPlanes/InView)와 **같은 식**으로 바닥 사다리꼴 + 여유 폭 넓이를 몬테카를로로 잰다 static float GroundArea(UnityEngine.Camera cam, float margin, float y, float maxD, float extent, int n) { var poly = new UnityEngine.Vector2[4]; for (int i = 0; i < 4; i++) { float vx = (i == 1 || i == 2) ? 1f : 0f, vy = (i >= 2) ? 1f : 0f; var ray = cam.ViewportPointToRay(new UnityEngine.Vector3(vx, vy, 0f)); float t = maxD; if (ray.direction.y < -1e-4f) t = UnityEngine.Mathf.Min((y - ray.origin.y) / ray.direction.y, maxD); var h = ray.origin + ray.direction * t; poly[i] = new UnityEngine.Vector2(h.x, h.z); } var c2 = UnityEngine.Vector2.zero; for (int i = 0; i < 4; i++) c2 += poly[i]; c2 *= 0.25f; var nn = new UnityEngine.Vector2[4]; var dd = new float[4]; for (int i = 0; i < 4; i++) { var a = poly[i]; var bb = poly[(i + 1) & 3]; var e = bb - a; var nv = new UnityEngine.Vector2(e.y, -e.x); if (nv.sqrMagnitude < 1e-8f) nv = new UnityEngine.Vector2(1f, 0f); nv.Normalize(); if (UnityEngine.Vector2.Dot(nv, a - c2) < 0f) nv = -nv; nn[i] = nv; dd[i] = UnityEngine.Vector2.Dot(nv, a); } int hit = 0; var rnd = new System.Random(12345); for (int i = 0; i < n; i++) { float x = c2.x + (float)(rnd.NextDouble() * 2 - 1) * extent; float z = c2.y + (float)(rnd.NextDouble() * 2 - 1) * extent; var p = new UnityEngine.Vector2(x, z); bool ok = true; for (int k = 0; k < 4; k++) if (UnityEngine.Vector2.Dot(nn[k], p) - dd[k] > margin) { ok = false; break; } if (ok) hit++; } return 4f * extent * extent * hit / n; } static int PixDiff(UnityEngine.Color32[] a, UnityEngine.Color32[] b) { int n = 0; for (int i = 0; i < a.Length; i++) if (System.Math.Abs(a[i].r - b[i].r) + System.Math.Abs(a[i].g - b[i].g) + System.Math.Abs(a[i].b - b[i].b) > 6) n++; return n; } static string Metric(UnityEngine.Color32[] px, int w, int h) { int bs = 8, bw = w / bs, bh = h / bs; var bm = new float[bw * bh]; var bok = new bool[bw * bh]; double hf = 0; int hfn = 0; for (int by = 0; by < bh; by++) for (int bx = 0; bx < bw; bx++) { double s = 0; int n = 0; for (int y = 0; y < bs; y++) for (int x = 0; x < bs; x++) { var p = px[(by * bs + y) * w + (bx * bs + x)]; if (!Gr(p)) continue; s += Lum(p); n++; } if (n < bs * bs / 2) continue; float mean = (float)(s / n); bm[by * bw + bx] = mean; bok[by * bw + bx] = true; for (int y = 0; y < bs; y++) for (int x = 0; x < bs; x++) { var p = px[(by * bs + y) * w + (bx * bs + x)]; if (!Gr(p)) continue; hf += System.Math.Abs(Lum(p) - mean); hfn++; } } double s1 = 0, s2 = 0; int bn = 0; var lv = new int[40]; for (int i = 0; i < bm.Length; i++) { if (!bok[i]) continue; s1 += bm[i]; s2 += (double)bm[i] * bm[i]; bn++; int q = (int)(bm[i] / 8f); if (q >= 0 && q < 40) lv[q]++; } if (bn == 0 || hfn == 0) return "지면 0"; double mu = s1 / bn, lf = System.Math.Sqrt(System.Math.Max(0, s2 / bn - mu * mu)); int levels = 0; for (int i = 0; i < 40; i++) if (lv[i] >= bn * 0.005) levels++; int ed = 0, edn = 0; for (int y = 0; y < h - 1; y++) for (int x = 0; x < w - 1; x++) { var p = px[y * w + x]; if (!Gr(p)) continue; var r = px[y * w + x + 1]; var d = px[(y + 1) * w + x]; edn++; if ((Gr(r) && System.Math.Abs(Lum(p) - Lum(r)) > 8) || (Gr(d) && System.Math.Abs(Lum(p) - Lum(d)) > 8)) ed++; } return string.Format("띠단계={0} LF={1:F2} HF={2:F2} 에지%={3:F2} 밝기={4:F1}", levels, lf, hf / hfn, 100.0 * ed / System.Math.Max(1, edn), mu); } static bool Gr(UnityEngine.Color32 p) { return p.g > p.b + 8 && p.g > 45; } static float Lum(UnityEngine.Color32 p) { return 0.299f * p.r + 0.587f * p.g + 0.114f * p.b; } static string Stat() { return "인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 삼각형=" + WL.Look.Farm.WLIslandGrass.Triangles + " 드로우콜+" + WL.Look.Farm.WLIslandGrass.DrawnConfigs; } static UnityEngine.Camera MakeCam(string name, bool top, float cx, float cz, float size) { var go = UnityEngine.GameObject.Find(name); if (go == null) go = new UnityEngine.GameObject(name); var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.nearClipPlane = 0.1f; c.farClipPlane = top ? 200f : 300f; if (top) { c.orthographic = true; c.orthographicSize = size; c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f); c.transform.position = new UnityEngine.Vector3(cx, 60f, cz); } else { UnityEngine.Vector3 tgt = UnityEngine.Vector3.zero; var pc = UnityEngine.GameObject.FindGameObjectWithTag("Player"); if (pc != null) tgt = pc.transform.position; c.orthographic = false; c.fieldOfView = 60f; c.transform.rotation = UnityEngine.Quaternion.Euler(45f, 45f, 0f); c.transform.position = tgt - c.transform.forward * 12f; } c.enabled = false; return c; } static UnityEngine.Texture2D Grab(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 string Shot(UnityEngine.Camera cam, int w, int h, string name) { var tex = Grab(cam, w, h); System.IO.File.WriteAllBytes(Dir + "/" + name + ".png", UnityEngine.ImageConversion.EncodeToPNG(tex)); string s = Metric(tex.GetPixels32(), w, h); UnityEngine.Object.DestroyImmediate(tex); return s; } 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); Flush(); } static void Flush() { System.IO.File.WriteAllText("AgentScripts/WL816q_FINAL.txt", sb.ToString()); UnityEngine.Debug.Log("[816q final]\n" + sb.ToString()); } }