Project_WL/AgentScripts/WL816b_Play.cs

430 lines
25 KiB
C#
Raw Permalink 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-816b — 🔴 **Play 모드에서만** 재는 무기 측정/촬영 하니스 (814x2~x4 방식 그대로)
// 편집 모드 렌더는 스킨 메시가 갱신되지 않아 무의미하다(814x 실측).
// run_script --file AgentScripts/WL816b_Play.cs --entry WL816b.Probe (Play 중에 부른다)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using WL.Look.Arena;
using WL.Look.Character;
public static class WL816b
{
public const string RAW = "Screenshots_WL/WL816b/raw/";
const int W = 1080, H = 1920;
public const float ORTHO = 10f; // 🔴 게임 크기(814x~x4 와 동일 축)
const string WDIR = "Assets/Res_Addr/Weapon/";
static readonly StringBuilder sb = new StringBuilder();
static void L(string s) { sb.AppendLine(s); }
static GameObject s_root;
static Camera s_cam;
static Vector3 s_camPos, s_center, s_pos;
static Quaternion s_rot, s_camRot;
static Animator s_an;
static float s_camSize, s_ts;
static Transform[] s_sockets;
static readonly List<GameObject> s_spawned = new List<GameObject>();
static float s_charHeight;
// ─────────────────────────────────────────────────────────── 표: 무기 → 소켓 index (ClassConfig 실측)
public static readonly string[][] WEAPONS = new string[][] {
// 이름 소켓idx 클래스
new[]{ "PP_Theme_10_Mace_003", "1", "10501 기본" },
new[]{ "PP_Theme_10_Shield_003", "0", "10501 기본" },
new[]{ "Elven_Sword_01", "2", "10101/10102" },
new[]{ "PP_Theme_08_Mace_003", "1", "10502" },
new[]{ "PP_Theme_08_Shield_003", "0", "10502" },
new[]{ "PP_Theme_05_Bow_003", "6", "10402" },
new[]{ "PP_Theme_05_Bow_004", "6", "10401" },
new[]{ "PP_Theme_03_Scepter_001","1", "10303" },
new[]{ "PP_Theme_03_Wand_001", "1", "10302" },
new[]{ "Dagger1", "3", "10201/10202" },
new[]{ "Dagger2", "4", "10201/10202" },
new[]{ "Orb", "5", "10301" },
};
static void Setup()
{
s_root = GameObject.Find("PC_LH_M05");
if (s_root == null)
foreach (var g in UnityEngine.Object.FindObjectsByType<Animator>(FindObjectsSortMode.None))
if (g.name.Contains("LH_M05")) { s_root = g.gameObject; break; }
s_ts = Time.timeScale; Time.timeScale = 0f;
s_an = s_root.GetComponentInChildren<Animator>();
if (s_an != null) { s_an.speed = 0f; try { s_an.Play("idle", 0, 0.25f); s_an.Update(0f); } catch { } }
s_cam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsSortMode.None).First(c => c.orthographic);
s_camPos = s_cam.transform.position; s_camSize = s_cam.orthographicSize; s_camRot = s_cam.transform.rotation;
s_root.transform.rotation = Quaternion.Euler(0, s_cam.transform.eulerAngles.y + 180f, 0);
s_pos = s_root.transform.position; s_rot = s_root.transform.rotation;
// 🔴 키 = 소켓 아래(무기)를 뺀 몸 렌더러만. 814p `MeasureBodyLocalHeight` 와 같은 정의.
var body = s_root.GetComponentsInChildren<Renderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
var b = new Bounds(s_root.transform.position, Vector3.zero);
foreach (var r in body) b.Encapsulate(r.bounds);
s_center = b.center; s_charHeight = b.size.y;
// 소켓 — 이름으로 찾는다(LH_M05 본 계층)
string[] names = { "w_shield", "w_righthand", "w_onehand", "w_bladedancer_l", "w_bladedancer_r", "w_orb", "w_archer_l" };
s_sockets = new Transform[names.Length];
foreach (var t in s_root.GetComponentsInChildren<Transform>(true))
for (int i = 0; i < names.Length; i++)
if (s_sockets[i] == null && t.name == names[i]) s_sockets[i] = t;
L("root=" + s_root.name + " 키 " + s_charHeight.ToString("F4") + " m · center " + s_center.ToString("F2")
+ " · rootLossy " + s_root.transform.lossyScale.ToString("F4"));
for (int i = 0; i < names.Length; i++)
L(" socket[" + i + "] " + names[i] + " = " + (s_sockets[i] == null ? "없음" : s_sockets[i].name + " lossy " + s_sockets[i].lossyScale.ToString("F4")));
L(" renderers " + string.Join(",", body.Select(r => r.name)));
var pc = WLCharacterPaletteSettings.Instance;
L(" palette SO enabled=" + (pc == null ? "SO없음" : pc.enabled_.ToString()) + " rows=" + (pc == null || pc.rendererNames == null ? 0 : pc.rendererNames.Length));
}
static void Teardown()
{
Clear();
s_cam.transform.SetPositionAndRotation(s_camPos, s_camRot); s_cam.orthographicSize = s_camSize;
Time.timeScale = s_ts;
}
// ────────────────────────────────────────────────────────── 무기 부착 (814p 규칙: 키의 60 % · clamp 0.3~1.0)
public static GameObject Spawn(string weapon, int socketIdx, out float ratioBefore, out float mult)
{
ratioBefore = 0f; mult = 1f;
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(WDIR + weapon + ".prefab");
if (pf == null || s_sockets[socketIdx] == null) return null;
var go = UnityEngine.Object.Instantiate(pf, s_sockets[socketIdx]);
go.name = weapon + "(Clone)"; // 🔴 실제 게임과 같은 이름(DSUtil.Get_Clone) — 팔레트 표 매칭이 이름 기반이다
go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one;
s_spawned.Add(go);
// 길이 = 무기 루트 로컬 공간의 렌더러 localBounds 합 × lossyScale (814p 정의 · 포즈 무관)
var rs = go.GetComponentsInChildren<Renderer>(true).Where(r => !(r is ParticleSystemRenderer) && !(r is TrailRenderer) && !(r is LineRenderer)).ToArray();
if (rs.Length == 0) return go; // Orb = 파티클만 → 미보정(814p 미확인 ②)
var wb = new Bounds(); bool first = true;
foreach (var r in rs)
{
var lb = r.localBounds; var m = go.transform.worldToLocalMatrix * r.transform.localToWorldMatrix;
for (int c = 0; c < 8; c++)
{
var p = m.MultiplyPoint3x4(lb.center + Vector3.Scale(lb.extents, new Vector3((c & 1) == 0 ? -1 : 1, (c & 2) == 0 ? -1 : 1, (c & 4) == 0 ? -1 : 1)));
if (first) { wb = new Bounds(p, Vector3.zero); first = false; } else wb.Encapsulate(p);
}
}
var ls = go.transform.lossyScale;
float len = Mathf.Max(wb.size.x * Mathf.Abs(ls.x), Mathf.Max(wb.size.y * Mathf.Abs(ls.y), wb.size.z * Mathf.Abs(ls.z)));
ratioBefore = len / Mathf.Max(s_charHeight, 1e-6f);
mult = Mathf.Clamp(0.60f / Mathf.Max(ratioBefore, 1e-6f), 0.3f, 1.0f);
go.transform.localScale = Vector3.one * mult;
return go;
}
public static void Clear()
{
foreach (var g in s_spawned) if (g != null) UnityEngine.Object.DestroyImmediate(g);
s_spawned.Clear();
}
// ────────────────────────────────────────────────────────── 렌더
class Shot { public Color32[] px; }
static Shot Render(float ortho)
{
s_root.transform.SetPositionAndRotation(s_pos, s_rot);
if (s_an != null) { s_an.speed = 0f; try { s_an.Play("idle", 0, 0.25f); s_an.Update(0f); } catch { } }
s_cam.orthographicSize = ortho;
s_cam.transform.SetPositionAndRotation(s_center - (s_camRot * Vector3.forward) * 30f, s_camRot);
var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
rt.antiAliasing = 1; rt.filterMode = FilterMode.Point; rt.Create();
var prev = s_cam.targetTexture; s_cam.targetTexture = rt; s_cam.Render(); s_cam.targetTexture = prev;
var pa = RenderTexture.active; RenderTexture.active = rt;
var t = new Texture2D(W, H, TextureFormat.RGBA32, false); t.ReadPixels(new Rect(0, 0, W, H), 0, 0); t.Apply();
RenderTexture.active = pa;
var s = new Shot { px = t.GetPixels32() };
UnityEngine.Object.Destroy(t); rt.Release(); UnityEngine.Object.Destroy(rt);
return s;
}
static void Save(Shot s, string name)
{
Directory.CreateDirectory(Path.GetDirectoryName(RAW + name));
var t = new Texture2D(W, H, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply();
File.WriteAllBytes(RAW + name, t.EncodeToPNG()); UnityEngine.Object.Destroy(t);
}
static void Shoot(string name, float ortho) { Render(ortho); Save(Render(ortho), name); }
/// <summary>지정한 렌더러만 남긴 흑백 마스크(배경·포스트 off · 레이어 31 단독).</summary>
static bool[] Mask(Renderer[] on, float ortho, string savePng)
{
var all = UnityEngine.Object.FindObjectsByType<Renderer>(FindObjectsSortMode.None);
var was = all.Select(r => r.enabled).ToArray();
var ucd = s_cam.GetComponent<UnityEngine.Rendering.Universal.UniversalAdditionalCameraData>();
bool post = ucd != null && ucd.renderPostProcessing; if (ucd != null) ucd.renderPostProcessing = false;
int cull = s_cam.cullingMask; var clr = s_cam.clearFlags; var bg = s_cam.backgroundColor;
var lay = on.Select(r => r.gameObject.layer).ToArray();
for (int i = 0; i < all.Length; i++) all[i].enabled = on.Contains(all[i]);
foreach (var r in on) r.gameObject.layer = 31;
s_cam.cullingMask = 1 << 31; s_cam.clearFlags = CameraClearFlags.SolidColor; s_cam.backgroundColor = Color.black;
Render(ortho); // 🔴 갓 Instantiate 한 렌더러는 첫 렌더에 안 잡힌다(814x §6-7 과 같은 증상) — 1장 버린다
var sh = Render(ortho);
for (int i = 0; i < on.Length; i++) on[i].gameObject.layer = lay[i];
s_cam.cullingMask = cull; s_cam.clearFlags = clr; s_cam.backgroundColor = bg;
for (int i = 0; i < all.Length; i++) if (all[i] != null) all[i].enabled = was[i];
if (ucd != null) ucd.renderPostProcessing = post;
var m = new bool[sh.px.Length];
for (int i = 0; i < m.Length; i++) m[i] = sh.px[i].r + sh.px[i].g + sh.px[i].b > 24;
if (!string.IsNullOrEmpty(savePng))
{
var q = new Shot { px = new Color32[m.Length] };
for (int i = 0; i < m.Length; i++) q.px[i] = m[i] ? new Color32(255, 255, 255, 255) : new Color32(0, 0, 0, 255);
Save(q, savePng);
}
return m;
}
public struct Met { public int px, uniq, steps; public float ratio; public Vector3 mean; }
/// <summary>포스트 포함 게임 렌더에서 마스크 영역의 고유색/비율/단계를 잰다.</summary>
static Met Measure(Shot game, bool[] mask)
{
var set = new HashSet<int>(); int n = 0; long r = 0, g = 0, b = 0;
var cols = new List<Color32>();
for (int i = 0; i < mask.Length; i++)
{
if (!mask[i]) continue;
var p = game.px[i]; n++; r += p.r; g += p.g; b += p.b;
set.Add((p.r << 16) | (p.g << 8) | p.b);
cols.Add(p);
}
var m = new Met { px = n, uniq = set.Count, ratio = n > 0 ? set.Count * 100f / n : 0f };
if (n > 0) m.mean = new Vector3(r / (float)n, g / (float)n, b / (float)n);
m.steps = Steps(cols);
return m;
}
/// <summary>단계 = RGB 거리 16 이내로 묶었을 때 면적 90 % 를 덮는 군집 수(814x 정의).</summary>
static int Steps(List<Color32> cols)
{
if (cols.Count == 0) return 0;
var cent = new List<Vector3>(); var cnt = new List<int>();
foreach (var c in cols)
{
var v = new Vector3(c.r, c.g, c.b);
int bj = -1; float bd = 16f;
for (int j = 0; j < cent.Count; j++) { float d = (cent[j] / cnt[j] - v).magnitude; if (d < bd) { bd = d; bj = j; } }
if (bj < 0) { cent.Add(v); cnt.Add(1); }
else { cent[bj] += v; cnt[bj]++; }
}
var order = Enumerable.Range(0, cnt.Count).OrderByDescending(i => cnt[i]).ToArray();
int tot = cols.Count, acc = 0, k = 0;
foreach (var i in order) { acc += cnt[i]; k++; if (acc >= tot * 0.90f) break; }
return k;
}
// ────────────────────────────────────────────────────────── 팔레트 적용 (런타임 · 에셋 저장 0)
public static Material s_baseMat;
static readonly List<UnityEngine.Object> s_tmp = new List<UnityEngine.Object>();
/// <summary>구운 결과를 스폰된 무기에 끼운다. 에셋을 만들지 않는다(스윕용).</summary>
public static void ApplyBaked(GameObject go, Vector3[] pal, Mesh mesh)
{
if (s_baseMat == null) s_baseMat = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>("Assets/WL/Look/Character/Materials/M05_ToonP.mat");
int N = pal.Length, G = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(N))), CELL = 8, S = G * CELL;
var tex = new Texture2D(S, S, TextureFormat.RGBA32, false); // mip 없음
tex.filterMode = FilterMode.Point; tex.wrapMode = TextureWrapMode.Clamp;
var px = new Color32[S * S];
for (int i = 0; i < px.Length; i++) px[i] = new Color32(255, 0, 255, 255);
for (int j = 0; j < N; j++)
{
int gx = j % G, gy = j / G;
var c = new Color32((byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].x) * 255f), (byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].y) * 255f), (byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].z) * 255f), 255);
for (int y = 0; y < CELL; y++) for (int x = 0; x < CELL; x++) px[(gy * CELL + y) * S + gx * CELL + x] = c;
}
tex.SetPixels32(px); tex.Apply(false, false);
var mat = new Material(s_baseMat) { name = "W_ToonP_tmp" };
if (mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", tex);
if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", tex);
s_tmp.Add(tex); s_tmp.Add(mat);
foreach (var mf in go.GetComponentsInChildren<MeshFilter>(true)) mf.sharedMesh = mesh;
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
{
if (r is ParticleSystemRenderer) continue;
var ms = r.sharedMaterials; for (int i = 0; i < ms.Length; i++) ms[i] = mat; r.sharedMaterials = ms;
}
}
public static void FreeTmp() { foreach (var o in s_tmp) if (o != null) UnityEngine.Object.DestroyImmediate(o); s_tmp.Clear(); }
/// <summary>씬의 참조 룩(외곽선·대비)을 새 렌더러까지 포함해 다시 얹는다(멱등).</summary>
/// <summary>참조 룩 장식을 벗긴다. 🔴 메시·머티리얼을 갈아끼우기 **전에** 반드시 부른다 —
/// 안 그러면 다음 Apply 의 Restore 가 예전 머티리얼을 도로 덮어써서 교체가 사라진다.</summary>
public static void UnLook()
{
WLReferenceLook.Restore();
foreach (var g in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (g != null && g.name == "WL814t_ReferenceLook_Volume") UnityEngine.Object.DestroyImmediate(g);
}
public static void ReLook()
{
var cfg = WLReferenceLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
// 🔴 Play 에서 `Destroy` 는 프레임 끝에 실행된다. run_script 는 한 콜백 안에서 끝나므로
// Apply 를 반복하면 임시 Volume 이 겹겹이 살아남아 포스트가 누적된다(캐릭터 수치가 회차마다 나빠진다).
// → Restore 로 참조를 끊은 뒤 남은 Volume 을 즉시 지우고 새로 얹는다.
WLReferenceLook.Restore();
foreach (var g in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (g != null && g.name == "WL814t_ReferenceLook_Volume") UnityEngine.Object.DestroyImmediate(g);
WLReferenceLook.Apply(WLReferenceLook.Opt.FromSettings(cfg), cfg);
}
/// <summary>🔴 「부위(팔레트 칸)당 색 단계」 — 814x 정의. 칸마다 고유색을 찍은 지도(Unlit·빛 0)로 칸을 가른다.</summary>
public static string CellSteps(GameObject go, Vector3[] pal, out int maxStep, out float avgStep)
{
maxStep = 0; avgStep = 0f;
int N = pal.Length, G = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(N))), CELL = 8, S = G * CELL;
var idTex = new Texture2D(S, S, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp };
var px = new Color32[S * S]; var idCol = new Color32[N];
for (int i = 0; i < px.Length; i++) px[i] = new Color32(0, 0, 0, 255);
for (int j = 0; j < N; j++)
{
idCol[j] = new Color32((byte)(20 + j * 13 % 236), (byte)(250 - j * 29 % 200), (byte)(40 + j * 47 % 200), 255);
int gx = j % G, gy = j / G;
for (int y = 0; y < CELL; y++) for (int x = 0; x < CELL; x++) px[(gy * CELL + y) * S + gx * CELL + x] = idCol[j];
}
idTex.SetPixels32(px); idTex.Apply(false, false);
var rends = go.GetComponentsInChildren<Renderer>(true).Where(r => !(r is ParticleSystemRenderer)).ToArray();
var keep = rends.Select(r => r.sharedMaterials).ToArray();
var idMat = new Material(Shader.Find("Unlit/Texture")); idMat.mainTexture = idTex;
foreach (var r in rends) { var ms = new Material[r.sharedMaterials.Length]; for (int i = 0; i < ms.Length; i++) ms[i] = idMat; r.sharedMaterials = ms; }
var idShot = RenderIsolated(rends);
for (int i = 0; i < rends.Length; i++) rends[i].sharedMaterials = keep[i];
UnityEngine.Object.DestroyImmediate(idMat); UnityEngine.Object.DestroyImmediate(idTex);
var game = Render(ORTHO);
var sbb = new StringBuilder(); int sum = 0, cnt = 0;
for (int j = 0; j < N; j++)
{
var cols = new List<Color32>();
for (int i = 0; i < idShot.px.Length; i++)
{
var p = idShot.px[i];
if (Mathf.Abs(p.r - idCol[j].r) <= 10 && Mathf.Abs(p.g - idCol[j].g) <= 10 && Mathf.Abs(p.b - idCol[j].b) <= 10) cols.Add(game.px[i]);
}
if (cols.Count < 3) continue; // 3 px 미만 칸은 통계 의미 없음
int st = Steps(cols);
sbb.Append(j + ":" + cols.Count + "px/" + st + "단 ");
if (st > maxStep) maxStep = st;
sum += st; cnt++;
}
avgStep = cnt > 0 ? sum / (float)cnt : 0f;
return "칸 " + cnt + "개 측정 · 평균 " + avgStep.ToString("F2") + " · 최대 " + maxStep + " · " + sbb;
}
static Shot RenderIsolated(Renderer[] on)
{
var all = UnityEngine.Object.FindObjectsByType<Renderer>(FindObjectsSortMode.None);
var was = all.Select(r => r.enabled).ToArray();
var ucd = s_cam.GetComponent<UnityEngine.Rendering.Universal.UniversalAdditionalCameraData>();
bool post = ucd != null && ucd.renderPostProcessing; if (ucd != null) ucd.renderPostProcessing = false;
int cull = s_cam.cullingMask; var clr = s_cam.clearFlags; var bg = s_cam.backgroundColor;
var lay = on.Select(r => r.gameObject.layer).ToArray();
for (int i = 0; i < all.Length; i++) all[i].enabled = on.Contains(all[i]);
foreach (var r in on) r.gameObject.layer = 31;
s_cam.cullingMask = 1 << 31; s_cam.clearFlags = CameraClearFlags.SolidColor; s_cam.backgroundColor = Color.black;
Render(ortho: ORTHO);
var sh = Render(ORTHO);
for (int i = 0; i < on.Length; i++) on[i].gameObject.layer = lay[i];
s_cam.cullingMask = cull; s_cam.clearFlags = clr; s_cam.backgroundColor = bg;
for (int i = 0; i < all.Length; i++) if (all[i] != null) all[i].enabled = was[i];
if (ucd != null) ucd.renderPostProcessing = post;
return sh;
}
public static Met MeasureNow(GameObject go, string maskName, string shotName)
{
var wr = go.GetComponentsInChildren<Renderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy && !(r is ParticleSystemRenderer)).ToArray();
var mk = Mask(wr, ORTHO, maskName);
var sh = Render(ORTHO); Save(sh, shotName);
return Measure(sh, mk);
}
public static Met MeasureChar(string maskName, string shotName)
{
var br = s_root.GetComponentsInChildren<Renderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy
&& !r.transform.IsChildOf(s_sockets[0]) && !r.transform.IsChildOf(s_sockets[1]) && !r.transform.IsChildOf(s_sockets[2])
&& !r.transform.IsChildOf(s_sockets[3]) && !r.transform.IsChildOf(s_sockets[4]) && !r.transform.IsChildOf(s_sockets[5]) && !r.transform.IsChildOf(s_sockets[6])).ToArray();
var mk = Mask(br, ORTHO, maskName);
var sh = Render(ORTHO); if (!string.IsNullOrEmpty(shotName)) Save(sh, shotName);
return Measure(sh, mk);
}
public static void PubSetup() { Setup(); }
public static void PubTeardown() { Teardown(); }
public static void Log(string s) { L(s); }
public static void Flush(string f) { Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/" + f, sb.ToString(), new UTF8Encoding(true)); }
public static void Reset() { sb.Length = 0; }
public static void ShootGame(string name) { Shoot(name, ORTHO); }
// ── 디버그: 마스크가 0 px 로 나온 무기를 캔다
public static void Dbg()
{
sb.Length = 0; Setup();
foreach (var wn in new[] { "PP_Theme_10_Mace_003", "PP_Theme_08_Mace_003" })
{
Clear();
float rb, mult; var go = Spawn(wn, 1, out rb, out mult);
var r = go.GetComponentsInChildren<Renderer>(true)[0];
L(wn + " scale=" + go.transform.localScale.ToString("F4") + " lossy=" + go.transform.lossyScale.ToString("F4")
+ " worldBounds=" + r.bounds.ToString("F3") + " vis=" + r.isVisible
+ " mat=" + r.sharedMaterial.name + " shader=" + r.sharedMaterial.shader.name
+ " layer=" + r.gameObject.layer + " pos=" + go.transform.position.ToString("F3"));
var mk = Mask(new[] { r }, ORTHO, "dbg_" + wn + ".png");
int n = 0; for (int i = 0; i < mk.Length; i++) if (mk[i]) n++;
L(" mask px=" + n);
}
Clear(); Teardown();
File.WriteAllText("AgentScripts/WL816b_DBG.txt", sb.ToString(), new UTF8Encoding(true));
}
// ────────────────────────────────────────────────────────── ① 우선순위 프로브
public static void Probe()
{
sb.Length = 0; Setup();
L("");
L("=== 무기별 화면 실측 (인게임 Play · 포스트 포함 · 1080x1920 · ortho " + ORTHO.ToString("F0") + ") ===");
var bodyR = s_root.GetComponentsInChildren<Renderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
var mBody = Mask(bodyR, ORTHO, "mask_char.png");
var gm = Render(ORTHO); Save(gm, "a_before_none.png");
var mc = Measure(gm, mBody);
L(string.Format("캐릭터(무기 없음) : {0} px · 고유색 {1} · 비율 {2:F2} % · 단계 {3}", mc.px, mc.uniq, mc.ratio, mc.steps));
L("");
L("| 무기 | 소켓 | 클래스 | 배수 | 화면 px | 고유색 | 서로다른색 % | 단계 |");
L("|---|---|---|---|---|---|---|---|");
foreach (var w in WEAPONS)
{
Clear();
float rb, mult;
var go = Spawn(w[0], int.Parse(w[1]), out rb, out mult);
if (go == null) { L("| " + w[0] + " | " + w[1] + " | " + w[2] + " | — | 부착실패 | | | |"); continue; }
var wr = go.GetComponentsInChildren<Renderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
if (wr.Length == 0) { L("| " + w[0] + " | " + w[1] + " | " + w[2] + " | — | 렌더러 0 | | | |"); continue; }
var mk = Mask(wr, ORTHO, "mask_" + w[0] + ".png");
var sh = Render(ORTHO); Save(sh, "a_before_" + w[0] + ".png");
var mm = Measure(sh, mk);
L(string.Format("| {0} | {1} | {2} | ×{3:F4} | **{4}** | {5} | {6:F2} | {7} |",
w[0], w[1], w[2], mult, mm.px, mm.uniq, mm.ratio, mm.steps));
}
Clear(); Teardown();
Directory.CreateDirectory("AgentScripts");
File.WriteAllText("AgentScripts/WL816b_PLAY_PROBE.txt", sb.ToString(), new UTF8Encoding(true));
Debug.Log("[WL816b] play probe done");
}
}