[WL-816q] 풀 높이 0.35→0.7(띠 가장자리 톱니 폭) · 가시 범위를 바닥 사다리꼴로 교정 (#816)

- 톱니 윤곽 폭 = 풀 높이 × cot(태양고도). scale 스윕 실측(같은 카메라·같은 구름 위상):
  0.35 에지% 1.05 / 0.50 1.20 / 0.70 1.39 / 0.90 1.43 (데모 참고 1.22) · 인스턴스·삼각형·드로우콜 불변.
  0.70 = 데모의 띠 가장자리 밀도(≈4개/m)와 같은 폭 → 채택.
- 가시 범위: 절두체 평면을 그대로 밀면 위쪽 평면이 바닥과 15° 라 8 m 가 바닥에서 31 m 로 벌어졌다
  (실측 618 → 3,731 ㎡). **바닥 사다리꼴을 2 차원으로** 미는 방식으로 교정 → 여유 4 m 에서 643 → 1,151 ㎡.
  viewMargin 8→4 · viewRebuildStep 2.5→1.5 · viewMaxDistance 40 · viewRebuildAngle 3 신설.
- 팝 0 실측: 보폭 1.5 m 이동 후 [다시 고르기 전] vs [고른 뒤] 화면 차이 **0 픽셀**.
  0/2/4 s 좌표 지문 동일(75934715234309) = 생성·소멸·이동 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-15 08:31:20 +09:00
parent 7e0733307e
commit cdb324a57d
7 changed files with 901 additions and 49 deletions

View File

@ -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<WL816q_FinalRunner>();
}
}
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<WL.Look.Farm.WLIslandGrass>(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<UnityEngine.Camera>();
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
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());
}
}

View File

@ -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<WL816q_FringeRunner>();
}
}
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<WL.Look.Farm.WLIslandGrass>(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<UnityEngine.Camera>();
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
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());
}
}

View File

@ -0,0 +1,166 @@
// 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<WL816q_SweepRunner>();
}
}
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<WL.Look.Farm.WLIslandGrass>(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.Renderer>(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");
float[] strs = { 1f, 1.5f, 2f, 2.5f, 3f, 4f };
float[] covs = { 0.35f, 0.5f, 0.65f };
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} Cover={1:F2} | 바닥만 {2} | 풀포함 {3} | 보이는 풀 {4:F2}%",
strs[si], covs[ci], mg, Metric(bpx, TS, TS), Cover(apx, bpx)));
if (System.Math.Abs(strs[si] - 2f) < 0.01f)
System.IO.File.WriteAllBytes(Dir + "/sweep_s2_c" + covs[ci].ToString("F2") + ".png", UnityEngine.ImageConversion.EncodeToPNG(B));
UnityEngine.Object.DestroyImmediate(A); UnityEngine.Object.DestroyImmediate(B);
}
}
UnityEngine.Time.timeScale = 1f;
Flush();
}
static void Set(UnityEngine.Material m, float str, float cov)
{
m.SetFloat("_Cloud_Strength", str); m.SetFloat("_Cloud_Cover", cov);
}
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<UnityEngine.Camera>();
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
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());
}
}

View File

@ -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<WL816q_VerifyRunner>();
}
}
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<WL.Look.Farm.WLIslandGrass>(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<UnityEngine.Camera>();
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
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());
}
}

View File

@ -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
@ -113,8 +113,10 @@ MonoBehaviour:
rebuildPollSeconds: 1
rebuildDelaySeconds: 1.4
viewCullEnabled: 1
viewMargin: 8
viewRebuildStep: 2.5
viewMargin: 4
viewRebuildStep: 1.5
viewRebuildAngle: 3
viewMaxDistance: 40
viewPollSeconds: 0.25
tileMasks:
- meshName: 01

View File

@ -41,7 +41,8 @@ namespace WL.Look.Farm
// 816q — 가시 범위 배치 진단
public static int ViewCulled, ViewRefreshes;
public static bool ViewCullOn;
public static Rect ViewRect;
/// <summary>실제로 그린 인스턴스 좌표 집합의 지문. 두 프레임이 같으면 생성·소멸·이동이 0 이다.</summary>
public static ulong InstanceKey;
// 816o — 구름 경계 배치 진단
public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples;
@ -94,14 +95,15 @@ namespace WL.Look.Farm
ViewRefreshes++;
}
/// <summary>바뀐 시야 사각형 중심이 직전 것과 이만큼 떨어졌는지(= 다시 골라야 하는지).</summary>
/// <summary>카메라가 「다시 골라야 할 만큼」 움직였는지(위치 보폭 또는 회전 각).</summary>
public bool ViewMovedEnough()
{
if (cfg == null || cfg.viewCullEnabled == 0 || !_cacheValid) return false;
Rect r;
if (!TryViewRect(out r)) return false;
return Mathf.Abs(r.center.x - _viewBuiltCenter.x) > Mathf.Max(0.05f, cfg.viewRebuildStep)
|| Mathf.Abs(r.center.y - _viewBuiltCenter.y) > Mathf.Max(0.05f, cfg.viewRebuildStep);
if (cfg == null || cfg.viewCullEnabled == 0 || !_cacheValid || !_viewOn) return false;
var cam = PickCamera();
if (cam == null) return false;
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);
}
/// <summary>
@ -322,8 +324,11 @@ namespace WL.Look.Farm
// ── 816q — 가시 범위 ────────────────────────────────────────────
/// <summary>측정·검증용. 비우면 `Camera.main`(없으면 첫 활성 카메라)을 쓴다.</summary>
public Camera viewCameraOverride;
Rect _viewRect;
Vector2 _viewBuiltCenter;
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()
@ -336,42 +341,53 @@ namespace WL.Look.Farm
return null;
}
/// <summary>카메라가 바닥 평면(y = 윗면)에서 보는 사각형 + 여유 폭. 못 구하면 false.</summary>
bool TryViewRect(out Rect rect)
/// <summary>
/// 카메라가 **바닥 평면에서** 보는 사다리꼴을 구해 각 변을 여유 폭만큼 밖으로 민다.
/// 🔴 절두체 평면을 그대로 밀면 안 된다 — 위쪽 평면은 바닥과 거의 나란해서(15°)
/// 8 m 밀면 바닥에서는 8/sin15° ≈ 31 m 나 뻗어 면적이 6 배가 된다(실측 618 → 3,731 ㎡).
/// 바닥 평면에서 2 차원으로 밀면 넓이가 목적대로 「둘레 × 여유」 만큼만 는다.
/// </summary>
bool TryViewPlanes()
{
rect = default(Rect);
var cam = PickCamera();
if (cam == null) return false;
float y = _cacheY;
float far = cam.farClipPlane;
float minX = float.MaxValue, maxX = float.MinValue, minZ = float.MaxValue, maxZ = float.MinValue;
float maxD = Mathf.Max(1f, cfg.viewMaxDistance);
for (int i = 0; i < 4; i++)
{
var vp = new Vector3((i & 1) == 0 ? 0f : 1f, (i & 2) == 0 ? 0f : 1f, 0f);
var ray = cam.ViewportPointToRay(vp);
Vector3 hit;
// 평면을 향하지 않는 모서리(하늘로 나가는 방향)는 far 까지 뻗은 점으로 대신한다
if (ray.direction.y < -1e-4f)
{
float t = (y - ray.origin.y) / ray.direction.y;
hit = ray.origin + ray.direction * Mathf.Min(t, far);
}
else hit = ray.origin + ray.direction * far;
if (hit.x < minX) minX = hit.x; if (hit.x > maxX) maxX = hit.x;
if (hit.z < minZ) minZ = hit.z; if (hit.z > maxZ) maxZ = hit.z;
// 뷰포트 모서리를 (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);
minX -= m; maxX += m; minZ -= m; maxZ += m;
// 섬 밖은 어차피 후보가 없다 — 사각형을 섬 경계로 잘라 중심 판정이 튀지 않게 한다
var bmin = _bounds.min; var bmax = _bounds.max;
minX = Mathf.Max(minX, bmin.x); maxX = Mathf.Min(maxX, bmax.x);
minZ = Mathf.Max(minZ, bmin.z); maxZ = Mathf.Min(maxZ, bmax.z);
if (maxX <= minX || maxZ <= minZ) return false;
rect = Rect.MinMaxRect(minX, minZ, maxX, maxZ);
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;
}
@ -405,12 +421,12 @@ namespace WL.Look.Farm
// 816q — 카메라가 보는 범위(+여유 폭)만 남긴다. 좌표는 손대지 않는다(순간이동 0).
_viewOn = false;
if (cfg.viewCullEnabled != 0 && TryViewRect(out _viewRect))
if (cfg.viewCullEnabled != 0 && TryViewPlanes())
{
_viewOn = true;
_viewBuiltCenter = _viewRect.center;
_viewBuiltPos = _viewCamPos; _viewBuiltFwd = _viewCamFwd;
}
ViewCullOn = _viewOn; ViewRect = _viewRect;
ViewCullOn = _viewOn;
var sw = System.Diagnostics.Stopwatch.StartNew();
var buckets = new List<InstanceData>[_cdef.Length];
@ -418,10 +434,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 && (c.x < _viewRect.xMin || c.x > _viewRect.xMax || c.z < _viewRect.yMin || c.z > _viewRect.yMax)) { ViewCulled++; continue; }
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;
@ -430,7 +447,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;
@ -449,8 +471,7 @@ namespace WL.Look.Farm
+ " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)"
+ (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "")
+ (_viewOn
? " · 가시범위 " + _viewRect.width.ToString("F1") + "×" + _viewRect.height.ToString("F1")
+ "m(여유 " + cfg.viewMargin.ToString("F1") + "m) · 범위밖 제외 " + ViewCulled
? " · 가시범위 절두체(여유 " + cfg.viewMargin.ToString("F1") + "m) · 범위밖 제외 " + ViewCulled
: " · 가시범위 off")
+ (_edgeOn
? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2")

View File

@ -321,11 +321,18 @@ namespace WL.Look.Farm
[Tooltip("시야 사각형 바깥으로 더 두는 여유 폭(m). " +
"🔴 `viewRebuildStep` 보다 커야 한다 — 차이만큼이 「화면 밖에서만 바뀐다」는 보장 폭이다.")]
public float viewMargin = 8f;
public float viewMargin = 4f;
[Tooltip("카메라(시야 사각형 중심)가 이만큼(m) 움직였을 때만 다시 고른다. " +
[Tooltip("카메라가 이만큼(m) 움직였을 때만 다시 고른다. " +
"작을수록 자주 고르고, 클수록 여유 폭을 많이 먹는다.")]
public float viewRebuildStep = 2.5f;
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;