440 lines
22 KiB
C#
440 lines
22 KiB
C#
// PD #747 2단계 — Play 검증 프로브 (상태 덤프 · 공격 · CSV+캡처 · GC 측정 · 셰이더 감사)
|
|
// unity command run_script --file AgentScripts/WL747_Probe.cs --entry WL747_Probe.Status
|
|
// unity command run_script --file AgentScripts/WL747_Probe.cs --entry WL747_Probe.ShaderAudit
|
|
// unity command run_script --file AgentScripts/WL747_Probe.cs --entry WL747_Probe.Run --args '[\"10501\", 6.0]'
|
|
// unity command run_script --file AgentScripts/WL747_Probe.cs --entry WL747_Probe.Measure --args '[\"on\", 6.0, true]'
|
|
// unity command run_script --file AgentScripts/WL747_Probe.cs --entry WL747_Probe.SetEnable --args '[false]'
|
|
//
|
|
// 🔴 run_script 는 호출마다 **새 임시 어셈블리**로 컴파일된다 → 호출 간 static 상태가 이어지지 않는다.
|
|
// 그래서 프레임에 걸친 작업(샘플링·캡처)은 전부 **한 번의 호출 안에서** 자기 종료하도록 짰다.
|
|
// 이전 호출이 남긴 EditorApplication.update 구독은 CleanupTicks 가 리플렉션으로 떼어 낸다
|
|
// (안 떼면 죽은 어셈블리의 틱이 매 프레임 돌면서 GC 측정을 오염시킨다).
|
|
// 산출물은 전부 Screenshots_WL/trail/ (Assets 밖).
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public static class WL747_Probe
|
|
{
|
|
const string OutDir = "Screenshots_WL/trail";
|
|
|
|
static PCActor MainPC()
|
|
{
|
|
foreach (var a in UnityEngine.Object.FindObjectsByType<PCActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
|
|
if (a.IsMainPC()) return a;
|
|
return null;
|
|
}
|
|
|
|
static string V(Vector3 v) { return v.x.ToString("F3") + "," + v.y.ToString("F3") + "," + v.z.ToString("F3"); }
|
|
|
|
/// <summary>이전 run_script 어셈블리가 남긴 이 프로브의 update 구독을 전부 떼어 낸다.</summary>
|
|
public static object CleanupTicks()
|
|
{
|
|
int n = 0;
|
|
var d = EditorApplication.update;
|
|
if (d != null)
|
|
foreach (var sub in d.GetInvocationList())
|
|
{
|
|
var dt = sub.Method.DeclaringType;
|
|
if (dt != null && dt.Name == "WL747_Probe")
|
|
{ EditorApplication.update -= (EditorApplication.CallbackFunction)sub; n++; }
|
|
}
|
|
return "removed stale ticks = " + n;
|
|
}
|
|
|
|
// ── 상태 덤프 ────────────────────────────────────────────────────────────
|
|
public static object Status()
|
|
{
|
|
var sb = new StringBuilder();
|
|
var set = WL.Combat.SlashTrailSettings.Instance;
|
|
sb.Append("settings=").Append(set != null ? set.name : "NULL")
|
|
.Append(" enable=").Append(set != null && set.enableWeaponTrail)
|
|
.Append(" verbose=").Append(set != null && set.verboseLog)
|
|
.Append(" matAdd=").Append(set != null && set.materialAdditive != null ? set.materialAdditive.shader.name : "-")
|
|
.Append(" rules=").Append(set != null ? set.crescentRules.Count : -1)
|
|
.Append(" burstTiming=").Append(set != null ? set.burstTiming.ToString() : "-").Append('\n');
|
|
|
|
var pc = MainPC();
|
|
if (pc == null) { sb.Append("MainPC = null\n"); return sb.ToString(); }
|
|
|
|
sb.Append("PC=").Append(pc.name).Append(" classId=").Append(pc.Get_ID())
|
|
.Append(" animSpeed=").Append(pc.m_animation != null ? pc.m_animation.speed.ToString("F2") : "-").Append('\n');
|
|
|
|
var cfg = table_classconfig.Ins != null ? table_classconfig.Ins.Get_Data_orNull(pc.Get_ID()) : null;
|
|
if (cfg != null) sb.Append("classcfg battleType=").Append(cfg.e_BattleType).Append(" range=").Append(cfg.f_AttackRange)
|
|
.Append(" LH=").Append(cfg.n_WeaponLHIndex).Append(" RH=").Append(cfg.n_WeaponRHIndex).Append('\n');
|
|
|
|
var socks = pc.tfs_weapon;
|
|
if (socks != null)
|
|
for (int i = 0; i < socks.Length; i++)
|
|
if (socks[i] != null && socks[i].childCount > 0)
|
|
sb.Append(" socket[").Append(i).Append("] ").Append(socks[i].name).Append(" -> ").Append(socks[i].GetChild(0).name).Append('\n');
|
|
|
|
var drv = pc.GetComponent<WL.Combat.WeaponTrailDriver>();
|
|
sb.Append("driver=").Append(drv != null).Append('\n');
|
|
if (drv != null)
|
|
{
|
|
sb.Append(" swinging=").Append(drv.IsSwinging).Append(" swingCount=").Append(drv.SwingCount)
|
|
.Append(" slots=").Append(drv.ActiveSlotCount).Append('\n');
|
|
for (int i = 0; i < drv.ActiveSlotCount; i++)
|
|
{
|
|
var t = drv.GetTrail(i);
|
|
sb.Append(" slot").Append(i).Append(" bladeLen=").Append(drv.GetBladeLength(i).ToString("F3"))
|
|
.Append(" tip=").Append(V(drv.GetTipWorld(i))).Append(" hilt=").Append(V(drv.GetHiltWorld(i)))
|
|
.Append(" trail=").Append(t != null ? ("samples=" + t.SampleCount + " visible=" + t.IsVisible + " hasMat=" + t.HasMaterial) : "null")
|
|
.Append('\n');
|
|
}
|
|
}
|
|
sb.Append("externalSpawner=").Append(WL.Combat.WeaponTrailDriver.ExternalSpawner != null).Append('\n');
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ── 셰이더 감사 ──────────────────────────────────────────────────────────
|
|
public static object ShaderAudit()
|
|
{
|
|
var sb = new StringBuilder();
|
|
var guids = AssetDatabase.FindAssets("Effect_Slash_ t:Prefab");
|
|
var seen = new SortedDictionary<string, string>();
|
|
int prefabs = 0;
|
|
foreach (var g in guids)
|
|
{
|
|
var path = AssetDatabase.GUIDToAssetPath(g);
|
|
if (!System.IO.Path.GetFileName(path).StartsWith("Effect_Slash_")) continue;
|
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|
if (go == null) continue;
|
|
prefabs++;
|
|
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
|
|
foreach (var m in r.sharedMaterials)
|
|
{
|
|
if (m == null || m.shader == null) continue;
|
|
string tex = m.HasProperty("_MainTex") && m.GetTexture("_MainTex") != null ? m.GetTexture("_MainTex").name
|
|
: (m.HasProperty("_BaseMap") && m.GetTexture("_BaseMap") != null ? m.GetTexture("_BaseMap").name : "(no tex)");
|
|
seen[m.name + " | " + m.shader.name] = "supported=" + m.shader.isSupported + " passes=" + m.passCount + " tex=" + tex;
|
|
}
|
|
}
|
|
sb.Append("Effect_Slash prefabs=").Append(prefabs).Append(" distinct=").Append(seen.Count).Append('\n');
|
|
foreach (var kv in seen) sb.Append(" ").Append(kv.Key).Append(" -> ").Append(kv.Value).Append('\n');
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ── 공격 · 설정 토글 ─────────────────────────────────────────────────────
|
|
public static object Attack() { var pc = MainPC(); if (pc == null) return "MainPC null"; pc.Play_Attack(0, 1f, null); return "attack0 class=" + pc.Get_ID(); }
|
|
public static object AttackN(int n) { var pc = MainPC(); if (pc == null) return "MainPC null"; pc.Play_Attack(n, 1f, null); return "attack" + n; }
|
|
|
|
public static object SetEnable(bool v)
|
|
{
|
|
var set = WL.Combat.SlashTrailSettings.Instance;
|
|
if (set == null) return "settings null";
|
|
set.enableWeaponTrail = v;
|
|
return "enableWeaponTrail=" + set.enableWeaponTrail;
|
|
}
|
|
|
|
public static object SetVerbose(bool v)
|
|
{
|
|
var set = WL.Combat.SlashTrailSettings.Instance;
|
|
if (set == null) return "settings null";
|
|
set.verboseLog = v;
|
|
return "verboseLog=" + set.verboseLog;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 클라이언트 전용 클래스 전환. PCActor.Change_Class(cfg, _ui:true) 는 m_ID·m_ClassData·Set_Obj()
|
|
/// (모델 스케일 · 애니메이터 컨트롤러 · Change_Weapon) 만 건드린다 — **서버 쓰기 호출이 없다**(실측).
|
|
/// Play 를 멈추면 전부 사라진다. 드라이버는 classId 변경을 감지해 무기 슬롯을 다시 잰다.
|
|
/// </summary>
|
|
public static object SwitchClass(int classId)
|
|
{
|
|
var pc = MainPC(); if (pc == null) return "MainPC null";
|
|
if (table_classconfig.Ins == null) return "table null";
|
|
var cfg = table_classconfig.Ins.Get_Data_orNull(classId);
|
|
if (cfg == null) return "no classconfig " + classId;
|
|
pc.Change_Class(cfg, true);
|
|
return "Change_Class -> " + classId + " (LH=" + cfg.n_WeaponLHIndex + " RH=" + cfg.n_WeaponRHIndex
|
|
+ " lh=" + cfg.s_WeaponLHPrefab + " rh=" + cfg.s_WeaponRHPrefab + ")";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 무기 소켓의 메시 형상을 **필터와 무관하게** 실측한다.
|
|
/// WeaponTrailDriver.MeasureSlot 과 같은 계산(월드 크기 · 최장축 · 최장/차축 비율)이라
|
|
/// minBladeLength / minBladeAspect 임계값을 데이터로 정할 수 있다(C39·C45).
|
|
/// </summary>
|
|
public static object MeasureWeapons()
|
|
{
|
|
var pc = MainPC(); if (pc == null) return "MainPC null";
|
|
var sb = new StringBuilder();
|
|
var set = WL.Combat.SlashTrailSettings.Instance;
|
|
sb.Append("class=").Append(pc.Get_ID())
|
|
.Append(" minLen=").Append(set != null ? set.minBladeLength : -1)
|
|
.Append(" minAspect=").Append(set != null ? set.minBladeAspect : -1).Append('\n');
|
|
var socks = pc.tfs_weapon;
|
|
for (int i = 0; i < socks.Length; i++)
|
|
{
|
|
if (socks[i] == null || socks[i].childCount == 0) continue;
|
|
foreach (var r in socks[i].GetComponentsInChildren<Renderer>(true))
|
|
{
|
|
Mesh mesh = null;
|
|
var smr = r as SkinnedMeshRenderer;
|
|
if (smr != null) mesh = smr.sharedMesh;
|
|
else { var mf = r.GetComponent<MeshFilter>(); if (mf != null) mesh = mf.sharedMesh; }
|
|
if (mesh == null) continue;
|
|
var ls = r.transform.lossyScale;
|
|
var b = mesh.bounds;
|
|
var ws = new Vector3(b.size.x * Mathf.Abs(ls.x), b.size.y * Mathf.Abs(ls.y), b.size.z * Mathf.Abs(ls.z));
|
|
int axis = (ws.x >= ws.y && ws.x >= ws.z) ? 0 : (ws.y >= ws.z ? 1 : 2);
|
|
float longest = ws[axis];
|
|
float second = 0f;
|
|
for (int k = 0; k < 3; k++) if (k != axis && ws[k] > second) second = ws[k];
|
|
float aspect = second > 1e-5f ? longest / second : 999f;
|
|
sb.Append(" socket[").Append(i).Append("] ").Append(socks[i].name).Append(" / ").Append(r.name)
|
|
.Append(" worldSize=").Append(ws.ToString("F3"))
|
|
.Append(" longest=").Append(longest.ToString("F3")).Append("m second=").Append(second.ToString("F3"))
|
|
.Append("m aspect=").Append(aspect.ToString("F2"))
|
|
.Append(set != null ? ((longest >= set.minBladeLength && aspect >= set.minBladeAspect) ? " PASS" : " REJECT") : "")
|
|
.Append('\n');
|
|
}
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>살아 있는 트레일의 색·폭을 즉시 바꾼다 — 정점색이 실제로 먹는지 확인용(PLAN 미확인 ②).</summary>
|
|
public static object TintTest(double r, double g, double b, double w)
|
|
{
|
|
var pc = MainPC(); if (pc == null) return "MainPC null";
|
|
var drv = pc.GetComponent<WL.Combat.WeaponTrailDriver>(); if (drv == null) return "driver null";
|
|
var c = new Color((float)r, (float)g, (float)b, 1f);
|
|
int n = 0;
|
|
for (int i = 0; i < drv.ActiveSlotCount; i++)
|
|
{ var t = drv.GetTrail(i); if (t != null) { t.SetLook(c, c, (float)w); n++; } }
|
|
return "SetLook applied to " + n + " trails color=" + c + " width=" + w;
|
|
}
|
|
|
|
// ── 한 호출 = CSV 샘플링 + 스윙 동기 캡처 (자기 종료) ────────────────────
|
|
static readonly List<string> s_rows = new List<string>(8192);
|
|
static string s_tag;
|
|
static float s_t0, s_dur;
|
|
static double s_maxTipErr;
|
|
static int s_shots, s_maxShots, s_lastShotSwing;
|
|
static WL.Combat.WeaponTrailDriver s_drv;
|
|
|
|
public static object Run(string tag, double seconds) { return RunEx(tag, seconds, true); }
|
|
|
|
public static object RunEx(string tag, double seconds, bool close)
|
|
{
|
|
CleanupTicks();
|
|
s_close = close;
|
|
var pc = MainPC();
|
|
if (pc == null) return "MainPC null";
|
|
s_drv = pc.GetComponent<WL.Combat.WeaponTrailDriver>();
|
|
|
|
s_rows.Clear();
|
|
s_rows.Add("time,swingActive,swingCount,slotIndex,tipX,tipY,tipZ,hiltX,hiltY,hiltZ,sampleCount,meshTipX,meshTipY,meshTipZ,tipErr,visible");
|
|
s_tag = string.IsNullOrEmpty(tag) ? "sample" : tag;
|
|
s_dur = (float)seconds;
|
|
s_t0 = Time.realtimeSinceStartup;
|
|
s_maxTipErr = 0; s_shots = 0; s_maxShots = 4; s_lastShotSwing = -1;
|
|
EditorApplication.update += RunTick;
|
|
return "Run started tag=" + s_tag + " seconds=" + s_dur + " (결과는 콘솔 [WL747 Run] 로그로)";
|
|
}
|
|
|
|
/// <summary>드라이버와 무관하게 **살아 있는 Effect_Slash_* 인스턴스**가 보일 때 찍는다.
|
|
/// enableWeaponTrail=false 원복 확인(ⓕ)과 몬스터·원거리 회귀 캡처(ⓓ)에 쓴다.</summary>
|
|
public static bool s_baseMode;
|
|
|
|
public static object RunBase(string tag, double seconds)
|
|
{
|
|
CleanupTicks();
|
|
s_close = true; s_baseMode = true;
|
|
var pc = MainPC(); if (pc == null) return "MainPC null";
|
|
s_drv = pc.GetComponent<WL.Combat.WeaponTrailDriver>();
|
|
s_rows.Clear();
|
|
s_rows.Add("time,swingActive,swingCount,slotIndex,tipX,tipY,tipZ,hiltX,hiltY,hiltZ,sampleCount,meshTipX,meshTipY,meshTipZ,tipErr,visible");
|
|
s_tag = tag; s_dur = (float)seconds; s_t0 = Time.realtimeSinceStartup;
|
|
s_maxTipErr = 0; s_shots = 0; s_maxShots = 4; s_lastShotSwing = -1;
|
|
EditorApplication.update += RunTick;
|
|
return "RunBase started tag=" + tag;
|
|
}
|
|
|
|
static float s_lastBaseShot;
|
|
|
|
static void RunTick()
|
|
{
|
|
if (!Application.isPlaying) { Finish(); return; }
|
|
var pc = MainPC();
|
|
if (pc == null) return;
|
|
|
|
if (s_baseMode)
|
|
{
|
|
float tb = Time.realtimeSinceStartup - s_t0;
|
|
if (s_shots < s_maxShots && tb - s_lastBaseShot > 0.6f && AnySlashActive())
|
|
{ s_lastBaseShot = tb; Shoot(s_tag + "_" + s_shots); s_shots++; }
|
|
if (tb >= s_dur) { s_baseMode = false; Finish(); }
|
|
return;
|
|
}
|
|
|
|
if (s_drv == null) s_drv = pc.GetComponent<WL.Combat.WeaponTrailDriver>();
|
|
var drv = s_drv;
|
|
if (drv == null) return;
|
|
|
|
float t = Time.realtimeSinceStartup - s_t0;
|
|
for (int i = 0; i < drv.ActiveSlotCount; i++)
|
|
{
|
|
var tr = drv.GetTrail(i);
|
|
Vector3 tip = drv.GetTipWorld(i);
|
|
Vector3 hilt = drv.GetHiltWorld(i);
|
|
int n = tr != null ? tr.SampleCount : 0;
|
|
Vector3 meshTip = n > 0 ? tr.GetTip(n - 1) : Vector3.zero;
|
|
double err = n > 0 ? Vector3.Distance(tip, meshTip) : -1;
|
|
if (drv.IsSwinging && n > 0 && err > s_maxTipErr) s_maxTipErr = err;
|
|
s_rows.Add(t.ToString("F3") + "," + (drv.IsSwinging ? "1" : "0") + "," + drv.SwingCount + "," + i + ","
|
|
+ tip.x.ToString("F4") + "," + tip.y.ToString("F4") + "," + tip.z.ToString("F4") + ","
|
|
+ hilt.x.ToString("F4") + "," + hilt.y.ToString("F4") + "," + hilt.z.ToString("F4") + ","
|
|
+ n + "," + meshTip.x.ToString("F4") + "," + meshTip.y.ToString("F4") + "," + meshTip.z.ToString("F4") + ","
|
|
+ err.ToString("F5") + "," + ((tr != null && tr.IsVisible) ? "1" : "0"));
|
|
|
|
// 스윙 1회당 최대 1장 — 리본이 실제로 화면에 있는 프레임에서만 찍는다.
|
|
if (s_shots < s_maxShots && drv.IsSwinging && tr != null && tr.IsVisible && n >= 6 && drv.SwingCount != s_lastShotSwing)
|
|
{
|
|
s_lastShotSwing = drv.SwingCount;
|
|
Shoot(s_tag + "_swing" + drv.SwingCount + "_n" + n);
|
|
s_shots++;
|
|
}
|
|
}
|
|
|
|
if (t >= s_dur) Finish();
|
|
}
|
|
|
|
static bool AnySlashActive()
|
|
{
|
|
foreach (var ps in UnityEngine.Object.FindObjectsByType<ParticleSystem>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
|
|
{
|
|
var r = ps.transform;
|
|
while (r.parent != null && !r.name.StartsWith("Effect_Slash_")) r = r.parent;
|
|
if (r.name.StartsWith("Effect_Slash_") && r.gameObject.activeInHierarchy) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void Finish()
|
|
{
|
|
EditorApplication.update -= RunTick;
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
var path = System.IO.Path.Combine(OutDir, "trail_" + s_tag + ".csv");
|
|
System.IO.File.WriteAllLines(path, s_rows.ToArray(), new UTF8Encoding(true));
|
|
Debug.Log("[WL747 Run] csv=" + path + " rows=" + (s_rows.Count - 1)
|
|
+ " maxTipErr=" + s_maxTipErr.ToString("F5") + "m shots=" + s_shots);
|
|
}
|
|
|
|
// ── GC 측정 (틱 자체가 할당하지 않는다 · 판정 기준 ⓔ) ────────────────────
|
|
static long[] s_mono = new long[20000];
|
|
static int s_mi;
|
|
static float s_mt0, s_mdur;
|
|
static string s_mtag;
|
|
|
|
public static object Measure(string tag, double seconds, bool trailOn)
|
|
{
|
|
CleanupTicks();
|
|
var set = WL.Combat.SlashTrailSettings.Instance;
|
|
if (set != null) { set.enableWeaponTrail = trailOn; set.verboseLog = false; }
|
|
s_mtag = tag; s_mi = 0; s_mdur = (float)seconds; s_mt0 = Time.realtimeSinceStartup;
|
|
GC.Collect();
|
|
EditorApplication.update += MeasureTick;
|
|
return "Measure started tag=" + tag + " trailOn=" + trailOn + " (결과는 콘솔 [WL747 Measure])";
|
|
}
|
|
|
|
static void MeasureTick() // 할당 없음 — 배열은 미리 잡아 두었다
|
|
{
|
|
if (s_mi < s_mono.Length) s_mono[s_mi++] = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|
if (Time.realtimeSinceStartup - s_mt0 < s_mdur && s_mi < s_mono.Length) return;
|
|
|
|
EditorApplication.update -= MeasureTick;
|
|
long grow = 0; int growFrames = 0; long peak = 0;
|
|
for (int i = 1; i < s_mi; i++)
|
|
{
|
|
long d = s_mono[i] - s_mono[i - 1];
|
|
if (d > 0) { grow += d; growFrames++; if (d > peak) peak = d; }
|
|
}
|
|
Debug.Log("[WL747 Measure] tag=" + s_mtag + " frames=" + s_mi
|
|
+ " growFrames=" + growFrames + " totalGrowth=" + grow + "B"
|
|
+ " perFrame=" + (s_mi > 1 ? (grow / (double)(s_mi - 1)).ToString("F0") : "0") + "B"
|
|
+ " peakFrame=" + peak + "B");
|
|
}
|
|
|
|
// ── 캡처 ────────────────────────────────────────────────────────────────
|
|
public static object Capture(string tag) { return Shoot(tag); }
|
|
|
|
/// <summary>true 면 PC 를 가까이서 잡는다(리본 형상 판독용). Run 이 켠다.</summary>
|
|
public static bool s_close;
|
|
|
|
static string Shoot(string tag)
|
|
{
|
|
var cam = Camera.main;
|
|
if (cam == null) return "no Camera.main";
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
int w = 1280, h = 720;
|
|
var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
|
|
var go = new GameObject("__wl747cam");
|
|
var c = go.AddComponent<Camera>();
|
|
c.CopyFrom(cam);
|
|
if (s_close)
|
|
{
|
|
var pc = MainPC();
|
|
if (pc != null)
|
|
{
|
|
// 캐릭터 가슴 높이를 정면 살짝 위에서 근접으로 — 리본이 검날을 덮는지 보려면 이 거리라야 한다.
|
|
Vector3 center = pc.Get_Center_position();
|
|
Vector3 dir = (cam.transform.position - center).normalized;
|
|
c.transform.position = center + dir * 3.2f + Vector3.up * 0.5f;
|
|
c.transform.LookAt(center);
|
|
c.fieldOfView = 45f;
|
|
c.nearClipPlane = 0.05f;
|
|
}
|
|
}
|
|
c.targetTexture = rt;
|
|
c.enabled = false;
|
|
c.Render();
|
|
var prev = RenderTexture.active;
|
|
RenderTexture.active = rt;
|
|
var tex = new Texture2D(w, h, TextureFormat.RGB24, false);
|
|
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
|
|
tex.Apply();
|
|
RenderTexture.active = prev;
|
|
var px = tex.GetPixels32();
|
|
int magenta = 0;
|
|
foreach (var p in px) if (p.r > 200 && p.b > 200 && p.g < 80) magenta++;
|
|
var path = System.IO.Path.Combine(OutDir, tag + ".png");
|
|
System.IO.File.WriteAllBytes(path, tex.EncodeToPNG());
|
|
UnityEngine.Object.DestroyImmediate(tex);
|
|
c.targetTexture = null;
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
rt.Release();
|
|
UnityEngine.Object.DestroyImmediate(rt);
|
|
return path + " magentaRatio=" + ((float)magenta / px.Length).ToString("F5");
|
|
}
|
|
|
|
// ── 살아 있는 슬래시 인스턴스의 이미터·머티리얼 실측 ────────────────────
|
|
public static object LiveEffects()
|
|
{
|
|
var sb = new StringBuilder();
|
|
var roots = new HashSet<Transform>();
|
|
foreach (var ps in UnityEngine.Object.FindObjectsByType<ParticleSystem>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
{
|
|
var root = ps.transform;
|
|
while (root.parent != null && !root.name.StartsWith("Effect_Slash_")) root = root.parent;
|
|
if (!root.name.StartsWith("Effect_Slash_")) continue;
|
|
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
|
if (psr == null) continue;
|
|
roots.Add(root);
|
|
sb.Append(root.name).Append(" / ").Append(ps.gameObject.name)
|
|
.Append(" rendererEnabled=").Append(psr.enabled)
|
|
.Append(" mat=").Append(psr.sharedMaterial != null ? psr.sharedMaterial.name : "null")
|
|
.Append(" supported=").Append(psr.sharedMaterial != null && psr.sharedMaterial.shader != null && psr.sharedMaterial.shader.isSupported)
|
|
.Append('\n');
|
|
}
|
|
sb.Insert(0, "slash instances=" + roots.Count + "\n");
|
|
return sb.ToString();
|
|
}
|
|
}
|