// WL-816q 5회차 — 구름 세기를 올려 띠를 한 단 더 얻을 수 있는가. // 세기를 바꾸면 풀 색 일치가 깨지므로(3회차 실측) **세기마다 풀 색을 다시 수렴**시켜 비교한다. // 에셋 무변경(복제본) · 끝나면 원래 값으로 되돌린다. public static class WL816q_Tune { public static void Start() { var go = UnityEngine.GameObject.Find("~WL816qTune"); if (go != null) UnityEngine.Object.DestroyImmediate(go); go = new UnityEngine.GameObject("~WL816qTune"); go.AddComponent(); } } public class WL816q_TuneRunner : UnityEngine.MonoBehaviour { static System.Text.StringBuilder sb; const int TS = 512; static string Dir = "Screenshots_WL/WL816q"; void Start() { StartCoroutine(Co()); } System.Collections.IEnumerator Co() { sb = new System.Text.StringBuilder(); System.IO.Directory.CreateDirectory(Dir); if (UnityEngine.SceneManagement.SceneManager.sceneCount < 2) { var op = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Level01", UnityEngine.SceneManagement.LoadSceneMode.Additive); while (op != null && !op.isDone) yield return null; } for (int i = 0; i < 5; i++) yield return new UnityEngine.WaitForSeconds(1f); var g = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); if (g == null) { L("grass 없음"); Flush(); yield break; } var rt = UnityEngine.Object.Instantiate(WL.Look.Farm.WLIslandLookSettings.Instance); var arr = new WL.Look.Farm.WLScatterDef[rt.scatter.Length]; for (int i = 0; i < arr.Length; i++) { var s = rt.scatter[i]; arr[i] = new WL.Look.Farm.WLScatterDef { enabled_ = s.enabled_, label = s.label, mesh = s.mesh, material = s.material, probability = s.probability, scale = s.scale, normalOffset = s.normalOffset, density = s.density }; } rt.scatter = arr; g.cfg = rt; rt.viewCullEnabled = 0; var topAsset = rt.islandTopMaterial; var topClone = new UnityEngine.Material(topAsset); topClone.name = topAsset.name + "_tune"; var rends = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); for (int i = 0; i < rends.Length; i++) { var ms = rends[i].sharedMaterials; bool hit = false; for (int m = 0; m < ms.Length; m++) if (ms[m] == topAsset) { ms[m] = topClone; hit = true; } if (hit) rends[i].sharedMaterials = ms; } var grassAsset = arr[0].material; var gc = new UnityEngine.Material(grassAsset); gc.name = grassAsset.name + "_tune"; for (int i = 0; i < arr.Length; i++) if (arr[i].material == grassAsset) arr[i].material = gc; var dif0 = grassAsset.GetColor("_DiffuseColor"); var shd0 = grassAsset.GetColor("_ShadowDiffuseColor"); var b = g.CalculateInstancesBounds(); var cam = MakeTop(b.center.x, b.center.z, 6f); UnityEngine.Time.timeScale = 0f; yield return null; L("데모 참고 : 띠단계 8 · LF 16.27 · HF 0.33 · 에지% 1.22 · 밝기 174.2"); float[] strs = { 1.0f, 1.2f, 1.4f, 1.6f }; for (int s = 0; s < strs.Length; s++) { topClone.SetFloat("_Cloud_Strength", strs[s]); gc.SetFloat("_Cloud_Strength", strs[s]); rt.grassEnabled = 0; g.Rebuild(); yield return null; yield return null; var A = Grab(cam, TS, TS); var apx = A.GetPixels32(); string gm = Metric(apx, TS, TS); rt.grassEnabled = 1; // 이 세기에서 풀이 바닥에 녹아드는 배율 k 를 0.90~1.30 에서 0.01 씩 찾는다 float bestK = 1f, bestC = 999f; string prof = ""; for (int i = 0; i <= 40; i++) { float k = 0.90f + i * 0.01f; gc.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * k, dif0.g * k, dif0.b * k, dif0.a)); gc.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * k, shd0.g * k, shd0.b * k, shd0.a)); g.Rebuild(); yield return null; var B = Grab(cam, TS, TS); float c = Cover(apx, B.GetPixels32()); UnityEngine.Object.DestroyImmediate(B); if (c < bestC) { bestC = c; bestK = k; } if (c < 5f) prof += k.ToString("F2") + "=" + c.ToString("F1") + " "; } gc.SetColor("_DiffuseColor", new UnityEngine.Color(dif0.r * bestK, dif0.g * bestK, dif0.b * bestK, dif0.a)); gc.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(shd0.r * bestK, shd0.g * bestK, shd0.b * bestK, shd0.a)); g.Rebuild(); yield return null; yield return null; var F = Grab(cam, TS, TS); L(string.Format("Strength={0:F1} | 바닥만 {1} | 최적 k={2:F2} 보이는 풀 {3:F2}% | 풀포함 {4}\n 낮은 구간: {5}", strs[s], gm, bestK, bestC, Metric(F.GetPixels32(), TS, TS), prof.Length > 0 ? prof : "없음")); System.IO.File.WriteAllBytes(Dir + "/tune_s" + strs[s].ToString("F1") + ".png", UnityEngine.ImageConversion.EncodeToPNG(F)); UnityEngine.Object.DestroyImmediate(F); UnityEngine.Object.DestroyImmediate(A); } topClone.SetFloat("_Cloud_Strength", topAsset.GetFloat("_Cloud_Strength")); gc.SetFloat("_Cloud_Strength", grassAsset.GetFloat("_Cloud_Strength")); gc.SetColor("_DiffuseColor", dif0); gc.SetColor("_ShadowDiffuseColor", shd0); g.Rebuild(); UnityEngine.Time.timeScale = 1f; Flush(); } static float Cover(UnityEngine.Color32[] a, UnityEngine.Color32[] b) { int n = 0, tot = 0; for (int i = 0; i < a.Length; i++) { var p = a[i]; if (!Gr(p)) continue; tot++; var q = b[i]; if (q.g <= q.b + 4) continue; if (System.Math.Abs(p.r - q.r) + System.Math.Abs(p.g - q.g) + System.Math.Abs(p.b - q.b) > 6) n++; } return tot == 0 ? 0f : 100f * n / tot; } static string Metric(UnityEngine.Color32[] px, int w, int h) { int bs = 8, bw = w / bs, bh = h / bs; var bm = new float[bw * bh]; var bok = new bool[bw * bh]; double hf = 0; int hfn = 0; for (int by = 0; by < bh; by++) for (int bx = 0; bx < bw; bx++) { double s = 0; int n = 0; for (int y = 0; y < bs; y++) for (int x = 0; x < bs; x++) { var p = px[(by * bs + y) * w + (bx * bs + x)]; if (!Gr(p)) continue; s += Lum(p); n++; } if (n < bs * bs / 2) continue; float mean = (float)(s / n); bm[by * bw + bx] = mean; bok[by * bw + bx] = true; for (int y = 0; y < bs; y++) for (int x = 0; x < bs; x++) { var p = px[(by * bs + y) * w + (bx * bs + x)]; if (!Gr(p)) continue; hf += System.Math.Abs(Lum(p) - mean); hfn++; } } double s1 = 0, s2 = 0; int bn = 0; var lv = new int[40]; for (int i = 0; i < bm.Length; i++) { if (!bok[i]) continue; s1 += bm[i]; s2 += (double)bm[i] * bm[i]; bn++; int q = (int)(bm[i] / 8f); if (q >= 0 && q < 40) lv[q]++; } if (bn == 0 || hfn == 0) return "지면 0"; double mu = s1 / bn, lf = System.Math.Sqrt(System.Math.Max(0, s2 / bn - mu * mu)); int levels = 0; for (int i = 0; i < 40; i++) if (lv[i] >= bn * 0.005) levels++; int ed = 0, edn = 0; for (int y = 0; y < h - 1; y++) for (int x = 0; x < w - 1; x++) { var p = px[y * w + x]; if (!Gr(p)) continue; var r = px[y * w + x + 1]; var d = px[(y + 1) * w + x]; edn++; if ((Gr(r) && System.Math.Abs(Lum(p) - Lum(r)) > 8) || (Gr(d) && System.Math.Abs(Lum(p) - Lum(d)) > 8)) ed++; } return string.Format("단계={0} LF={1:F2} HF={2:F2} 에지%={3:F2} 밝기={4:F1}", levels, lf, hf / hfn, 100.0 * ed / System.Math.Max(1, edn), mu); } static bool Gr(UnityEngine.Color32 p) { return p.g > p.b + 8 && p.g > 45; } static float Lum(UnityEngine.Color32 p) { return 0.299f * p.r + 0.587f * p.g + 0.114f * p.b; } static UnityEngine.Camera MakeTop(float cx, float cz, float size) { var go = UnityEngine.GameObject.Find("~816qTop"); if (go == null) go = new UnityEngine.GameObject("~816qTop"); var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.orthographic = true; c.orthographicSize = size; c.nearClipPlane = 0.1f; c.farClipPlane = 200f; c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f); c.transform.position = new UnityEngine.Vector3(cx, 60f, cz); c.enabled = false; return c; } static UnityEngine.Texture2D Grab(UnityEngine.Camera cam, int w, int h) { var rt = new UnityEngine.RenderTexture(w, h, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB); rt.Create(); cam.targetTexture = rt; cam.Render(); var prev = UnityEngine.RenderTexture.active; UnityEngine.RenderTexture.active = rt; var tex = new UnityEngine.Texture2D(w, h, UnityEngine.TextureFormat.RGB24, false); tex.ReadPixels(new UnityEngine.Rect(0, 0, w, h), 0, 0); tex.Apply(); UnityEngine.RenderTexture.active = prev; cam.targetTexture = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); return tex; } static void L(string s) { sb.AppendLine(s); Flush(); } static void Flush() { System.IO.File.WriteAllText("AgentScripts/WL816q_TUNE.txt", sb.ToString()); UnityEngine.Debug.Log("[816q tune]\n" + sb.ToString()); } }