Merge branch 'wl/gameplay/WL-816q-grass-demo-exact'

This commit is contained in:
깃 관리자 2026-09-15 08:39:51 +09:00
commit 32ec4ea192
16 changed files with 1885 additions and 9 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());
}
}

140
AgentScripts/WL816q_Iter.cs Normal file
View File

@ -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<WL816q_IterRunner>();
}
}
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<WL.Look.Farm.WLIslandGrass>(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<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_ITER.txt", sb.ToString());
UnityEngine.Debug.Log("[816q iter]\n" + sb.ToString());
}
}

268
AgentScripts/WL816q_Play.cs Normal file
View File

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

141
AgentScripts/WL816q_Scan.cs Normal file
View File

@ -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<WL816q_ScanRunner>();
}
}
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<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 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<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_SCAN.txt", sb.ToString());
UnityEngine.Debug.Log("[816q scan]\n" + sb.ToString());
}
}

View File

@ -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<WL816q_Scan2Runner>();
}
}
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<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 + "_s2";
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;
}
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<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_SCAN2.txt", sb.ToString());
UnityEngine.Debug.Log("[816q scan2]\n" + sb.ToString());
}
}

View File

@ -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<WL.Look.Farm.WLIslandGrass>(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());
}
}

View File

@ -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<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 · 밝기 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<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());
}
}

175
AgentScripts/WL816q_Tune.cs Normal file
View File

@ -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<WL816q_TuneRunner>();
}
}
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<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 + "_tune";
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;
}
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<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_TUNE.txt", sb.ToString());
UnityEngine.Debug.Log("[816q tune]\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

@ -109,7 +109,7 @@ Material:
- _CLOUDSENABLED: 1 - _CLOUDSENABLED: 1
- _ClearCoatMask: 0 - _ClearCoatMask: 0
- _ClearCoatSmoothness: 0 - _ClearCoatSmoothness: 0
- _Cloud_Change: 0.001 - _Cloud_Change: 0.005
- _Cloud_Cover: 0.5 - _Cloud_Cover: 0.5
- _Cloud_Density: 0.01 - _Cloud_Density: 0.01
- _Cloud_Strength: 1 - _Cloud_Strength: 1
@ -147,12 +147,12 @@ Material:
- _ZWrite: 1 - _ZWrite: 1
m_Colors: m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1} - _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} - _Cloud_Step: {r: 13, g: 17, b: 0, a: 0}
- _Color: {r: 1, g: 1, b: 1, a: 1} - _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} - _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} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
- _WindMovement: {r: 6, g: 0, b: 0, a: 0} - _WindMovement: {r: 6, g: 0, b: 0, a: 0}
m_BuildTextureStacks: [] m_BuildTextureStacks: []

View File

@ -56,7 +56,7 @@ Material:
- _Blend: 0 - _Blend: 0
- _Brightness: 0.25 - _Brightness: 0.25
- _CastShadows: 1 - _CastShadows: 1
- _Cloud_Change: 0.001 - _Cloud_Change: 0.005
- _Cloud_Cover: 0.5 - _Cloud_Cover: 0.5
- _Cloud_Density: 0.01 - _Cloud_Density: 0.01
- _Cloud_Strength: 1 - _Cloud_Strength: 1
@ -80,7 +80,7 @@ Material:
- _ZWrite: 1 - _ZWrite: 1
- _ZWriteControl: 0 - _ZWriteControl: 0
m_Colors: 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} - _Cloud_Step: {r: 13, g: 17, b: 0, a: 0}
- _DiffuseColor: {r: 1, g: 1, b: 1, a: 1} - _DiffuseColor: {r: 1, g: 1, b: 1, a: 1}
- _NormalBias: {r: 1, g: 1, b: 1, a: 0} - _NormalBias: {r: 1, g: 1, b: 1, a: 0}

View File

@ -82,7 +82,7 @@ MonoBehaviour:
mesh: {fileID: -4413095505993930501, guid: 1f298817fdd2a184480e8af5b21278bc, type: 3} mesh: {fileID: -4413095505993930501, guid: 1f298817fdd2a184480e8af5b21278bc, type: 3}
material: {fileID: 2100000, guid: dc21f047088dee14f9b080798ebe8560, type: 2} material: {fileID: 2100000, guid: dc21f047088dee14f9b080798ebe8560, type: 2}
probability: 100 probability: 100
scale: 0.35 scale: 0.7
normalOffset: 0.04 normalOffset: 0.04
density: 2.5 density: 2.5
- enabled_: 1 - enabled_: 1
@ -112,6 +112,12 @@ MonoBehaviour:
maxInstances: 40000 maxInstances: 40000
rebuildPollSeconds: 1 rebuildPollSeconds: 1
rebuildDelaySeconds: 1.4 rebuildDelaySeconds: 1.4
viewCullEnabled: 1
viewMargin: 4
viewRebuildStep: 1.5
viewRebuildAngle: 3
viewMaxDistance: 40
viewPollSeconds: 0.25
tileMasks: tileMasks:
- meshName: 01 - meshName: 01
res: 64 res: 64

View File

@ -38,6 +38,12 @@ namespace WL.Look.Farm
public static float UsedDensity; public static float UsedDensity;
public static string LastLog = ""; public static string LastLog = "";
// 816q — 가시 범위 배치 진단
public static int ViewCulled, ViewRefreshes;
public static bool ViewCullOn;
/// <summary>실제로 그린 인스턴스 좌표 집합의 지문. 두 프레임이 같으면 생성·소멸·이동이 0 이다.</summary>
public static ulong InstanceKey;
// 816o — 구름 경계 배치 진단 // 816o — 구름 경계 배치 진단
public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples; public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples;
public static float EdgeFraction, EdgeCellSize, EdgeSampleCell, BakeMs, FilterMs; public static float EdgeFraction, EdgeCellSize, EdgeSampleCell, BakeMs, FilterMs;
@ -74,6 +80,33 @@ namespace WL.Look.Farm
Rebuilds++; Rebuilds++;
} }
/// <summary>
/// 816q — 카메라가 움직인 만큼 **가시 범위만** 다시 고른다(타일 재수집 없음).
/// 좌표는 월드 고정 격자라 이미 보이던 풀은 제자리 그대로다 — 늘고 주는 것은 여유 폭 안에서만.
/// </summary>
public void RefreshView()
{
if (!_cacheValid || !isActiveAndEnabled) return;
if (cfg == null || cfg.viewCullEnabled == 0) return;
_fastRefresh = true;
enabled = false;
enabled = true;
_fastRefresh = false;
ViewRefreshes++;
}
/// <summary>카메라가 「다시 골라야 할 만큼」 움직였는지(위치 보폭 또는 회전 각).</summary>
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);
}
/// <summary> /// <summary>
/// 816o — 구름이 흐른 만큼 **띠만** 다시 고른다. 타일·제외 사각형은 다시 훑지 않는다 /// 816o — 구름이 흐른 만큼 **띠만** 다시 고른다. 타일·제외 사각형은 다시 훑지 않는다
/// (`FindObjectsByType` 를 타지 않으므로 전체 재생성보다 훨씬 싸다). /// (`FindObjectsByType` 를 타지 않으므로 전체 재생성보다 훨씬 싸다).
@ -289,6 +322,76 @@ namespace WL.Look.Farm
float _budgetScale = 1f; float _budgetScale = 1f;
bool _edgeOn; bool _edgeOn;
// ── 816q — 가시 범위 ────────────────────────────────────────────
/// <summary>측정·검증용. 비우면 `Camera.main`(없으면 첫 활성 카메라)을 쓴다.</summary>
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<Camera>(FindObjectsSortMode.None);
for (int i = 0; i < all.Length; i++) if (all[i].isActiveAndEnabled) return all[i];
return null;
}
/// <summary>
/// 카메라가 **바닥 평면에서** 보는 사다리꼴을 구해 각 변을 여유 폭만큼 밖으로 민다.
/// 🔴 절두체 평면을 그대로 밀면 안 된다 — 위쪽 평면은 바닥과 거의 나란해서(15°)
/// 8 m 밀면 바닥에서는 8/sin15° ≈ 31 m 나 뻗어 면적이 6 배가 된다(실측 618 → 3,731 ㎡).
/// 바닥 평면에서 2 차원으로 밀면 넓이가 목적대로 「둘레 × 여유」 만큼만 는다.
/// </summary>
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() void BakeField()
{ {
float cell = Mathf.Max(0.05f, cfg.cloudEdgeCell); float cell = Mathf.Max(0.05f, cfg.cloudEdgeCell);
@ -315,7 +418,16 @@ namespace WL.Look.Farm
BakeField(); 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 sw = System.Diagnostics.Stopwatch.StartNew();
var buckets = new List<InstanceData>[_cdef.Length]; var buckets = new List<InstanceData>[_cdef.Length];
@ -323,9 +435,11 @@ namespace WL.Look.Farm
Vector3 bc = _bounds.center; Vector3 bc = _bounds.center;
bool edge = _edgeOn; bool edge = _edgeOn;
ulong key = 0UL;
for (int i = 0; i < _candCount; i++) for (int i = 0; i < _candCount; i++)
{ {
var c = _cand[i]; var c = _cand[i];
if (_viewOn && !InView(c.x, c.z)) { ViewCulled++; continue; }
if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; } if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; }
var rot = c.yaw != 0f ? Quaternion.Euler(0f, c.yaw, 0f) : Quaternion.identity; 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, trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity,
new Vector3(def.scale, def.scale, def.scale)); new Vector3(def.scale, def.scale, def.scale));
buckets[c.def].Add(new InstanceData { TRS = trs, Normal = Vector3.up }); 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(); sw.Stop();
FilterMs = (float)sw.Elapsed.TotalMilliseconds; FilterMs = (float)sw.Elapsed.TotalMilliseconds;
@ -352,6 +471,9 @@ namespace WL.Look.Farm
+ " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded + " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded
+ " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)" + " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)"
+ (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "") + (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "")
+ (_viewOn
? " · 가시범위 절두체(여유 " + cfg.viewMargin.ToString("F1") + "m) · 범위밖 제외 " + ViewCulled
: " · 가시범위 off")
+ (_edgeOn + (_edgeOn
? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2") ? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2")
+ "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2") + "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2")

View File

@ -167,10 +167,24 @@ namespace WL.Look.Farm
{ {
int last = CountUnlocked(scene); int last = CountUnlocked(scene);
float nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds); 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) 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 (!scene.isLoaded) yield break;
if (cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f && Time.time >= nextView)
{
nextView = Time.time + cfg.viewPollSeconds;
var gv = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (gv != null && gv.ViewMovedEnough()) gv.RefreshView();
}
if (Time.time < nextPoll) continue;
nextPoll = Time.time + cfg.rebuildPollSeconds;
HookIslands(cfg, scene); HookIslands(cfg, scene);
int now = CountUnlocked(scene); int now = CountUnlocked(scene);
if (now != last) if (now != last)

View File

@ -311,6 +311,32 @@ namespace WL.Look.Farm
"바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")] "바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")]
public Material cloudEdgeSourceMaterial; 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 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")] [Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")]
public WLTileMask[] tileMasks = new WLTileMask[0]; public WLTileMask[] tileMasks = new WLTileMask[0];