Project_WL/AgentScripts/WL816q_Final.cs

268 lines
16 KiB
C#
Raw Permalink Normal View History

// 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());
}
}