Project_WL/AgentScripts/WL816q_Final.cs

268 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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