diff --git a/AgentScripts/WL816q_Final.cs b/AgentScripts/WL816q_Final.cs new file mode 100644 index 000000000..998cc2a34 --- /dev/null +++ b/AgentScripts/WL816q_Final.cs @@ -0,0 +1,267 @@ +// 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()); + } +} diff --git a/AgentScripts/WL816q_Fringe.cs b/AgentScripts/WL816q_Fringe.cs new file mode 100644 index 000000000..4f0c86071 --- /dev/null +++ b/AgentScripts/WL816q_Fringe.cs @@ -0,0 +1,153 @@ +// WL-816q 3회차 — 띠 가장자리 「톱니 윤곽」의 촘촘함을 데모 수준으로. +// 지렛대 = 풀 높이(scale) · 밀도. 인스턴스 수를 늘리지 않는 쪽(scale)부터 본다. 에셋 무변경. +public static class WL816q_Fringe +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qFringe"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qFringe"); + go.AddComponent(); + } +} + +public class WL816q_FringeRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 768; + static string Dir = "Screenshots_WL/WL816q"; + + 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; rt.viewCullEnabled = 0; + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + UnityEngine.Time.timeScale = 0f; yield return null; + + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); + System.IO.File.WriteAllBytes(Dir + "/fringe_ground.png", UnityEngine.ImageConversion.EncodeToPNG(A)); + L("바닥만 " + Metric(apx, TS, TS)); + L("데모 참고 : 단계 8 LF 16.27 HF 0.33 에지% 1.22 · 이 카메라 = 64 px/m"); + rt.grassEnabled = 1; + + float[] scales = { 0.35f, 0.5f, 0.7f, 0.9f, 1.2f }; + float[] dens = { 2.5f, 3.5f }; + for (int di = 0; di < dens.Length; di++) + { + for (int si = 0; si < scales.Length; si++) + { + for (int i = 0; i < arr.Length; i++) + { + if (arr[i].label != null && arr[i].label.Contains("자갈")) continue; // 자갈은 그대로 + arr[i].scale = scales[si] * (arr[i].label != null && arr[i].label.Contains("꽃") ? 1f : 1f); + arr[i].density = dens[di]; + } + g.Rebuild(); yield return null; yield return null; + var B = Grab(cam, TS, TS); var bpx = B.GetPixels32(); + L(string.Format("scale={0:F2} density={1:F1} | {2} | 보이는 풀 {3:F2}% | 인스턴스 {4} 삼각형 {5}", + scales[si], dens[di], Metric(bpx, TS, TS), Cover(apx, bpx), + WL.Look.Farm.WLIslandGrass.Instances, WL.Look.Farm.WLIslandGrass.Triangles)); + System.IO.File.WriteAllBytes(Dir + "/fringe_s" + scales[si].ToString("F2") + "_d" + dens[di].ToString("F1") + ".png", + UnityEngine.ImageConversion.EncodeToPNG(B)); + UnityEngine.Object.DestroyImmediate(B); + } + } + UnityEngine.Object.DestroyImmediate(A); + UnityEngine.Time.timeScale = 1f; + Flush(); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; if (!Gr(p)) continue; tot++; + var q = b[i]; if (q.g <= q.b + 4) continue; + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + 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 UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_FRINGE.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q fringe]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Iter.cs b/AgentScripts/WL816q_Iter.cs new file mode 100644 index 000000000..a508d62fa --- /dev/null +++ b/AgentScripts/WL816q_Iter.cs @@ -0,0 +1,140 @@ +// WL-816q 회차 — 풀이 「띠 안쪽에서 바닥과 같은 픽셀」이 되도록 풀 색을 수렴시킨다. +// 바닥만 찍은 기준 이미지와 같은 자리를 비교해 채널별 배율을 뉴턴식으로 갱신. +// 에셋 무변경(런타임 복제본만). +public static class WL816q_Iter +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qIter"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qIter"); + go.AddComponent(); + } +} + +public class WL816q_IterRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 768; + static string Dir = "Screenshots_WL/WL816q"; + + 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 < 5; i++) yield return new UnityEngine.WaitForSeconds(1f); + + var g = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); + if (g == null) { L("WLIslandGrass 없음"); 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; + + var baseMat = arr[0].material; + var dif0 = baseMat.GetColor("_DiffuseColor"); + var shd0 = baseMat.GetColor("_ShadowDiffuseColor"); + var clone = new UnityEngine.Material(baseMat); clone.name = baseMat.name + "_iter"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == baseMat) arr[i].material = clone; + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + + // 시간을 멈춘다 — 구름이 흐르면 회차마다 바닥이 달라져 비교가 무효가 된다 + float ts = UnityEngine.Time.timeScale; + UnityEngine.Time.timeScale = 0f; + yield return null; + + // 기준 = 바닥만 + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); + System.IO.File.WriteAllBytes(Dir + "/iter_A_ground.png", UnityEngine.ImageConversion.EncodeToPNG(A)); + var apx = A.GetPixels32(); + + rt.grassEnabled = 1; + float kr = 1f, kg = 1f, kb = 1f; + for (int it = 0; it < 5; it++) + { + clone.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * kr, dif0.g * kg, dif0.b * kb, dif0.a)); + clone.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * kr, shd0.g * kg, shd0.b * kb, shd0.a)); + g.Rebuild(); yield return null; yield return null; + var Bt = Grab(cam, TS, TS); + var bpx = Bt.GetPixels32(); + double rr, rg, rb; int cov; + Ratio(apx, bpx, out rr, out rg, out rb, out cov); + L(string.Format("회차{0} k=({1:F4},{2:F4},{3:F4}) → 풀/바닥 비=({4:F4},{5:F4},{6:F4}) 덮임={7:F1}% Dif=({8:F3},{9:F3},{10:F3})", + it, kr, kg, kb, rr, rg, rb, cov * 100.0 / (TS * TS), dif0.r * kr, dif0.g * kg, dif0.b * kb)); + System.IO.File.WriteAllBytes(Dir + "/iter_" + it + ".png", UnityEngine.ImageConversion.EncodeToPNG(Bt)); + UnityEngine.Object.DestroyImmediate(Bt); + if (System.Math.Abs(rr - 1) < 0.004 && System.Math.Abs(rg - 1) < 0.004 && System.Math.Abs(rb - 1) < 0.004) { L("수렴"); break; } + kr = (float)UnityEngine.Mathf.Clamp((float)(kr / System.Math.Max(0.5, rr)), 0.5f, 2.5f); + kg = (float)UnityEngine.Mathf.Clamp((float)(kg / System.Math.Max(0.5, rg)), 0.5f, 2.5f); + kb = (float)UnityEngine.Mathf.Clamp((float)(kb / System.Math.Max(0.5, rb)), 0.5f, 2.5f); + } + L(string.Format("채택 _DiffuseColor = {0:F4} {1:F4} {2:F4}", dif0.r * kr, dif0.g * kg, dif0.b * kb)); + L(string.Format("채택 _ShadowDiffuseColor = {0:F4} {1:F4} {2:F4}", shd0.r * kr, shd0.g * kg, shd0.b * kb)); + UnityEngine.Object.DestroyImmediate(A); + UnityEngine.Time.timeScale = ts; + Flush(); + } + + // 바닥만 이미지와 다른 픽셀 = 풀. 그 자리의 바닥색 대비 평균비. + static void Ratio(UnityEngine.Color32[] a, UnityEngine.Color32[] b, out double rr, out double rg, out double rb, out int cov) + { + double sa = 0, sb = 0, sa2 = 0, sb2 = 0, sa3 = 0, sb3 = 0; int n = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; var q = b[i]; + if (!(p.g > p.b + 8 && p.g > 45)) continue; // 초록 지면만 + int d = System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b); + if (d <= 6) continue; // 안 바뀐 = 풀 없음 + if (q.g <= q.b + 4) continue; // 꽃(노랑)·자갈 제외 + sa += p.r; sb += q.r; sa2 += p.g; sb2 += q.g; sa3 += p.b; sb3 += q.b; n++; + } + cov = n; + rr = n == 0 ? 1 : sb / sa; rg = n == 0 ? 1 : sb2 / sa2; rb = n == 0 ? 1 : sb3 / sa3; + } + + static UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_ITER.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q iter]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Play.cs b/AgentScripts/WL816q_Play.cs new file mode 100644 index 000000000..32a8c8c91 --- /dev/null +++ b/AgentScripts/WL816q_Play.cs @@ -0,0 +1,268 @@ +// WL-816q — 「그라데이션(구름 띠)이 왜 안 보이나」 요소별 ON/OFF 실측. +// 에셋 무변경(SO·머티리얼은 전부 런타임 복제본). Debug.LogError 금지. +public static class WL816q_Play +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qPlay"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qPlay"); + go.AddComponent(); + } +} + +public class WL816q_PlayRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int W = 1080, H = 1920; + const int TS = 768; + static string Dir = "Screenshots_WL/WL816q"; + + 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 < 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); + // scatter 정의를 새 인스턴스로 깊은 복사 — 에셋 쪽 객체를 절대 만지지 않는다 + if (rt.scatter != null) + { + 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; + + // ── 요소 진단 ──────────────────────────────────────────────── + L("=== 요소 진단 === t=" + UnityEngine.Time.timeSinceLevelLoad); + L("ⓑ WLReferenceLook.IsApplied=" + WL.Look.Arena.WLReferenceLook.IsApplied + + " · Outlined=" + WL.Look.Arena.WLReferenceLook.OutlinedRenderers + + " · Contrast=" + WL.Look.Arena.WLReferenceLook.ContrastMaterials); + int hull = 0, toonR = 0; + var rends = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + for (int i = 0; i < rends.Length; i++) + { + var ms = rends[i].sharedMaterials; + for (int m = 0; m < ms.Length; m++) + { + if (ms[m] == null || ms[m].shader == null) continue; + if (ms[m].shader.name == "WL/HullOutline") hull++; + if (ms[m].shader.name.Contains("Toon")) toonR++; + } + } + L("ⓑ 씬 안 HullOutline 머티리얼 슬롯=" + hull + " · Toon 슬롯=" + toonR); + var vols = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + L("ⓓ Volume 개수=" + vols.Length + (vols.Length > 0 ? " 첫 프로필=" + (vols[0].sharedProfile ? vols[0].sharedProfile.name : "-") : "")); + var sun = UnityEngine.RenderSettings.sun; + var lights = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + for (int i = 0; i < lights.Length; i++) + if (lights[i].type == UnityEngine.LightType.Directional) + L("ⓐ Directional " + lights[i].name + " shadows=" + lights[i].shadows + " int=" + lights[i].intensity + " rot=" + lights[i].transform.eulerAngles); + + var top = rt.islandTopMaterial; + var grassMat = (rt.scatter != null && rt.scatter.Length > 0) ? rt.scatter[0].material : null; + L("ⓒⓔ 바닥=" + (top ? top.name : "-") + " sh=" + (top && top.shader ? top.shader.name : "-") + " " + MatDump(top)); + L("ⓒⓔ 풀 =" + (grassMat ? grassMat.name : "-") + " sh=" + (grassMat && grassMat.shader ? grassMat.shader.name : "-") + " " + MatDump(grassMat)); + L("ⓔ 그림자비(Shadow/Diffuse) 바닥=" + RatioStr(top) + " · 풀=" + RatioStr(grassMat)); + + var b = g.CalculateInstancesBounds(); + L("섬 바운즈 center=" + b.center + " size=" + b.size); + + var camTop = MakeTop(b.center.x, b.center.z, 6f); + var camPd = MakePd(); + L("탑다운 = ortho6 @(" + b.center.x.ToString("F2") + ",60," + b.center.z.ToString("F2") + ") · PD = 12m/45°/fov60"); + + // ── A: 풀 끔 (바닥만) ─────────────────────────────────────── + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + L("[A 바닥만] " + Shot(camTop, TS, TS, "A_top_groundonly") + " | " + Stat()); + Shot(camPd, W, H, "A_pd_groundonly"); + + // ── B: 현재 풀 (816n 상태) ────────────────────────────────── + rt.grassEnabled = 1; g.Rebuild(); yield return null; yield return null; + L("[B 현재풀] " + Shot(camTop, TS, TS, "B_top_before") + " | " + Stat()); + Shot(camPd, W, H, "B_pd_before"); + + // ── C: 풀 그림자색만 데모 비율로 (다른 값 전부 동일) ──────── + if (grassMat != null) + { + var clone = new UnityEngine.Material(grassMat); + clone.name = grassMat.name + "_816qTest"; + var dif = clone.GetColor("_DiffuseColor"); + clone.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(dif.r * 0.3232f, dif.g * 0.3414f, dif.b * 0.3948f, dif.a)); + for (int i = 0; i < rt.scatter.Length; i++) + if (rt.scatter[i].material == grassMat) rt.scatter[i].material = clone; + g.Rebuild(); yield return null; yield return null; + L("[C 풀그림자=데모비율] " + Shot(camTop, TS, TS, "C_top_shadowfix") + " | " + Stat()); + Shot(camPd, W, H, "C_pd_shadowfix"); + } + + // ── D: 참고 — 데모 기준 캡처에 같은 지표 ──────────────────── + Ref("Screenshots_WL/WL816f/x_demo_ground.png", "D 데모참고(스케일 미상)"); + Ref("Screenshots_WL/WL816f/x_ours_ground.png", "D 우리참고(스케일 미상)"); + + Flush(); + } + + // ─────────────────────────────────────────── 지표 + // LF = 8×8 블록 평균 밝기의 표준편차(= 띠 세기) · HF = 블록 내 편차 평균(= 포기 잡음) + // levels = 블록 밝기를 8단계로 양자화했을 때 면적 0.5% 이상인 단계 수(= 띠 단계) + 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 (!Ground(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 (!Ground(p)) continue; + hf += System.Math.Abs(Lum(p) - mean); hfn++; + } + } + double bs1 = 0, bs2 = 0; int bn = 0; + var lv = new int[40]; + for (int i = 0; i < bm.Length; i++) { if (!bok[i]) continue; bs1 += bm[i]; bs2 += (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 = bs1 / bn; + double lf = System.Math.Sqrt(System.Math.Max(0, bs2 / bn - mu * mu)); + int levels = 0; for (int i = 0; i < 40; i++) if (lv[i] >= bn * 0.005) levels++; + // 에지% — 인접 픽셀 밝기차 8 이상 + 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 (!Ground(p)) continue; + var r = px[y * w + x + 1]; var d = px[(y + 1) * w + x]; + edn++; + if ((Ground(r) && System.Math.Abs(Lum(p) - Lum(r)) > 8) || (Ground(d) && System.Math.Abs(Lum(p) - Lum(d)) > 8)) ed++; + } + return string.Format("지면 {0:F1}% · 띠세기LF={1:F2} · 포기잡음HF={2:F2} · LF/HF={3:F2} · 띠단계={4} · 에지%={5:F2} · 평균밝기={6:F1}", + 100.0 * edn / (w * h), lf, hf / hfn, lf / System.Math.Max(0.01, hf / hfn), levels, 100.0 * ed / System.Math.Max(1, edn), mu); + } + + static bool Ground(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 void Ref(string path, string tag) + { + if (!System.IO.File.Exists(path)) { L(tag + " : 파일 없음 " + path); return; } + var tex = new UnityEngine.Texture2D(2, 2, UnityEngine.TextureFormat.RGB24, false); + if (!UnityEngine.ImageConversion.LoadImage(tex, System.IO.File.ReadAllBytes(path))) { L(tag + " : 읽기 실패"); return; } + L(tag + " " + tex.width + "x" + tex.height + " : " + Metric(tex.GetPixels32(), tex.width, tex.height)); + UnityEngine.Object.DestroyImmediate(tex); + } + + static string MatDump(UnityEngine.Material m) + { + if (m == null) return ""; + return "Shades=" + F(m, "_Shades") + " Bright=" + F(m, "_Brightness") + " MinDark=" + F(m, "_MinimumDarkness") + + " CloudStr=" + F(m, "_Cloud_Strength") + " CloudDens=" + F(m, "_Cloud_Density") + " Cover=" + F(m, "_Cloud_Cover") + + " Change=" + F(m, "_Cloud_Change") + " Move=" + (m.HasProperty("_Cloud_Movement") ? m.GetVector("_Cloud_Movement").ToString() : "-") + + " Dif=" + C(m, "_DiffuseColor") + " Shd=" + C(m, "_ShadowDiffuseColor") + + " kw=[" + string.Join(",", m.shaderKeywords) + "]"; + } + static string F(UnityEngine.Material m, string n) { return m.HasProperty(n) ? m.GetFloat(n).ToString("F4") : "-"; } + static string C(UnityEngine.Material m, string n) { if (!m.HasProperty(n)) return "-"; var c = m.GetColor(n); return "(" + c.r.ToString("F3") + "," + c.g.ToString("F3") + "," + c.b.ToString("F3") + ")"; } + static string RatioStr(UnityEngine.Material m) + { + if (m == null || !m.HasProperty("_DiffuseColor") || !m.HasProperty("_ShadowDiffuseColor")) return "-"; + var d = m.GetColor("_DiffuseColor"); var s = m.GetColor("_ShadowDiffuseColor"); + return "(" + Safe(s.r, d.r) + "," + Safe(s.g, d.g) + "," + Safe(s.b, d.b) + ")"; + } + static string Safe(float a, float b) { return b < 0.001f ? "-" : (a / b).ToString("F3"); } + + static string Stat() + { + return "인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 삼각형=" + WL.Look.Farm.WLIslandGrass.Triangles + + " 드로우콜+" + WL.Look.Farm.WLIslandGrass.DrawnConfigs + " 타일=" + WL.Look.Farm.WLIslandGrass.Tiles; + } + + // ─────────────────────────────────────────── 카메라·캡처 + static UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + c.enabled = false; return c; + } + static UnityEngine.Camera MakePd() + { + UnityEngine.Vector3 tgt = UnityEngine.Vector3.zero; + var pc = UnityEngine.GameObject.FindGameObjectWithTag("Player"); + if (pc != null) tgt = pc.transform.position; + var go = UnityEngine.GameObject.Find("~816qPd"); + if (go == null) go = new UnityEngine.GameObject("~816qPd"); + var c = go.GetComponent(); + if (c == null) c = go.AddComponent(); + c.orthographic = false; c.fieldOfView = 60f; 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.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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_PLAY.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q play]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Scan.cs b/AgentScripts/WL816q_Scan.cs new file mode 100644 index 000000000..57a7b8ffc --- /dev/null +++ b/AgentScripts/WL816q_Scan.cs @@ -0,0 +1,141 @@ +// WL-816q 스캔 — 풀이 바닥과 같은 톤 단계에 들어가는 「고원」의 폭과 중심을 찾는다. +// 구름 위상 3곳에서 재확인해 특정 순간에만 맞는 값을 배제한다. 에셋 무변경. +public static class WL816q_Scan +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qScan"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qScan"); + go.AddComponent(); + } +} + +public class WL816q_ScanRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 512; + static string Dir = "Screenshots_WL/WL816q"; + static readonly float[] K0 = { 1.0737f, 1.1189f, 1.1365f }; + + 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 < 5; 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; + + var baseMat = arr[0].material; + var dif0 = baseMat.GetColor("_DiffuseColor"); + var shd0 = baseMat.GetColor("_ShadowDiffuseColor"); + var clone = new UnityEngine.Material(baseMat); clone.name = baseMat.name + "_scan"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == baseMat) arr[i].material = clone; + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + + float[] phases = { 0f, 3.5f, 7f }; // 구름이 흘러간 서로 다른 순간 3곳 + float[] ss = { 0.97f, 0.98f, 0.99f, 1.00f, 1.01f, 1.02f, 1.03f, 1.04f, 1.05f, 1.06f, 1.07f, 1.08f }; + var cov = new float[phases.Length, ss.Length]; + + for (int p = 0; p < phases.Length; p++) + { + if (p > 0) { UnityEngine.Time.timeScale = 1f; float t0 = UnityEngine.Time.realtimeSinceStartup; while (UnityEngine.Time.realtimeSinceStartup - t0 < 3.5f) yield return null; } + UnityEngine.Time.timeScale = 0f; yield return null; + + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); + rt.grassEnabled = 1; + for (int i = 0; i < ss.Length; i++) + { + float s = ss[i]; + clone.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * K0[0] * s, dif0.g * K0[1] * s, dif0.b * K0[2] * s, dif0.a)); + clone.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * K0[0] * s, shd0.g * K0[1] * s, shd0.b * K0[2] * s, shd0.a)); + g.Rebuild(); yield return null; yield return null; + var Bt = Grab(cam, TS, TS); + cov[p, i] = Cover(apx, Bt.GetPixels32()); + UnityEngine.Object.DestroyImmediate(Bt); + } + UnityEngine.Object.DestroyImmediate(A); + var line = "구름 위상 " + p + " (t=" + UnityEngine.Time.timeSinceLevelLoad.ToString("F1") + "s) 보이는 풀 % : "; + for (int i = 0; i < ss.Length; i++) line += ss[i].ToString("F2") + "=" + cov[p, i].ToString("F2") + " "; + L(line); + } + + // 세 위상 모두에서 최소인 구간의 중심 + int best = 0; float bestv = 999f; + for (int i = 0; i < ss.Length; i++) + { + float m = 0f; for (int p = 0; p < phases.Length; p++) m = UnityEngine.Mathf.Max(m, cov[p, i]); + if (m < bestv) { bestv = m; best = i; } + } + float sBest = ss[best]; + L("최적 s=" + sBest.ToString("F2") + " (세 위상 최대 보이는 풀 " + bestv.ToString("F2") + "%)"); + L(string.Format("채택 _DiffuseColor = {0:F4} {1:F4} {2:F4}", dif0.r * K0[0] * sBest, dif0.g * K0[1] * sBest, dif0.b * K0[2] * sBest)); + L(string.Format("채택 _ShadowDiffuseColor = {0:F4} {1:F4} {2:F4}", shd0.r * K0[0] * sBest, shd0.g * K0[1] * sBest, shd0.b * K0[2] * sBest)); + UnityEngine.Time.timeScale = 1f; + Flush(); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; + if (!(p.g > p.b + 8 && p.g > 45)) continue; + tot++; + var q = b[i]; + if (q.g <= q.b + 4) continue; // 꽃·자갈 제외 + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + + static UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_SCAN.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q scan]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Scan2.cs b/AgentScripts/WL816q_Scan2.cs new file mode 100644 index 000000000..ab310160e --- /dev/null +++ b/AgentScripts/WL816q_Scan2.cs @@ -0,0 +1,138 @@ +// WL-816q 4회차 — 「구조적으로」 같은 색 만들기. +// 바닥은 lerp(알베도×0.3248/0.3416/0.3936, 알베도, t). 풀도 **같은 비**를 주고 알베도만 맞추면 +// 모든 램프 단계 t 에서 같은 색이 된다(= 조명·구름 값이 바뀌어도 안 깨진다). +// 지금 채택값은 두 단계에서만 우연히 맞은 것이라 구름 세기를 바꾸면 깨진다(3회차 실측). +public static class WL816q_Scan2 +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qScan2"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qScan2"); + go.AddComponent(); + } +} + +public class WL816q_Scan2Runner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 512; + static string Dir = "Screenshots_WL/WL816q"; + + 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 < 5; 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; rt.viewCullEnabled = 0; + + var topAsset = rt.islandTopMaterial; + var topClone = new UnityEngine.Material(topAsset); topClone.name = topAsset.name + "_s2"; + var rends = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + for (int i = 0; i < rends.Length; i++) + { + var ms = rends[i].sharedMaterials; bool hit = false; + for (int m = 0; m < ms.Length; m++) if (ms[m] == topAsset) { ms[m] = topClone; hit = true; } + if (hit) rends[i].sharedMaterials = ms; + } + var grassAsset = arr[0].material; + var gc = new UnityEngine.Material(grassAsset); gc.name = grassAsset.name + "_s2"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == grassAsset) arr[i].material = gc; + + var gndShd = topAsset.GetColor("_ShadowDiffuseColor"); // 바닥의 그림자/확산 비 + var cur = grassAsset.GetColor("_DiffuseColor"); + L("바닥 비 = (" + gndShd.r.ToString("F4") + "," + gndShd.g.ToString("F4") + "," + gndShd.b.ToString("F4") + ") · 현재 풀 Dif = " + cur); + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + UnityEngine.Time.timeScale = 0f; yield return null; + + float[] strs = { 1.0f, 1.8f }; + float[] ks = { 0.90f, 0.95f, 1.00f, 1.05f, 1.10f, 1.15f, 1.20f, 1.25f, 1.30f, 1.40f, 1.50f }; + for (int s = 0; s < strs.Length; s++) + { + topClone.SetFloat("_Cloud_Strength", strs[s]); gc.SetFloat("_Cloud_Strength", strs[s]); + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); + rt.grassEnabled = 1; + string line = "Strength=" + strs[s].ToString("F1") + " 보이는 풀 % : "; + for (int i = 0; i < ks.Length; i++) + { + var dif = new UnityEngine.Color(cur.r * ks[i], cur.g * ks[i], cur.b * ks[i], cur.a); + gc.SetColor("_DiffuseColor", dif); + gc.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(dif.r * gndShd.r, dif.g * gndShd.g, dif.b * gndShd.b, dif.a)); + g.Rebuild(); yield return null; yield return null; + var B = Grab(cam, TS, TS); + line += ks[i].ToString("F2") + "=" + Cover(apx, B.GetPixels32()).ToString("F1") + " "; + UnityEngine.Object.DestroyImmediate(B); + } + L(line); + UnityEngine.Object.DestroyImmediate(A); + } + // 되돌린다 + topClone.SetFloat("_Cloud_Strength", topAsset.GetFloat("_Cloud_Strength")); + gc.SetFloat("_Cloud_Strength", grassAsset.GetFloat("_Cloud_Strength")); + gc.SetColor("_DiffuseColor", cur); gc.SetColor("_ShadowDiffuseColor", grassAsset.GetColor("_ShadowDiffuseColor")); + g.Rebuild(); + UnityEngine.Time.timeScale = 1f; + Flush(); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; if (!(p.g > p.b + 8 && p.g > 45)) continue; tot++; + var q = b[i]; if (q.g <= q.b + 4) continue; + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + static UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_SCAN2.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q scan2]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Stat.cs b/AgentScripts/WL816q_Stat.cs new file mode 100644 index 000000000..3c34eebe8 --- /dev/null +++ b/AgentScripts/WL816q_Stat.cs @@ -0,0 +1,20 @@ +public static class WL816q_Stat +{ + public static void Go() + { + var sb = new System.Text.StringBuilder(); + int n = UnityEngine.SceneManagement.SceneManager.sceneCount; + for (int i = 0; i < n; i++) + { + var s = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + sb.Append(s.name + "(loaded=" + s.isLoaded + ") "); + } + var r = UnityEngine.GameObject.Find("~WL816qPlay"); + sb.Append(" | runner=" + (r != null) + " | t=" + UnityEngine.Time.timeSinceLevelLoad); + var g = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); + sb.Append(" | grass=" + (g != null) + " inst=" + WL.Look.Farm.WLIslandGrass.Instances + " tiles=" + WL.Look.Farm.WLIslandGrass.Tiles); + sb.Append(" | files=" + (System.IO.Directory.Exists("Screenshots_WL/WL816q") ? System.IO.Directory.GetFiles("Screenshots_WL/WL816q").Length : -1)); + System.IO.File.WriteAllText("AgentScripts/WL816q_STAT.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q stat] " + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Sweep.cs b/AgentScripts/WL816q_Sweep.cs new file mode 100644 index 000000000..0c85754ab --- /dev/null +++ b/AgentScripts/WL816q_Sweep.cs @@ -0,0 +1,170 @@ +// WL-816q 2회차 — 띠 단계 수·대비를 데모 수준(띠단계 8 · LF 16.3)까지 올린다. +// 바닥·풀 **양쪽** 구름 값을 똑같이 바꾼다(같아야 풀이 띠 안에서 녹아든다). 에셋 무변경(복제본 교체). +public static class WL816q_Sweep +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qSweep"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qSweep"); + go.AddComponent(); + } +} + +public class WL816q_SweepRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 768; + static string Dir = "Screenshots_WL/WL816q"; + + 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; rt.viewCullEnabled = 0; + + // 바닥 머티리얼을 쓰는 렌더러를 전부 복제본으로 바꿔 둔다(에셋 무변경) + var topAsset = rt.islandTopMaterial; + var topClone = new UnityEngine.Material(topAsset); topClone.name = topAsset.name + "_sweep"; + int swapped = 0; + var rends = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + for (int i = 0; i < rends.Length; i++) + { + var ms = rends[i].sharedMaterials; bool hit = false; + for (int m = 0; m < ms.Length; m++) if (ms[m] == topAsset) { ms[m] = topClone; hit = true; } + if (hit) { rends[i].sharedMaterials = ms; swapped++; } + } + var grassAsset = arr[0].material; + var grassClone = new UnityEngine.Material(grassAsset); grassClone.name = grassAsset.name + "_sweep"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == grassAsset) arr[i].material = grassClone; + L("바닥 복제 교체 렌더러=" + swapped + " · 풀 복제 교체 완료"); + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + UnityEngine.Time.timeScale = 0f; yield return null; + + L("목표(데모 참고 x_demo_ground) : 띠단계 8 · LF 16.27 · HF 0.33 · 에지% 1.22 · 밝기 174.2"); + float[] strs = { 1f, 1.4f, 1.8f, 2.2f }; + float[] covs = { 0.25f, 0.9f, 1.6f, 2.4f }; // = _Brightness (밝기 보정) + for (int ci = 0; ci < covs.Length; ci++) + { + for (int si = 0; si < strs.Length; si++) + { + Set(topClone, strs[si], covs[ci]); Set(grassClone, strs[si], covs[ci]); + // 바닥만 + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); + string mg = Metric(apx, TS, TS); + // 풀까지 + rt.grassEnabled = 1; g.Rebuild(); yield return null; yield return null; + var B = Grab(cam, TS, TS); var bpx = B.GetPixels32(); + L(string.Format("Strength={0:F1} Brightness={1:F2} | 바닥만 {2} | 풀포함 {3} | 보이는 풀 {4:F2}%", + strs[si], covs[ci], mg, Metric(bpx, TS, TS), Cover(apx, bpx))); + System.IO.File.WriteAllBytes(Dir + "/sweep_s" + strs[si].ToString("F1") + "_b" + covs[ci].ToString("F2") + ".png", + UnityEngine.ImageConversion.EncodeToPNG(B)); + UnityEngine.Object.DestroyImmediate(A); UnityEngine.Object.DestroyImmediate(B); + } + } + // 🔴 원래 값으로 되돌린다 — 안 하면 다음 실행이 오염된 씬에서 재기 시작한다(2회차 사고) + Set(topClone, topAsset.GetFloat("_Cloud_Strength"), topAsset.GetFloat("_Brightness")); + Set(grassClone, grassAsset.GetFloat("_Cloud_Strength"), grassAsset.GetFloat("_Brightness")); + g.Rebuild(); + UnityEngine.Time.timeScale = 1f; + Flush(); + } + + static void Set(UnityEngine.Material m, float str, float bright) + { + m.SetFloat("_Cloud_Strength", str); m.SetFloat("_Brightness", bright); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; if (!Gr(p)) continue; tot++; + var q = b[i]; if (q.g <= q.b + 4) continue; + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + 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 UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_SWEEP.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q sweep]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Tune.cs b/AgentScripts/WL816q_Tune.cs new file mode 100644 index 000000000..4c66112f2 --- /dev/null +++ b/AgentScripts/WL816q_Tune.cs @@ -0,0 +1,175 @@ +// WL-816q 5회차 — 구름 세기를 올려 띠를 한 단 더 얻을 수 있는가. +// 세기를 바꾸면 풀 색 일치가 깨지므로(3회차 실측) **세기마다 풀 색을 다시 수렴**시켜 비교한다. +// 에셋 무변경(복제본) · 끝나면 원래 값으로 되돌린다. +public static class WL816q_Tune +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qTune"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qTune"); + go.AddComponent(); + } +} + +public class WL816q_TuneRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int TS = 512; + static string Dir = "Screenshots_WL/WL816q"; + + 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 < 5; 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; rt.viewCullEnabled = 0; + + var topAsset = rt.islandTopMaterial; + var topClone = new UnityEngine.Material(topAsset); topClone.name = topAsset.name + "_tune"; + var rends = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); + for (int i = 0; i < rends.Length; i++) + { + var ms = rends[i].sharedMaterials; bool hit = false; + for (int m = 0; m < ms.Length; m++) if (ms[m] == topAsset) { ms[m] = topClone; hit = true; } + if (hit) rends[i].sharedMaterials = ms; + } + var grassAsset = arr[0].material; + var gc = new UnityEngine.Material(grassAsset); gc.name = grassAsset.name + "_tune"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == grassAsset) arr[i].material = gc; + var dif0 = grassAsset.GetColor("_DiffuseColor"); + var shd0 = grassAsset.GetColor("_ShadowDiffuseColor"); + + var b = g.CalculateInstancesBounds(); + var cam = MakeTop(b.center.x, b.center.z, 6f); + UnityEngine.Time.timeScale = 0f; yield return null; + + L("데모 참고 : 띠단계 8 · LF 16.27 · HF 0.33 · 에지% 1.22 · 밝기 174.2"); + float[] strs = { 1.0f, 1.2f, 1.4f, 1.6f }; + for (int s = 0; s < strs.Length; s++) + { + topClone.SetFloat("_Cloud_Strength", strs[s]); gc.SetFloat("_Cloud_Strength", strs[s]); + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); + string gm = Metric(apx, TS, TS); + rt.grassEnabled = 1; + + // 이 세기에서 풀이 바닥에 녹아드는 배율 k 를 0.90~1.30 에서 0.01 씩 찾는다 + float bestK = 1f, bestC = 999f; string prof = ""; + for (int i = 0; i <= 40; i++) + { + float k = 0.90f + i * 0.01f; + gc.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * k, dif0.g * k, dif0.b * k, dif0.a)); + gc.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * k, shd0.g * k, shd0.b * k, shd0.a)); + g.Rebuild(); yield return null; + var B = Grab(cam, TS, TS); + float c = Cover(apx, B.GetPixels32()); + UnityEngine.Object.DestroyImmediate(B); + if (c < bestC) { bestC = c; bestK = k; } + if (c < 5f) prof += k.ToString("F2") + "=" + c.ToString("F1") + " "; + } + gc.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * bestK, dif0.g * bestK, dif0.b * bestK, dif0.a)); + gc.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * bestK, shd0.g * bestK, shd0.b * bestK, shd0.a)); + g.Rebuild(); yield return null; yield return null; + var F = Grab(cam, TS, TS); + L(string.Format("Strength={0:F1} | 바닥만 {1} | 최적 k={2:F2} 보이는 풀 {3:F2}% | 풀포함 {4}\n 낮은 구간: {5}", + strs[s], gm, bestK, bestC, Metric(F.GetPixels32(), TS, TS), prof.Length > 0 ? prof : "없음")); + System.IO.File.WriteAllBytes(Dir + "/tune_s" + strs[s].ToString("F1") + ".png", UnityEngine.ImageConversion.EncodeToPNG(F)); + UnityEngine.Object.DestroyImmediate(F); UnityEngine.Object.DestroyImmediate(A); + } + topClone.SetFloat("_Cloud_Strength", topAsset.GetFloat("_Cloud_Strength")); + gc.SetFloat("_Cloud_Strength", grassAsset.GetFloat("_Cloud_Strength")); + gc.SetColor("_DiffuseColor", dif0); gc.SetColor("_ShadowDiffuseColor", shd0); + g.Rebuild(); + UnityEngine.Time.timeScale = 1f; + Flush(); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; if (!Gr(p)) continue; tot++; + var q = b[i]; if (q.g <= q.b + 4) continue; + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + 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 UnityEngine.Camera MakeTop(float cx, float cz, float size) + { + var go = UnityEngine.GameObject.Find("~816qTop"); + if (go == null) go = new UnityEngine.GameObject("~816qTop"); + 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, 60f, cz); + 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 void L(string s) { sb.AppendLine(s); Flush(); } + static void Flush() + { + System.IO.File.WriteAllText("AgentScripts/WL816q_TUNE.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q tune]\n" + sb.ToString()); + } +} diff --git a/AgentScripts/WL816q_Verify.cs b/AgentScripts/WL816q_Verify.cs new file mode 100644 index 000000000..1d805a45e --- /dev/null +++ b/AgentScripts/WL816q_Verify.cs @@ -0,0 +1,236 @@ +// WL-816q 검증 — 전/후 · 예산 · 0/2/4 s 좌표 불변(순간이동 0). 에셋 무변경(런타임 복제본). +public static class WL816q_Verify +{ + public static void Start() + { + var go = UnityEngine.GameObject.Find("~WL816qVerify"); + if (go != null) UnityEngine.Object.DestroyImmediate(go); + go = new UnityEngine.GameObject("~WL816qVerify"); + go.AddComponent(); + } +} + +public class WL816q_VerifyRunner : UnityEngine.MonoBehaviour +{ + static System.Text.StringBuilder sb; + const int W = 1080, H = 1920, TS = 768; + static string Dir = "Screenshots_WL/WL816q"; + // 816n 이 쓰던 값 = 「전」 + 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); + + 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; + + var baseMat = arr[0].material; + var clone = new UnityEngine.Material(baseMat); clone.name = baseMat.name + "_verify"; + for (int i = 0; i < arr.Length; i++) if (arr[i].material == baseMat) arr[i].material = clone; + L("풀 머티리얼 채택값 Dif=" + baseMat.GetColor("_DiffuseColor") + " Shd=" + baseMat.GetColor("_ShadowDiffuseColor") + + " 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); + L("섬 " + b.center + " " + b.size + " · 탑다운 ortho6 · PD 12m/45°/fov60"); + + UnityEngine.Time.timeScale = 0f; yield return null; + + // ── 전 ──────────────────────────────────────────────────────── + SetGrass(clone, BeforeDif, BeforeShd); + g.viewCameraOverride = camTop; rt.viewCullEnabled = 0; g.Rebuild(); yield return null; yield return null; + L("[전 top] " + Shot(camTop, TS, TS, "iter1_before_top")); + g.viewCameraOverride = camPd; g.Rebuild(); yield return null; yield return null; + L("[전 PD ] " + Shot(camPd, W, H, "iter1_before_pd") + " | " + Stat() + " | " + Bench(camPd, 60).ToString("F3") + " ms/frame"); + + // ── 후 ──────────────────────────────────────────────────────── + SetGrass(clone, baseMat.GetColor("_DiffuseColor"), baseMat.GetColor("_ShadowDiffuseColor")); + g.viewCameraOverride = camTop; rt.viewCullEnabled = 0; g.Rebuild(); yield return null; yield return null; + L("[후 top 범위off] " + Shot(camTop, TS, TS, "iter1_after_top")); + g.viewCameraOverride = camPd; g.Rebuild(); yield return null; yield return null; + L("[후 PD 범위off] " + Shot(camPd, W, H, "iter1_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, "iter1_after_pd") + " | " + Stat() + " | " + Bench(camPd, 60).ToString("F3") + " ms/frame"); + L(" " + WL.Look.Farm.WLIslandGrass.LastLog); + + // ── 띠 안쪽 vs 가장자리 ─────────────────────────────────────── + rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; + g.viewCameraOverride = camTop; + var A = Grab(camTop, TS, TS); var apx = A.GetPixels32(); + rt.grassEnabled = 1; rt.viewCullEnabled = 0; g.Rebuild(); yield return null; yield return null; + var B = Grab(camTop, TS, TS); + L("[후] 보이는 풀 = " + Cover(apx, B.GetPixels32()).ToString("F2") + "% of 지면 (띠 안쪽에서 바닥과 같은 픽셀이면 낮다)"); + SetGrass(clone, BeforeDif, BeforeShd); g.Rebuild(); yield return null; yield return null; + var B0 = Grab(camTop, TS, TS); + L("[전] 보이는 풀 = " + Cover(apx, B0.GetPixels32()).ToString("F2") + "%"); + SetGrass(clone, baseMat.GetColor("_DiffuseColor"), baseMat.GetColor("_ShadowDiffuseColor")); + UnityEngine.Object.DestroyImmediate(A); UnityEngine.Object.DestroyImmediate(B); UnityEngine.Object.DestroyImmediate(B0); + + // ── 0 / 2 / 4 s 연속 — 좌표 집합이 그대로인가 ───────────────── + rt.viewCullEnabled = 1; g.viewCameraOverride = camPd; g.Rebuild(); + 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 + "/iter1_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)" : " 🔴 달라졌다"))); + } + + // ── 카메라를 움직였을 때만 바뀌는가 ─────────────────────────── + var p0 = camPd.transform.position; + L("카메라 이동 전 인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 지문=" + WL.Look.Farm.WLIslandGrass.InstanceKey); + camPd.transform.position = p0 + new UnityEngine.Vector3(6f, 0f, 6f); + L(" 움직임 판정(2.5 m 보폭) = " + g.ViewMovedEnough()); + g.RefreshView(); yield return null; yield return null; + L(" 이동 후 인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 지문=" + WL.Look.Farm.WLIslandGrass.InstanceKey + " · " + WL.Look.Farm.WLIslandGrass.LastLog); + camPd.transform.position = p0; g.RefreshView(); yield return null; yield return null; + L(" 되돌린 뒤 인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances + " 지문=" + WL.Look.Farm.WLIslandGrass.InstanceKey + (WL.Look.Farm.WLIslandGrass.InstanceKey == k0 ? " → 처음과 동일(격자 고정 확인)" : "")); + + g.viewCameraOverride = null; + Flush(); + } + + static void SetGrass(UnityEngine.Material m, UnityEngine.Color dif, UnityEngine.Color shd) + { + m.SetColor("_DiffuseColor", dif); m.SetColor("_ShadowDiffuseColor", shd); + } + + static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) + { + int n = 0, tot = 0; + for (int i = 0; i < a.Length; i++) + { + var p = a[i]; if (!(p.g > p.b + 8 && p.g > 45)) continue; tot++; + var q = b[i]; if (q.g <= q.b + 4) continue; + if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; + } + return tot == 0 ? 0f : 100f * n / tot; + } + + 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("띠세기LF={0:F2} 포기잡음HF={1:F2} LF/HF={2:F2} 띠단계={3} 에지%={4:F2} 평균밝기={5:F1}", + lf, hf / hfn, lf / System.Math.Max(0.01, hf / hfn), levels, 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_VERIFY.txt", sb.ToString()); + UnityEngine.Debug.Log("[816q verify]\n" + sb.ToString()); + } +} diff --git a/Assets/WL/Look/Farm/Materials/Farm_Grass_Arena.mat b/Assets/WL/Look/Farm/Materials/Farm_Grass_Arena.mat index dd0415f5c..5e9c48621 100644 --- a/Assets/WL/Look/Farm/Materials/Farm_Grass_Arena.mat +++ b/Assets/WL/Look/Farm/Materials/Farm_Grass_Arena.mat @@ -109,7 +109,7 @@ Material: - _CLOUDSENABLED: 1 - _ClearCoatMask: 0 - _ClearCoatSmoothness: 0 - - _Cloud_Change: 0.001 + - _Cloud_Change: 0.005 - _Cloud_Cover: 0.5 - _Cloud_Density: 0.01 - _Cloud_Strength: 1 @@ -147,12 +147,12 @@ Material: - _ZWrite: 1 m_Colors: - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - - _Cloud_Movement: {r: 0.2, g: 0.2, b: 0, a: 0} + - _Cloud_Movement: {r: 1, g: 1, b: 0, a: 0} - _Cloud_Step: {r: 13, g: 17, b: 0, a: 0} - _Color: {r: 1, g: 1, b: 1, a: 1} - - _DiffuseColor: {r: 0.60541177, g: 0.82788235, b: 0.5798824, a: 1} + - _DiffuseColor: {r: 0.650031, g: 0.926315, b: 0.659036, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - - _ShadowDiffuseColor: {r: 0.5434118, g: 0.744, b: 0.5215294, a: 1} + - _ShadowDiffuseColor: {r: 0.58346, g: 0.832462, b: 0.592718, a: 1} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} - _WindMovement: {r: 6, g: 0, b: 0, a: 0} m_BuildTextureStacks: [] diff --git a/Assets/WL/Look/Farm/Materials/Farm_IslandTop_Arena.mat b/Assets/WL/Look/Farm/Materials/Farm_IslandTop_Arena.mat index 17dd2d1dc..9b22245d7 100644 --- a/Assets/WL/Look/Farm/Materials/Farm_IslandTop_Arena.mat +++ b/Assets/WL/Look/Farm/Materials/Farm_IslandTop_Arena.mat @@ -56,7 +56,7 @@ Material: - _Blend: 0 - _Brightness: 0.25 - _CastShadows: 1 - - _Cloud_Change: 0.001 + - _Cloud_Change: 0.005 - _Cloud_Cover: 0.5 - _Cloud_Density: 0.01 - _Cloud_Strength: 1 @@ -80,7 +80,7 @@ Material: - _ZWrite: 1 - _ZWriteControl: 0 m_Colors: - - _Cloud_Movement: {r: 0.2, g: 0.2, b: 0, a: 0} + - _Cloud_Movement: {r: 1, g: 1, b: 0, a: 0} - _Cloud_Step: {r: 13, g: 17, b: 0, a: 0} - _DiffuseColor: {r: 1, g: 1, b: 1, a: 1} - _NormalBias: {r: 1, g: 1, b: 1, a: 0} diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index 67be40b7b..953497735 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -82,7 +82,7 @@ MonoBehaviour: mesh: {fileID: -4413095505993930501, guid: 1f298817fdd2a184480e8af5b21278bc, type: 3} material: {fileID: 2100000, guid: dc21f047088dee14f9b080798ebe8560, type: 2} probability: 100 - scale: 0.35 + scale: 0.7 normalOffset: 0.04 density: 2.5 - enabled_: 1 @@ -112,6 +112,12 @@ MonoBehaviour: maxInstances: 40000 rebuildPollSeconds: 1 rebuildDelaySeconds: 1.4 + viewCullEnabled: 1 + viewMargin: 4 + viewRebuildStep: 1.5 + viewRebuildAngle: 3 + viewMaxDistance: 40 + viewPollSeconds: 0.25 tileMasks: - meshName: 01 res: 64 diff --git a/Assets/WL/Look/Farm/WLIslandGrass.cs b/Assets/WL/Look/Farm/WLIslandGrass.cs index e01c504a6..c45e51f0b 100644 --- a/Assets/WL/Look/Farm/WLIslandGrass.cs +++ b/Assets/WL/Look/Farm/WLIslandGrass.cs @@ -38,6 +38,12 @@ namespace WL.Look.Farm public static float UsedDensity; public static string LastLog = ""; + // 816q — 가시 범위 배치 진단 + public static int ViewCulled, ViewRefreshes; + public static bool ViewCullOn; + /// 실제로 그린 인스턴스 좌표 집합의 지문. 두 프레임이 같으면 생성·소멸·이동이 0 이다. + public static ulong InstanceKey; + // 816o — 구름 경계 배치 진단 public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples; public static float EdgeFraction, EdgeCellSize, EdgeSampleCell, BakeMs, FilterMs; @@ -74,6 +80,33 @@ namespace WL.Look.Farm Rebuilds++; } + /// + /// 816q — 카메라가 움직인 만큼 **가시 범위만** 다시 고른다(타일 재수집 없음). + /// 좌표는 월드 고정 격자라 이미 보이던 풀은 제자리 그대로다 — 늘고 주는 것은 여유 폭 안에서만. + /// + public void RefreshView() + { + if (!_cacheValid || !isActiveAndEnabled) return; + if (cfg == null || cfg.viewCullEnabled == 0) return; + _fastRefresh = true; + enabled = false; + enabled = true; + _fastRefresh = false; + ViewRefreshes++; + } + + /// 카메라가 「다시 골라야 할 만큼」 움직였는지(위치 보폭 또는 회전 각). + public bool ViewMovedEnough() + { + if (cfg == null || cfg.viewCullEnabled == 0 || !_cacheValid) return false; + var cam = PickCamera(); + if (cam == null) return false; + if (!_viewOn) return true; // 처음 깔 때 카메라가 아직 없었다 → 이제 생겼으니 다시 고른다 + if ((cam.transform.position - _viewBuiltPos).sqrMagnitude + > Mathf.Max(0.05f, cfg.viewRebuildStep) * Mathf.Max(0.05f, cfg.viewRebuildStep)) return true; + return Vector3.Angle(cam.transform.forward, _viewBuiltFwd) > Mathf.Max(0.1f, cfg.viewRebuildAngle); + } + /// /// 816o — 구름이 흐른 만큼 **띠만** 다시 고른다. 타일·제외 사각형은 다시 훑지 않는다 /// (`FindObjectsByType` 를 타지 않으므로 전체 재생성보다 훨씬 싸다). @@ -289,6 +322,76 @@ namespace WL.Look.Farm float _budgetScale = 1f; bool _edgeOn; + // ── 816q — 가시 범위 ──────────────────────────────────────────── + /// 측정·검증용. 비우면 `Camera.main`(없으면 첫 활성 카메라)을 쓴다. + public Camera viewCameraOverride; + readonly Vector2[] _poly = new Vector2[4]; // 카메라가 바닥에서 보는 사다리꼴 + readonly Vector2[] _polyN = new Vector2[4]; // 변의 바깥 법선 + readonly float[] _polyD = new float[4]; + Vector3 _viewCamPos, _viewCamFwd; // 지금 심어 둔 절두체를 만든 카메라 자세 + Vector3 _viewBuiltPos, _viewBuiltFwd; // 마지막으로 **다시 고른** 시점의 카메라 자세 + bool _viewOn; + + Camera PickCamera() + { + if (viewCameraOverride != null) return viewCameraOverride; + var c = Camera.main; + if (c != null) return c; + var all = Object.FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < all.Length; i++) if (all[i].isActiveAndEnabled) return all[i]; + return null; + } + + /// + /// 카메라가 **바닥 평면에서** 보는 사다리꼴을 구해 각 변을 여유 폭만큼 밖으로 민다. + /// 🔴 절두체 평면을 그대로 밀면 안 된다 — 위쪽 평면은 바닥과 거의 나란해서(15°) + /// 8 m 밀면 바닥에서는 8/sin15° ≈ 31 m 나 뻗어 면적이 6 배가 된다(실측 618 → 3,731 ㎡). + /// 바닥 평면에서 2 차원으로 밀면 넓이가 목적대로 「둘레 × 여유」 만큼만 는다. + /// + bool TryViewPlanes() + { + var cam = PickCamera(); + if (cam == null) return false; + float y = _cacheY; + float maxD = Mathf.Max(1f, cfg.viewMaxDistance); + for (int i = 0; i < 4; i++) + { + // 뷰포트 모서리를 (0,0) → (1,0) → (1,1) → (0,1) 순서로 돈다(사다리꼴이 꼬이지 않게) + float vx = (i == 1 || i == 2) ? 1f : 0f; + float vy = (i >= 2) ? 1f : 0f; + var ray = cam.ViewportPointToRay(new Vector3(vx, vy, 0f)); + float t = maxD; + if (ray.direction.y < -1e-4f) t = Mathf.Min((y - ray.origin.y) / ray.direction.y, maxD); + var h = ray.origin + ray.direction * t; + _poly[i] = new Vector2(h.x, h.z); + } + // 변마다 바깥 법선. 사다리꼴의 감김 방향은 카메라 자세에 따라 뒤집히므로 부호를 맞춘다 + var c2 = Vector2.zero; + for (int i = 0; i < 4; i++) c2 += _poly[i]; + c2 *= 0.25f; + for (int i = 0; i < 4; i++) + { + var a = _poly[i]; var bb = _poly[(i + 1) & 3]; + var e = bb - a; + var n = new Vector2(e.y, -e.x); + if (n.sqrMagnitude < 1e-8f) n = new Vector2(1f, 0f); + n.Normalize(); + if (Vector2.Dot(n, a - c2) < 0f) n = -n; // 중심 반대쪽 = 바깥 + _polyN[i] = n; _polyD[i] = Vector2.Dot(n, a); + } + _viewCamPos = cam.transform.position; + _viewCamFwd = cam.transform.forward; + return true; + } + + bool InView(float x, float z) + { + float m = Mathf.Max(0f, cfg.viewMargin); + var p = new Vector2(x, z); + for (int i = 0; i < 4; i++) if (Vector2.Dot(_polyN[i], p) - _polyD[i] > m) return false; + return true; + } + void BakeField() { float cell = Mathf.Max(0.05f, cfg.cloudEdgeCell); @@ -315,7 +418,16 @@ namespace WL.Look.Farm BakeField(); } - Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0; + Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0; ViewCulled = 0; + + // 816q — 카메라가 보는 범위(+여유 폭)만 남긴다. 좌표는 손대지 않는다(순간이동 0). + _viewOn = false; + if (cfg.viewCullEnabled != 0 && TryViewPlanes()) + { + _viewOn = true; + _viewBuiltPos = _viewCamPos; _viewBuiltFwd = _viewCamFwd; + } + ViewCullOn = _viewOn; var sw = System.Diagnostics.Stopwatch.StartNew(); var buckets = new List[_cdef.Length]; @@ -323,9 +435,11 @@ namespace WL.Look.Farm Vector3 bc = _bounds.center; bool edge = _edgeOn; + ulong key = 0UL; for (int i = 0; i < _candCount; i++) { var c = _cand[i]; + if (_viewOn && !InView(c.x, c.z)) { ViewCulled++; continue; } if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; } var rot = c.yaw != 0f ? Quaternion.Euler(0f, c.yaw, 0f) : Quaternion.identity; @@ -334,7 +448,12 @@ namespace WL.Look.Farm trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity, new Vector3(def.scale, def.scale, def.scale)); buckets[c.def].Add(new InstanceData { TRS = trs, Normal = Vector3.up }); + // 좌표 지문 — 순서에 무관하게 더한다(집합이 같으면 값도 같다) + key += (ulong)Mathf.RoundToInt(c.x * 1000f) * 73856093UL + ^ (ulong)Mathf.RoundToInt(c.z * 1000f) * 19349663UL + ^ (ulong)(c.def + 1) * 83492791UL; } + InstanceKey = key; sw.Stop(); FilterMs = (float)sw.Elapsed.TotalMilliseconds; @@ -352,6 +471,9 @@ namespace WL.Look.Farm + " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded + " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)" + (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "") + + (_viewOn + ? " · 가시범위 절두체(여유 " + cfg.viewMargin.ToString("F1") + "m) · 범위밖 제외 " + ViewCulled + : " · 가시범위 off") + (_edgeOn ? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2") + "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2") diff --git a/Assets/WL/Look/Farm/WLIslandLook.cs b/Assets/WL/Look/Farm/WLIslandLook.cs index 3ac11ebea..9e9793be6 100644 --- a/Assets/WL/Look/Farm/WLIslandLook.cs +++ b/Assets/WL/Look/Farm/WLIslandLook.cs @@ -167,10 +167,24 @@ namespace WL.Look.Farm { 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) { - yield return new WaitForSeconds(cfg.rebuildPollSeconds); + // 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(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) diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index 48e724362..c5459a8d5 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -311,6 +311,32 @@ namespace WL.Look.Farm "바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")] public Material cloudEdgeSourceMaterial; + // ───────────────────────────────── §3 가시 범위 배치 (816q) + [Header("§3 — 카메라가 보는 범위에만 인스턴스를 둔다 (816q · 배치가 아니라 범위로 최적화)")] + [Tooltip("🔴 되돌리기 스위치 — 0 이면 섬 전체에 인스턴스를 둔다(816n 상태). " + + "1 이면 카메라 시야 사각형 + 여유 폭 안에만 둔다. " + + "격자는 월드 고정이라 같은 자리의 풀은 항상 같은 자리다(순간이동 0) — " + + "늘고 주는 것은 **화면 밖 여유 폭 안에서만** 일어난다.")] + public int viewCullEnabled = 1; + + [Tooltip("시야 사각형 바깥으로 더 두는 여유 폭(m). " + + "🔴 `viewRebuildStep` 보다 커야 한다 — 차이만큼이 「화면 밖에서만 바뀐다」는 보장 폭이다.")] + public float viewMargin = 4f; + + [Tooltip("카메라가 이만큼(m) 움직였을 때만 다시 고른다. " + + "작을수록 자주 고르고, 클수록 여유 폭을 많이 먹는다.")] + public float viewRebuildStep = 1.5f; + + [Tooltip("카메라가 이만큼(°) 돌았을 때도 다시 고른다. 회전은 시야를 통째로 돌리므로 위치보다 민감하다.")] + public float viewRebuildAngle = 3f; + + [Tooltip("시야 사다리꼴의 먼 쪽을 이 거리(m)에서 끊는다. 하늘을 향하는 모서리도 여기로 자른다. " + + "PD 구도(12 m·45°)에서 화면 위끝의 바닥은 약 32 m 라 40 이면 남는다.")] + public float viewMaxDistance = 40f; + + [Tooltip("시야 사각형을 다시 재는 주기(초). 0 이면 감시 안 함(한 번 고르고 고정).")] + public float viewPollSeconds = 0.25f; + // ───────────────────────────────── 구운 마스크 [Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")] public WLTileMask[] tileMasks = new WLTileMask[0];