687 lines
37 KiB
C#
687 lines
37 KiB
C#
|
|
// WL-814k 프로브 + 캡처 4장 (게임 코드 아님 · 에디트 모드 전용 · 임시 씬 저장 0 · 커밋 0)
|
|||
|
|
// probe: unity command run_script --file AgentScripts/WL814k_Probe.cs --entry WL814k_Probe.RunAll
|
|||
|
|
// cap : unity command run_script --file AgentScripts/WL814k_Probe.cs --entry WL814k_Probe.Capture
|
|||
|
|
// 🔴 원본 프리팹/머티리얼/애니메이션 수정 0. 씬 저장 0.
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEditor.SceneManagement;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.Profiling;
|
|||
|
|
using UnityEngine.SceneManagement;
|
|||
|
|
using WL.Look.SpriteBake;
|
|||
|
|
|
|||
|
|
public static class WL814k_Probe
|
|||
|
|
{
|
|||
|
|
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814k";
|
|||
|
|
const string kTxt = "AgentScripts/WL814k_PROBE.txt";
|
|||
|
|
const string kTxtCap = "AgentScripts/WL814k_CAPTURE.txt";
|
|||
|
|
const string kRes = "Assets/WL/Look/Sprite/Resources/WL";
|
|||
|
|
const string kPcPrefab = "Assets/Res_Addr/PC/Ai01.prefab";
|
|||
|
|
const string kMobPrefab = "Assets/Res_Addr/Mobs/Mob/Batty_A.prefab";
|
|||
|
|
|
|||
|
|
// 도트A 프리셋 실측 = orthoSize 3.4 · pixelHeight 240 · 내림각 43.609°
|
|||
|
|
const float kOrtho = 3.4f, kPitch = 43.609f;
|
|||
|
|
const int kLowH = 240, kLowW = 135, kHiW = 1080, kHiH = 1920;
|
|||
|
|
|
|||
|
|
static StringBuilder s_sb;
|
|||
|
|
static int s_pass, s_fail;
|
|||
|
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
|||
|
|
static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); }
|
|||
|
|
static void Write(string p) { File.WriteAllText(p, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814k] → " + p); }
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
// 프로브
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
public static void RunAll()
|
|||
|
|
{
|
|||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|||
|
|
L("WL-814k 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
A_Assets();
|
|||
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|||
|
|
B_AttachRestore();
|
|||
|
|
C_FrameDir();
|
|||
|
|
D_Gc();
|
|||
|
|
E_Sheet();
|
|||
|
|
F_C8();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
SpriteBillboard.RestoreNow("probe-end");
|
|||
|
|
WLSpriteLookSettings.RuntimeDisabled = false;
|
|||
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " — " + s_pass + " pass / " + s_fail + " fail");
|
|||
|
|
Write(kTxt);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── A. 에셋 · 메타 ──────────────────────────────────────────────────────
|
|||
|
|
static void A_Assets()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== A. 에셋 · 메타 ==");
|
|||
|
|
var bake = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kRes + "/WLSpriteBakeSettings.asset");
|
|||
|
|
var look = AssetDatabase.LoadAssetAtPath<WLSpriteLookSettings>(kRes + "/WLSpriteLookSettings.asset");
|
|||
|
|
Chk("A1 굽기 SO", bake != null, bake == null ? "없음" : "내림각 " + bake.PitchDeg.ToString("0.000") + "° · 픽셀 " + bake.WorldPerPixel.ToString("0.00000") + " m");
|
|||
|
|
Chk("A2 런타임 SO", look != null && look.sets != null && look.sets.Length == 2, look == null ? "없음" : "sets " + look.sets.Length);
|
|||
|
|
if (look == null) return;
|
|||
|
|
foreach (var s in look.sets)
|
|||
|
|
{
|
|||
|
|
Chk("A3 시트 " + (s == null ? "null" : s.name), s != null && s.IsUsable,
|
|||
|
|
s == null ? "null" : string.Format("{0} · 프레임 {1}×{2} · {3}열×{4}행 · 방향 {5} · 총 {6}칸 · 텍스처 {7}×{8}",
|
|||
|
|
s.actorNamePrefix, s.frameWidth, s.frameHeight, s.columns, s.rows, s.dirCount, s.TotalFrames,
|
|||
|
|
s.sheet == null ? 0 : s.sheet.width, s.sheet == null ? 0 : s.sheet.height));
|
|||
|
|
if (s == null || s.sheet == null) continue;
|
|||
|
|
var ti = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(s.sheet)) as TextureImporter;
|
|||
|
|
Chk("A4 임포트 " + s.name, ti != null && ti.filterMode == FilterMode.Point && !ti.mipmapEnabled
|
|||
|
|
&& ti.textureCompression == TextureImporterCompression.Uncompressed && ti.alphaIsTransparency,
|
|||
|
|
ti == null ? "임포터 없음" : "filter " + ti.filterMode + " · mipmap " + ti.mipmapEnabled + " · 압축 " + ti.textureCompression
|
|||
|
|
+ " · alphaIsTransparency " + ti.alphaIsTransparency + " · maxSize " + ti.maxTextureSize + " · type " + ti.textureType);
|
|||
|
|
int need = s.TotalFrames;
|
|||
|
|
int cap = s.columns * s.rows;
|
|||
|
|
Chk("A5 격자 " + s.name, cap >= need && s.sheet.width == s.columns * s.frameWidth && s.sheet.height == s.rows * s.frameHeight,
|
|||
|
|
"칸 " + cap + " ≥ 필요 " + need + " · 텍스처 " + s.sheet.width + "×" + s.sheet.height);
|
|||
|
|
var act = new StringBuilder();
|
|||
|
|
foreach (var a in s.actions) act.Append(a.key + "[" + a.startIndex + "+" + a.frameCount + "] ");
|
|||
|
|
Chk("A6 동작표 " + s.name, s.actions.Length >= 4, s.actions.Length + "개 · " + act.ToString().TrimEnd());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── B. 부착 → 원복 ──────────────────────────────────────────────────────
|
|||
|
|
static void B_AttachRestore()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== B. 부착 → 원복 (원본 파괴 0 · 렌더러 원값 복원) ==");
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
var actor = Spawn(kPcPrefab, Vector3.zero, 0.7f);
|
|||
|
|
var set = SpriteBillboard.MatchSet(actor.name, look);
|
|||
|
|
Chk("B1 이름 매칭", set != null, "액터 '" + actor.name + "' → " + (set == null ? "매칭 실패" : set.name));
|
|||
|
|
if (set == null) { UnityEngine.Object.DestroyImmediate(actor); return; }
|
|||
|
|
|
|||
|
|
var smrs = actor.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|||
|
|
int before = 0; foreach (var r in smrs) if (r.enabled) before++;
|
|||
|
|
int childBefore = CountAll(actor);
|
|||
|
|
|
|||
|
|
var e = SpriteBillboard.TryAttach(actor, set);
|
|||
|
|
int after = 0; foreach (var r in smrs) if (r.enabled) after++;
|
|||
|
|
Chk("B2 원본 끔", e != null && after == 0 && before > 0, "SMR " + before + "개 → 켜져 있는 것 " + after + "개");
|
|||
|
|
Chk("B3 쿼드 1개", e != null && e.quad != null, e == null ? "-" : e.quad.name + " · 부모 " + e.quad.transform.parent.name + " · 레이어 " + e.quad.layer);
|
|||
|
|
Chk("B4 파괴 0", CountAll(actor) == childBefore + 1, "노드 " + childBefore + " → " + CountAll(actor) + " (쿼드 +1)");
|
|||
|
|
|
|||
|
|
SpriteBillboard.Detach(actor);
|
|||
|
|
int restored = 0; foreach (var r in smrs) if (r != null && r.enabled) restored++;
|
|||
|
|
Chk("B5 원복", restored == before && CountAll(actor) == childBefore, "SMR 켜짐 " + restored + "/" + before + " · 노드 " + CountAll(actor) + "/" + childBefore);
|
|||
|
|
UnityEngine.Object.DestroyImmediate(actor);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── C. 프레임 · 방향 선택 ───────────────────────────────────────────────
|
|||
|
|
static void C_FrameDir()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== C. 프레임 · 방향 선택 (Animator 폴링 · 원본 코드 수정 0) ==");
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
var cam = MakeCam(kLowW, kLowH);
|
|||
|
|
var actor = Spawn(kPcPrefab, Vector3.zero, 0.7f);
|
|||
|
|
var set = SpriteBillboard.MatchSet(actor.name, look);
|
|||
|
|
var anim = actor.GetComponent<Animator>();
|
|||
|
|
anim.applyRootMotion = false; anim.cullingMode = AnimatorCullingMode.AlwaysAnimate; anim.Rebind();
|
|||
|
|
var e = SpriteBillboard.TryAttach(actor, set);
|
|||
|
|
|
|||
|
|
// ⓐ 상태 해시 → 동작
|
|||
|
|
int okState = 0, allState = 0;
|
|||
|
|
foreach (var a in set.actions)
|
|||
|
|
foreach (var n in a.stateNames)
|
|||
|
|
{
|
|||
|
|
allState++;
|
|||
|
|
var found = set.FindByStateHash(Animator.StringToHash(n));
|
|||
|
|
if (found == a) okState++;
|
|||
|
|
}
|
|||
|
|
Chk("C1 상태→동작", okState == allState, okState + "/" + allState + "개 상태가 정확한 동작으로 간다");
|
|||
|
|
|
|||
|
|
// ⓑ normalizedTime → 프레임
|
|||
|
|
var idle = set.FindByKey("idle");
|
|||
|
|
var lines = new StringBuilder();
|
|||
|
|
bool frameOk = true;
|
|||
|
|
for (int f = 0; f < idle.frameCount; f++)
|
|||
|
|
{
|
|||
|
|
float nt = (f + 0.5f) / idle.frameCount;
|
|||
|
|
anim.Play(Animator.StringToHash("idle"), 0, nt);
|
|||
|
|
anim.Update(0f);
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
if (e.frame != f) frameOk = false;
|
|||
|
|
lines.Append("nt " + nt.ToString("0.00") + "→f" + e.frame + " ");
|
|||
|
|
}
|
|||
|
|
Chk("C2 시간→프레임", frameOk, lines.ToString().Trim());
|
|||
|
|
|
|||
|
|
// ⓑ-2 기본 상태가 아닌 동작도 폴링으로 맞는가 (에디트 모드 실측)
|
|||
|
|
lines.Length = 0; int hit = 0, tot = 0;
|
|||
|
|
foreach (var a in set.actions)
|
|||
|
|
{
|
|||
|
|
int h = Animator.StringToHash(a.stateNames[0]);
|
|||
|
|
if (!anim.HasState(0, h)) continue;
|
|||
|
|
tot++;
|
|||
|
|
anim.Play(h, 0, 0.5f);
|
|||
|
|
anim.Update(0f);
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
bool ok = e.action != null && e.action.key == a.key;
|
|||
|
|
if (ok) hit++;
|
|||
|
|
lines.Append(a.key + "→" + (e.action == null ? "-" : e.action.key) + (ok ? " " : "✗ "));
|
|||
|
|
}
|
|||
|
|
Chk("C2b 상태 폴링(에디트 모드)", hit == tot, hit + "/" + tot + " · " + lines.ToString().Trim());
|
|||
|
|
|
|||
|
|
// ⓒ 상대 yaw → 방향
|
|||
|
|
lines.Length = 0; bool dirOk = true;
|
|||
|
|
anim.Play(Animator.StringToHash("idle"), 0, 0f); anim.Update(0f);
|
|||
|
|
for (int d = 0; d < set.dirCount; d++)
|
|||
|
|
{
|
|||
|
|
actor.transform.rotation = Quaternion.Euler(0f, d * (360f / set.dirCount), 0f);
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
if (e.dir != d) dirOk = false;
|
|||
|
|
lines.Append(d * 45 + "°→d" + e.dir + " ");
|
|||
|
|
}
|
|||
|
|
Chk("C3 각도→방향", dirOk, lines.ToString().Trim());
|
|||
|
|
|
|||
|
|
// ⓓ 쿼드 크기 = 프레임 px × 픽셀 월드 크기 (스케일 보정 포함)
|
|||
|
|
actor.transform.rotation = Quaternion.identity;
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
float expect = set.frameWidth * set.worldPerPixel * (0.7f / set.bakeScale);
|
|||
|
|
float got = e.quadTf.lossyScale.x;
|
|||
|
|
Chk("C4 쿼드 크기", Mathf.Abs(got - expect) < 0.002f,
|
|||
|
|
string.Format("월드 {0:0.0000} m (기대 {1:0.0000}) · 스케일 보정 {2:0.000}", got, expect, e.scaleK));
|
|||
|
|
Chk("C5 쿼드 자세", Quaternion.Angle(e.quadTf.rotation, cam.cam.transform.rotation) < 0.01f,
|
|||
|
|
"카메라 정렬 오차 " + Quaternion.Angle(e.quadTf.rotation, cam.cam.transform.rotation).ToString("0.000") + "°");
|
|||
|
|
|
|||
|
|
SpriteBillboard.Detach(actor);
|
|||
|
|
UnityEngine.Object.DestroyImmediate(actor);
|
|||
|
|
cam.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── D. GC ───────────────────────────────────────────────────────────────
|
|||
|
|
static void D_Gc()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== D. GC (프레임당 할당 0) ==");
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
var cam = MakeCam(kLowW, kLowH);
|
|||
|
|
var a1 = Spawn(kPcPrefab, Vector3.zero, 0.7f);
|
|||
|
|
var a2 = Spawn(kMobPrefab, new Vector3(1.5f, 0f, 0f), 0f);
|
|||
|
|
SpriteBillboard.TryAttach(a1, SpriteBillboard.MatchSet(a1.name, look));
|
|||
|
|
SpriteBillboard.TryAttach(a2, SpriteBillboard.MatchSet(a2.name, look));
|
|||
|
|
|
|||
|
|
for (int i = 0; i < 200; i++) SpriteBillboard.UpdateAll(cam.cam); // 워밍업
|
|||
|
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
|||
|
|
long m0 = Profiler.GetMonoUsedSizeLong();
|
|||
|
|
int g0 = GC.CollectionCount(0);
|
|||
|
|
for (int i = 0; i < 200000; i++) SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
long m1 = Profiler.GetMonoUsedSizeLong();
|
|||
|
|
int g1 = GC.CollectionCount(0);
|
|||
|
|
Chk("D1 UpdateAll ×200k", m1 - m0 == 0 && g1 == g0, "mono Δ " + (m1 - m0) + " B · gen0 " + (g1 - g0) + "회");
|
|||
|
|
|
|||
|
|
SpriteBillboard.RestoreNow("gc-probe");
|
|||
|
|
UnityEngine.Object.DestroyImmediate(a1); UnityEngine.Object.DestroyImmediate(a2);
|
|||
|
|
cam.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── E. 시트 내용 ────────────────────────────────────────────────────────
|
|||
|
|
static void E_Sheet()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== E. 시트 내용 (도트화가 실제로 강제됐는가) ==");
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
foreach (var s in look.sets)
|
|||
|
|
{
|
|||
|
|
if (s == null || s.sheet == null) continue;
|
|||
|
|
var path = AssetDatabase.GetAssetPath(s.sheet);
|
|||
|
|
var bytes = File.ReadAllBytes(path);
|
|||
|
|
var t = new Texture2D(2, 2, TextureFormat.RGBA32, false, false);
|
|||
|
|
t.LoadImage(bytes);
|
|||
|
|
var px = t.GetPixels32();
|
|||
|
|
// 기대 팔레트 = 채널당 levels 단계 · 여기에 외곽선 색(SO)만 예외로 허용한다
|
|||
|
|
var bake = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kRes + "/WLSpriteBakeSettings.asset");
|
|||
|
|
int lv = bake != null ? bake.paletteLevels : 6;
|
|||
|
|
var allowed = new HashSet<int>();
|
|||
|
|
for (int i = 0; i < lv; i++) allowed.Add(Mathf.RoundToInt((float)i / (lv - 1) * 255f));
|
|||
|
|
Color32 oc = bake != null ? (Color32)bake.outlineColor : new Color32(14, 11, 17, 255);
|
|||
|
|
|
|||
|
|
bool bands = bake == null || bake.paletteValueBands;
|
|||
|
|
int opaque = 0, semi = 0, outline = 0, offPalette = 0;
|
|||
|
|
var levels = new HashSet<int>();
|
|||
|
|
var colors = new HashSet<int>();
|
|||
|
|
for (int i = 0; i < px.Length; i++)
|
|||
|
|
{
|
|||
|
|
if (px[i].a == 0) continue;
|
|||
|
|
if (px[i].a == 255) opaque++; else semi++;
|
|||
|
|
levels.Add(bands ? Mathf.Max(px[i].r, Mathf.Max(px[i].g, px[i].b)) : px[i].r);
|
|||
|
|
if (!bands) { levels.Add(px[i].g); levels.Add(px[i].b); }
|
|||
|
|
colors.Add((px[i].r << 16) | (px[i].g << 8) | px[i].b);
|
|||
|
|
bool isOutline = px[i].r == oc.r && px[i].g == oc.g && px[i].b == oc.b;
|
|||
|
|
if (isOutline) { outline++; continue; }
|
|||
|
|
if (bands)
|
|||
|
|
{
|
|||
|
|
if (!allowed.Contains(Mathf.Max(px[i].r, Mathf.Max(px[i].g, px[i].b)))) offPalette++;
|
|||
|
|
}
|
|||
|
|
else if (!allowed.Contains(px[i].r) || !allowed.Contains(px[i].g) || !allowed.Contains(px[i].b)) offPalette++;
|
|||
|
|
}
|
|||
|
|
Chk("E1 알파 이진화 " + s.name, semi == 0, "불투명 " + opaque + " · 반투명 " + semi + "(0 이어야 한다)");
|
|||
|
|
Chk("E2 팔레트 양자화 " + s.name, offPalette == 0,
|
|||
|
|
(bands ? "밝기 " : "채널 ") + lv + "단계 " + Join(allowed) + " 밖 픽셀 " + offPalette + "개 · 실제 " + levels.Count + "가지 " + Join(levels)
|
|||
|
|
+ " (외곽선 색 " + oc.r + "," + oc.g + "," + oc.b + " 제외) · 색 " + colors.Count + "종");
|
|||
|
|
Chk("E3 외곽선 " + s.name, outline > opaque * 0.05f, "어두운 테두리 픽셀 " + outline + " / 불투명 " + opaque
|
|||
|
|
+ " (" + (100f * outline / Mathf.Max(1, opaque)).ToString("0.0") + " %)");
|
|||
|
|
Chk("E4 PNG 크기 " + s.name, bytes.Length > 0, bytes.Length.ToString("N0") + " B (" + (bytes.Length / 1024.0).ToString("0.0") + " KB) · "
|
|||
|
|
+ s.sheet.width + "×" + s.sheet.height + " · RGBA32 런타임 " + (s.sheet.width * s.sheet.height * 4 / 1024.0 / 1024.0).ToString("0.00") + " MB");
|
|||
|
|
UnityEngine.Object.DestroyImmediate(t);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string Join(HashSet<int> s)
|
|||
|
|
{
|
|||
|
|
var l = new List<int>(s); l.Sort();
|
|||
|
|
var sb = new StringBuilder("[");
|
|||
|
|
foreach (var v in l) sb.Append(v).Append(' ');
|
|||
|
|
return sb.ToString().TrimEnd() + "]";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── F. C8 (SO off → 원본 3D 100 %) ─────────────────────────────────────
|
|||
|
|
static void F_C8()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("== F. C8 롤백 (SO off → 원본 3D 100 %) ==");
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
var actor = Spawn(kPcPrefab, Vector3.zero, 0.7f);
|
|||
|
|
var smrs = actor.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|||
|
|
var mrs = actor.GetComponentsInChildren<MeshRenderer>(true);
|
|||
|
|
int smrOn0 = 0; foreach (var r in smrs) if (r.enabled) smrOn0++;
|
|||
|
|
int mrOn0 = 0; foreach (var r in mrs) if (r.enabled) mrOn0++;
|
|||
|
|
int nodes0 = CountAll(actor);
|
|||
|
|
|
|||
|
|
SpriteBillboard.TryAttach(actor, SpriteBillboard.MatchSet(actor.name, look));
|
|||
|
|
WLSpriteLookSettings.RuntimeDisabled = true;
|
|||
|
|
Chk("F1 게이트", !WLSpriteLookSettings.Enabled && !SpriteBillboard.ShouldBeOn, "RuntimeDisabled → Enabled=false · ShouldBeOn=false");
|
|||
|
|
int n = SpriteBillboard.RestoreNow("C8");
|
|||
|
|
WLSpriteLookSettings.RuntimeDisabled = false;
|
|||
|
|
|
|||
|
|
int smrOn1 = 0; foreach (var r in smrs) if (r != null && r.enabled) smrOn1++;
|
|||
|
|
int mrOn1 = 0; foreach (var r in mrs) if (r != null && r.enabled) mrOn1++;
|
|||
|
|
Chk("F2 렌더러 100 % 복원", smrOn1 == smrOn0 && mrOn1 == mrOn0, "SMR " + smrOn1 + "/" + smrOn0 + " · MR " + mrOn1 + "/" + mrOn0);
|
|||
|
|
Chk("F3 쿼드 0", CountAll(actor) == nodes0 && SpriteBillboard.ActiveCount == 0, "노드 " + CountAll(actor) + "/" + nodes0 + " · 대상 " + SpriteBillboard.ActiveCount);
|
|||
|
|
|
|||
|
|
// 시트를 안 가진 액터는 아예 안 걸린다
|
|||
|
|
var other = Spawn("Assets/Res_Addr/PC/Ai02.prefab", Vector3.zero, 0.7f);
|
|||
|
|
if (other != null)
|
|||
|
|
{
|
|||
|
|
Chk("F4 목록 밖 액터", SpriteBillboard.MatchSet(other.name, look) == null, "'" + other.name + "' → 매칭 0 = 원본 3D 그대로");
|
|||
|
|
UnityEngine.Object.DestroyImmediate(other);
|
|||
|
|
}
|
|||
|
|
UnityEngine.Object.DestroyImmediate(actor);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
// 캡처 4장
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
public static void Capture()
|
|||
|
|
{
|
|||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|||
|
|
L("WL-814k 캡처 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|||
|
|
Directory.CreateDirectory(kOutDir);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|||
|
|
World();
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
|
|||
|
|
// 두 캐릭터를 나란히 (PC 왼쪽 · 몹 오른쪽)
|
|||
|
|
var pc = Spawn(kPcPrefab, new Vector3(-1.0f, 0f, 0f), 0.7f);
|
|||
|
|
var mob = Spawn(kMobPrefab, new Vector3(1.1f, 0f, 0f), 0f);
|
|||
|
|
// 🔴 구운 프레임과 **정확히 같은 시각**으로 3D 를 세운다(프레임 양자화 차이를 A/B 에서 빼기 위해)
|
|||
|
|
PoseFrame(pc, look, "attack", 2, 315f);
|
|||
|
|
PoseFrame(mob, look, "attack", 3, 45f);
|
|||
|
|
|
|||
|
|
var ground = Ground();
|
|||
|
|
var cam = MakeCam(kLowW, kLowH);
|
|||
|
|
cam.cam.transform.position = new Vector3(0f, 1.0f, 0f) - cam.cam.transform.forward * 40f;
|
|||
|
|
cam.cam.orthographicSize = kOrtho;
|
|||
|
|
|
|||
|
|
// 지면만 찍은 기준 컷(실루엣을 차분으로 정확히 잰다)
|
|||
|
|
pc.SetActive(false); mob.SetActive(false);
|
|||
|
|
var bg = cam.Grab();
|
|||
|
|
pc.SetActive(true); mob.SetActive(true);
|
|||
|
|
|
|||
|
|
// ⓐ 3D 원본
|
|||
|
|
var a = cam.Grab();
|
|||
|
|
SavePoint(a, kLowW, kLowH, kOutDir + @"\k_1_3d_original.png");
|
|||
|
|
L(" ⓐ 3D 원본 → k_1_3d_original.png");
|
|||
|
|
|
|||
|
|
// ⓑ 구운 스프라이트로 교체(같은 구도 · 같은 포즈 · 같은 방향)
|
|||
|
|
var ePc = SpriteBillboard.TryAttach(pc, SpriteBillboard.MatchSet(pc.name, look));
|
|||
|
|
var eMob = SpriteBillboard.TryAttach(mob, SpriteBillboard.MatchSet(mob.name, look));
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
L(string.Format(" 폴링 결과 — PC {0} d{1} f{2} / 몹 {3} d{4} f{5}",
|
|||
|
|
ePc.action != null ? ePc.action.key : "-", ePc.dir, ePc.frame,
|
|||
|
|
eMob.action != null ? eMob.action.key : "-", eMob.dir, eMob.frame));
|
|||
|
|
// 🔴 A/B 는 「같은 포즈」여야 하므로 프레임을 못 박는다(폴링 정확도는 프로브 C 가 따로 잰다)
|
|||
|
|
SpriteBillboard.SetManualFrame(pc, "attack", 7, 2);
|
|||
|
|
SpriteBillboard.SetManualFrame(mob, "attack", 1, 3);
|
|||
|
|
SpriteBillboard.UpdateAll(cam.cam);
|
|||
|
|
var b = cam.Grab();
|
|||
|
|
SavePoint(b, kLowW, kLowH, kOutDir + @"\k_2_sprite_baked.png");
|
|||
|
|
L(" ⓑ 스프라이트 교체 → k_2_sprite_baked.png");
|
|||
|
|
|
|||
|
|
// 정렬 실측 — 같은 구도에서 3D 실루엣과 스프라이트 실루엣이 몇 px 어긋나는가
|
|||
|
|
var boxA = Box(a, bg, kLowW, kLowH, 0, kLowW / 2);
|
|||
|
|
var boxB = Box(b, bg, kLowW, kLowH, 0, kLowW / 2);
|
|||
|
|
L(string.Format(" 정렬 실측(PC 쪽 절반) — 3D x[{0}..{1}] y[{2}..{3}] / 스프라이트 x[{4}..{5}] y[{6}..{7}] → Δ좌 {8} · Δ우 {9} · Δ하 {10} · Δ상 {11} px",
|
|||
|
|
boxA[0], boxA[1], boxA[2], boxA[3], boxB[0], boxB[1], boxB[2], boxB[3],
|
|||
|
|
boxB[0] - boxA[0], boxB[1] - boxA[1], boxB[2] - boxA[2], boxB[3] - boxA[3]));
|
|||
|
|
L(string.Format(" 밝기 실측 — 3D 평균 {0:0.0} · 스프라이트 평균 {1:0.0} (0~255 · 캐릭터 픽셀만) · 캐릭터 픽셀 3D {2} / 스프라이트 {3} (외곽선 +{4})",
|
|||
|
|
MeanLum(a, bg), MeanLum(b, bg), CountDiff(a, bg), CountDiff(b, bg), CountDiff(b, bg) - CountDiff(a, bg)));
|
|||
|
|
|
|||
|
|
// ⓒ 2배 클로즈업 — 위 3D / 아래 스프라이트
|
|||
|
|
CloseUp(a, b, bg, kLowW, kLowH, kOutDir + @"\k_3_closeup_2x.png");
|
|||
|
|
L(" ⓒ 클로즈업 → k_3_closeup_2x.png");
|
|||
|
|
|
|||
|
|
// ⓓ 방향 8종 한 장
|
|||
|
|
SpriteBillboard.RestoreNow("capture-dir");
|
|||
|
|
Directions(cam, pc, mob, ground, kOutDir + @"\k_4_directions.png");
|
|||
|
|
L(" ⓓ 방향 8종 → k_4_directions.png");
|
|||
|
|
|
|||
|
|
cam.Dispose();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
SpriteBillboard.RestoreNow("capture-end");
|
|||
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L("캡처 " + (s_fail == 0 ? "PASS" : "FAIL") + " — " + kOutDir);
|
|||
|
|
Write(kTxtCap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 방향 8종을 한 장에 — 위 줄 = 3D · 아래 줄 = 스프라이트
|
|||
|
|
static void Directions(CamRig cam, GameObject pc, GameObject mob, GameObject ground, string file)
|
|||
|
|
{
|
|||
|
|
int cell = 64; // 셀 하나의 저해상도 픽셀 크기
|
|||
|
|
var rig = MakeCam(cell, cell);
|
|||
|
|
rig.cam.transform.rotation = cam.cam.transform.rotation;
|
|||
|
|
rig.cam.orthographicSize = cell * (2f * kOrtho / kLowH) * 0.5f;
|
|||
|
|
mob.SetActive(false);
|
|||
|
|
pc.transform.position = Vector3.zero;
|
|||
|
|
|
|||
|
|
int cols = 8, rows = 2;
|
|||
|
|
var sheet = new Color32[cols * cell * rows * cell];
|
|||
|
|
var look = WLSpriteLookSettings.Instance;
|
|||
|
|
|
|||
|
|
for (int pass = 0; pass < 2; pass++)
|
|||
|
|
{
|
|||
|
|
if (pass == 1)
|
|||
|
|
{
|
|||
|
|
var e = SpriteBillboard.TryAttach(pc, SpriteBillboard.MatchSet(pc.name, look));
|
|||
|
|
}
|
|||
|
|
for (int d = 0; d < cols; d++)
|
|||
|
|
{
|
|||
|
|
Pose(pc, "idle", 0.0f, d * 45f);
|
|||
|
|
rig.cam.transform.position = new Vector3(0f, 0.6f, 0f) - rig.cam.transform.forward * 40f;
|
|||
|
|
if (pass == 1) SpriteBillboard.UpdateAll(rig.cam);
|
|||
|
|
var px = rig.Grab();
|
|||
|
|
BlitInto(px, cell, cell, sheet, cols * cell, d * cell, (rows - 1 - pass) * cell);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
SpriteBillboard.RestoreNow("dir-capture");
|
|||
|
|
mob.SetActive(true);
|
|||
|
|
SavePoint(sheet, cols * cell, rows * cell, file, 4);
|
|||
|
|
rig.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 클로즈업 — 캐릭터 주변을 잘라 확대해 위/아래로 붙인다
|
|||
|
|
static void CloseUp(Color32[] a, Color32[] b, Color32[] bg, int w, int h, string file)
|
|||
|
|
{
|
|||
|
|
var box = Box(a, bg, w, h, 0, w);
|
|||
|
|
int pad = 6;
|
|||
|
|
int x0 = Mathf.Max(0, box[0] - pad), x1 = Mathf.Min(w - 1, box[1] + pad);
|
|||
|
|
int y0 = Mathf.Max(0, box[2] - pad), y1 = Mathf.Min(h - 1, box[3] + pad);
|
|||
|
|
int cw = x1 - x0 + 1, ch = y1 - y0 + 1;
|
|||
|
|
int zoom = Mathf.Max(2, Mathf.Min(1080 / Mathf.Max(1, cw), 900 / Mathf.Max(1, ch)));
|
|||
|
|
int outW = cw * zoom, outH = ch * zoom * 2 + 8;
|
|||
|
|
var outPx = new Color32[outW * outH];
|
|||
|
|
for (int i = 0; i < outPx.Length; i++) outPx[i] = new Color32(24, 26, 30, 255);
|
|||
|
|
// 아래 = 스프라이트 · 위 = 3D
|
|||
|
|
CropZoom(b, w, h, x0, y0, cw, ch, zoom, outPx, outW, 0, 0);
|
|||
|
|
CropZoom(a, w, h, x0, y0, cw, ch, zoom, outPx, outW, 0, ch * zoom + 8);
|
|||
|
|
L(string.Format(" 클로즈업 crop {0}×{1} px → ×{2} = {3}×{4} (위 3D · 아래 스프라이트)", cw, ch, zoom, outW, outH));
|
|||
|
|
SaveRaw(outPx, outW, outH, file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void CropZoom(Color32[] src, int w, int h, int x0, int y0, int cw, int ch, int zoom,
|
|||
|
|
Color32[] dst, int dw, int dx, int dy)
|
|||
|
|
{
|
|||
|
|
for (int y = 0; y < ch * zoom; y++)
|
|||
|
|
for (int x = 0; x < cw * zoom; x++)
|
|||
|
|
{
|
|||
|
|
int sx = x0 + x / zoom, sy = y0 + y / zoom;
|
|||
|
|
if (sx < 0 || sy < 0 || sx >= w || sy >= h) continue;
|
|||
|
|
int di = (dy + y) * dw + (dx + x);
|
|||
|
|
if (di < 0 || di >= dst.Length) continue;
|
|||
|
|
dst[di] = src[sy * w + sx];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>지면만 찍은 컷과 다른 픽셀 = 캐릭터 픽셀.</summary>
|
|||
|
|
static bool Diff(Color32[] px, Color32[] bg, int i)
|
|||
|
|
{
|
|||
|
|
if (bg == null) return px[i].a >= 8;
|
|||
|
|
return Mathf.Abs(px[i].r - bg[i].r) + Mathf.Abs(px[i].g - bg[i].g) + Mathf.Abs(px[i].b - bg[i].b) > 8
|
|||
|
|
|| Mathf.Abs(px[i].a - bg[i].a) > 8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float MeanLum(Color32[] px, Color32[] bg)
|
|||
|
|
{
|
|||
|
|
double sum = 0; int n = 0;
|
|||
|
|
for (int i = 0; i < px.Length; i++)
|
|||
|
|
{
|
|||
|
|
if (!Diff(px, bg, i)) continue;
|
|||
|
|
sum += 0.299 * px[i].r + 0.587 * px[i].g + 0.114 * px[i].b; n++;
|
|||
|
|
}
|
|||
|
|
return n == 0 ? 0f : (float)(sum / n);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static int CountDiff(Color32[] px, Color32[] bg)
|
|||
|
|
{
|
|||
|
|
int n = 0;
|
|||
|
|
for (int i = 0; i < px.Length; i++) if (Diff(px, bg, i)) n++;
|
|||
|
|
return n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static int[] Box(Color32[] px, Color32[] bg, int w, int h, int xFrom, int xTo)
|
|||
|
|
{
|
|||
|
|
int minX = int.MaxValue, maxX = -1, minY = int.MaxValue, maxY = -1;
|
|||
|
|
for (int y = 0; y < h; y++)
|
|||
|
|
for (int x = xFrom; x < Mathf.Min(w, xTo); x++)
|
|||
|
|
{
|
|||
|
|
if (!Diff(px, bg, y * w + x)) continue;
|
|||
|
|
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|||
|
|
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|||
|
|
}
|
|||
|
|
return new[] { minX, maxX, minY, maxY };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>지면 — PD 가 「게임 화면처럼」 볼 수 있게 단색 평면 1장(커밋 0 · 임시 씬).</summary>
|
|||
|
|
static GameObject Ground()
|
|||
|
|
{
|
|||
|
|
var go = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
|||
|
|
go.name = "[WL814k] Ground";
|
|||
|
|
go.transform.position = Vector3.zero;
|
|||
|
|
go.transform.localScale = new Vector3(6f, 1f, 6f);
|
|||
|
|
UnityEngine.Object.DestroyImmediate(go.GetComponent<Collider>());
|
|||
|
|
var mr = go.GetComponent<MeshRenderer>();
|
|||
|
|
var sh = Shader.Find("Universal Render Pipeline/Lit");
|
|||
|
|
var m = new Material(sh != null ? sh : Shader.Find("Standard"));
|
|||
|
|
m.hideFlags = HideFlags.HideAndDontSave;
|
|||
|
|
m.SetColor("_BaseColor", new Color(0.34f, 0.40f, 0.25f, 1f));
|
|||
|
|
m.SetFloat("_Smoothness", 0f);
|
|||
|
|
mr.sharedMaterial = m;
|
|||
|
|
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
|||
|
|
mr.receiveShadows = false;
|
|||
|
|
return go;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
// 공통
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
static void World()
|
|||
|
|
{
|
|||
|
|
var bake = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kRes + "/WLSpriteBakeSettings.asset");
|
|||
|
|
RenderSettings.fog = false;
|
|||
|
|
RenderSettings.skybox = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Skybox.mat");
|
|||
|
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Skybox;
|
|||
|
|
RenderSettings.ambientIntensity = 1f;
|
|||
|
|
DynamicGI.UpdateEnvironment();
|
|||
|
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
|||
|
|
sun.type = LightType.Directional; sun.shadows = LightShadows.None;
|
|||
|
|
sun.color = bake != null ? bake.sunColor : Color.white;
|
|||
|
|
sun.intensity = bake != null ? bake.sunIntensity : 1.2f;
|
|||
|
|
sun.transform.rotation = Quaternion.Euler(bake != null ? bake.sunEuler : new Vector3(26.4f, 238.6f, 0f));
|
|||
|
|
if (bake != null && bake.fillIntensity > 0f)
|
|||
|
|
{
|
|||
|
|
var fill = new GameObject("Fill").AddComponent<Light>();
|
|||
|
|
fill.type = LightType.Directional; fill.shadows = LightShadows.None;
|
|||
|
|
fill.color = bake.fillColor; fill.intensity = bake.fillIntensity;
|
|||
|
|
fill.transform.rotation = Quaternion.Euler(bake.fillEuler);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static GameObject Spawn(string prefabPath, Vector3 pos, float scale)
|
|||
|
|
{
|
|||
|
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|||
|
|
if (src == null) return null;
|
|||
|
|
var go = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
|||
|
|
PrefabUtility.UnpackPrefabInstance(go, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
|||
|
|
go.name = Path.GetFileNameWithoutExtension(prefabPath) + "(Clone)";
|
|||
|
|
go.transform.position = pos; go.transform.rotation = Quaternion.identity;
|
|||
|
|
if (scale > 0.0001f) go.transform.localScale = Vector3.one * scale;
|
|||
|
|
foreach (var c in go.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
|||
|
|
foreach (var c in go.GetComponentsInChildren<UnityEngine.AI.NavMeshAgent>(true)) UnityEngine.Object.DestroyImmediate(c);
|
|||
|
|
foreach (var c in go.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(c);
|
|||
|
|
foreach (var c in go.GetComponentsInChildren<ParticleSystem>(true)) UnityEngine.Object.DestroyImmediate(c.gameObject);
|
|||
|
|
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true)) if (mb != null) UnityEngine.Object.DestroyImmediate(mb);
|
|||
|
|
foreach (var sm in go.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
|||
|
|
{ sm.updateWhenOffscreen = true; sm.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; }
|
|||
|
|
var an = go.GetComponent<Animator>();
|
|||
|
|
if (an != null) { an.applyRootMotion = false; an.cullingMode = AnimatorCullingMode.AlwaysAnimate; an.Rebind(); }
|
|||
|
|
// 굽기와 같은 Toon 머티리얼로(같은 조건 비교)
|
|||
|
|
var bake = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kRes + "/WLSpriteBakeSettings.asset");
|
|||
|
|
if (bake != null && bake.useToonMaterials)
|
|||
|
|
foreach (var rend in go.GetComponentsInChildren<Renderer>(true))
|
|||
|
|
{
|
|||
|
|
if (rend == null || rend is ParticleSystemRenderer) continue;
|
|||
|
|
var mats = rend.sharedMaterials; bool any = false;
|
|||
|
|
for (int i = 0; i < mats.Length; i++)
|
|||
|
|
{
|
|||
|
|
if (mats[i] == null) continue;
|
|||
|
|
var t = AssetDatabase.LoadAssetAtPath<Material>(bake.toonMaterialDir + "/" + mats[i].name + bake.toonMaterialSuffix + ".mat");
|
|||
|
|
if (t != null) { mats[i] = t; any = true; }
|
|||
|
|
}
|
|||
|
|
if (any) rend.sharedMaterials = mats;
|
|||
|
|
}
|
|||
|
|
return go;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Pose(GameObject go, string state, float nt, float yaw)
|
|||
|
|
{
|
|||
|
|
go.transform.rotation = Quaternion.Euler(0f, yaw, 0f);
|
|||
|
|
var an = go.GetComponent<Animator>();
|
|||
|
|
if (an == null) return;
|
|||
|
|
int h = Animator.StringToHash(state);
|
|||
|
|
if (!an.HasState(0, h)) return;
|
|||
|
|
an.Play(h, 0, nt);
|
|||
|
|
an.Update(0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>시트가 가진 프레임 f 와 **같은 시각**으로 3D 를 세운다(A/B 를 같은 포즈로 맞추기 위해).</summary>
|
|||
|
|
static void PoseFrame(GameObject go, WLSpriteLookSettings look, string key, int frame, float yaw)
|
|||
|
|
{
|
|||
|
|
var set = SpriteBillboard.MatchSet(go.name, look);
|
|||
|
|
var a = set != null ? set.FindByKey(key) : null;
|
|||
|
|
if (a == null) { Pose(go, key, 0f, yaw); return; }
|
|||
|
|
float nt = a.loop ? (frame + 0.001f) / a.frameCount : (a.frameCount > 1 ? (float)frame / (a.frameCount - 1) : 0f);
|
|||
|
|
Pose(go, key, nt, yaw);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static int CountAll(GameObject go) { return go.GetComponentsInChildren<Transform>(true).Length; }
|
|||
|
|
|
|||
|
|
sealed class CamRig
|
|||
|
|
{
|
|||
|
|
public Camera cam; public RenderTexture rt; public Texture2D tex; public int w, h;
|
|||
|
|
public Color32[] Grab()
|
|||
|
|
{
|
|||
|
|
cam.targetTexture = rt; cam.Render(); cam.targetTexture = null;
|
|||
|
|
var active = RenderTexture.active; RenderTexture.active = rt;
|
|||
|
|
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0); tex.Apply(false, false);
|
|||
|
|
RenderTexture.active = active;
|
|||
|
|
return tex.GetPixels32();
|
|||
|
|
}
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
RenderTexture.active = null;
|
|||
|
|
if (rt != null) { rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
|
|||
|
|
if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
|
|||
|
|
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static CamRig MakeCam(int w, int h)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject("[WL814k] CapCam");
|
|||
|
|
var c = go.AddComponent<Camera>();
|
|||
|
|
c.orthographic = true;
|
|||
|
|
c.orthographicSize = h * (2f * kOrtho / kLowH) * 0.5f;
|
|||
|
|
c.nearClipPlane = 0.01f; c.farClipPlane = 200f;
|
|||
|
|
c.clearFlags = CameraClearFlags.SolidColor;
|
|||
|
|
c.backgroundColor = new Color(0.27f, 0.34f, 0.22f, 1f); // 지면 평면 밖은 이 색(지면이 화면을 덮는다)
|
|||
|
|
c.allowHDR = false; c.allowMSAA = false; c.useOcclusionCulling = false;
|
|||
|
|
go.transform.rotation = Quaternion.Euler(kPitch, 0f, 0f);
|
|||
|
|
go.transform.position = new Vector3(0f, 1.0f, 0f) - go.transform.forward * 40f;
|
|||
|
|
var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
|||
|
|
rt.filterMode = FilterMode.Point; rt.antiAliasing = 1; rt.Create();
|
|||
|
|
var tex = new Texture2D(w, h, TextureFormat.RGBA32, false, false);
|
|||
|
|
return new CamRig { cam = c, rt = rt, tex = tex, w = w, h = h };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 저해상도 → Point 확대 저장(도트 모드가 화면에 내보내는 방식 그대로)
|
|||
|
|
static void SavePoint(Color32[] px, int w, int h, string file, int zoomOverride = 0)
|
|||
|
|
{
|
|||
|
|
int zoom = zoomOverride > 0 ? zoomOverride : Mathf.Max(1, kHiH / h);
|
|||
|
|
int ow = w * zoom, oh = h * zoom;
|
|||
|
|
var outPx = new Color32[ow * oh];
|
|||
|
|
for (int y = 0; y < oh; y++)
|
|||
|
|
for (int x = 0; x < ow; x++)
|
|||
|
|
outPx[y * ow + x] = px[(y / zoom) * w + (x / zoom)];
|
|||
|
|
SaveRaw(outPx, ow, oh, file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void SaveRaw(Color32[] px, int w, int h, string file)
|
|||
|
|
{
|
|||
|
|
var t = new Texture2D(w, h, TextureFormat.RGBA32, false, false);
|
|||
|
|
t.SetPixels32(px); t.Apply(false, false);
|
|||
|
|
File.WriteAllBytes(file, t.EncodeToPNG());
|
|||
|
|
UnityEngine.Object.DestroyImmediate(t);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void BlitInto(Color32[] src, int sw, int sh, Color32[] dst, int dw, int dx, int dy)
|
|||
|
|
{
|
|||
|
|
for (int y = 0; y < sh; y++)
|
|||
|
|
for (int x = 0; x < sw; x++)
|
|||
|
|
{
|
|||
|
|
int di = (dy + y) * dw + (dx + x);
|
|||
|
|
if (di >= 0 && di < dst.Length) dst[di] = src[y * sw + x];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|