[WL-814k] 캐릭터 도트 스프라이트 베이크 1차: 베이크 툴 + 런타임 빌보드(도트 모드 한정) + 시범 2종 (#814)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9b17460bef
commit
f78c2c23f7
|
|
@ -0,0 +1,35 @@
|
||||||
|
WL-814k 굽기 · 2026-09-12 18:38:16 · Unity 6000.3.23f1
|
||||||
|
|
||||||
|
──────── Assets/Res_Addr/PC/Ai01.prefab (루트 스케일 0.7)
|
||||||
|
Toon 머티리얼 교체 = 12슬롯 (인스턴스에만 · 원본 .mat 무변경)
|
||||||
|
레이어0 상태 17개: hit, die, idle, run, casting, skill2, attack, attack2, dodge, land, skill3, skill4, skill5, stun, casting_end, lying, getup
|
||||||
|
동작 idle ← 상태 [idle/land/getup] · 클립 Idle (2.000s · 4프레임 · loop True)
|
||||||
|
동작 run ← 상태 [run/dodge] · 클립 Run (0.533s · 6프레임 · loop True)
|
||||||
|
동작 attack ← 상태 [attack/attack2/skill2/skill3/skill4/skill5/casting/casting_end] · 클립 Atk2_wing (1.500s · 6프레임 · loop True)
|
||||||
|
동작 hit ← 상태 [hit/stun] · 클립 LightHit (1.000s · 2프레임 · loop True)
|
||||||
|
동작 die ← 상태 [die/lying] · 클립 death_F3441 (2.100s · 6프레임 · loop False)
|
||||||
|
실루엣 실측(프로브 192px) — x [73..119] y [79..129] · 캐릭터 높이 51 px · 폭 47 px → 프레임 56×56 · 발밑 픽셀 (28.5, 19.5)
|
||||||
|
OK — 프레임 56×56 px · 방향 8 · 총 192칸 · 시트 896×672 · PNG 165,716 B (161.8 KB) · 1.80초 · 캐릭터 51 px
|
||||||
|
PNG = Assets/WL/Look/Sprite/Baked/Ai01.png
|
||||||
|
SO = Assets/WL/Look/Sprite/Baked/Ai01_SpriteSet.asset
|
||||||
|
동작표 = idle(시작 0·4프레임·2.0fps·상태 [idle/land/getup]·클립 Idle) run(시작 32·6프레임·11.3fps·상태 [run/dodge]·클립 Run) attack(시작 80·6프레임·4.0fps·상태 [attack/attack2/skill2/skill3/skill4/skill5/casting/casting_end]·클립 Atk2_wing) hit(시작 128·2프레임·2.0fps·상태 [hit/stun]·클립 LightHit) die(시작 144·6프레임·2.9fps·상태 [die/lying]·클립 death_F3441)
|
||||||
|
발밑 픽셀 (28.5, 19.5) · 픽셀 1개 0.02833 m · 베이크 스케일 0.700
|
||||||
|
|
||||||
|
──────── Assets/Res_Addr/Mobs/Mob/Batty_A.prefab (프리팹 스케일 그대로)
|
||||||
|
Toon 머티리얼 교체 = 1슬롯 (인스턴스에만 · 원본 .mat 무변경)
|
||||||
|
레이어0 상태 8개: run, idle, attack, hit, die, stun, casting, casting_end
|
||||||
|
동작 idle ← 상태 [idle] · 클립 Idle (0.958s · 4프레임 · loop True)
|
||||||
|
동작 run ← 상태 [run] · 클립 Move (0.958s · 6프레임 · loop True)
|
||||||
|
동작 attack ← 상태 [attack/casting/casting_end] · 클립 Attack (0.583s · 6프레임 · loop True)
|
||||||
|
동작 hit ← 상태 [hit/stun] · 클립 Damage (0.583s · 2프레임 · loop True)
|
||||||
|
동작 die ← 상태 [die] · 클립 Die (1.000s · 6프레임 · loop False)
|
||||||
|
실루엣 실측(프로브 192px) — x [75..117] y [84..127] · 캐릭터 높이 44 px · 폭 43 px → 프레임 48×48 · 발밑 픽셀 (24.5, 14.5)
|
||||||
|
OK — 프레임 48×48 px · 방향 8 · 총 192칸 · 시트 768×576 · PNG 107,778 B (105.3 KB) · 1.06초 · 캐릭터 44 px
|
||||||
|
PNG = Assets/WL/Look/Sprite/Baked/Batty_A.png
|
||||||
|
SO = Assets/WL/Look/Sprite/Baked/Batty_A_SpriteSet.asset
|
||||||
|
동작표 = idle(시작 0·4프레임·4.2fps·상태 [idle]·클립 Idle) run(시작 32·6프레임·6.3fps·상태 [run]·클립 Move) attack(시작 80·6프레임·10.3fps·상태 [attack/casting/casting_end]·클립 Attack) hit(시작 128·2프레임·3.4fps·상태 [hit/stun]·클립 Damage) die(시작 144·6프레임·6.0fps·상태 [die]·클립 Die)
|
||||||
|
발밑 픽셀 (24.5, 14.5) · 픽셀 1개 0.02833 m · 베이크 스케일 1.500
|
||||||
|
|
||||||
|
런타임 SO 연결 — sets 2개 (Assets/WL/Look/Sprite/Resources/WL/WLSpriteLookSettings.asset)
|
||||||
|
|
||||||
|
합계 굽기 시간 = 2.87초 / 2종 → 1종당 1.43초
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// WL-814k 베이크 드라이버 (에디트 모드 전용 · 게임 코드 아님)
|
||||||
|
// 에셋 만들기 : unity command run_script --file AgentScripts/WL814k_Bake.cs --entry WL814k_Bake.MakeAssets
|
||||||
|
// 시범 2종 굽기: unity command run_script --file AgentScripts/WL814k_Bake.cs --entry WL814k_Bake.BakeDemo
|
||||||
|
// 임의 프리팹 : WL814k_Bake.Targets 를 바꾸거나 에디터 창(WL ▸ 도트 스프라이트 굽기)을 쓴다.
|
||||||
|
//
|
||||||
|
// 🔴 이 드라이버에만 「어떤 프리팹을 구울지」가 적혀 있다. 툴(Assets/WL/Look/Sprite/Editor/WLSpriteBaker.cs)
|
||||||
|
// 에는 캐릭터 이름이 0개다 — PD 가 캐릭터 에셋을 바꿔도 툴은 그대로 쓴다.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using WL.Look.SpriteBake;
|
||||||
|
using WL.Look.SpriteBake.EditorTools;
|
||||||
|
|
||||||
|
public static class WL814k_Bake
|
||||||
|
{
|
||||||
|
const string kResDir = "Assets/WL/Look/Sprite/Resources/WL";
|
||||||
|
const string kOutDir = "Assets/WL/Look/Sprite/Baked";
|
||||||
|
const string kTxt = "AgentScripts/WL814k_BAKE.txt";
|
||||||
|
|
||||||
|
// 🔴 1차 시범 = PC 1종 + 잡몹 1종 (전수 전환은 PD 승인 뒤)
|
||||||
|
// 두 번째 값 = 인게임 실제 루트 스케일(0 이면 프리팹 자체 값).
|
||||||
|
// PC 는 ClassConfig.f_Scale 0.7 로 스폰된다(814b·발주서 실측) → 그 크기로 구워야
|
||||||
|
// 스프라이트 1px 이 화면 1px 과 정확히 맞는다. 몹은 프리팹 스케일 그대로(Batty_A = 1.5).
|
||||||
|
public static readonly string[] Targets = { "Assets/Res_Addr/PC/Ai01.prefab", "Assets/Res_Addr/Mobs/Mob/Batty_A.prefab" };
|
||||||
|
public static readonly float[] TargetScales = { 0.7f, 0f };
|
||||||
|
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
// ① 설정 에셋 2개 만들기
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
public static void MakeAssets()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 설정 에셋 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
Directory.CreateDirectory(kResDir);
|
||||||
|
Directory.CreateDirectory(kOutDir);
|
||||||
|
|
||||||
|
var bake = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kResDir + "/WLSpriteBakeSettings.asset");
|
||||||
|
if (bake == null)
|
||||||
|
{
|
||||||
|
bake = ScriptableObject.CreateInstance<WLSpriteBakeSettings>();
|
||||||
|
AssetDatabase.CreateAsset(bake, kResDir + "/WLSpriteBakeSettings.asset");
|
||||||
|
L("만듦 " + kResDir + "/WLSpriteBakeSettings.asset");
|
||||||
|
}
|
||||||
|
else L("있음 " + kResDir + "/WLSpriteBakeSettings.asset");
|
||||||
|
|
||||||
|
var look = AssetDatabase.LoadAssetAtPath<WLSpriteLookSettings>(kResDir + "/WLSpriteLookSettings.asset");
|
||||||
|
if (look == null)
|
||||||
|
{
|
||||||
|
look = ScriptableObject.CreateInstance<WLSpriteLookSettings>();
|
||||||
|
AssetDatabase.CreateAsset(look, kResDir + "/WLSpriteLookSettings.asset");
|
||||||
|
L("만듦 " + kResDir + "/WLSpriteLookSettings.asset");
|
||||||
|
}
|
||||||
|
else L("있음 " + kResDir + "/WLSpriteLookSettings.asset");
|
||||||
|
|
||||||
|
AssetDatabase.SaveAssetIfDirty(bake);
|
||||||
|
AssetDatabase.SaveAssetIfDirty(look);
|
||||||
|
WLSpriteBakeSettings.ClearCache();
|
||||||
|
WLSpriteLookSettings.ClearCache();
|
||||||
|
|
||||||
|
L(string.Format("굽기 기본값 — 내림각 {0:0.000}° · 픽셀 {1:0.00000} m · 방향 {2} · 프레임 {3}(자동 {4}) · 팔레트 {5}단계 · 알파컷 {6}",
|
||||||
|
bake.PitchDeg, bake.WorldPerPixel, bake.dirCount, bake.frameSize, bake.autoFrameSize, bake.paletteLevels, bake.alphaCutoff));
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
// ② 시범 2종 굽기 + SO 목록 연결
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
public static void BakeDemo() { BakeList(Targets, TargetScales); }
|
||||||
|
|
||||||
|
public static void BakeList(string[] paths, float[] scales)
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 굽기 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
||||||
|
|
||||||
|
var sets = new List<WLSpriteSet>();
|
||||||
|
double total = 0;
|
||||||
|
for (int ti = 0; ti < paths.Length; ti++)
|
||||||
|
{
|
||||||
|
var p = paths[ti];
|
||||||
|
float scale = (scales != null && ti < scales.Length) ? scales[ti] : 0f;
|
||||||
|
L("");
|
||||||
|
L("──────── " + p + (scale > 0f ? " (루트 스케일 " + scale + ")" : " (프리팹 스케일 그대로)"));
|
||||||
|
// 🔴 굽기마다 설정 에셋을 다시 읽는다 — PNG 임포트가 AssetDatabase 를 돌려
|
||||||
|
// 참조만 들고 있던 SO 가 언로드돼 null 이 되는 것을 실측했다(첫 시도 실패 원인).
|
||||||
|
var cfg = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>(kResDir + "/WLSpriteBakeSettings.asset");
|
||||||
|
if (cfg == null) { L("FAIL — 굽기 설정 에셋 없음. MakeAssets 먼저."); break; }
|
||||||
|
var r = WLSpriteBaker.Bake(p, cfg, kOutDir, null, scale);
|
||||||
|
total += r.seconds;
|
||||||
|
L(r.log.ToString().TrimEnd());
|
||||||
|
foreach (var n in r.notes) L(" · 「미확인/주의」 " + n);
|
||||||
|
if (!r.ok) { L("FAIL"); continue; }
|
||||||
|
L(string.Format("OK — 프레임 {0}×{0} px · 방향 {1} · 총 {2}칸 · 시트 {3}×{4} · PNG {5:N0} B ({6:N1} KB) · {7:0.00}초 · 캐릭터 {8:0} px",
|
||||||
|
r.frameSize, r.dirCount, r.totalFrames, r.sheetW, r.sheetH, r.pngBytes, r.pngBytes / 1024.0, r.seconds, r.charPixelHeight));
|
||||||
|
L(" PNG = " + r.pngPath);
|
||||||
|
L(" SO = " + r.setPath);
|
||||||
|
var set = AssetDatabase.LoadAssetAtPath<WLSpriteSet>(r.setPath);
|
||||||
|
if (set != null)
|
||||||
|
{
|
||||||
|
sets.Add(set);
|
||||||
|
var sb2 = new StringBuilder();
|
||||||
|
foreach (var a in set.actions)
|
||||||
|
sb2.Append(string.Format("{0}(시작 {1}·{2}프레임·{3:0.0}fps·상태 [{4}]·클립 {5}) ",
|
||||||
|
a.key, a.startIndex, a.frameCount, a.fps, string.Join("/", a.stateNames), a.clipName));
|
||||||
|
L(" 동작표 = " + sb2.ToString().TrimEnd());
|
||||||
|
L(string.Format(" 발밑 픽셀 ({0:0.0}, {1:0.0}) · 픽셀 1개 {2:0.00000} m · 베이크 스케일 {3:0.000}",
|
||||||
|
set.footPixelX, set.footPixelY, set.worldPerPixel, set.bakeScale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 런타임 SO 에 시트 목록 연결
|
||||||
|
var look = AssetDatabase.LoadAssetAtPath<WLSpriteLookSettings>(kResDir + "/WLSpriteLookSettings.asset");
|
||||||
|
if (look != null)
|
||||||
|
{
|
||||||
|
look.sets = sets.ToArray();
|
||||||
|
EditorUtility.SetDirty(look);
|
||||||
|
AssetDatabase.SaveAssetIfDirty(look);
|
||||||
|
L("");
|
||||||
|
L("런타임 SO 연결 — sets " + sets.Count + "개 (" + kResDir + "/WLSpriteLookSettings.asset)");
|
||||||
|
}
|
||||||
|
L("");
|
||||||
|
L(string.Format("합계 굽기 시간 = {0:0.00}초 / {1}종 → 1종당 {2:0.00}초", total, paths.Length, total / Math.Max(1, paths.Length)));
|
||||||
|
Write();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Write() { File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814k] → " + kTxt); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
WL-814k 캡처 · 2026-09-12 18:43:29
|
||||||
|
ⓐ 3D 원본 → k_1_3d_original.png
|
||||||
|
폴링 결과 — PC idle d7 f0 / 몹 idle d1 f0
|
||||||
|
ⓑ 스프라이트 교체 → k_2_sprite_baked.png
|
||||||
|
정렬 실측(PC 쪽 절반) — 3D x[21..41] y[87..120] / 스프라이트 x[19..40] y[85..121] → Δ좌 -2 · Δ우 -1 · Δ하 -2 · Δ상 1 px
|
||||||
|
밝기 실측 — 3D 평균 63.1 · 스프라이트 평균 48.4 (0~255 · 캐릭터 픽셀만) · 캐릭터 픽셀 3D 466 / 스프라이트 619 (외곽선 +153)
|
||||||
|
클로즈업 crop 116×46 px → ×9 = 1044×836 (위 3D · 아래 스프라이트)
|
||||||
|
ⓒ 클로즈업 → k_3_closeup_2x.png
|
||||||
|
ⓓ 방향 8종 → k_4_directions.png
|
||||||
|
|
||||||
|
캡처 PASS — E:\NerdNavis\nn_himminji\Screenshots_WL\WL814k
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
WL-814k 진단 ③ · 18:24:55
|
||||||
|
Animator 살아있나 = True
|
||||||
|
샘플링 없음 덮인 426 · x[74..118] y[94..136]
|
||||||
|
Idle_f0 덮인 445 · x[75..114] y[96..136]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Idle_f1 덮인 436 · x[76..114] y[95..135]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Idle_f2 덮인 439 · x[76..113] y[94..135]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Idle_f3 덮인 441 · x[76..114] y[95..136]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Run_f0 덮인 503 · x[74..119] y[91..144]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Run_f1 덮인 496 · x[75..118] y[107..156]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Run_f2 덮인 502 · x[73..118] y[130..176]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Run_f3 덮인 503 · x[74..118] y[140..189]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Atk2_wing_f0 덮인 513 · x[71..112] y[88..132]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Atk2_wing_f1 덮인 447 · x[74..104] y[89..139]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Atk2_wing_f2 덮인 483 · x[68..109] y[91..131]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
Atk2_wing_f3 덮인 524 · x[69..104] y[83..133]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
LightHit_f0 덮인 448 · x[75..114] y[95..135]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
LightHit_f1 덮인 365 · x[75..113] y[81..113]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
LightHit_f2 덮인 449 · x[79..110] y[83..131]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
LightHit_f3 덮인 443 · x[75..114] y[87..127]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
death_F3441_f0 덮인 453 · x[78..114] y[78..121]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
death_F3441_f1 덮인 351 · x[91..109] y[0..37]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
death_F3441_f2 덮인 338 · x[86..104] y[0..35]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
death_F3441_f3 덮인 374 · x[83..105] y[0..32]
|
||||||
|
루트 localPos (0.000, 0.000, 0.000) · lossyScale (1.000, 1.000, 1.000)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
WL-814k 진단 ④ · 18:25:49
|
||||||
|
|
||||||
|
== Assets/Res_Addr/PC/Ai01.prefab ==
|
||||||
|
직속 자식: blen Group Mesh Root_M
|
||||||
|
클립 Run · 위치 커브가 걸린 경로 0개:
|
||||||
|
f0 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (0.001, 0.990, 0.016)
|
||||||
|
f1 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (-0.001, 0.845, 0.691)
|
||||||
|
f2 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (-0.004, 0.990, 1.362)
|
||||||
|
f3 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (-0.002, 0.845, 2.044)
|
||||||
|
클립 death_F3441 · 위치 커브가 걸린 경로 0개:
|
||||||
|
f0 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (0.021, 0.238, -0.078)
|
||||||
|
f1 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (0.097, -0.496, -2.949)
|
||||||
|
f2 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (0.066, -0.456, -2.963)
|
||||||
|
f3 rootLocal (0.000, 0.000, 0.000) | blen (0.000, 0.000, 0.000) | Group (0.000, 0.000, 0.000) | Mesh (0.000, 0.000, 0.000) | Root_M (0.077, -0.433, -3.045)
|
||||||
|
|
||||||
|
== Assets/Res_Addr/Mobs/Mob/Batty_A.prefab ==
|
||||||
|
직속 자식: Batty root
|
||||||
|
클립 Move · 위치 커브가 걸린 경로 8개: root/Center, root/Center/spine, root/Center/spine/wing_l1, root/Center/spine/wing_l1/wing_l2, root/Center/spine/wing_l1/wing_l2/wing_l3, root/Center/spine/wing_r1, root/Center/spine/wing_r1/wing_r2, root/Center/spine/wing_r1/wing_r2/wing_r3
|
||||||
|
f0 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f1 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f2 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f3 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
클립 Die · 위치 커브가 걸린 경로 8개: root/Center, root/Center/spine, root/Center/spine/wing_l1, root/Center/spine/wing_l1/wing_l2, root/Center/spine/wing_l1/wing_l2/wing_l3, root/Center/spine/wing_r1, root/Center/spine/wing_r1/wing_r2, root/Center/spine/wing_r1/wing_r2/wing_r3
|
||||||
|
f0 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f1 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f2 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
f3 rootLocal (0.000, 0.000, 0.000) | Batty (0.000, 0.000, 0.000) | root (0.000, 0.000, 0.000)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
WL-814k 진단 ⑤ · 18:27:44
|
||||||
|
|
||||||
|
== Assets/Res_Addr/PC/Ai01.prefab (anim.Play + anim.Update 경로) ==
|
||||||
|
idle_f0 덮인 413 · x[86..110] y[92..136]
|
||||||
|
idle_f1 덮인 405 · x[86..110] y[92..135]
|
||||||
|
idle_f2 덮인 410 · x[86..110] y[92..135]
|
||||||
|
idle_f3 덮인 410 · x[86..110] y[92..136]
|
||||||
|
run_f0 덮인 474 · x[85..108] y[92..144]
|
||||||
|
run_f1 덮인 488 · x[85..111] y[91..139]
|
||||||
|
run_f2 덮인 486 · x[86..112] y[97..144]
|
||||||
|
run_f3 덮인 490 · x[86..110] y[91..139]
|
||||||
|
attack_f0 덮인 503 · x[75..109] y[86..132]
|
||||||
|
attack_f1 덮인 402 · x[80..101] y[89..134]
|
||||||
|
attack_f2 덮인 460 · x[78..109] y[88..131]
|
||||||
|
attack_f3 덮인 535 · x[75..103] y[83..133]
|
||||||
|
hit_f0 덮인 412 · x[86..110] y[91..135]
|
||||||
|
hit_f1 덮인 349 · x[86..110] y[89..120]
|
||||||
|
hit_f2 덮인 490 · x[86..111] y[91..141]
|
||||||
|
hit_f3 덮인 416 · x[86..110] y[92..136]
|
||||||
|
die_f0 덮인 457 · x[86..107] y[73..121]
|
||||||
|
die_f1 덮인 512 · x[77..123] y[86..116]
|
||||||
|
die_f2 덮인 518 · x[79..125] y[81..117]
|
||||||
|
die_f3 덮인 515 · x[79..123] y[78..114]
|
||||||
|
|
||||||
|
== Assets/Res_Addr/Mobs/Mob/Batty_A.prefab (anim.Play + anim.Update 경로) ==
|
||||||
|
idle_f0 덮인 231 · x[84..108] y[98..112]
|
||||||
|
idle_f1 덮인 221 · x[84..108] y[97..111]
|
||||||
|
idle_f2 덮인 231 · x[84..108] y[96..110]
|
||||||
|
idle_f3 덮인 234 · x[84..108] y[97..112]
|
||||||
|
run_f0 덮인 256 · x[84..108] y[101..117]
|
||||||
|
run_f1 덮인 254 · x[84..108] y[99..115]
|
||||||
|
run_f2 덮인 250 · x[84..108] y[98..113]
|
||||||
|
run_f3 덮인 254 · x[84..108] y[100..116]
|
||||||
|
attack_f0 덮인 223 · x[84..108] y[97..111]
|
||||||
|
attack_f1 덮인 260 · x[84..108] y[106..122]
|
||||||
|
attack_f2 덮인 256 · x[84..108] y[102..118]
|
||||||
|
attack_f3 덮인 236 · x[84..108] y[96..111]
|
||||||
|
hit_f0 덮인 223 · x[84..108] y[97..111]
|
||||||
|
hit_f1 덮인 195 · x[84..108] y[90..102]
|
||||||
|
hit_f2 덮인 213 · x[84..108] y[95..109]
|
||||||
|
hit_f3 덮인 233 · x[84..108] y[99..113]
|
||||||
|
die_f0 덮인 223 · x[84..108] y[97..111]
|
||||||
|
die_f1 덮인 215 · x[84..108] y[99..112]
|
||||||
|
die_f2 덮인 266 · x[84..108] y[87..103]
|
||||||
|
die_f3 덮인 251 · x[84..108] y[87..101]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
WL-814k 진단 ⑥ · 18:44:46
|
||||||
|
3D f0 → 상태 해시 1203776827(attack=1203776827) nt 0.000
|
||||||
|
3D f1 → 상태 해시 1203776827(attack=1203776827) nt 0.167
|
||||||
|
3D f2 → 상태 해시 1203776827(attack=1203776827) nt 0.334
|
||||||
|
3D f3 → 상태 해시 1203776827(attack=1203776827) nt 0.500
|
||||||
|
3D f4 → 상태 해시 1203776827(attack=1203776827) nt 0.667
|
||||||
|
3D f5 → 상태 해시 1203776827(attack=1203776827) nt 0.834
|
||||||
|
스프라이트 f0 → 인덱스 122 (시작 80 + 7*6 + 0)
|
||||||
|
스프라이트 f1 → 인덱스 123 (시작 80 + 7*6 + 1)
|
||||||
|
스프라이트 f2 → 인덱스 124 (시작 80 + 7*6 + 2)
|
||||||
|
스프라이트 f3 → 인덱스 125 (시작 80 + 7*6 + 3)
|
||||||
|
스프라이트 f4 → 인덱스 126 (시작 80 + 7*6 + 4)
|
||||||
|
스프라이트 f5 → 인덱스 127 (시작 80 + 7*6 + 5)
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
// WL-814k 진단 ③ — AnimationMode 샘플링이 실루엣을 어떻게 바꾸는지 프레임별 실측
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Diag3.cs --entry WL814k_Diag3.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Diag3
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_DIAG3.txt";
|
||||||
|
static string OutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814k_diag";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
Directory.CreateDirectory(OutDir);
|
||||||
|
L("WL-814k 진단 ③ · " + DateTime.Now.ToString("HH:mm:ss"));
|
||||||
|
Test("Assets/Res_Addr/PC/Ai01.prefab", new[] { "Idle", "Run", "Atk2_wing", "LightHit", "death_F3441" });
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Test(string prefabPath, string[] clipNames)
|
||||||
|
{
|
||||||
|
GameObject pivot = null, inst = null, camGo = null;
|
||||||
|
RenderTexture rt = null; Texture2D tex = null;
|
||||||
|
bool am = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
|
||||||
|
RenderSettings.ambientLight = new Color(0.212f, 0.227f, 0.259f, 1f);
|
||||||
|
RenderSettings.fog = false; RenderSettings.skybox = null;
|
||||||
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
||||||
|
sun.type = LightType.Directional; sun.intensity = 1.2f; sun.shadows = LightShadows.None;
|
||||||
|
sun.transform.rotation = Quaternion.Euler(26.4f, 238.6f, 0f);
|
||||||
|
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
|
pivot = new GameObject("Pivot");
|
||||||
|
inst = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
PrefabUtility.UnpackPrefabInstance(inst, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||||
|
inst.transform.SetParent(pivot.transform, false);
|
||||||
|
inst.transform.localPosition = Vector3.zero; inst.transform.localRotation = Quaternion.identity;
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<UnityEngine.AI.NavMeshAgent>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<ParticleSystem>(true)) UnityEngine.Object.DestroyImmediate(c.gameObject);
|
||||||
|
foreach (var mb in inst.GetComponentsInChildren<MonoBehaviour>(true)) if (mb != null) UnityEngine.Object.DestroyImmediate(mb);
|
||||||
|
var anim = inst.GetComponent<Animator>();
|
||||||
|
L(" Animator 살아있나 = " + (anim != null));
|
||||||
|
|
||||||
|
float wpp = 3.4f * 2f / 240f;
|
||||||
|
int P = 192;
|
||||||
|
camGo = new GameObject("Cam");
|
||||||
|
var cam = camGo.AddComponent<Camera>();
|
||||||
|
cam.orthographic = true; cam.orthographicSize = P * wpp * 0.5f;
|
||||||
|
cam.nearClipPlane = 0.01f; cam.farClipPlane = 200f;
|
||||||
|
cam.clearFlags = CameraClearFlags.SolidColor; cam.allowHDR = false; cam.allowMSAA = false;
|
||||||
|
camGo.transform.rotation = Quaternion.Euler(43.609f, 0f, 0f);
|
||||||
|
camGo.transform.position = -camGo.transform.right * (wpp * 0.5f) - camGo.transform.up * (wpp * 0.5f) - camGo.transform.forward * 50f;
|
||||||
|
|
||||||
|
rt = new RenderTexture(P, P, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||||
|
rt.filterMode = FilterMode.Point; rt.antiAliasing = 1; rt.Create();
|
||||||
|
tex = new Texture2D(P, P, TextureFormat.RGBA32, false, false);
|
||||||
|
|
||||||
|
var rac = anim.runtimeAnimatorController;
|
||||||
|
var black = new Color32[P * P]; var white = new Color32[P * P];
|
||||||
|
|
||||||
|
// ⓐ 샘플링 없이
|
||||||
|
Box(cam, rt, tex, P, black, white, "샘플링 없음", null);
|
||||||
|
|
||||||
|
AnimationMode.StartAnimationMode(); am = true;
|
||||||
|
foreach (var name in clipNames)
|
||||||
|
{
|
||||||
|
AnimationClip clip = null;
|
||||||
|
foreach (var c in rac.animationClips) if (c.name == name) { clip = c; break; }
|
||||||
|
if (clip == null) { L(" 클립 " + name + " 없음"); continue; }
|
||||||
|
for (int f = 0; f < 4; f++)
|
||||||
|
{
|
||||||
|
float t = clip.length * f / 4f;
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, clip, t);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
inst.transform.localPosition = Vector3.zero;
|
||||||
|
inst.transform.localRotation = Quaternion.identity;
|
||||||
|
string tag = name + "_f" + f;
|
||||||
|
Box(cam, rt, tex, P, black, white, tag, (name == "Idle" && f == 0) ? OutDir + "\\am_" + tag + ".png" : null);
|
||||||
|
L(" 루트 localPos " + inst.transform.localPosition.ToString("F3") + " · lossyScale " + inst.transform.lossyScale.ToString("F3"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimationMode.StopAnimationMode(); am = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (am && AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode();
|
||||||
|
if (rt != null) { RenderTexture.active = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
|
||||||
|
if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
|
||||||
|
if (camGo != null) UnityEngine.Object.DestroyImmediate(camGo);
|
||||||
|
if (pivot != null) UnityEngine.Object.DestroyImmediate(pivot);
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Box(Camera cam, RenderTexture rt, Texture2D tex, int P, Color32[] black, Color32[] white, string tag, string savePng)
|
||||||
|
{
|
||||||
|
Grab(cam, rt, tex, P, Color.black, black, savePng == null ? null : savePng.Replace(".png", "_black.png"));
|
||||||
|
Grab(cam, rt, tex, P, Color.white, white, savePng == null ? null : savePng.Replace(".png", "_white.png"));
|
||||||
|
int cov = 0, minX = int.MaxValue, maxX = -1, minY = int.MaxValue, maxY = -1;
|
||||||
|
for (int y = 0; y < P; y++)
|
||||||
|
for (int x = 0; x < P; x++)
|
||||||
|
{
|
||||||
|
int i = y * P + x;
|
||||||
|
float c = 1f - (((white[i].r - black[i].r) + (white[i].g - black[i].g) + (white[i].b - black[i].b)) / 765f);
|
||||||
|
if (c < 0.5f) continue;
|
||||||
|
cov++;
|
||||||
|
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
L(string.Format(" {0,-18} 덮인 {1,6} · x[{2}..{3}] y[{4}..{5}]", tag, cov, minX, maxX, minY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Grab(Camera cam, RenderTexture rt, Texture2D tex, int P, Color bg, Color32[] outPx, string file)
|
||||||
|
{
|
||||||
|
cam.backgroundColor = bg;
|
||||||
|
cam.targetTexture = rt; cam.Render(); cam.targetTexture = null;
|
||||||
|
var active = RenderTexture.active; RenderTexture.active = rt;
|
||||||
|
tex.ReadPixels(new Rect(0, 0, P, P), 0, 0); tex.Apply(false, false);
|
||||||
|
RenderTexture.active = active;
|
||||||
|
if (file != null) File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||||
|
Array.Copy(tex.GetPixels32(), outPx, outPx.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
// WL-814k 진단 ④ — 루트 모션이 어느 트랜스폼에 실리는지 실측
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Diag4.cs --entry WL814k_Diag4.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Diag4
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_DIAG4.txt";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 진단 ④ · " + DateTime.Now.ToString("HH:mm:ss"));
|
||||||
|
Test("Assets/Res_Addr/PC/Ai01.prefab", new[] { "Run", "death_F3441" });
|
||||||
|
Test("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab", new[] { "Move", "Die" });
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Test(string prefabPath, string[] clipNames)
|
||||||
|
{
|
||||||
|
GameObject pivot = null, inst = null;
|
||||||
|
bool am = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
|
pivot = new GameObject("Pivot");
|
||||||
|
inst = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
PrefabUtility.UnpackPrefabInstance(inst, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||||
|
inst.transform.SetParent(pivot.transform, false);
|
||||||
|
inst.transform.localPosition = Vector3.zero; inst.transform.localRotation = Quaternion.identity;
|
||||||
|
var anim = inst.GetComponent<Animator>();
|
||||||
|
var rac = anim.runtimeAnimatorController;
|
||||||
|
|
||||||
|
L("");
|
||||||
|
L("== " + prefabPath + " ==");
|
||||||
|
var kids = new StringBuilder();
|
||||||
|
for (int i = 0; i < inst.transform.childCount; i++) kids.Append(inst.transform.GetChild(i).name + " ");
|
||||||
|
L(" 직속 자식: " + kids);
|
||||||
|
|
||||||
|
AnimationMode.StartAnimationMode(); am = true;
|
||||||
|
foreach (var name in clipNames)
|
||||||
|
{
|
||||||
|
AnimationClip clip = null;
|
||||||
|
foreach (var c in rac.animationClips) if (c.name == name) { clip = c; break; }
|
||||||
|
if (clip == null) continue;
|
||||||
|
var binds = AnimationUtility.GetCurveBindings(clip);
|
||||||
|
var paths = new System.Collections.Generic.HashSet<string>();
|
||||||
|
foreach (var b in binds) if (b.propertyName.StartsWith("m_LocalPosition")) paths.Add(b.path == "" ? "<root>" : b.path);
|
||||||
|
L(" 클립 " + name + " · 위치 커브가 걸린 경로 " + paths.Count + "개: " + string.Join(", ", new System.Collections.Generic.List<string>(paths).ToArray()));
|
||||||
|
|
||||||
|
for (int f = 0; f < 4; f++)
|
||||||
|
{
|
||||||
|
float t = clip.length * f / 4f;
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, clip, t);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append(string.Format(" f{0} rootLocal {1}", f, inst.transform.localPosition.ToString("F3")));
|
||||||
|
for (int i = 0; i < inst.transform.childCount; i++)
|
||||||
|
{
|
||||||
|
var ch = inst.transform.GetChild(i);
|
||||||
|
sb.Append(" | " + ch.name + " " + ch.localPosition.ToString("F3"));
|
||||||
|
}
|
||||||
|
L(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimationMode.StopAnimationMode(); am = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (am && AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode();
|
||||||
|
if (pivot != null) UnityEngine.Object.DestroyImmediate(pivot);
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
// WL-814k 진단 ⑤ — 「진짜 Animator 로 구동」 경로가 에디트 모드에서 되는지 실측
|
||||||
|
// (applyRootMotion=false 인 게임과 똑같은 포즈를 얻을 수 있으면 그게 정답 경로)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Diag5.cs --entry WL814k_Diag5.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Diag5
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_DIAG5.txt";
|
||||||
|
static string OutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814k_diag";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
Directory.CreateDirectory(OutDir);
|
||||||
|
L("WL-814k 진단 ⑤ · " + DateTime.Now.ToString("HH:mm:ss"));
|
||||||
|
Test("Assets/Res_Addr/PC/Ai01.prefab", new[] { "idle", "run", "attack", "hit", "die" });
|
||||||
|
Test("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab", new[] { "idle", "run", "attack", "hit", "die" });
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Test(string prefabPath, string[] states)
|
||||||
|
{
|
||||||
|
GameObject pivot = null, inst = null, camGo = null;
|
||||||
|
RenderTexture rt = null; Texture2D tex = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
|
||||||
|
RenderSettings.ambientLight = new Color(0.212f, 0.227f, 0.259f, 1f);
|
||||||
|
RenderSettings.fog = false; RenderSettings.skybox = null;
|
||||||
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
||||||
|
sun.type = LightType.Directional; sun.intensity = 1.2f; sun.shadows = LightShadows.None;
|
||||||
|
sun.transform.rotation = Quaternion.Euler(26.4f, 238.6f, 0f);
|
||||||
|
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
|
pivot = new GameObject("Pivot");
|
||||||
|
inst = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
PrefabUtility.UnpackPrefabInstance(inst, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||||
|
inst.transform.SetParent(pivot.transform, false);
|
||||||
|
inst.transform.localPosition = Vector3.zero; inst.transform.localRotation = Quaternion.identity;
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<UnityEngine.AI.NavMeshAgent>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<ParticleSystem>(true)) UnityEngine.Object.DestroyImmediate(c.gameObject);
|
||||||
|
foreach (var mb in inst.GetComponentsInChildren<MonoBehaviour>(true)) if (mb != null) UnityEngine.Object.DestroyImmediate(mb);
|
||||||
|
foreach (var sm in inst.GetComponentsInChildren<SkinnedMeshRenderer>(true)) if (sm != null) sm.updateWhenOffscreen = true;
|
||||||
|
|
||||||
|
var anim = inst.GetComponent<Animator>();
|
||||||
|
anim.applyRootMotion = false;
|
||||||
|
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||||
|
anim.Rebind();
|
||||||
|
|
||||||
|
float wpp = 3.4f * 2f / 240f;
|
||||||
|
int P = 192;
|
||||||
|
camGo = new GameObject("Cam");
|
||||||
|
var cam = camGo.AddComponent<Camera>();
|
||||||
|
cam.orthographic = true; cam.orthographicSize = P * wpp * 0.5f;
|
||||||
|
cam.nearClipPlane = 0.01f; cam.farClipPlane = 200f;
|
||||||
|
cam.clearFlags = CameraClearFlags.SolidColor; cam.allowHDR = false; cam.allowMSAA = false;
|
||||||
|
camGo.transform.rotation = Quaternion.Euler(43.609f, 0f, 0f);
|
||||||
|
camGo.transform.position = -camGo.transform.right * (wpp * 0.5f) - camGo.transform.up * (wpp * 0.5f) - camGo.transform.forward * 50f;
|
||||||
|
|
||||||
|
rt = new RenderTexture(P, P, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||||
|
rt.filterMode = FilterMode.Point; rt.antiAliasing = 1; rt.Create();
|
||||||
|
tex = new Texture2D(P, P, TextureFormat.RGBA32, false, false);
|
||||||
|
var black = new Color32[P * P]; var white = new Color32[P * P];
|
||||||
|
|
||||||
|
L("");
|
||||||
|
L("== " + prefabPath + " (anim.Play + anim.Update 경로) ==");
|
||||||
|
foreach (var st in states)
|
||||||
|
{
|
||||||
|
int hash = Animator.StringToHash(st);
|
||||||
|
if (!anim.HasState(0, hash)) { L(" 상태 " + st + " 없음"); continue; }
|
||||||
|
for (int f = 0; f < 4; f++)
|
||||||
|
{
|
||||||
|
float nt = f / 4f;
|
||||||
|
anim.Play(hash, 0, nt);
|
||||||
|
anim.Update(0f);
|
||||||
|
inst.transform.localPosition = Vector3.zero;
|
||||||
|
inst.transform.localRotation = Quaternion.identity;
|
||||||
|
string tag = st + "_f" + f;
|
||||||
|
Box(cam, rt, tex, P, black, white, tag, (f == 0) ? OutDir + "\\play_" + Path.GetFileNameWithoutExtension(prefabPath) + "_" + tag + ".png" : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rt != null) { RenderTexture.active = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
|
||||||
|
if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
|
||||||
|
if (camGo != null) UnityEngine.Object.DestroyImmediate(camGo);
|
||||||
|
if (pivot != null) UnityEngine.Object.DestroyImmediate(pivot);
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Box(Camera cam, RenderTexture rt, Texture2D tex, int P, Color32[] black, Color32[] white, string tag, string savePng)
|
||||||
|
{
|
||||||
|
Grab(cam, rt, tex, P, Color.black, black, null);
|
||||||
|
Grab(cam, rt, tex, P, Color.white, white, savePng);
|
||||||
|
int cov = 0, minX = int.MaxValue, maxX = -1, minY = int.MaxValue, maxY = -1;
|
||||||
|
for (int y = 0; y < P; y++)
|
||||||
|
for (int x = 0; x < P; x++)
|
||||||
|
{
|
||||||
|
int i = y * P + x;
|
||||||
|
float c = 1f - (((white[i].r - black[i].r) + (white[i].g - black[i].g) + (white[i].b - black[i].b)) / 765f);
|
||||||
|
if (c < 0.5f) continue;
|
||||||
|
cov++;
|
||||||
|
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
L(string.Format(" {0,-14} 덮인 {1,6} · x[{2}..{3}] y[{4}..{5}]", tag, cov, minX, maxX, minY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Grab(Camera cam, RenderTexture rt, Texture2D tex, int P, Color bg, Color32[] outPx, string file)
|
||||||
|
{
|
||||||
|
cam.backgroundColor = bg;
|
||||||
|
cam.targetTexture = rt; cam.Render(); cam.targetTexture = null;
|
||||||
|
var active = RenderTexture.active; RenderTexture.active = rt;
|
||||||
|
tex.ReadPixels(new Rect(0, 0, P, P), 0, 0); tex.Apply(false, false);
|
||||||
|
RenderTexture.active = active;
|
||||||
|
if (file != null) File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||||
|
Array.Copy(tex.GetPixels32(), outPx, outPx.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
// WL-814k 진단 ⑥ — 3D 프레임 k 와 스프라이트 프레임 k 가 정말 같은 그림인가(격자 비교)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Diag6.cs --entry WL814k_Diag6.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using WL.Look.SpriteBake;
|
||||||
|
|
||||||
|
public static class WL814k_Diag6
|
||||||
|
{
|
||||||
|
const string kOut = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814k_diag";
|
||||||
|
const string kTxt = "AgentScripts/WL814k_DIAG6.txt";
|
||||||
|
const float kOrtho = 3.4f, kPitch = 43.609f;
|
||||||
|
const int kLowH = 240;
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
Directory.CreateDirectory(kOut);
|
||||||
|
L("WL-814k 진단 ⑥ · " + DateTime.Now.ToString("HH:mm:ss"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
||||||
|
sun.type = LightType.Directional; sun.shadows = LightShadows.None;
|
||||||
|
sun.color = new Color(1f, 0.92f, 0.703f, 1f); sun.intensity = 1.2f;
|
||||||
|
sun.transform.rotation = Quaternion.Euler(26.4f, 238.6f, 0f);
|
||||||
|
var fill = new GameObject("Fill").AddComponent<Light>();
|
||||||
|
fill.type = LightType.Directional; fill.shadows = LightShadows.None;
|
||||||
|
fill.color = new Color(0.596f, 0.703f, 0.839f, 1f); fill.intensity = 0.3f;
|
||||||
|
fill.transform.rotation = Quaternion.Euler(36.1f, 116.1f, 0f);
|
||||||
|
RenderSettings.skybox = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Skybox.mat");
|
||||||
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Skybox;
|
||||||
|
RenderSettings.ambientIntensity = 1f; RenderSettings.fog = false;
|
||||||
|
DynamicGI.UpdateEnvironment();
|
||||||
|
|
||||||
|
var look = WLSpriteLookSettings.Instance;
|
||||||
|
var actor = Spawn("Assets/Res_Addr/PC/Ai01.prefab", 0.7f);
|
||||||
|
var set = SpriteBillboard.MatchSet(actor.name, look);
|
||||||
|
var act = set.FindByKey("attack");
|
||||||
|
var anim = actor.GetComponent<Animator>();
|
||||||
|
|
||||||
|
int cell = 64, cols = act.frameCount, rows = 2, dir = 7;
|
||||||
|
var rig = MakeCam(cell, cell);
|
||||||
|
var sheet = new Color32[cols * cell * rows * cell];
|
||||||
|
|
||||||
|
// 위 줄 = 3D (매 컷 직전에 포즈를 다시 찍는다)
|
||||||
|
for (int f = 0; f < cols; f++)
|
||||||
|
{
|
||||||
|
actor.transform.rotation = Quaternion.Euler(0f, dir * 45f, 0f);
|
||||||
|
anim.Play(Animator.StringToHash("attack"), 0, (f + 0.001f) / act.frameCount);
|
||||||
|
anim.Update(0f);
|
||||||
|
var st = anim.GetCurrentAnimatorStateInfo(0);
|
||||||
|
L(string.Format(" 3D f{0} → 상태 해시 {1}(attack={2}) nt {3:0.000}", f, st.shortNameHash, Animator.StringToHash("attack"), st.normalizedTime));
|
||||||
|
BlitInto(rig.Grab(), cell, cell, sheet, cols * cell, f * cell, 1 * cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 아래 줄 = 스프라이트
|
||||||
|
var e = SpriteBillboard.TryAttach(actor, set);
|
||||||
|
for (int f = 0; f < cols; f++)
|
||||||
|
{
|
||||||
|
actor.transform.rotation = Quaternion.Euler(0f, dir * 45f, 0f);
|
||||||
|
SpriteBillboard.SetManualFrame(actor, "attack", dir, f);
|
||||||
|
SpriteBillboard.UpdateAll(rig.cam);
|
||||||
|
L(string.Format(" 스프라이트 f{0} → 인덱스 {1} (시작 {2} + {3}*{4} + {5})", f,
|
||||||
|
act.startIndex + dir * act.frameCount + f, act.startIndex, dir, act.frameCount, f));
|
||||||
|
BlitInto(rig.Grab(), cell, cell, sheet, cols * cell, f * cell, 0 * cell);
|
||||||
|
}
|
||||||
|
SpriteBillboard.RestoreNow("diag6");
|
||||||
|
|
||||||
|
Save(sheet, cols * cell, rows * cell, kOut + @"\frames_attack_dir7.png", 6);
|
||||||
|
rig.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SpriteBillboard.RestoreNow("diag6-end");
|
||||||
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
}
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
static GameObject Spawn(string p, float scale)
|
||||||
|
{
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(p);
|
||||||
|
var go = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
PrefabUtility.UnpackPrefabInstance(go, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||||
|
go.name = Path.GetFileNameWithoutExtension(p) + "(Clone)";
|
||||||
|
go.transform.position = Vector3.zero;
|
||||||
|
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;
|
||||||
|
var an = go.GetComponent<Animator>();
|
||||||
|
an.applyRootMotion = false; an.cullingMode = AnimatorCullingMode.AlwaysAnimate; an.Rebind();
|
||||||
|
var bake = WLSpriteBakeSettings.Instance;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Rig
|
||||||
|
{
|
||||||
|
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 a = RenderTexture.active; RenderTexture.active = rt;
|
||||||
|
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0); tex.Apply(false, false);
|
||||||
|
RenderTexture.active = a;
|
||||||
|
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 Rig MakeCam(int w, int h)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Cam");
|
||||||
|
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.34f, 0.40f, 0.25f, 1f);
|
||||||
|
c.allowHDR = false; c.allowMSAA = false;
|
||||||
|
go.transform.rotation = Quaternion.Euler(kPitch, 0f, 0f);
|
||||||
|
go.transform.position = new Vector3(0f, 0.6f, 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 Rig { cam = c, rt = rt, tex = tex, w = w, h = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Save(Color32[] px, int w, int h, string file, int zoom)
|
||||||
|
{
|
||||||
|
int ow = w * zoom, oh = h * zoom;
|
||||||
|
var o = new Color32[ow * oh];
|
||||||
|
for (int y = 0; y < oh; y++) for (int x = 0; x < ow; x++) o[y * ow + x] = px[(y / zoom) * w + (x / zoom)];
|
||||||
|
var t = new Texture2D(ow, oh, TextureFormat.RGBA32, false, false);
|
||||||
|
t.SetPixels32(o); t.Apply(false, false);
|
||||||
|
File.WriteAllBytes(file, t.EncodeToPNG());
|
||||||
|
UnityEngine.Object.DestroyImmediate(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
WL-814k 이름 규약 표 · 2026-09-12 18:50:03
|
||||||
|
후보 idle = [idle / Idle / wait / IdleA / IdleB] (+보기만: land / getup)
|
||||||
|
후보 run = [run / Run / move / Move / walk] (+보기만: dodge)
|
||||||
|
후보 attack = [attack / Attack / atk / ATK0 / ATK1] (+보기만: ATK2 / ATK3 / ATK4 / Shoot / attack2 / skill2 / skill3 / skill4 / skill5 / casting / casting_end)
|
||||||
|
후보 hit = [hit / Hit / damage / Damage] (+보기만: stun / Swoon)
|
||||||
|
후보 die = [die / Die / death / Die0 / Die1] (+보기만: lying / Die2)
|
||||||
|
|
||||||
|
== 개별 프리팹 ==
|
||||||
|
Ai01.prefab 컨트롤러 pcanim_mage 상태 17개 · 매칭 5/5 · idle→idle run→run attack→attack hit→hit die→die
|
||||||
|
Batty_A.prefab 컨트롤러 Batty_Animations_Check 상태 8개 · 매칭 5/5 · idle→idle run→run attack→attack hit→hit die→die
|
||||||
|
F06.prefab 컨트롤러 Animations_check 상태 32개 · 매칭 5/5 · idle→IdleA run→Run attack→ATK0 hit→Damage die→Die0
|
||||||
|
M05.prefab 컨트롤러 Animations_check 상태 32개 · 매칭 5/5 · idle→IdleA run→Run attack→ATK0 hit→Damage die→Die0
|
||||||
|
|
||||||
|
== Res_Addr/Mobs 전수 (확장 견적 근거) ==
|
||||||
|
Animator+Controller 보유 프리팹 = 299 · 동작 5종 전부 매칭 = 288 (96.3 %)
|
||||||
|
매칭 4/5 동작 = 11종
|
||||||
|
매칭 5/5 동작 = 288종
|
||||||
|
못 찾은 동작 'hit' = 11종
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// WL-814k 이름 규약 표 — 「굽기 SO 의 상태 이름 후보」가 어떤 캐릭터에 얼마나 걸리는지 실측 (읽기만 · 굽기 0)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Naming.cs --entry WL814k_Naming.Run
|
||||||
|
// 🔴 여기서도 굽지 않는다 — 1차 시범은 PC 1종 + 잡몹 1종(발주서). 이 표는 「확장하면 뭐가 걸리나」의 근거다.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using WL.Look.SpriteBake;
|
||||||
|
|
||||||
|
public static class WL814k_Naming
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_NAMING.txt";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 이름 규약 표 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
var cfg = AssetDatabase.LoadAssetAtPath<WLSpriteBakeSettings>("Assets/WL/Look/Sprite/Resources/WL/WLSpriteBakeSettings.asset");
|
||||||
|
if (cfg == null) { L("FAIL — 굽기 SO 없음"); File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false)); return; }
|
||||||
|
|
||||||
|
foreach (var a in cfg.actions)
|
||||||
|
L(" 후보 " + a.key.PadRight(7) + " = [" + string.Join(" / ", a.stateNames) + "] (+보기만: " + string.Join(" / ", a.alsoMapStateNames) + ")");
|
||||||
|
|
||||||
|
// ── ① 시범 2종 + PD 가 방금 임포트한 치비 팩(Little Heroes) ─────────────
|
||||||
|
L("");
|
||||||
|
L("== 개별 프리팹 ==");
|
||||||
|
foreach (var p in new[]
|
||||||
|
{
|
||||||
|
"Assets/Res_Addr/PC/Ai01.prefab",
|
||||||
|
"Assets/Res_Addr/Mobs/Mob/Batty_A.prefab",
|
||||||
|
"Assets/Suriyun/Characters/Nana/Prefab/F06.prefab",
|
||||||
|
"Assets/Suriyun/Characters/RedKnight/Prefab/M05/M05.prefab",
|
||||||
|
})
|
||||||
|
One(p, cfg);
|
||||||
|
|
||||||
|
// ── ② 몹 전수 — 확장했을 때 몇 %가 그냥 걸리는지 ───────────────────────
|
||||||
|
L("");
|
||||||
|
L("== Res_Addr/Mobs 전수 (확장 견적 근거) ==");
|
||||||
|
var guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Res_Addr/Mobs" });
|
||||||
|
int total = 0, full = 0;
|
||||||
|
var hist = new Dictionary<int, int>();
|
||||||
|
var missing = new Dictionary<string, int>();
|
||||||
|
foreach (var g in guids)
|
||||||
|
{
|
||||||
|
var path = AssetDatabase.GUIDToAssetPath(g);
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||||
|
if (go == null) continue;
|
||||||
|
var anim = go.GetComponentInChildren<Animator>(true);
|
||||||
|
if (anim == null || anim.runtimeAnimatorController == null) continue;
|
||||||
|
total++;
|
||||||
|
var states = States(anim);
|
||||||
|
int hit = 0;
|
||||||
|
foreach (var a in cfg.actions)
|
||||||
|
{
|
||||||
|
if (Match(a, states) != null) hit++;
|
||||||
|
else { int c; missing.TryGetValue(a.key, out c); missing[a.key] = c + 1; }
|
||||||
|
}
|
||||||
|
if (hit == cfg.actions.Length) full++;
|
||||||
|
int n; hist.TryGetValue(hit, out n); hist[hit] = n + 1;
|
||||||
|
}
|
||||||
|
L(" Animator+Controller 보유 프리팹 = " + total + " · 동작 5종 전부 매칭 = " + full + " (" + (100f * full / Mathf.Max(1, total)).ToString("0.0") + " %)");
|
||||||
|
var keys = new List<int>(hist.Keys); keys.Sort();
|
||||||
|
foreach (var k in keys) L(" 매칭 " + k + "/5 동작 = " + hist[k] + "종");
|
||||||
|
foreach (var kv in missing) L(" 못 찾은 동작 '" + kv.Key + "' = " + kv.Value + "종");
|
||||||
|
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void One(string path, WLSpriteBakeSettings cfg)
|
||||||
|
{
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||||
|
if (go == null) { L(" " + path + " → 없음"); return; }
|
||||||
|
var anim = go.GetComponentInChildren<Animator>(true);
|
||||||
|
if (anim == null || anim.runtimeAnimatorController == null) { L(" " + path + " → Animator/Controller 없음"); return; }
|
||||||
|
var states = States(anim);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int hit = 0;
|
||||||
|
foreach (var a in cfg.actions)
|
||||||
|
{
|
||||||
|
var m = Match(a, states);
|
||||||
|
if (m != null) hit++;
|
||||||
|
sb.Append(a.key + "→" + (m ?? "✗") + " ");
|
||||||
|
}
|
||||||
|
L(string.Format(" {0,-56} 컨트롤러 {1,-24} 상태 {2,2}개 · 매칭 {3}/{4} · {5}",
|
||||||
|
Path.GetFileName(path), anim.runtimeAnimatorController.name, states.Count, hit, cfg.actions.Length, sb.ToString().Trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, string> States(Animator anim)
|
||||||
|
{
|
||||||
|
var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var ac = anim.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
|
||||||
|
if (ac != null && ac.layers.Length > 0 && ac.layers[0].stateMachine != null)
|
||||||
|
foreach (var ch in ac.layers[0].stateMachine.states)
|
||||||
|
if (ch.state.motion != null && !d.ContainsKey(ch.state.name)) d[ch.state.name] = ch.state.name;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Match(WLSpriteBakeAction a, Dictionary<string, string> states)
|
||||||
|
{
|
||||||
|
foreach (var n in a.stateNames) { string real; if (states.TryGetValue(n, out real)) return real; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
WL-814k 프로브 · 2026-09-12 18:43:19 · Unity 6000.3.23f1
|
||||||
|
|
||||||
|
== A. 에셋 · 메타 ==
|
||||||
|
PASS A1 굽기 SO — 내림각 43.609° · 픽셀 0.02833 m
|
||||||
|
PASS A2 런타임 SO — sets 2
|
||||||
|
PASS A3 시트 Ai01_SpriteSet — Ai01 · 프레임 56×56 · 16열×12행 · 방향 8 · 총 192칸 · 텍스처 896×672
|
||||||
|
PASS A4 임포트 Ai01_SpriteSet — filter Point · mipmap False · 압축 Uncompressed · alphaIsTransparency True · maxSize 1024 · type Default
|
||||||
|
PASS A5 격자 Ai01_SpriteSet — 칸 192 ≥ 필요 192 · 텍스처 896×672
|
||||||
|
PASS A6 동작표 Ai01_SpriteSet — 5개 · idle[0+4] run[32+6] attack[80+6] hit[128+2] die[144+6]
|
||||||
|
PASS A3 시트 Batty_A_SpriteSet — Batty_A · 프레임 48×48 · 16열×12행 · 방향 8 · 총 192칸 · 텍스처 768×576
|
||||||
|
PASS A4 임포트 Batty_A_SpriteSet — filter Point · mipmap False · 압축 Uncompressed · alphaIsTransparency True · maxSize 1024 · type Default
|
||||||
|
PASS A5 격자 Batty_A_SpriteSet — 칸 192 ≥ 필요 192 · 텍스처 768×576
|
||||||
|
PASS A6 동작표 Batty_A_SpriteSet — 5개 · idle[0+4] run[32+6] attack[80+6] hit[128+2] die[144+6]
|
||||||
|
|
||||||
|
== B. 부착 → 원복 (원본 파괴 0 · 렌더러 원값 복원) ==
|
||||||
|
PASS B1 이름 매칭 — 액터 'Ai01(Clone)' → Ai01_SpriteSet
|
||||||
|
PASS B2 원본 끔 — SMR 11개 → 켜져 있는 것 0개
|
||||||
|
PASS B3 쿼드 1개 — [WL814k] SpriteQuad · 부모 Ai01(Clone) · 레이어 3
|
||||||
|
PASS B4 파괴 0 — 노드 223 → 224 (쿼드 +1)
|
||||||
|
PASS B5 원복 — SMR 켜짐 11/11 · 노드 223/223
|
||||||
|
|
||||||
|
== C. 프레임 · 방향 선택 (Animator 폴링 · 원본 코드 수정 0) ==
|
||||||
|
PASS C1 상태→동작 — 17/17개 상태가 정확한 동작으로 간다
|
||||||
|
PASS C2 시간→프레임 — nt 0.13→f0 nt 0.38→f1 nt 0.63→f2 nt 0.88→f3
|
||||||
|
PASS C2b 상태 폴링(에디트 모드) — 5/5 · idle→idle run→run attack→attack hit→hit die→die
|
||||||
|
PASS C3 각도→방향 — 0°→d0 45°→d1 90°→d2 135°→d3 180°→d4 225°→d5 270°→d6 315°→d7
|
||||||
|
PASS C4 쿼드 크기 — 월드 1.5867 m (기대 1.5867) · 스케일 보정 1.000
|
||||||
|
PASS C5 쿼드 자세 — 카메라 정렬 오차 0.000°
|
||||||
|
|
||||||
|
== D. GC (프레임당 할당 0) ==
|
||||||
|
PASS D1 UpdateAll ×200k — mono Δ 0 B · gen0 0회
|
||||||
|
|
||||||
|
== E. 시트 내용 (도트화가 실제로 강제됐는가) ==
|
||||||
|
PASS E1 알파 이진화 Ai01_SpriteSet — 불투명 63548 · 반투명 0(0 이어야 한다)
|
||||||
|
PASS E2 팔레트 양자화 Ai01_SpriteSet — 밝기 6단계 [0 51 102 153 204 255] 밖 픽셀 0개 · 실제 7가지 [0 17 51 102 153 204 255] (외곽선 색 14,11,17 제외) · 색 1819종
|
||||||
|
PASS E3 외곽선 Ai01_SpriteSet — 어두운 테두리 픽셀 22159 / 불투명 63548 (34.9 %)
|
||||||
|
PASS E4 PNG 크기 Ai01_SpriteSet — 165,716 B (161.8 KB) · 896×672 · RGBA32 런타임 2.30 MB
|
||||||
|
PASS E1 알파 이진화 Batty_A_SpriteSet — 불투명 53848 · 반투명 0(0 이어야 한다)
|
||||||
|
PASS E2 팔레트 양자화 Batty_A_SpriteSet — 밝기 6단계 [0 51 102 153 204 255] 밖 픽셀 0개 · 실제 6가지 [17 51 102 153 204 255] (외곽선 색 14,11,17 제외) · 색 398종
|
||||||
|
PASS E3 외곽선 Batty_A_SpriteSet — 어두운 테두리 픽셀 12096 / 불투명 53848 (22.5 %)
|
||||||
|
PASS E4 PNG 크기 Batty_A_SpriteSet — 107,778 B (105.3 KB) · 768×576 · RGBA32 런타임 1.69 MB
|
||||||
|
|
||||||
|
== F. C8 롤백 (SO off → 원본 3D 100 %) ==
|
||||||
|
PASS F1 게이트 — RuntimeDisabled → Enabled=false · ShouldBeOn=false
|
||||||
|
PASS F2 렌더러 100 % 복원 — SMR 11/11 · MR 2/2
|
||||||
|
PASS F3 쿼드 0 — 노드 223/223 · 대상 0
|
||||||
|
PASS F4 목록 밖 액터 — 'Ai02(Clone)' → 매칭 0 = 원본 3D 그대로
|
||||||
|
|
||||||
|
RESULT PASS — 34 pass / 0 fail
|
||||||
|
|
@ -0,0 +1,686 @@
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
WL-814k 실측 조사 · 2026-09-12 18:04:40 · Unity 6000.3.23f1
|
||||||
|
|
||||||
|
== ① 카메라 각 (WLLookModeSettings 프리셋 실측) ==
|
||||||
|
[0] 기본 pixelRig=0 h=80 d=3 up=3 ortho=3.8 pxH=480 → 내림각 43.609° · 높이 5.433 m · 수평 4.591 m
|
||||||
|
[1] 도트A pixelRig=1 h=80 d=3 up=3 ortho=3.4 pxH=240 → 내림각 43.609° · 높이 5.433 m · 수평 4.591 m
|
||||||
|
[2] 도트B(넓게) pixelRig=1 h=80 d=3 up=3 ortho=4.6 pxH=240 → 내림각 43.609° · 높이 5.433 m · 수평 4.591 m
|
||||||
|
[3] 도트C(굵게) pixelRig=1 h=80 d=3 up=3 ortho=4.6 pxH=180 → 내림각 43.609° · 높이 5.433 m · 수평 4.591 m
|
||||||
|
도트A: orthoSize 3.4 · pixelHeight 240 → 월드 1 m = 35.294 px · 픽셀 1개 = 0.02833 m
|
||||||
|
|
||||||
|
== ② 프리팹 Assets/Res_Addr/PC/Ai01.prefab ==
|
||||||
|
Animator = Ai01
|
||||||
|
avatar = AiAvatar · isHuman=True · isValid=True
|
||||||
|
applyRootMotion = False · cullingMode = CullUpdateTransforms
|
||||||
|
controller = pcanim_mage (Assets/Res_Addr/Animations/pcanim_mage.controller)
|
||||||
|
clips = 26
|
||||||
|
· LightHit len 1.000s fps 30 frames 30 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· death_F3441 len 2.100s fps 30 frames 63 loop False legacy False hasMotion False hasRoot True empty False
|
||||||
|
· Idle len 2.000s fps 30 frames 60 loop True legacy False hasMotion True hasRoot True empty False
|
||||||
|
· Run len 0.533s fps 30 frames 16 loop True legacy False hasMotion True hasRoot True empty False
|
||||||
|
· CastspellB_wing len 1.208s fps 24 frames 29 loop True legacy False hasMotion False hasRoot True empty False
|
||||||
|
· RangeAttack1 len 2.100s fps 30 frames 63 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· Atk2_wing len 1.500s fps 30 frames 45 loop True legacy False hasMotion False hasRoot True empty False
|
||||||
|
· Atk_A1_wing len 1.167s fps 24 frames 28 loop True legacy False hasMotion False hasRoot True empty False
|
||||||
|
· DashForward len 1.333s fps 30 frames 40 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· Land len 0.500s fps 30 frames 15 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· RangeAttack2 len 2.167s fps 30 frames 65 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· SpecialAttack1 len 2.167s fps 30 frames 65 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· SpecialAttack2 len 2.167s fps 30 frames 65 loop False legacy False hasMotion True hasRoot True empty False
|
||||||
|
· Stunned len 2.000s fps 30 frames 60 loop True legacy False hasMotion True hasRoot True empty False
|
||||||
|
· CastspellC_wing len 2.667s fps 24 frames 64 loop False legacy False hasMotion False hasRoot True empty False
|
||||||
|
· LyingFront len 1.333s fps 30 frames 40 loop True legacy False hasMotion False hasRoot True empty False
|
||||||
|
· LyingFront_WakeUp len 0.667s fps 30 frames 20 loop False legacy False hasMotion False hasRoot True empty False
|
||||||
|
· Face_SmileA len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_Ang1 len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_SmileB len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_Ang3 len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_Ang2 len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_SadA len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_SadB len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_Surprise len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Face_EyeClose len 1.633s fps 30 frames 49 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
layers = 2
|
||||||
|
layer[0] Base Layer · states 17 · default=idle
|
||||||
|
hit|LightHit|hash 1523721793|full -62788552
|
||||||
|
die|death_F3441|hash 964231127|full -1611975250
|
||||||
|
idle|Idle|hash -601574123|full -178524345
|
||||||
|
run|Run|hash 1349952704|full -152736071
|
||||||
|
casting|CastspellB_wing|hash -786711984|full 1053661652
|
||||||
|
skill2|RangeAttack1|hash -722835594|full -673593035
|
||||||
|
attack|Atk2_wing|hash 1203776827|full 1156809592
|
||||||
|
attack2|Atk_A1_wing|hash -1415997692|full 1146074240
|
||||||
|
dodge|DashForward|hash -1854944626|full 337872140
|
||||||
|
land|Land|hash -1476340264|full -2122346614
|
||||||
|
skill3|RangeAttack2|hash -1544726560|full -1595999837
|
||||||
|
skill4|SpecialAttack1|hash 1032438339|full 1052404736
|
||||||
|
skill5|SpecialAttack2|hash 1250882261|full 1237146774
|
||||||
|
stun|Stunned|hash -210448114|full -636777636
|
||||||
|
casting_end|CastspellC_wing|hash 380729335|full -1453968558
|
||||||
|
lying|LyingFront|hash -478560198|full 1714786232
|
||||||
|
getup|LyingFront_WakeUp|hash -1333958798|full 892414192
|
||||||
|
layer[1] Emotion · states 11 · default=New State
|
||||||
|
New State|-|hash -499022955|full 833042322
|
||||||
|
SmileA (Emotion)|Face_SmileA|hash 371072321|full -1047046492
|
||||||
|
ANG1 (Emotion) 0|Face_Ang1|hash 1042426016|full -374643899
|
||||||
|
Indifferent (Emotion)|-|hash 1209094866|full -995248490
|
||||||
|
SmileB (Emotion)|Face_SmileB|hash -1879278784|full 1484075173
|
||||||
|
ANG3 (Emotion)|Face_Ang3|hash 199512751|full 749628860
|
||||||
|
ANG2 (Emotion)|Face_Ang2|hash -898986641|full -316624260
|
||||||
|
SadA (Emotion)|Face_SadA|hash -774383980|full -157423225
|
||||||
|
SadB (Emotion)|Face_SadB|hash 1211478165|full 1870630790
|
||||||
|
Surprise (Emotion)|Face_Surprise|hash -1812836920|full 1375012336
|
||||||
|
EyeClose (Emotion)|Face_EyeClose|hash -302154122|full 804927054
|
||||||
|
SkinnedMeshRenderer = 11 · MeshRenderer = 2
|
||||||
|
삼각형 합계 = 16645 · 머티리얼 10종
|
||||||
|
Ai_hair [Toon/Toon]
|
||||||
|
Ai_cloth [Toon/Toon]
|
||||||
|
Ai_skin [Toon/Toon]
|
||||||
|
bikini10 [Toon/Toon]
|
||||||
|
Ai_eyeline [Toon/Toon]
|
||||||
|
Ai_eyeL [Unlit/Transparent]
|
||||||
|
Ai_eyeR [Unlit/Transparent]
|
||||||
|
Ai_face [Toon/Toon]
|
||||||
|
Ai_Lash [Toon/Toon]
|
||||||
|
cheek [Unlit/Transparent]
|
||||||
|
bones[0] = 41
|
||||||
|
transform 노드 = 223
|
||||||
|
root localScale = (1.0000, 1.0000, 1.0000)
|
||||||
|
root comp: UnityEngine.Transform
|
||||||
|
root comp: UnityEngine.Animator
|
||||||
|
root comp: WizardActor
|
||||||
|
root comp: UnityEngine.AI.NavMeshAgent
|
||||||
|
root comp: UnityEngine.Rigidbody
|
||||||
|
root comp: UnityEngine.CapsuleCollider
|
||||||
|
root comp: SkinnedMeshAfterImage
|
||||||
|
바운드(스케일 1) center (0.0000, 0.7372, -0.0195) size (1.3047, 1.7017, 1.0770) · 높이 1.7017 m
|
||||||
|
바운드(스케일 0.7) 높이 1.1912 m · 폭 0.9133 m · 깊이 0.7539 m
|
||||||
|
|
||||||
|
== ② 프리팹 Assets/Res_Addr/Mobs/Mob/Batty_A.prefab ==
|
||||||
|
Animator = Batty_A
|
||||||
|
avatar = BattyAvatar · isHuman=False · isValid=True
|
||||||
|
applyRootMotion = False · cullingMode = CullUpdateTransforms
|
||||||
|
controller = Batty_Animations_Check (Assets/ResWork/Suriyun/Monster Pack Darkness/Animations/Batty/Batty_Animations_Check.controller)
|
||||||
|
clips = 8
|
||||||
|
· Move len 0.958s fps 24 frames 23 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Idle len 0.958s fps 24 frames 23 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Attack len 0.583s fps 24 frames 14 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Damage len 0.583s fps 24 frames 14 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· Die len 1.000s fps 24 frames 24 loop False legacy False hasMotion False hasRoot False empty False
|
||||||
|
· stun len 0.958s fps 24 frames 23 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· casting len 0.958s fps 24 frames 23 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
· casting_end len 0.583s fps 24 frames 14 loop True legacy False hasMotion False hasRoot False empty False
|
||||||
|
layers = 1
|
||||||
|
layer[0] Base Layer · states 8 · default=idle
|
||||||
|
run|Move|hash 1349952704|full -152736071
|
||||||
|
idle|Idle|hash -601574123|full -178524345
|
||||||
|
attack|Attack|hash 1203776827|full 1156809592
|
||||||
|
hit|Damage|hash 1523721793|full -62788552
|
||||||
|
die|Die|hash 964231127|full -1611975250
|
||||||
|
stun|stun|hash -210448114|full -636777636
|
||||||
|
casting|casting|hash -786711984|full 1053661652
|
||||||
|
casting_end|casting_end|hash 380729335|full -1453968558
|
||||||
|
SkinnedMeshRenderer = 1 · MeshRenderer = 0
|
||||||
|
삼각형 합계 = 896 · 머티리얼 1종
|
||||||
|
T_Batty_A [MK/Toon/URP/Standard/Simple]
|
||||||
|
bones[0] = 8
|
||||||
|
transform 노드 = 11
|
||||||
|
root localScale = (1.5000, 1.5000, 1.5000)
|
||||||
|
root comp: UnityEngine.Transform
|
||||||
|
root comp: UnityEngine.Animator
|
||||||
|
root comp: MobActor
|
||||||
|
root comp: UnityEngine.BoxCollider
|
||||||
|
root comp: NoSeeLongDMob
|
||||||
|
root comp: UnityEngine.AI.NavMeshAgent
|
||||||
|
root comp: UnityEngine.Rigidbody
|
||||||
|
바운드(스케일 1) center (0.0000, 0.3453, -0.0006) size (0.9355, 0.5372, 0.3566) · 높이 0.5372 m
|
||||||
|
바운드(스케일 0.7) 높이 0.2507 m · 폭 0.4366 m · 깊이 0.1664 m
|
||||||
|
|
||||||
|
== ③ 캐릭터 프리팹 수 (확장 견적 근거) ==
|
||||||
|
Res_Addr/PC 프리팹 = 46
|
||||||
|
Res_Addr/Mobs 프리팹 = 299
|
||||||
|
그중 Animator+Controller 보유 = 299 · 서로 다른 컨트롤러 79종
|
||||||
|
|
||||||
|
== ④ 814c Toon 머티리얼 ==
|
||||||
|
Assets/WL/Look/Env/Materials = 258개
|
||||||
|
'Ai_*' → 6개: Ai_cloth_Toon.mat, Ai_eyeline_Toon.mat, Ai_face_Toon.mat, Ai_hair_Toon.mat, Ai_Lash_Toon.mat, Ai_skin_Toon.mat
|
||||||
|
'Batty*' → 0개:
|
||||||
|
WLEnvLookSettings = 있음
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
WL-814k 실측 ② · 2026-09-12 18:06:23
|
||||||
|
|
||||||
|
== 방향광 (Res_Addr/Map/WL_Nature.prefab + Ingame 씬) ==
|
||||||
|
[map] Sun type=Directional color=RGBA(1.000, 0.920, 0.703, 1.000) intensity=1.2 shadows=Soft euler=(26.40, 238.60, 0.00)
|
||||||
|
[map] Sun_Ambient type=Directional color=RGBA(0.596, 0.703, 0.839, 1.000) intensity=0.3 shadows=None euler=(36.10, 116.10, 0.00)
|
||||||
|
Assets/Scenes/Ingame.unity · Light(dir) 블록 없음 · 파일 21830 B
|
||||||
|
Assets/Scenes/Title.unity · Light(dir) 블록 있음 · 파일 54278 B
|
||||||
|
RenderSettings.ambientMode(현재 씬) = Skybox · ambientLight = RGBA(0.212, 0.227, 0.259, 1.000) · ambientIntensity = 1 · sun = null
|
||||||
|
|
||||||
|
== Toon 매칭 Assets/Res_Addr/PC/Ai01.prefab ==
|
||||||
|
Ai_hair [Toon/Toon] → Toon 있음
|
||||||
|
Ai_cloth [Toon/Toon] → Toon 있음
|
||||||
|
Ai_skin [Toon/Toon] → Toon 있음
|
||||||
|
bikini10 [Toon/Toon] → Toon 있음
|
||||||
|
Ai_eyeline [Toon/Toon] → Toon 있음
|
||||||
|
cheek [Unlit/Transparent] → Toon 없음
|
||||||
|
Ai_eyeL [Unlit/Transparent] → Toon 없음
|
||||||
|
Ai_eyeR [Unlit/Transparent] → Toon 없음
|
||||||
|
Ai_face [Toon/Toon] → Toon 있음
|
||||||
|
Ai_Lash [Toon/Toon] → Toon 있음
|
||||||
|
|
||||||
|
== Toon 매칭 Assets/Res_Addr/Mobs/Mob/Batty_A.prefab ==
|
||||||
|
T_Batty_A [MK/Toon/URP/Standard/Simple] → Toon 있음
|
||||||
|
|
||||||
|
== 샘플링 경로 (AnimationMode.SampleAnimationClip) ==
|
||||||
|
Ai01 · clip Idle · probe joint31 · Δpos 0.0000 · Δrot 0.00° → 변화 없음(확인필요)
|
||||||
|
Batty_A · clip Attack · probe wing_l1 · Δpos 0.0000 · Δrot 49.40° → 샘플링 OK
|
||||||
|
|
||||||
|
== 렌더 파이프라인 ==
|
||||||
|
defaultRenderPipeline = URP-Performant (Assets/Settings/URP-Performant.asset)
|
||||||
|
currentRenderPipeline = URP-Balanced
|
||||||
|
QualitySettings.renderPipeline = URP-Balanced
|
||||||
|
SystemInfo.graphicsDeviceType = Direct3D11 · supportsComputeShaders = True
|
||||||
|
Application.isBatchMode = True
|
||||||
|
Shader.Find("Sprites/Default") = OK
|
||||||
|
Shader.Find("Unlit/Transparent") = OK
|
||||||
|
Shader.Find("Universal Render Pipeline/Unlit") = OK
|
||||||
|
Shader.Find("Universal Render Pipeline/2D/Sprite-Unlit-Default") = OK
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
WL-814k 실측 ③ 샘플링 · 2026-09-12 18:07:00
|
||||||
|
|
||||||
|
== Assets/Res_Addr/PC/Ai01.prefab ==
|
||||||
|
Idle len 2.00s · AnimationMode Δbounds 2.3995 m · SampleAnimation Δbounds 2.3995 m
|
||||||
|
Run len 0.53s · AnimationMode Δbounds 2.7951 m · SampleAnimation Δbounds 4.2027 m
|
||||||
|
Atk2_wing len 1.50s · AnimationMode Δbounds 2.6478 m · SampleAnimation Δbounds 3.8505 m
|
||||||
|
LightHit len 1.00s · AnimationMode Δbounds 3.1135 m · SampleAnimation Δbounds 3.1256 m
|
||||||
|
death_F3441 len 2.10s · AnimationMode Δbounds 2.7651 m · SampleAnimation Δbounds 5.0011 m
|
||||||
|
|
||||||
|
== Assets/Res_Addr/Mobs/Mob/Batty_A.prefab ==
|
||||||
|
Idle len 0.96s · AnimationMode Δbounds 1.1362 m · SampleAnimation Δbounds 1.1362 m
|
||||||
|
Move len 0.96s · AnimationMode Δbounds 1.1362 m · SampleAnimation Δbounds 1.1362 m
|
||||||
|
Attack len 0.58s · AnimationMode Δbounds 1.1362 m · SampleAnimation Δbounds 1.1362 m
|
||||||
|
Damage len 0.58s · AnimationMode Δbounds 1.1362 m · SampleAnimation Δbounds 1.1362 m
|
||||||
|
Die len 1.00s · AnimationMode Δbounds 1.1362 m · SampleAnimation Δbounds 1.1362 m
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
// WL-814k 실측 조사 (게임 코드 아님 · 에디트 모드 전용 · 에셋 수정 0 · 읽기만)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Survey.cs --entry WL814k_Survey.Run
|
||||||
|
// 🔴 원본 프리팹/머티리얼/애니메이션 수정 0. AssetDatabase.SaveAssets 호출 0.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Survey
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_SURVEY.txt";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 실측 조사 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Cam();
|
||||||
|
Prefab("Assets/Res_Addr/PC/Ai01.prefab");
|
||||||
|
Prefab("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
|
||||||
|
Counts();
|
||||||
|
ToonMats();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ① 카메라 각 (811o 수식 그대로 · 814d 프리셋 값) ──────────────────────
|
||||||
|
static void Cam()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== ① 카메라 각 (WLLookModeSettings 프리셋 실측) ==");
|
||||||
|
var so = Resources.Load<WL.Look.Toggle.WLLookModeSettings>("WL/WLLookModeSettings");
|
||||||
|
if (so == null) { L(" WLLookModeSettings 없음"); return; }
|
||||||
|
for (int i = 0; i < so.presets.Length; i++)
|
||||||
|
{
|
||||||
|
var p = so.presets[i];
|
||||||
|
float down = WL.CameraFraming.CameraFramingOverride.DownAngleDeg(p.cameraHeight, p.distance, p.distanceUp);
|
||||||
|
float above = WL.CameraFraming.CameraFramingOverride.CamHeightAbove(p.cameraHeight, p.distance, p.distanceUp);
|
||||||
|
float horiz = WL.CameraFraming.CameraFramingOverride.CamHorizontal(p.cameraHeight, p.distance);
|
||||||
|
L(string.Format(" [{0}] {1} pixelRig={2} h={3} d={4} up={5} ortho={6} pxH={7} → 내림각 {8:0.000}° · 높이 {9:0.000} m · 수평 {10:0.000} m",
|
||||||
|
i, p.name, p.pixelRig ? 1 : 0, p.cameraHeight, p.distance, p.distanceUp, p.orthoSize, p.pixelHeight, down, above, horiz));
|
||||||
|
}
|
||||||
|
// 도트A 프리셋 기준 캐릭터 픽셀
|
||||||
|
var d = so.presets.Length > 1 ? so.presets[1] : null;
|
||||||
|
if (d != null)
|
||||||
|
L(string.Format(" 도트A: orthoSize {0} · pixelHeight {1} → 월드 1 m = {2:0.000} px · 픽셀 1개 = {3:0.00000} m",
|
||||||
|
d.orthoSize, d.pixelHeight, d.pixelHeight / (2f * d.orthoSize), (2f * d.orthoSize) / d.pixelHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ② 프리팹 전수 ────────────────────────────────────────────────────────
|
||||||
|
static void Prefab(string path)
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== ② 프리팹 " + path + " ==");
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||||
|
if (go == null) { L(" 로드 실패"); return; }
|
||||||
|
|
||||||
|
var anim = go.GetComponentInChildren<Animator>(true);
|
||||||
|
L(" Animator = " + (anim == null ? "없음" : anim.gameObject.name));
|
||||||
|
if (anim != null)
|
||||||
|
{
|
||||||
|
L(" avatar = " + (anim.avatar == null ? "null" : anim.avatar.name + " · isHuman=" + anim.avatar.isHuman + " · isValid=" + anim.avatar.isValid));
|
||||||
|
L(" applyRootMotion = " + anim.applyRootMotion + " · cullingMode = " + anim.cullingMode);
|
||||||
|
var rac = anim.runtimeAnimatorController;
|
||||||
|
L(" controller = " + (rac == null ? "null" : rac.name + " (" + AssetDatabase.GetAssetPath(rac) + ")"));
|
||||||
|
if (rac != null)
|
||||||
|
{
|
||||||
|
var clips = rac.animationClips;
|
||||||
|
L(" clips = " + clips.Length);
|
||||||
|
var seen = new HashSet<string>();
|
||||||
|
foreach (var c in clips)
|
||||||
|
{
|
||||||
|
if (c == null || !seen.Add(c.name)) continue;
|
||||||
|
L(string.Format(" · {0,-28} len {1,6:0.000}s fps {2,5:0.#} frames {3,4} loop {4} legacy {5} hasMotion {6} hasRoot {7} empty {8}",
|
||||||
|
c.name, c.length, c.frameRate, Mathf.RoundToInt(c.length * c.frameRate), c.isLooping, c.legacy, c.hasMotionCurves, c.hasRootCurves, c.empty));
|
||||||
|
}
|
||||||
|
var ac = rac as UnityEditor.Animations.AnimatorController;
|
||||||
|
if (ac != null)
|
||||||
|
{
|
||||||
|
L(" layers = " + ac.layers.Length);
|
||||||
|
for (int li = 0; li < ac.layers.Length; li++)
|
||||||
|
{
|
||||||
|
var lay = ac.layers[li];
|
||||||
|
var sm = lay.stateMachine;
|
||||||
|
if (sm == null) { L(" layer[" + li + "] " + lay.name + " (stateMachine null)"); continue; }
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var st in sm.states)
|
||||||
|
names.Add(st.state.name + "|" + (st.state.motion == null ? "-" : st.state.motion.name) + "|hash " + Animator.StringToHash(st.state.name)
|
||||||
|
+ "|full " + Animator.StringToHash(lay.name + "." + st.state.name));
|
||||||
|
L(" layer[" + li + "] " + lay.name + " · states " + names.Count + " · default=" + (sm.defaultState == null ? "-" : sm.defaultState.name));
|
||||||
|
foreach (var n in names) L(" " + n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var smrs = go.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
var mrs = go.GetComponentsInChildren<MeshRenderer>(true);
|
||||||
|
L(" SkinnedMeshRenderer = " + smrs.Length + " · MeshRenderer = " + mrs.Length);
|
||||||
|
int tri = 0; var mats = new HashSet<string>();
|
||||||
|
foreach (var r in smrs)
|
||||||
|
{
|
||||||
|
if (r.sharedMesh != null) tri += r.sharedMesh.triangles.Length / 3;
|
||||||
|
foreach (var m in r.sharedMaterials) if (m != null) mats.Add(m.name + " [" + (m.shader == null ? "-" : m.shader.name) + "]");
|
||||||
|
}
|
||||||
|
foreach (var r in mrs)
|
||||||
|
{
|
||||||
|
var mf = r.GetComponent<MeshFilter>();
|
||||||
|
if (mf != null && mf.sharedMesh != null) tri += mf.sharedMesh.triangles.Length / 3;
|
||||||
|
foreach (var m in r.sharedMaterials) if (m != null) mats.Add(m.name + " [" + (m.shader == null ? "-" : m.shader.name) + "]");
|
||||||
|
}
|
||||||
|
L(" 삼각형 합계 = " + tri + " · 머티리얼 " + mats.Count + "종");
|
||||||
|
foreach (var m in mats) L(" " + m);
|
||||||
|
|
||||||
|
// 본 수
|
||||||
|
if (smrs.Length > 0 && smrs[0].bones != null) L(" bones[0] = " + smrs[0].bones.Length);
|
||||||
|
var allT = go.GetComponentsInChildren<Transform>(true);
|
||||||
|
L(" transform 노드 = " + allT.Length);
|
||||||
|
|
||||||
|
// 스케일·ClassConfig
|
||||||
|
L(" root localScale = " + go.transform.localScale.ToString("F4"));
|
||||||
|
foreach (var c in go.GetComponents<Component>())
|
||||||
|
if (c != null) L(" root comp: " + c.GetType().FullName);
|
||||||
|
|
||||||
|
// 인스턴스를 임시로 만들어 바운드 실측(씬 저장 0)
|
||||||
|
var inst = (GameObject)PrefabUtility.InstantiatePrefab(go);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
inst.transform.position = Vector3.zero;
|
||||||
|
inst.transform.rotation = Quaternion.identity;
|
||||||
|
var b = WorldBounds(inst);
|
||||||
|
L(string.Format(" 바운드(스케일 1) center {0} size {1} · 높이 {2:0.0000} m", b.center.ToString("F4"), b.size.ToString("F4"), b.size.y));
|
||||||
|
inst.transform.localScale = Vector3.one * 0.7f;
|
||||||
|
var b7 = WorldBounds(inst);
|
||||||
|
L(string.Format(" 바운드(스케일 0.7) 높이 {0:0.0000} m · 폭 {1:0.0000} m · 깊이 {2:0.0000} m", b7.size.y, b7.size.x, b7.size.z));
|
||||||
|
}
|
||||||
|
finally { UnityEngine.Object.DestroyImmediate(inst); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bounds WorldBounds(GameObject root)
|
||||||
|
{
|
||||||
|
var rs = root.GetComponentsInChildren<Renderer>(true);
|
||||||
|
bool any = false; var b = new Bounds();
|
||||||
|
foreach (var r in rs)
|
||||||
|
{
|
||||||
|
if (r == null || r is ParticleSystemRenderer) continue;
|
||||||
|
if (!any) { b = r.bounds; any = true; }
|
||||||
|
else b.Encapsulate(r.bounds);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ③ 전 캐릭터 수 (확장 견적용) ────────────────────────────────────────
|
||||||
|
static void Counts()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== ③ 캐릭터 프리팹 수 (확장 견적 근거) ==");
|
||||||
|
var pc = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Res_Addr/PC" });
|
||||||
|
var mob = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Res_Addr/Mobs" });
|
||||||
|
L(" Res_Addr/PC 프리팹 = " + pc.Length);
|
||||||
|
L(" Res_Addr/Mobs 프리팹 = " + mob.Length);
|
||||||
|
int mobWithAnim = 0; var ctrls = new HashSet<string>();
|
||||||
|
foreach (var g in mob)
|
||||||
|
{
|
||||||
|
var p = AssetDatabase.GUIDToAssetPath(g);
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(p);
|
||||||
|
if (go == null) continue;
|
||||||
|
var a = go.GetComponentInChildren<Animator>(true);
|
||||||
|
if (a != null && a.runtimeAnimatorController != null) { mobWithAnim++; ctrls.Add(a.runtimeAnimatorController.name); }
|
||||||
|
}
|
||||||
|
L(" 그중 Animator+Controller 보유 = " + mobWithAnim + " · 서로 다른 컨트롤러 " + ctrls.Count + "종");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ④ Toon 머티리얼 (814c) ──────────────────────────────────────────────
|
||||||
|
static void ToonMats()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== ④ 814c Toon 머티리얼 ==");
|
||||||
|
string dir = "Assets/WL/Look/Env/Materials";
|
||||||
|
var all = AssetDatabase.FindAssets("t:Material", new[] { dir });
|
||||||
|
L(" " + dir + " = " + all.Length + "개");
|
||||||
|
foreach (var key in new[] { "Ai_", "Batty" })
|
||||||
|
{
|
||||||
|
var hits = new List<string>();
|
||||||
|
foreach (var g in all)
|
||||||
|
{
|
||||||
|
var p = AssetDatabase.GUIDToAssetPath(g);
|
||||||
|
if (Path.GetFileName(p).StartsWith(key, StringComparison.OrdinalIgnoreCase)) hits.Add(Path.GetFileName(p));
|
||||||
|
}
|
||||||
|
L(" '" + key + "*' → " + hits.Count + "개: " + string.Join(", ", hits.ToArray()));
|
||||||
|
}
|
||||||
|
var envSo = Resources.Load<ScriptableObject>("WL/WLEnvLookSettings");
|
||||||
|
L(" WLEnvLookSettings = " + (envSo == null ? "없음" : "있음"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// WL-814k 실측 조사 ② — 조명 · Toon 머티리얼 매칭 · 샘플링 경로 (읽기만)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Survey2.cs --entry WL814k_Survey2.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Survey2
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_SURVEY2.txt";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 실측 ② · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Lights();
|
||||||
|
ToonMatch("Assets/Res_Addr/PC/Ai01.prefab");
|
||||||
|
ToonMatch("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
|
||||||
|
SamplePath();
|
||||||
|
Srp();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 게임 맵의 방향광 실측
|
||||||
|
static void Lights()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== 방향광 (Res_Addr/Map/WL_Nature.prefab + Ingame 씬) ==");
|
||||||
|
var map = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Map/WL_Nature.prefab");
|
||||||
|
if (map != null)
|
||||||
|
{
|
||||||
|
foreach (var lt in map.GetComponentsInChildren<Light>(true))
|
||||||
|
L(string.Format(" [map] {0} type={1} color={2} intensity={3} shadows={4} euler={5}",
|
||||||
|
lt.name, lt.type, lt.color, lt.intensity, lt.shadows, lt.transform.eulerAngles.ToString("F2")));
|
||||||
|
}
|
||||||
|
else L(" WL_Nature.prefab 없음");
|
||||||
|
|
||||||
|
foreach (var sc in new[] { "Assets/Scenes/Ingame.unity", "Assets/Scenes/Title.unity" })
|
||||||
|
{
|
||||||
|
if (!File.Exists(sc)) { L(" " + sc + " 없음"); continue; }
|
||||||
|
var txt = File.ReadAllText(sc);
|
||||||
|
int i = txt.IndexOf("m_Type: 1");
|
||||||
|
L(" " + sc + " · Light(dir) 블록 " + (i >= 0 ? "있음" : "없음") + " · 파일 " + new FileInfo(sc).Length + " B");
|
||||||
|
}
|
||||||
|
L(" RenderSettings.ambientMode(현재 씬) = " + RenderSettings.ambientMode + " · ambientLight = " + RenderSettings.ambientLight
|
||||||
|
+ " · ambientIntensity = " + RenderSettings.ambientIntensity + " · sun = " + (RenderSettings.sun == null ? "null" : RenderSettings.sun.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 머티리얼 → *_Toon.mat 존재 여부
|
||||||
|
static void ToonMatch(string prefab)
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== Toon 매칭 " + prefab + " ==");
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(prefab);
|
||||||
|
if (go == null) { L(" 로드 실패"); return; }
|
||||||
|
var seen = new HashSet<string>();
|
||||||
|
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
|
||||||
|
foreach (var m in r.sharedMaterials)
|
||||||
|
{
|
||||||
|
if (m == null || !seen.Add(m.name)) continue;
|
||||||
|
string toon = "Assets/WL/Look/Env/Materials/" + m.name + "_Toon.mat";
|
||||||
|
var t = AssetDatabase.LoadAssetAtPath<Material>(toon);
|
||||||
|
L(" " + m.name.PadRight(16) + " [" + (m.shader == null ? "-" : m.shader.name) + "] → " + (t != null ? "Toon 있음" : "Toon 없음"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 샘플링 경로 확인 — Generic/Humanoid 둘 다 AnimationMode 로 찍히는가
|
||||||
|
static void SamplePath()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== 샘플링 경로 (AnimationMode.SampleAnimationClip) ==");
|
||||||
|
foreach (var pair in new[] {
|
||||||
|
new[]{ "Assets/Res_Addr/PC/Ai01.prefab", "Idle" },
|
||||||
|
new[]{ "Assets/Res_Addr/Mobs/Mob/Batty_A.prefab", "Attack" } })
|
||||||
|
{
|
||||||
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(pair[0]);
|
||||||
|
if (go == null) { L(" " + pair[0] + " 로드 실패"); continue; }
|
||||||
|
var inst = (GameObject)PrefabUtility.InstantiatePrefab(go);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var anim = inst.GetComponentInChildren<Animator>(true);
|
||||||
|
AnimationClip clip = null;
|
||||||
|
foreach (var c in anim.runtimeAnimatorController.animationClips) if (c.name == pair[1]) { clip = c; break; }
|
||||||
|
if (clip == null) { L(" 클립 " + pair[1] + " 없음"); continue; }
|
||||||
|
|
||||||
|
// 대표 본의 t=0 / t=len/2 포즈 차이를 잰다
|
||||||
|
var bones = inst.GetComponentsInChildren<Transform>(true);
|
||||||
|
Transform probe = bones.Length > 3 ? bones[bones.Length / 2] : inst.transform;
|
||||||
|
AnimationMode.StartAnimationMode();
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, clip, 0f);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
Vector3 p0 = probe.localPosition; Quaternion r0 = probe.localRotation;
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, clip, clip.length * 0.5f);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
Vector3 p1 = probe.localPosition; Quaternion r1 = probe.localRotation;
|
||||||
|
AnimationMode.StopAnimationMode();
|
||||||
|
L(string.Format(" {0} · clip {1} · probe {2} · Δpos {3:0.0000} · Δrot {4:0.00}° → {5}",
|
||||||
|
Path.GetFileNameWithoutExtension(pair[0]), clip.name, probe.name,
|
||||||
|
Vector3.Distance(p0, p1), Quaternion.Angle(r0, r1),
|
||||||
|
(Vector3.Distance(p0, p1) > 0.0001f || Quaternion.Angle(r0, r1) > 0.05f) ? "샘플링 OK" : "변화 없음(확인필요)"));
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L(" EXCEPTION " + ex.Message); if (AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode(); }
|
||||||
|
finally { UnityEngine.Object.DestroyImmediate(inst); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Srp()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== 렌더 파이프라인 ==");
|
||||||
|
var rp = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline;
|
||||||
|
L(" defaultRenderPipeline = " + (rp == null ? "null(빌트인)" : rp.name + " (" + AssetDatabase.GetAssetPath(rp) + ")"));
|
||||||
|
L(" currentRenderPipeline = " + (UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline == null ? "null" : UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline.name));
|
||||||
|
L(" QualitySettings.renderPipeline = " + (QualitySettings.renderPipeline == null ? "null" : QualitySettings.renderPipeline.name));
|
||||||
|
L(" SystemInfo.graphicsDeviceType = " + SystemInfo.graphicsDeviceType + " · supportsComputeShaders = " + SystemInfo.supportsComputeShaders);
|
||||||
|
L(" Application.isBatchMode = " + Application.isBatchMode);
|
||||||
|
// Unlit 투명 셰이더 후보
|
||||||
|
foreach (var s in new[] { "Sprites/Default", "Unlit/Transparent", "Universal Render Pipeline/Unlit", "Universal Render Pipeline/2D/Sprite-Unlit-Default" })
|
||||||
|
L(" Shader.Find(\"" + s + "\") = " + (Shader.Find(s) != null ? "OK" : "없음"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// WL-814k 실측 조사 ③ — Humanoid/Generic 샘플링 경로 확정 (읽기만)
|
||||||
|
// run: unity command run_script --file AgentScripts/WL814k_Survey3.cs --entry WL814k_Survey3.Run
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public static class WL814k_Survey3
|
||||||
|
{
|
||||||
|
const string kTxt = "AgentScripts/WL814k_SURVEY3.txt";
|
||||||
|
static StringBuilder s_sb;
|
||||||
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814k] " + s); }
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
s_sb = new StringBuilder();
|
||||||
|
L("WL-814k 실측 ③ 샘플링 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Test("Assets/Res_Addr/PC/Ai01.prefab", new[] { "Idle", "Run", "Atk2_wing", "LightHit", "death_F3441" });
|
||||||
|
Test("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab", new[] { "Idle", "Move", "Attack", "Damage", "Die" });
|
||||||
|
}
|
||||||
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
||||||
|
finally { if (AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode(); }
|
||||||
|
File.WriteAllText(kTxt, s_sb.ToString(), new UTF8Encoding(false));
|
||||||
|
Debug.Log("[WL814k] → " + kTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Test(string prefab, string[] clipNames)
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("== " + prefab + " ==");
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefab);
|
||||||
|
var inst = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
inst.transform.position = Vector3.zero; inst.transform.rotation = Quaternion.identity;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var anim = inst.GetComponentInChildren<Animator>(true);
|
||||||
|
var rac = anim.runtimeAnimatorController;
|
||||||
|
foreach (var name in clipNames)
|
||||||
|
{
|
||||||
|
AnimationClip clip = null;
|
||||||
|
foreach (var c in rac.animationClips) if (c.name == name) { clip = c; break; }
|
||||||
|
if (clip == null) { L(" " + name + " → 클립 없음"); continue; }
|
||||||
|
|
||||||
|
// ⓐ AnimationMode 경로
|
||||||
|
float aSpread = Spread(inst, clip, true);
|
||||||
|
// ⓑ clip.SampleAnimation 경로
|
||||||
|
float bSpread = Spread(inst, clip, false);
|
||||||
|
L(string.Format(" {0,-14} len {1,5:0.00}s · AnimationMode Δbounds {2:0.0000} m · SampleAnimation Δbounds {3:0.0000} m",
|
||||||
|
name, clip.length, aSpread, bSpread));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { if (AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode(); UnityEngine.Object.DestroyImmediate(inst); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클립을 8등분해 찍고, 전체 렌더러 바운드 중심의 최대 이동량(= 실제로 포즈가 바뀌었는가)
|
||||||
|
static float Spread(GameObject inst, AnimationClip clip, bool useAnimationMode)
|
||||||
|
{
|
||||||
|
var rs = inst.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
Vector3 min = Vector3.positiveInfinity, max = Vector3.negativeInfinity;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
float t = clip.length * i / 8f;
|
||||||
|
if (useAnimationMode)
|
||||||
|
{
|
||||||
|
AnimationMode.StartAnimationMode();
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, clip, t);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
AnimationMode.StopAnimationMode();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clip.SampleAnimation(inst, t);
|
||||||
|
}
|
||||||
|
Bounds b = new Bounds(); bool any = false;
|
||||||
|
foreach (var r in rs) { if (!any) { b = r.bounds; any = true; } else b.Encapsulate(r.bounds); }
|
||||||
|
if (!any) continue;
|
||||||
|
min = Vector3.Min(min, b.min); max = Vector3.Max(max, b.max);
|
||||||
|
}
|
||||||
|
if (float.IsInfinity(min.x)) return -1f;
|
||||||
|
return (max - min).magnitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3b1e4280b824eb84c9918c582b2df1b1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f60da21b8811ca438a64ff9cf16bd2e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
|
|
@ -0,0 +1,181 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5e256fc5c31a4943b1afb9820681cec
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable:
|
||||||
|
- first:
|
||||||
|
213: 5980685433440855488
|
||||||
|
second: Ai01_0
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 0
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 1
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 0
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 1024
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: 4
|
||||||
|
textureCompression: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Server
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Android
|
||||||
|
maxTextureSize: 1024
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: 4
|
||||||
|
textureCompression: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 1
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: iOS
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites:
|
||||||
|
- serializedVersion: 2
|
||||||
|
name: Ai01_0
|
||||||
|
rect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 2048
|
||||||
|
height: 1536
|
||||||
|
alignment: 0
|
||||||
|
pivot: {x: 0, y: 0}
|
||||||
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
customData:
|
||||||
|
outline: []
|
||||||
|
physicsShape: []
|
||||||
|
tessellationDetail: -1
|
||||||
|
bones: []
|
||||||
|
spriteID: 0c56895f6b9aff250800000000000000
|
||||||
|
internalID: 5980685433440855488
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 7485ff60a4b963544b28a1b345440fb9, type: 3}
|
||||||
|
m_Name: Ai01_SpriteSet
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.SpriteBake.WLSpriteSet
|
||||||
|
sourcePrefab: Assets/Res_Addr/PC/Ai01.prefab
|
||||||
|
actorNamePrefix: Ai01
|
||||||
|
bakeStamp: "2026-09-12 18:38 \xB7 WL-814k"
|
||||||
|
sheet: {fileID: 2800000, guid: d5e256fc5c31a4943b1afb9820681cec, type: 3}
|
||||||
|
frameWidth: 56
|
||||||
|
frameHeight: 56
|
||||||
|
columns: 16
|
||||||
|
rows: 12
|
||||||
|
dirCount: 8
|
||||||
|
worldPerPixel: 0.028333334
|
||||||
|
bakeScale: 0.7
|
||||||
|
footPixelX: 28.5
|
||||||
|
footPixelY: 19.5
|
||||||
|
pitchDeg: 43.60939
|
||||||
|
actions:
|
||||||
|
- key: idle
|
||||||
|
stateNames:
|
||||||
|
- idle
|
||||||
|
- land
|
||||||
|
- getup
|
||||||
|
clipName: Idle
|
||||||
|
startIndex: 0
|
||||||
|
frameCount: 4
|
||||||
|
fps: 2
|
||||||
|
loop: 1
|
||||||
|
clipLength: 2
|
||||||
|
- key: run
|
||||||
|
stateNames:
|
||||||
|
- run
|
||||||
|
- dodge
|
||||||
|
clipName: Run
|
||||||
|
startIndex: 32
|
||||||
|
frameCount: 6
|
||||||
|
fps: 11.249999
|
||||||
|
loop: 1
|
||||||
|
clipLength: 0.53333336
|
||||||
|
- key: attack
|
||||||
|
stateNames:
|
||||||
|
- attack
|
||||||
|
- attack2
|
||||||
|
- skill2
|
||||||
|
- skill3
|
||||||
|
- skill4
|
||||||
|
- skill5
|
||||||
|
- casting
|
||||||
|
- casting_end
|
||||||
|
clipName: Atk2_wing
|
||||||
|
startIndex: 80
|
||||||
|
frameCount: 6
|
||||||
|
fps: 4
|
||||||
|
loop: 1
|
||||||
|
clipLength: 1.5
|
||||||
|
- key: hit
|
||||||
|
stateNames:
|
||||||
|
- hit
|
||||||
|
- stun
|
||||||
|
clipName: LightHit
|
||||||
|
startIndex: 128
|
||||||
|
frameCount: 2
|
||||||
|
fps: 2
|
||||||
|
loop: 1
|
||||||
|
clipLength: 1
|
||||||
|
- key: die
|
||||||
|
stateNames:
|
||||||
|
- die
|
||||||
|
- lying
|
||||||
|
clipName: death_F3441
|
||||||
|
startIndex: 144
|
||||||
|
frameCount: 6
|
||||||
|
fps: 2.8571427
|
||||||
|
loop: 0
|
||||||
|
clipLength: 2.1000001
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9afd69d5485f7584b8f40e7673bca04e
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -0,0 +1,181 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1437ff6eefe10ba4cb73442f8f923071
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable:
|
||||||
|
- first:
|
||||||
|
213: -3965868031581062660
|
||||||
|
second: Batty_A_0
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 0
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 1
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 0
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 1024
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: 4
|
||||||
|
textureCompression: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Server
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Android
|
||||||
|
maxTextureSize: 1024
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: 4
|
||||||
|
textureCompression: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 1
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: iOS
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites:
|
||||||
|
- serializedVersion: 2
|
||||||
|
name: Batty_A_0
|
||||||
|
rect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 768
|
||||||
|
height: 576
|
||||||
|
alignment: 0
|
||||||
|
pivot: {x: 0, y: 0}
|
||||||
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
customData:
|
||||||
|
outline: []
|
||||||
|
physicsShape: []
|
||||||
|
tessellationDetail: -1
|
||||||
|
bones: []
|
||||||
|
spriteID: cf9788f890866f8c0800000000000000
|
||||||
|
internalID: -3965868031581062660
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 7485ff60a4b963544b28a1b345440fb9, type: 3}
|
||||||
|
m_Name: Batty_A_SpriteSet
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.SpriteBake.WLSpriteSet
|
||||||
|
sourcePrefab: Assets/Res_Addr/Mobs/Mob/Batty_A.prefab
|
||||||
|
actorNamePrefix: Batty_A
|
||||||
|
bakeStamp: "2026-09-12 18:38 \xB7 WL-814k"
|
||||||
|
sheet: {fileID: 2800000, guid: 1437ff6eefe10ba4cb73442f8f923071, type: 3}
|
||||||
|
frameWidth: 48
|
||||||
|
frameHeight: 48
|
||||||
|
columns: 16
|
||||||
|
rows: 12
|
||||||
|
dirCount: 8
|
||||||
|
worldPerPixel: 0.028333334
|
||||||
|
bakeScale: 1.5
|
||||||
|
footPixelX: 24.5
|
||||||
|
footPixelY: 14.5
|
||||||
|
pitchDeg: 43.60939
|
||||||
|
actions:
|
||||||
|
- key: idle
|
||||||
|
stateNames:
|
||||||
|
- idle
|
||||||
|
clipName: Idle
|
||||||
|
startIndex: 0
|
||||||
|
frameCount: 4
|
||||||
|
fps: 4.173913
|
||||||
|
loop: 1
|
||||||
|
clipLength: 0.9583333
|
||||||
|
- key: run
|
||||||
|
stateNames:
|
||||||
|
- run
|
||||||
|
clipName: Move
|
||||||
|
startIndex: 32
|
||||||
|
frameCount: 6
|
||||||
|
fps: 6.2608695
|
||||||
|
loop: 1
|
||||||
|
clipLength: 0.9583334
|
||||||
|
- key: attack
|
||||||
|
stateNames:
|
||||||
|
- attack
|
||||||
|
- casting
|
||||||
|
- casting_end
|
||||||
|
clipName: Attack
|
||||||
|
startIndex: 80
|
||||||
|
frameCount: 6
|
||||||
|
fps: 10.285716
|
||||||
|
loop: 1
|
||||||
|
clipLength: 0.58333325
|
||||||
|
- key: hit
|
||||||
|
stateNames:
|
||||||
|
- hit
|
||||||
|
- stun
|
||||||
|
clipName: Damage
|
||||||
|
startIndex: 128
|
||||||
|
frameCount: 2
|
||||||
|
fps: 3.4285705
|
||||||
|
loop: 1
|
||||||
|
clipLength: 0.5833335
|
||||||
|
- key: die
|
||||||
|
stateNames:
|
||||||
|
- die
|
||||||
|
clipName: Die
|
||||||
|
startIndex: 144
|
||||||
|
frameCount: 6
|
||||||
|
fps: 6
|
||||||
|
loop: 0
|
||||||
|
clipLength: 1
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 02557ce4e57225d4db041caa2e6e44d7
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 518e04114c590e847b8493855c603c59
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,696 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLSpriteBaker.cs — 3D 캐릭터 프리팹 → 「도트 스프라이트 시트」 굽기 (에디터 전용 · WL-814k · #814)
|
||||||
|
//
|
||||||
|
// PD 지시(2026-09-12) 「치비 느낌이 나지 않아도 도트 그림으로 구워 쓰는 방식으로 해봐.」
|
||||||
|
//
|
||||||
|
// ■ 🔴 이 툴은 **프리팹 무관**하다 — 대상 프리팹은 인자로 받는다. 캐릭터 이름이 코드에 0개.
|
||||||
|
// (PD 가 캐릭터 에셋 교체를 검토 중 → 특정 프리팹에 하드코딩하면 툴이 통째로 버려진다.)
|
||||||
|
//
|
||||||
|
// ■ 절차
|
||||||
|
// ① 임시 씬(Empty · Single)에 프리팹을 인스턴스화하고 **프리팹 링크를 끊는다**(Unpack).
|
||||||
|
// → 이후 무슨 짓을 해도 원본 프리팹 에셋에 한 글자도 안 쓴다. 씬 저장 0.
|
||||||
|
// ② 814c Toon 머티리얼(`<이름>_Toon.mat`)이 있으면 **인스턴스에만** 갈아끼운다(원본 .mat 무변경).
|
||||||
|
// ③ 인게임과 같은 **내림각**(811o 궤도 수식 · 실측 43.609°)의 **직교 카메라**를 만들고,
|
||||||
|
// 픽셀 1개의 월드 크기를 도트 프리셋과 같게 맞춘다(= 스프라이트 1px 이 화면 1px).
|
||||||
|
// ④ `AnimationMode.SampleAnimationClip` 로 포즈를 고정(Generic·Humanoid 둘 다 실측 OK) →
|
||||||
|
// 방향(피벗 Y 회전) × 동작 × 프레임마다 **2번** 렌더한다(배경 검정 / 배경 흰색).
|
||||||
|
// 두 장의 차이로 **알파를 정확히 복원**한다 — URP 가 불투명 패스에서 알파를 어떻게 쓰든 안 흔들린다.
|
||||||
|
// ⑤ 도트화를 **굽는 시점에 강제**: 알파 이진화(0.5) → 팔레트 양자화(채널당 6단계) → 1px 검정 외곽선.
|
||||||
|
// ⑥ 전 프레임의 실루엣을 합쳐 **꽉 차는 프레임 크기**를 8의 배수로 자동 산출하고, 그 크기로 잘라
|
||||||
|
// 시트 1장(PNG)에 격자로 붙인다 + 메타 SO(WLSpriteSet)를 만든다.
|
||||||
|
//
|
||||||
|
// ■ 왜 서브 스프라이트(spriteImportMode Multiple)가 아니라 「단일 시트 + UV 산술」인가
|
||||||
|
// 런타임 빌보드는 `Sprite` 객체가 아니라 **쿼드 메시 + MaterialPropertyBlock 의 `_BaseMap_ST`** 로 그린다.
|
||||||
|
// Multiple 로 자르면 프레임 수만큼(시범 2종 = 384개) 서브 에셋과 .meta 줄이 생기고 런타임 이득은 0이다.
|
||||||
|
// 시트 1장 + `UvRect()` 산술이면 서브 에셋 0 · 프레임 전환 시 할당 0.
|
||||||
|
//
|
||||||
|
// 🔴 금지 준수: 원본 프리팹/머티리얼/애니메이션 **쓰기 0**. `AssetDatabase.SaveAssets()` 는
|
||||||
|
// 이 툴이 만든 에셋에만 쓰고, 임포트 설정은 대상 PNG 하나에만 건다.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake.EditorTools
|
||||||
|
{
|
||||||
|
public static class WLSpriteBaker
|
||||||
|
{
|
||||||
|
public sealed class Result
|
||||||
|
{
|
||||||
|
public bool ok;
|
||||||
|
public string prefabPath = "";
|
||||||
|
public string pngPath = "";
|
||||||
|
public string setPath = "";
|
||||||
|
public int frameSize, columns, rows, sheetW, sheetH;
|
||||||
|
public int dirCount, totalFrames;
|
||||||
|
public long pngBytes;
|
||||||
|
public double seconds;
|
||||||
|
public float worldPerPixel, pitchDeg, bakeScale;
|
||||||
|
public float charPixelHeight;
|
||||||
|
public bool clipped;
|
||||||
|
public readonly List<string> notes = new List<string>();
|
||||||
|
public readonly StringBuilder log = new StringBuilder();
|
||||||
|
public string Summary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format("{0} · {1}×{1} px · {2}방향 × {3}프레임 · 시트 {4}×{5} · {6:N0} KB · {7:0.0}초",
|
||||||
|
Path.GetFileNameWithoutExtension(prefabPath), frameSize, dirCount, totalFrames / Mathf.Max(1, dirCount),
|
||||||
|
sheetW, sheetH, pngBytes / 1024.0, seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 진입점 — 🔴 대상 프리팹은 **인자**
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public static Result Bake(string prefabPath, WLSpriteBakeSettings cfg,
|
||||||
|
string outputDirOverride = null, string actorNamePrefixOverride = null,
|
||||||
|
float rootScale = 0f)
|
||||||
|
{
|
||||||
|
var r = new Result { prefabPath = prefabPath };
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
if (cfg == null) { r.log.AppendLine("FAIL — WLSpriteBakeSettings 없음"); return r; }
|
||||||
|
|
||||||
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
|
if (src == null) { r.log.AppendLine("FAIL — 프리팹 로드 실패: " + prefabPath); return r; }
|
||||||
|
|
||||||
|
string outDir = string.IsNullOrEmpty(outputDirOverride) ? cfg.outputDir : outputDirOverride;
|
||||||
|
string baseName = Path.GetFileNameWithoutExtension(prefabPath);
|
||||||
|
string prefix = string.IsNullOrEmpty(actorNamePrefixOverride) ? baseName : actorNamePrefixOverride;
|
||||||
|
|
||||||
|
if (!Application.isBatchMode && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
|
||||||
|
{ r.log.AppendLine("취소 — 열린 씬 저장 거부"); return r; }
|
||||||
|
|
||||||
|
Scene temp = default(Scene);
|
||||||
|
GameObject pivot = null, inst = null, camGo = null;
|
||||||
|
RenderTexture rt = null;
|
||||||
|
Texture2D readTex = null;
|
||||||
|
bool animMode = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
temp = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
SetupWorld(cfg);
|
||||||
|
|
||||||
|
// ── ① 인스턴스 (프리팹 링크 완전 절단 → 원본 에셋 쓰기 0) ────
|
||||||
|
pivot = new GameObject("[WL814k] Pivot");
|
||||||
|
inst = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
||||||
|
PrefabUtility.UnpackPrefabInstance(inst, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||||
|
inst.transform.SetParent(pivot.transform, false);
|
||||||
|
inst.transform.localPosition = Vector3.zero;
|
||||||
|
inst.transform.localRotation = Quaternion.identity;
|
||||||
|
// 🔴 인게임에서 실제로 쓰는 크기로 굽는다(인자) — 0 이면 프리팹 자체 스케일.
|
||||||
|
// 런타임 스케일과 같게 구워야 스프라이트 1px = 화면 1px 이 유지된다.
|
||||||
|
if (rootScale > 0.0001f) inst.transform.localScale = Vector3.one * rootScale;
|
||||||
|
StripNonVisual(inst);
|
||||||
|
r.bakeScale = Mathf.Abs(inst.transform.lossyScale.y);
|
||||||
|
if (r.bakeScale < 0.0001f) r.bakeScale = 1f;
|
||||||
|
|
||||||
|
int toonHit = cfg.useToonMaterials ? SwapToonMaterials(inst, cfg, r) : 0;
|
||||||
|
r.log.AppendLine("Toon 머티리얼 교체 = " + toonHit + "슬롯 (인스턴스에만 · 원본 .mat 무변경)");
|
||||||
|
|
||||||
|
Vector3 srcLocalPos = inst.transform.localPosition;
|
||||||
|
Quaternion srcLocalRot = inst.transform.localRotation;
|
||||||
|
|
||||||
|
// ── ② 동작 수집 (이름 규약 · 프리팹 무관) ────────────────────
|
||||||
|
var anim = inst.GetComponentInChildren<Animator>(true);
|
||||||
|
if (anim == null || anim.runtimeAnimatorController == null)
|
||||||
|
{ r.log.AppendLine("FAIL — Animator/Controller 없음"); return r; }
|
||||||
|
var plan = CollectActions(anim, cfg, r);
|
||||||
|
if (plan.Count == 0) { r.log.AppendLine("FAIL — 구울 동작을 하나도 못 찾았다"); return r; }
|
||||||
|
|
||||||
|
// 🔴 샘플링 경로 — 기본은 **진짜 Animator 구동**(anim.Play + anim.Update).
|
||||||
|
// 실측(814k): AnimationMode.SampleAnimationClip 은 **루트 모션을 포즈에 얹는다**
|
||||||
|
// (Ai01 Run 이 2.0 m, death 가 3.0 m 밀려 프레임 밖으로 나갔다) — 그런데 게임의
|
||||||
|
// Animator 는 applyRootMotion=false 라 그 이동을 버린다. 즉 AnimationMode 로 구우면
|
||||||
|
// **게임에 안 나오는 그림**이 구워진다. Animator 구동은 컨트롤러·리타깃까지 게임과 같다.
|
||||||
|
anim.applyRootMotion = false;
|
||||||
|
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||||
|
anim.Rebind();
|
||||||
|
|
||||||
|
// ── ③ 카메라 ────────────────────────────────────────────────
|
||||||
|
float pitch = cfg.PitchDeg;
|
||||||
|
float wpp = cfg.WorldPerPixel;
|
||||||
|
r.pitchDeg = pitch; r.worldPerPixel = wpp;
|
||||||
|
int P = Mathf.Max(32, cfg.probeSize);
|
||||||
|
camGo = new GameObject("[WL814k] BakeCam");
|
||||||
|
var cam = camGo.AddComponent<Camera>();
|
||||||
|
cam.orthographic = true;
|
||||||
|
cam.orthographicSize = P * wpp * 0.5f;
|
||||||
|
cam.nearClipPlane = 0.01f;
|
||||||
|
cam.farClipPlane = 200f;
|
||||||
|
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||||
|
cam.allowHDR = false;
|
||||||
|
cam.allowMSAA = false;
|
||||||
|
cam.useOcclusionCulling = false;
|
||||||
|
camGo.transform.rotation = Quaternion.Euler(pitch, 0f, 0f);
|
||||||
|
// 월드 원점이 (P/2, P/2) 픽셀의 **중심**에 찍히도록 반 픽셀 민다.
|
||||||
|
Vector3 aim = Vector3.zero
|
||||||
|
- camGo.transform.right * (wpp * 0.5f)
|
||||||
|
- camGo.transform.up * (wpp * 0.5f);
|
||||||
|
camGo.transform.position = aim - camGo.transform.forward * 50f;
|
||||||
|
|
||||||
|
rt = new RenderTexture(P, P, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||||
|
rt.filterMode = FilterMode.Point;
|
||||||
|
rt.antiAliasing = 1;
|
||||||
|
rt.Create();
|
||||||
|
readTex = new Texture2D(P, P, TextureFormat.RGBA32, false, false);
|
||||||
|
|
||||||
|
// ── ④ 전 프레임 렌더 (프로브 해상도) ─────────────────────────
|
||||||
|
int dirN = Mathf.Max(1, cfg.dirCount);
|
||||||
|
r.dirCount = dirN;
|
||||||
|
float step = 360f / dirN;
|
||||||
|
var frames = new List<Color32[]>(256);
|
||||||
|
int minX = int.MaxValue, maxX = int.MinValue, minY = int.MaxValue, maxY = int.MinValue;
|
||||||
|
|
||||||
|
if (cfg.sampleWithAnimationMode) { AnimationMode.StartAnimationMode(); animMode = true; }
|
||||||
|
var black = new Color32[P * P];
|
||||||
|
var white = new Color32[P * P];
|
||||||
|
|
||||||
|
for (int ai = 0; ai < plan.Count; ai++)
|
||||||
|
{
|
||||||
|
var pa = plan[ai];
|
||||||
|
for (int d = 0; d < dirN; d++)
|
||||||
|
{
|
||||||
|
pivot.transform.rotation = Quaternion.Euler(0f, d * step, 0f);
|
||||||
|
for (int f = 0; f < pa.frames; f++)
|
||||||
|
{
|
||||||
|
float nt = pa.loop
|
||||||
|
? (float)f / pa.frames
|
||||||
|
: (pa.frames > 1 ? (float)f / (pa.frames - 1) : 0f);
|
||||||
|
|
||||||
|
if (animMode)
|
||||||
|
{
|
||||||
|
AnimationMode.BeginSampling();
|
||||||
|
AnimationMode.SampleAnimationClip(inst, pa.clip, pa.clip.length * nt);
|
||||||
|
AnimationMode.EndSampling();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
anim.Play(pa.stateHash, 0, nt);
|
||||||
|
anim.Update(0f);
|
||||||
|
}
|
||||||
|
// 제자리로(빌보드는 액터 위치에 붙으므로 스프라이트는 제자리 애니메이션이어야 한다)
|
||||||
|
inst.transform.localPosition = srcLocalPos;
|
||||||
|
inst.transform.localRotation = srcLocalRot;
|
||||||
|
|
||||||
|
RenderInto(cam, rt, readTex, Color.black, black);
|
||||||
|
RenderInto(cam, rt, readTex, Color.white, white);
|
||||||
|
|
||||||
|
var px = Compose(black, white, P, cfg);
|
||||||
|
frames.Add(px);
|
||||||
|
TightBox(px, P, ref minX, ref maxX, ref minY, ref maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (animMode) { AnimationMode.StopAnimationMode(); animMode = false; }
|
||||||
|
|
||||||
|
if (minX > maxX) { r.log.AppendLine("FAIL — 렌더 결과가 전부 비었다(알파 0)"); return r; }
|
||||||
|
|
||||||
|
// ── ⑤ 프레임 크기 · 발밑 픽셀 산출 ──────────────────────────
|
||||||
|
int origin = P / 2; // 월드 원점이 찍힌 픽셀 인덱스
|
||||||
|
int margin = Mathf.Max(0, cfg.marginPx);
|
||||||
|
int needW = 2 * Mathf.Max(origin - minX, maxX - origin) + 2 * margin;
|
||||||
|
int needH = (maxY - minY + 1) + 2 * margin;
|
||||||
|
int need = Mathf.Max(needW, needH);
|
||||||
|
int S = cfg.autoFrameSize
|
||||||
|
? Mathf.Clamp(RoundUp8(need), Mathf.Max(8, cfg.frameSizeMin), Mathf.Max(8, cfg.frameSizeMax))
|
||||||
|
: Mathf.Max(8, cfg.frameSize);
|
||||||
|
|
||||||
|
int below = (origin - minY) + margin;
|
||||||
|
int above = (maxY - origin) + margin + 1;
|
||||||
|
int fpxI = S / 2;
|
||||||
|
int fpyI;
|
||||||
|
if (below + above <= S) fpyI = below;
|
||||||
|
else { fpyI = Mathf.RoundToInt(S * (float)below / (below + above)); r.clipped = true; r.notes.Add("프레임 상한 초과 — 실루엣이 잘렸다(needed " + need + " px)"); }
|
||||||
|
fpyI = Mathf.Clamp(fpyI, 0, S);
|
||||||
|
|
||||||
|
r.frameSize = S;
|
||||||
|
r.charPixelHeight = (maxY - minY + 1);
|
||||||
|
r.log.AppendLine(string.Format("실루엣 실측(프로브 {0}px) — x [{1}..{2}] y [{3}..{4}] · 캐릭터 높이 {5} px · 폭 {6} px → 프레임 {7}×{7} · 발밑 픽셀 ({8}.5, {9}.5)",
|
||||||
|
P, minX, maxX, minY, maxY, maxY - minY + 1, maxX - minX + 1, S, fpxI, fpyI));
|
||||||
|
|
||||||
|
// ── ⑥ 자르기 + 외곽선 → 시트 ────────────────────────────────
|
||||||
|
int total = frames.Count;
|
||||||
|
r.totalFrames = total;
|
||||||
|
int cols = cfg.sheetColumns > 0 ? cfg.sheetColumns : Mathf.CeilToInt(Mathf.Sqrt(total));
|
||||||
|
while (cols > 1 && cols * S > Mathf.Max(S, cfg.sheetMaxWidth)) cols--;
|
||||||
|
int rows = Mathf.CeilToInt(total / (float)cols);
|
||||||
|
r.columns = cols; r.rows = rows;
|
||||||
|
r.sheetW = cols * S; r.sheetH = rows * S;
|
||||||
|
|
||||||
|
var sheet = new Color32[r.sheetW * r.sheetH]; // 기본 = 완전 투명(0,0,0,0)
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
var cropped = Crop(frames[i], P, S, origin - fpxI, origin - fpyI);
|
||||||
|
if (cfg.outline) Outline(cropped, S, cfg);
|
||||||
|
int col = i % cols, row = i / cols;
|
||||||
|
Blit(cropped, S, sheet, r.sheetW, col * S, r.sheetH - (row + 1) * S);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ⑦ PNG + 임포트 설정 ────────────────────────────────────
|
||||||
|
if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir);
|
||||||
|
string png = (outDir.TrimEnd('/') + "/" + baseName + ".png").Replace('\\', '/');
|
||||||
|
var tex = new Texture2D(r.sheetW, r.sheetH, TextureFormat.RGBA32, false, false);
|
||||||
|
tex.SetPixels32(sheet); tex.Apply(false, false);
|
||||||
|
File.WriteAllBytes(png, tex.EncodeToPNG());
|
||||||
|
UnityEngine.Object.DestroyImmediate(tex);
|
||||||
|
AssetDatabase.ImportAsset(png, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
||||||
|
ApplyImportSettings(png, Mathf.Max(r.sheetW, r.sheetH));
|
||||||
|
r.pngPath = png;
|
||||||
|
r.pngBytes = new FileInfo(png).Length;
|
||||||
|
|
||||||
|
// ── ⑧ 메타 SO ─────────────────────────────────────────────
|
||||||
|
string setPath = (outDir.TrimEnd('/') + "/" + baseName + "_SpriteSet.asset").Replace('\\', '/');
|
||||||
|
var set = AssetDatabase.LoadAssetAtPath<WLSpriteSet>(setPath);
|
||||||
|
bool created = set == null;
|
||||||
|
if (created) set = ScriptableObject.CreateInstance<WLSpriteSet>();
|
||||||
|
set.sourcePrefab = prefabPath;
|
||||||
|
set.actorNamePrefix = prefix;
|
||||||
|
set.bakeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm") + " · WL-814k";
|
||||||
|
set.sheet = AssetDatabase.LoadAssetAtPath<Texture2D>(png);
|
||||||
|
set.frameWidth = S; set.frameHeight = S;
|
||||||
|
set.columns = cols; set.rows = rows;
|
||||||
|
set.dirCount = dirN;
|
||||||
|
set.worldPerPixel = wpp;
|
||||||
|
set.bakeScale = r.bakeScale;
|
||||||
|
set.footPixelX = fpxI + 0.5f;
|
||||||
|
set.footPixelY = fpyI + 0.5f;
|
||||||
|
set.pitchDeg = pitch;
|
||||||
|
|
||||||
|
var acts = new WLSpriteAction[plan.Count];
|
||||||
|
int start = 0;
|
||||||
|
for (int i = 0; i < plan.Count; i++)
|
||||||
|
{
|
||||||
|
var pa = plan[i];
|
||||||
|
acts[i] = new WLSpriteAction
|
||||||
|
{
|
||||||
|
key = pa.key,
|
||||||
|
stateNames = pa.stateNames.ToArray(),
|
||||||
|
clipName = pa.clip.name,
|
||||||
|
startIndex = start,
|
||||||
|
frameCount = pa.frames,
|
||||||
|
fps = pa.frames / Mathf.Max(0.0001f, pa.clip.length),
|
||||||
|
loop = pa.loop,
|
||||||
|
clipLength = pa.clip.length,
|
||||||
|
};
|
||||||
|
start += pa.frames * dirN;
|
||||||
|
}
|
||||||
|
set.actions = acts;
|
||||||
|
|
||||||
|
if (created) AssetDatabase.CreateAsset(set, setPath);
|
||||||
|
else EditorUtility.SetDirty(set);
|
||||||
|
AssetDatabase.SaveAssetIfDirty(set);
|
||||||
|
r.setPath = setPath;
|
||||||
|
r.ok = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
r.log.AppendLine("EXCEPTION " + ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (animMode && AnimationMode.InAnimationMode()) AnimationMode.StopAnimationMode();
|
||||||
|
if (rt != null) { RenderTexture.active = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
|
||||||
|
if (readTex != null) UnityEngine.Object.DestroyImmediate(readTex);
|
||||||
|
if (camGo != null) UnityEngine.Object.DestroyImmediate(camGo);
|
||||||
|
if (pivot != null) UnityEngine.Object.DestroyImmediate(pivot);
|
||||||
|
// 🔴 임시 씬은 저장하지 않는다. 빈 씬으로 갈아 끼워 흔적 0.
|
||||||
|
if (temp.IsValid()) EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
r.seconds = sw.Elapsed.TotalSeconds;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 월드(조명 · 환경광)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
static void SetupWorld(WLSpriteBakeSettings cfg)
|
||||||
|
{
|
||||||
|
RenderSettings.fog = false;
|
||||||
|
if (cfg.useSkyboxAmbient)
|
||||||
|
{
|
||||||
|
// 🔴 인게임 `Assets/Scenes/Ingame.unity` 실측 = m_AmbientMode 0(Skybox) + 기본 스카이박스.
|
||||||
|
// Flat 0.212 회색으로 구우면 게임보다 어둡게 구워진다(실측으로 확인).
|
||||||
|
RenderSettings.skybox = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Skybox.mat");
|
||||||
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Skybox;
|
||||||
|
RenderSettings.ambientIntensity = cfg.ambientIntensity;
|
||||||
|
RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Skybox;
|
||||||
|
DynamicGI.UpdateEnvironment();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
|
||||||
|
RenderSettings.ambientLight = cfg.ambient;
|
||||||
|
RenderSettings.skybox = null;
|
||||||
|
RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Custom;
|
||||||
|
RenderSettings.customReflectionTexture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sun = new GameObject("[WL814k] Sun").AddComponent<Light>();
|
||||||
|
sun.type = LightType.Directional;
|
||||||
|
sun.color = cfg.sunColor;
|
||||||
|
sun.intensity = cfg.sunIntensity;
|
||||||
|
sun.shadows = LightShadows.None; // 🔴 그림자 off (시트에 그림자가 구워지면 안 된다)
|
||||||
|
sun.transform.rotation = Quaternion.Euler(cfg.sunEuler);
|
||||||
|
|
||||||
|
if (cfg.fillIntensity > 0.0001f)
|
||||||
|
{
|
||||||
|
var fill = new GameObject("[WL814k] Fill").AddComponent<Light>();
|
||||||
|
fill.type = LightType.Directional;
|
||||||
|
fill.color = cfg.fillColor;
|
||||||
|
fill.intensity = cfg.fillIntensity;
|
||||||
|
fill.shadows = LightShadows.None;
|
||||||
|
fill.transform.rotation = Quaternion.Euler(cfg.fillEuler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>렌더에 필요 없는 것(콜라이더·에이전트·리지드바디·파티클·게임 스크립트)을 인스턴스에서 뗀다.</summary>
|
||||||
|
static void StripNonVisual(GameObject inst)
|
||||||
|
{
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<UnityEngine.AI.NavMeshAgent>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<UnityEngine.AI.NavMeshObstacle>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||||
|
foreach (var c in inst.GetComponentsInChildren<ParticleSystem>(true)) UnityEngine.Object.DestroyImmediate(c.gameObject);
|
||||||
|
foreach (var mb in inst.GetComponentsInChildren<MonoBehaviour>(true))
|
||||||
|
if (mb != null) UnityEngine.Object.DestroyImmediate(mb);
|
||||||
|
foreach (var sm in inst.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||||
|
{
|
||||||
|
if (sm == null) continue;
|
||||||
|
sm.updateWhenOffscreen = true; // 에디트 모드에서 컬링으로 사라지지 않게
|
||||||
|
sm.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
sm.receiveShadows = false;
|
||||||
|
}
|
||||||
|
foreach (var mr in inst.GetComponentsInChildren<MeshRenderer>(true))
|
||||||
|
{
|
||||||
|
if (mr == null) continue;
|
||||||
|
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
mr.receiveShadows = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>814c Toon 머티리얼로 교체 — 🔴 인스턴스의 sharedMaterials 배열만 바꾼다(디스크 .mat 무변경).</summary>
|
||||||
|
static int SwapToonMaterials(GameObject inst, WLSpriteBakeSettings cfg, Result r)
|
||||||
|
{
|
||||||
|
int hit = 0;
|
||||||
|
var cache = new Dictionary<string, Material>();
|
||||||
|
foreach (var rend in inst.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++)
|
||||||
|
{
|
||||||
|
var m = mats[i];
|
||||||
|
if (m == null) continue;
|
||||||
|
Material toon;
|
||||||
|
if (!cache.TryGetValue(m.name, out toon))
|
||||||
|
{
|
||||||
|
string p = cfg.toonMaterialDir.TrimEnd('/') + "/" + m.name + cfg.toonMaterialSuffix + ".mat";
|
||||||
|
toon = AssetDatabase.LoadAssetAtPath<Material>(p);
|
||||||
|
cache[m.name] = toon;
|
||||||
|
}
|
||||||
|
if (toon != null) { mats[i] = toon; any = true; hit++; }
|
||||||
|
}
|
||||||
|
if (any) rend.sharedMaterials = mats;
|
||||||
|
}
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 동작 수집 — 이름 규약(SO)만 본다. 캐릭터 이름 0.
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
sealed class PlanAction
|
||||||
|
{
|
||||||
|
public string key;
|
||||||
|
public AnimationClip clip;
|
||||||
|
public string stateName;
|
||||||
|
public int stateHash;
|
||||||
|
public int frames;
|
||||||
|
public bool loop;
|
||||||
|
public readonly List<string> stateNames = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PlanAction> CollectActions(Animator anim, WLSpriteBakeSettings cfg, Result r)
|
||||||
|
{
|
||||||
|
var list = new List<PlanAction>();
|
||||||
|
var ac = anim.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
|
||||||
|
var byState = new Dictionary<string, AnimationClip>(StringComparer.Ordinal);
|
||||||
|
var realName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // 후보(대소문자 무시) → 실제 상태 이름
|
||||||
|
|
||||||
|
if (ac != null && ac.layers.Length > 0 && ac.layers[0].stateMachine != null)
|
||||||
|
{
|
||||||
|
foreach (var ch in ac.layers[0].stateMachine.states)
|
||||||
|
{
|
||||||
|
var clip = FirstClip(ch.state.motion);
|
||||||
|
if (clip == null) continue;
|
||||||
|
if (!byState.ContainsKey(ch.state.name)) byState[ch.state.name] = clip;
|
||||||
|
if (!realName.ContainsKey(ch.state.name)) realName[ch.state.name] = ch.state.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// AnimatorOverrideController 등 — 클립 이름으로만 맞춘다
|
||||||
|
foreach (var c in anim.runtimeAnimatorController.animationClips)
|
||||||
|
if (c != null && !byState.ContainsKey(c.name)) { byState[c.name] = c; realName[c.name] = c.name; }
|
||||||
|
r.notes.Add("AnimatorController 가 아니어서 상태 이름 대신 클립 이름으로 맞췄다");
|
||||||
|
}
|
||||||
|
r.log.AppendLine("레이어0 상태 " + byState.Count + "개: " + string.Join(", ", new List<string>(byState.Keys).ToArray()));
|
||||||
|
|
||||||
|
foreach (var ba in cfg.actions)
|
||||||
|
{
|
||||||
|
if (ba == null) continue;
|
||||||
|
AnimationClip clip = null; string matched = null;
|
||||||
|
foreach (var n in ba.stateNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(n)) continue;
|
||||||
|
string real;
|
||||||
|
if (!realName.TryGetValue(n, out real)) continue;
|
||||||
|
if (byState.TryGetValue(real, out clip)) { matched = real; break; }
|
||||||
|
}
|
||||||
|
if (clip == null || matched == null || clip.length <= 0.0001f)
|
||||||
|
{
|
||||||
|
r.notes.Add("동작 '" + ba.key + "' — 상태를 못 찾음(후보: " + string.Join("/", ba.stateNames) + ") → 「미확인」 · 건너뜀");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var pa = new PlanAction { key = ba.key, clip = clip, frames = Mathf.Max(1, ba.frames), loop = ba.loop,
|
||||||
|
stateName = matched, stateHash = Animator.StringToHash(matched) };
|
||||||
|
if (ac != null && !anim.HasState(0, pa.stateHash))
|
||||||
|
{ r.notes.Add("동작 '" + ba.key + "' — 상태 '" + matched + "' 가 레이어0 에 없다 → 건너뜀"); continue; }
|
||||||
|
// 실제로 컨트롤러에 있는 상태 이름만 메타에 넣는다(런타임 해시 비교용)
|
||||||
|
pa.stateNames.Add(matched);
|
||||||
|
foreach (var n in ba.stateNames) { string real2; if (realName.TryGetValue(n, out real2) && !pa.stateNames.Contains(real2)) pa.stateNames.Add(real2); }
|
||||||
|
foreach (var n in ba.alsoMapStateNames) { string real3; if (realName.TryGetValue(n, out real3) && !pa.stateNames.Contains(real3)) pa.stateNames.Add(real3); }
|
||||||
|
list.Add(pa);
|
||||||
|
r.log.AppendLine(string.Format(" 동작 {0,-7} ← 상태 [{1}] · 클립 {2} ({3:0.000}s · {4}프레임 · loop {5})",
|
||||||
|
ba.key, string.Join("/", pa.stateNames.ToArray()), clip.name, clip.length, pa.frames, pa.loop));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AnimationClip FirstClip(UnityEngine.Motion m)
|
||||||
|
{
|
||||||
|
var c = m as AnimationClip;
|
||||||
|
if (c != null) return c;
|
||||||
|
var bt = m as UnityEditor.Animations.BlendTree;
|
||||||
|
if (bt == null) return null;
|
||||||
|
foreach (var ch in bt.children)
|
||||||
|
{
|
||||||
|
var r = FirstClip(ch.motion);
|
||||||
|
if (r != null) return r;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 렌더 · 도트화
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
static void RenderInto(Camera cam, RenderTexture rt, Texture2D read, Color bg, Color32[] outPx)
|
||||||
|
{
|
||||||
|
cam.backgroundColor = bg;
|
||||||
|
var prev = cam.targetTexture;
|
||||||
|
cam.targetTexture = rt;
|
||||||
|
cam.Render();
|
||||||
|
cam.targetTexture = prev;
|
||||||
|
|
||||||
|
var active = RenderTexture.active;
|
||||||
|
RenderTexture.active = rt;
|
||||||
|
read.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
||||||
|
read.Apply(false, false);
|
||||||
|
RenderTexture.active = active;
|
||||||
|
|
||||||
|
var src = read.GetPixels32();
|
||||||
|
Array.Copy(src, outPx, outPx.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>검정 배경 · 흰 배경 두 장에서 알파를 복원하고 도트화한다(이진화 → 양자화).</summary>
|
||||||
|
static Color32[] Compose(Color32[] black, Color32[] white, int P, WLSpriteBakeSettings cfg)
|
||||||
|
{
|
||||||
|
var outPx = new Color32[P * P];
|
||||||
|
int levels = cfg.paletteLevels;
|
||||||
|
bool quant = levels >= 2 && levels <= 256;
|
||||||
|
float cut = Mathf.Clamp01(cfg.alphaCutoff);
|
||||||
|
for (int i = 0; i < outPx.Length; i++)
|
||||||
|
{
|
||||||
|
Color32 b = black[i], w = white[i];
|
||||||
|
// 배경만 다르다 → 차이가 곧 「덜 덮인 정도」
|
||||||
|
float cov = 1f - (((w.r - b.r) + (w.g - b.g) + (w.b - b.b)) / (3f * 255f));
|
||||||
|
if (cov < cut) { outPx[i] = new Color32(0, 0, 0, 0); continue; }
|
||||||
|
// 검정 배경 위 값 = cov × 색 → 색 = 값 ÷ cov
|
||||||
|
float inv = cov > 0.001f ? 1f / cov : 1f;
|
||||||
|
float fr = Mathf.Clamp(b.r * inv, 0f, 255f);
|
||||||
|
float fg = Mathf.Clamp(b.g * inv, 0f, 255f);
|
||||||
|
float fb = Mathf.Clamp(b.b * inv, 0f, 255f);
|
||||||
|
if (quant && cfg.paletteValueBands)
|
||||||
|
{
|
||||||
|
// 밝기만 단계로 끊는다(색상·채도 유지) → 도트 특유의 평평한 명암, 색 얼룩 0
|
||||||
|
float v = Mathf.Max(fr, Mathf.Max(fg, fb));
|
||||||
|
if (v > 0.5f)
|
||||||
|
{
|
||||||
|
float n = levels - 1;
|
||||||
|
float qv = Mathf.Round(v / 255f * n) / n * 255f;
|
||||||
|
float k = qv / v;
|
||||||
|
fr *= k; fg *= k; fb *= k;
|
||||||
|
}
|
||||||
|
outPx[i] = new Color32((byte)Mathf.Clamp(Mathf.RoundToInt(fr), 0, 255),
|
||||||
|
(byte)Mathf.Clamp(Mathf.RoundToInt(fg), 0, 255),
|
||||||
|
(byte)Mathf.Clamp(Mathf.RoundToInt(fb), 0, 255), 255);
|
||||||
|
}
|
||||||
|
else outPx[i] = new Color32(Q(fr, levels, quant), Q(fg, levels, quant), Q(fb, levels, quant), 255);
|
||||||
|
}
|
||||||
|
return outPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte Q(float v, int levels, bool quant)
|
||||||
|
{
|
||||||
|
v = Mathf.Clamp(v, 0f, 255f);
|
||||||
|
if (!quant) return (byte)Mathf.RoundToInt(v);
|
||||||
|
float n = levels - 1;
|
||||||
|
float q = Mathf.Round(v / 255f * n) / n;
|
||||||
|
return (byte)Mathf.RoundToInt(q * 255f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TightBox(Color32[] px, int P, ref int minX, ref int maxX, ref int minY, ref int maxY)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < P; y++)
|
||||||
|
{
|
||||||
|
int row = y * P;
|
||||||
|
for (int x = 0; x < P; x++)
|
||||||
|
{
|
||||||
|
if (px[row + x].a == 0) continue;
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브 프레임에서 S×S 를 잘라낸다. (ox, oy) = 잘라낼 좌하단의 프로브 좌표.</summary>
|
||||||
|
static Color32[] Crop(Color32[] src, int P, int S, int ox, int oy)
|
||||||
|
{
|
||||||
|
var dst = new Color32[S * S];
|
||||||
|
for (int y = 0; y < S; y++)
|
||||||
|
{
|
||||||
|
int sy = oy + y;
|
||||||
|
if (sy < 0 || sy >= P) continue;
|
||||||
|
int srow = sy * P, drow = y * S;
|
||||||
|
for (int x = 0; x < S; x++)
|
||||||
|
{
|
||||||
|
int sx = ox + x;
|
||||||
|
if (sx < 0 || sx >= P) continue;
|
||||||
|
dst[drow + x] = src[srow + sx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>알파 경계 **바깥**에 1px 외곽선(색 = SO).</summary>
|
||||||
|
static void Outline(Color32[] px, int S, WLSpriteBakeSettings cfg)
|
||||||
|
{
|
||||||
|
var col = (Color32)cfg.outlineColor;
|
||||||
|
col.a = 255;
|
||||||
|
var add = new List<int>(S * 4);
|
||||||
|
for (int y = 0; y < S; y++)
|
||||||
|
{
|
||||||
|
int row = y * S;
|
||||||
|
for (int x = 0; x < S; x++)
|
||||||
|
{
|
||||||
|
int i = row + x;
|
||||||
|
if (px[i].a != 0) continue;
|
||||||
|
if (Solid(px, S, x - 1, y) || Solid(px, S, x + 1, y) || Solid(px, S, x, y - 1) || Solid(px, S, x, y + 1)
|
||||||
|
|| (cfg.outlineDiagonal && (Solid(px, S, x - 1, y - 1) || Solid(px, S, x + 1, y - 1) || Solid(px, S, x - 1, y + 1) || Solid(px, S, x + 1, y + 1))))
|
||||||
|
add.Add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < add.Count; i++) px[add[i]] = col;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Solid(Color32[] px, int S, int x, int y)
|
||||||
|
{
|
||||||
|
if (x < 0 || y < 0 || x >= S || y >= S) return false;
|
||||||
|
return px[y * S + x].a != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Blit(Color32[] src, int S, Color32[] dst, int dstW, int dx, int dy)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < S; y++)
|
||||||
|
{
|
||||||
|
int srow = y * S, drow = (dy + y) * dstW + dx;
|
||||||
|
Array.Copy(src, srow, dst, drow, S);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int RoundUp8(int v) { return ((v + 7) / 8) * 8; }
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 임포트 설정 — 🔴 코드로 지정(Point · 압축 없음 · mipmap off)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
public static void ApplyImportSettings(string pngPath, int maxSize)
|
||||||
|
{
|
||||||
|
var ti = AssetImporter.GetAtPath(pngPath) as TextureImporter;
|
||||||
|
if (ti == null) return;
|
||||||
|
ti.textureType = TextureImporterType.Default; // 🔴 Sprite(Multiple) 가 아니다 — UV 는 런타임 산술
|
||||||
|
ti.npotScale = TextureImporterNPOTScale.None;
|
||||||
|
ti.filterMode = FilterMode.Point;
|
||||||
|
ti.mipmapEnabled = false;
|
||||||
|
ti.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
ti.alphaIsTransparency = true;
|
||||||
|
ti.alphaSource = TextureImporterAlphaSource.FromInput;
|
||||||
|
ti.sRGBTexture = true;
|
||||||
|
ti.isReadable = false;
|
||||||
|
ti.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
ti.maxTextureSize = NextPow2AtLeast(maxSize);
|
||||||
|
var def = ti.GetDefaultPlatformTextureSettings();
|
||||||
|
def.format = TextureImporterFormat.RGBA32;
|
||||||
|
def.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
def.maxTextureSize = ti.maxTextureSize;
|
||||||
|
ti.SetPlatformTextureSettings(def);
|
||||||
|
var android = ti.GetPlatformTextureSettings("Android");
|
||||||
|
android.overridden = true;
|
||||||
|
android.format = TextureImporterFormat.RGBA32;
|
||||||
|
android.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
android.maxTextureSize = ti.maxTextureSize;
|
||||||
|
ti.SetPlatformTextureSettings(android);
|
||||||
|
ti.SaveAndReimport();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int NextPow2AtLeast(int v)
|
||||||
|
{
|
||||||
|
int p = 32;
|
||||||
|
while (p < v && p < 8192) p <<= 1;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19bec13898ca6d64d908bce11eacb109
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLSpriteBakerWindow.cs — 「도트로 굽기」 에디터 창 (WL-814k · #814)
|
||||||
|
//
|
||||||
|
// 메뉴: WL ▸ 도트 스프라이트 굽기
|
||||||
|
// 🔴 캐릭터 이름이 코드에 0개 — 창에 **아무 캐릭터 프리팹이나** 끌어다 놓으면 그것을 굽는다.
|
||||||
|
// (PD 가 캐릭터 에셋 교체를 검토 중 → 특정 프리팹에 하드코딩하지 않는다.)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake.EditorTools
|
||||||
|
{
|
||||||
|
public sealed class WLSpriteBakerWindow : EditorWindow
|
||||||
|
{
|
||||||
|
[SerializeField] List<GameObject> _targets = new List<GameObject>();
|
||||||
|
[SerializeField] WLSpriteBakeSettings _cfg;
|
||||||
|
[SerializeField] string _outDir = "";
|
||||||
|
[SerializeField] Vector2 _scroll;
|
||||||
|
[SerializeField] string _report = "";
|
||||||
|
|
||||||
|
[MenuItem("WL/도트 스프라이트 굽기 (3D → 도트 시트)", false, 30)]
|
||||||
|
static void Open()
|
||||||
|
{
|
||||||
|
var w = GetWindow<WLSpriteBakerWindow>(false, "도트 굽기", true);
|
||||||
|
w.minSize = new Vector2(460f, 360f);
|
||||||
|
if (w._cfg == null) w._cfg = WLSpriteBakeSettings.Instance;
|
||||||
|
w.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"3D 캐릭터 프리팹을 인게임 도트 카메라와 같은 각도로 미리 렌더해서 스프라이트 시트(PNG) + 메타 SO 로 굽는다.\n" +
|
||||||
|
"· 어떤 캐릭터 프리팹이든 아래 목록에 넣으면 된다(툴은 프리팹 이름을 모른다).\n" +
|
||||||
|
"· 굽는 시점에 1px 외곽선 · 팔레트 양자화 · 알파 이진화를 강제한다.\n" +
|
||||||
|
"· 원본 프리팹 / 머티리얼 / 애니메이션은 한 글자도 안 고친다(읽기·샘플링만).", MessageType.Info);
|
||||||
|
|
||||||
|
_cfg = (WLSpriteBakeSettings)EditorGUILayout.ObjectField("굽기 설정(SO)", _cfg, typeof(WLSpriteBakeSettings), false);
|
||||||
|
if (_cfg == null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Resources/WL/WLSpriteBakeSettings.asset 을 넣어라.", MessageType.Warning);
|
||||||
|
if (GUILayout.Button("기본 설정 에셋 찾기")) _cfg = WLSpriteBakeSettings.Instance;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_outDir = EditorGUILayout.TextField(new GUIContent("출력 폴더(비우면 SO 값)"), _outDir);
|
||||||
|
EditorGUILayout.LabelField(" → " + (string.IsNullOrEmpty(_outDir) ? _cfg.outputDir : _outDir), EditorStyles.miniLabel);
|
||||||
|
EditorGUILayout.LabelField(string.Format(" 내림각 {0:0.000}° · 픽셀 1개 {1:0.00000} m · 방향 {2} · 프레임 {3}×{3}{4}",
|
||||||
|
_cfg.PitchDeg, _cfg.WorldPerPixel, _cfg.dirCount, _cfg.frameSize, _cfg.autoFrameSize ? " (자동)" : ""), EditorStyles.miniLabel);
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EditorGUILayout.LabelField("구울 프리팹 (드래그해서 넣기)", EditorStyles.boldLabel);
|
||||||
|
var so = new SerializedObject(this);
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("_targets"), new GUIContent("대상"), true);
|
||||||
|
so.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
using (new EditorGUI.DisabledScope(_targets.Count == 0))
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("굽기 시작", GUILayout.Height(30f))) BakeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||||||
|
EditorGUILayout.TextArea(_report, GUILayout.ExpandHeight(true));
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BakeAll()
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < _targets.Count; i++)
|
||||||
|
{
|
||||||
|
var go = _targets[i];
|
||||||
|
if (go == null) continue;
|
||||||
|
string path = AssetDatabase.GetAssetPath(go);
|
||||||
|
if (string.IsNullOrEmpty(path)) { sb.AppendLine("건너뜀(프리팹 에셋이 아님): " + go.name); continue; }
|
||||||
|
EditorUtility.DisplayProgressBar("도트로 굽는 중", path, (i + 0.5f) / _targets.Count);
|
||||||
|
var r = WLSpriteBaker.Bake(path, _cfg, string.IsNullOrEmpty(_outDir) ? null : _outDir);
|
||||||
|
sb.AppendLine((r.ok ? "OK " : "FAIL ") + r.Summary);
|
||||||
|
sb.AppendLine(r.log.ToString());
|
||||||
|
foreach (var n in r.notes) sb.AppendLine(" · " + n);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
_report = sb.ToString();
|
||||||
|
Debug.Log("[WL814k] 굽기 완료\n" + _report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2a3e14cd2c39a344caa5c31b7582ba03
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bf6fc714ae9b81a49bc424ea8cfac3b7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 948528c800001d74d8e2f24cddc3b799
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e9dcf8e4e8011b84ab5e5df43a7e0255, type: 3}
|
||||||
|
m_Name: WLSpriteBakeSettings
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.SpriteBake.WLSpriteBakeSettings
|
||||||
|
pitchDegOverride: 0
|
||||||
|
orbitCameraHeight: 80
|
||||||
|
orbitDistance: 3
|
||||||
|
orbitDistanceUp: 3
|
||||||
|
dirCount: 8
|
||||||
|
worldPerPixelOverride: 0
|
||||||
|
refOrthoSize: 3.4
|
||||||
|
refPixelHeight: 240
|
||||||
|
frameSize: 64
|
||||||
|
autoFrameSize: 1
|
||||||
|
frameSizeMin: 16
|
||||||
|
frameSizeMax: 128
|
||||||
|
marginPx: 2
|
||||||
|
probeSize: 192
|
||||||
|
outline: 1
|
||||||
|
outlineColor: {r: 0.055, g: 0.043, b: 0.067, a: 1}
|
||||||
|
outlineDiagonal: 0
|
||||||
|
paletteLevels: 6
|
||||||
|
alphaCutoff: 0.5
|
||||||
|
useToonMaterials: 1
|
||||||
|
toonMaterialDir: Assets/WL/Look/Env/Materials
|
||||||
|
toonMaterialSuffix: _Toon
|
||||||
|
sunEuler: {x: 26.4, y: 238.6, z: 0}
|
||||||
|
sunColor: {r: 1, g: 0.92, b: 0.703, a: 1}
|
||||||
|
sunIntensity: 1.2
|
||||||
|
fillEuler: {x: 36.1, y: 116.1, z: 0}
|
||||||
|
fillColor: {r: 0.596, g: 0.703, b: 0.839, a: 1}
|
||||||
|
fillIntensity: 0.3
|
||||||
|
ambient: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||||
|
outputDir: Assets/WL/Look/Sprite/Baked
|
||||||
|
sheetColumns: 16
|
||||||
|
sheetMaxWidth: 2048
|
||||||
|
actions:
|
||||||
|
- key: idle
|
||||||
|
stateNames:
|
||||||
|
- idle
|
||||||
|
- Idle
|
||||||
|
- wait
|
||||||
|
- IdleA
|
||||||
|
- IdleB
|
||||||
|
alsoMapStateNames:
|
||||||
|
- land
|
||||||
|
- getup
|
||||||
|
frames: 4
|
||||||
|
loop: 1
|
||||||
|
required: 0
|
||||||
|
- key: run
|
||||||
|
stateNames:
|
||||||
|
- run
|
||||||
|
- Run
|
||||||
|
- move
|
||||||
|
- Move
|
||||||
|
- walk
|
||||||
|
alsoMapStateNames:
|
||||||
|
- dodge
|
||||||
|
frames: 6
|
||||||
|
loop: 1
|
||||||
|
required: 0
|
||||||
|
- key: attack
|
||||||
|
stateNames:
|
||||||
|
- attack
|
||||||
|
- Attack
|
||||||
|
- atk
|
||||||
|
- ATK0
|
||||||
|
- ATK1
|
||||||
|
alsoMapStateNames:
|
||||||
|
- ATK2
|
||||||
|
- ATK3
|
||||||
|
- ATK4
|
||||||
|
- Shoot
|
||||||
|
- attack2
|
||||||
|
- skill2
|
||||||
|
- skill3
|
||||||
|
- skill4
|
||||||
|
- skill5
|
||||||
|
- casting
|
||||||
|
- casting_end
|
||||||
|
frames: 6
|
||||||
|
loop: 1
|
||||||
|
required: 0
|
||||||
|
- key: hit
|
||||||
|
stateNames:
|
||||||
|
- hit
|
||||||
|
- Hit
|
||||||
|
- damage
|
||||||
|
- Damage
|
||||||
|
alsoMapStateNames:
|
||||||
|
- stun
|
||||||
|
- Swoon
|
||||||
|
frames: 2
|
||||||
|
loop: 1
|
||||||
|
required: 0
|
||||||
|
- key: die
|
||||||
|
stateNames:
|
||||||
|
- die
|
||||||
|
- Die
|
||||||
|
- death
|
||||||
|
- Die0
|
||||||
|
- Die1
|
||||||
|
alsoMapStateNames:
|
||||||
|
- lying
|
||||||
|
- Die2
|
||||||
|
frames: 6
|
||||||
|
loop: 0
|
||||||
|
required: 0
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5b40b06e2f8e6234580661b2c962fc01
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 8f39fe71df28ff94ebdc9f76766e84e9, type: 3}
|
||||||
|
m_Name: WLSpriteLookSettings
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.SpriteBake.WLSpriteLookSettings
|
||||||
|
enabled_: 1
|
||||||
|
verboseLog: 0
|
||||||
|
onlyInPixelMode: 1
|
||||||
|
sweepSeconds: 0.5
|
||||||
|
maxActors: 24
|
||||||
|
sets:
|
||||||
|
- {fileID: 11400000, guid: 9afd69d5485f7584b8f40e7673bca04e, type: 2}
|
||||||
|
- {fileID: 11400000, guid: 02557ce4e57225d4db041caa2e6e44d7, type: 2}
|
||||||
|
yAxisOnly: 0
|
||||||
|
yAxisOnlyCompensate: 1
|
||||||
|
depthBias: 0.02
|
||||||
|
alphaCutoff: 0.5
|
||||||
|
quadLayer: -1
|
||||||
|
alsoHideMeshRenderers: 1
|
||||||
|
castShadows: 0
|
||||||
|
statePollSeconds: 0
|
||||||
|
fallbackActionKey: idle
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 481482cb77548cb47959bef245114fc2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,620 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// SpriteBillboard.cs — 도트 모드에서 3D 캐릭터를 「구운 도트 스프라이트」로 바꿔 그린다 (WL-814k · #814)
|
||||||
|
//
|
||||||
|
// PD 지시(2026-09-12) 「치비 느낌이 나지 않아도 도트 그림으로 구워 쓰는 방식으로 해봐.」
|
||||||
|
//
|
||||||
|
// ■ 이 파일이 하는 것 — 🔴 원본 코드 수정 0 · 원본 파괴 0
|
||||||
|
// ① 814d `LookModeHub.CameraModeChanged` 를 구독해 **도트 모드(1~3)에서만** 켠다.
|
||||||
|
// ② 대상 액터의 원본 `SkinnedMeshRenderer` 를 **끄고**(`enabled=false` · 파괴 0),
|
||||||
|
// 자식으로 **빌보드 쿼드 1개**(MeshRenderer + Unlit 알파컷 · 런타임 인스턴스)를 붙인다.
|
||||||
|
// ③ 프레임은 원본 `Animator` **폴링**으로 고른다(813x/813t2 방식):
|
||||||
|
// 상태 해시(레이어 0 shortNameHash) → 동작 · `normalizedTime` → 프레임 · 상대 yaw → 방향.
|
||||||
|
// ④ 모드에서 나가거나 SO 를 끄면 **전부 원복**한다(끈 렌더러를 다시 켜고 쿼드를 지운다) = C8.
|
||||||
|
//
|
||||||
|
// ■ 왜 「카메라 정렬」이 기본인가 (yAxisOnly = false)
|
||||||
|
// 스프라이트는 내림각 43.6° 카메라로 **이미 눌린 그림**이다. 그걸 수직 쿼드에 붙이면 한 번 더 눌린다
|
||||||
|
// (cos 43.6° = 0.724 → 키가 28 % 줄어든다). 쿼드를 카메라와 같은 자세로 두면 구운 픽셀이
|
||||||
|
// 화면 픽셀과 1:1 로 맞는다. 도트 모드의 카메라는 롤도 없고 내림각도 고정이라 기울어 보이지 않는다.
|
||||||
|
// 「Y축만」 모드도 SO 스위치로 남겨 뒀다(발주서 문구 · 눌림 보정 옵션 포함).
|
||||||
|
//
|
||||||
|
// ■ GC 0 — 부착할 때만 배열을 만든다. 매 프레임은 구조체 읽기 + MaterialPropertyBlock.SetVector 뿐이다
|
||||||
|
// (문자열·람다·LINQ·GetComponent 0). 로그 문자열은 만들기 전에 verboseLog 로 막는다(811b FIX-3).
|
||||||
|
//
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using WL.Look.Toggle;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake
|
||||||
|
{
|
||||||
|
/// <summary>도트 모드 한정 3D→스프라이트 교체. 정적 · 러너 GameObject 1개.</summary>
|
||||||
|
public static class SpriteBillboard
|
||||||
|
{
|
||||||
|
// ── 액터 1명의 교체 상태 ──────────────────────────────────────────────
|
||||||
|
public sealed class Entry
|
||||||
|
{
|
||||||
|
public Transform actor;
|
||||||
|
public Animator anim;
|
||||||
|
public WLSpriteSet set;
|
||||||
|
|
||||||
|
public SkinnedMeshRenderer[] smrs;
|
||||||
|
public bool[] smrWas;
|
||||||
|
public MeshRenderer[] mrs;
|
||||||
|
public bool[] mrWas;
|
||||||
|
|
||||||
|
public GameObject quad;
|
||||||
|
public Transform quadTf;
|
||||||
|
public MeshRenderer quadMr;
|
||||||
|
public MaterialPropertyBlock mpb;
|
||||||
|
|
||||||
|
public WLSpriteAction action;
|
||||||
|
public int dir = -1;
|
||||||
|
public int frame = -1;
|
||||||
|
public int lastIndex = -1;
|
||||||
|
public float scaleK = 1f;
|
||||||
|
public bool manual; // 프로브/캡처가 프레임을 직접 지정한 상태
|
||||||
|
}
|
||||||
|
|
||||||
|
static WLSpriteLookSettings Cfg { get { return WLSpriteLookSettings.Instance; } }
|
||||||
|
|
||||||
|
static readonly List<Entry> s_entries = new List<Entry>(32);
|
||||||
|
static readonly HashSet<int> s_seen = new HashSet<int>();
|
||||||
|
static Material s_mat;
|
||||||
|
static Mesh s_mesh;
|
||||||
|
static bool s_applied;
|
||||||
|
static bool s_hooked;
|
||||||
|
static bool s_faulted;
|
||||||
|
static float s_nextSweep;
|
||||||
|
|
||||||
|
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
|
||||||
|
public static int Applies, Restores, Attached, Detached, HubEvents, Sweeps, FrameChanges;
|
||||||
|
public static string LastLog = "";
|
||||||
|
public static bool IsApplied { get { return s_applied; } }
|
||||||
|
public static int ActiveCount { get { return s_entries.Count; } }
|
||||||
|
public static List<Entry> EntriesForProbe { get { return s_entries; } }
|
||||||
|
|
||||||
|
static void Log(string msg)
|
||||||
|
{
|
||||||
|
LastLog = msg;
|
||||||
|
var c = Cfg;
|
||||||
|
if (c != null && c.verboseLog) Debug.Log("[WL814k SpriteBillboard] " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 켜고 끄기
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>지금 켜야 하는 상태인가(SO + 도트 모드 조건).</summary>
|
||||||
|
public static bool ShouldBeOn
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var c = Cfg;
|
||||||
|
if (c == null || !WLSpriteLookSettings.Enabled) return false;
|
||||||
|
if (!c.onlyInPixelMode) return true;
|
||||||
|
if (!WLLookModeSettings.Enabled) return false;
|
||||||
|
return LookModeHub.IsPixelMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>대상 액터를 찾아 스프라이트로 바꾼다. 반환 = 새로 바뀐 수.</summary>
|
||||||
|
public static int ApplyNow(string reason)
|
||||||
|
{
|
||||||
|
var c = Cfg;
|
||||||
|
if (c == null || !WLSpriteLookSettings.Enabled) { Log("건너뜀(SO off) — " + reason); return 0; }
|
||||||
|
s_applied = true;
|
||||||
|
Applies++;
|
||||||
|
int n = Sweep();
|
||||||
|
Log("적용(" + reason + ") — 대상 " + s_entries.Count + "명(+ " + n + ")");
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>전부 원복한다(끈 렌더러를 다시 켜고 쿼드를 지운다) = C8.</summary>
|
||||||
|
public static int RestoreNow(string reason)
|
||||||
|
{
|
||||||
|
int n = s_entries.Count;
|
||||||
|
for (int i = s_entries.Count - 1; i >= 0; i--) DetachAt(i);
|
||||||
|
s_entries.Clear();
|
||||||
|
s_seen.Clear();
|
||||||
|
s_applied = false;
|
||||||
|
if (n > 0) Restores++;
|
||||||
|
Log("원복(" + reason + ") — " + n + "명");
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 스윕 — 새로 스폰된 액터 찾기 (캐시 히트 시 할당 0 · 814c SweepNewSpawns 방식)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public static int Sweep()
|
||||||
|
{
|
||||||
|
var c = Cfg;
|
||||||
|
if (c == null || !s_applied) return 0;
|
||||||
|
Sweeps++;
|
||||||
|
int added = 0;
|
||||||
|
|
||||||
|
// ① PC
|
||||||
|
var pc = PcTransform();
|
||||||
|
if (pc != null) added += TryAttachIfNew(pc, c);
|
||||||
|
|
||||||
|
// ② 몹 루트의 직속 자식(814c 와 같은 경로 = InGameInfo.tf_Objs)
|
||||||
|
var root = MobRoot();
|
||||||
|
if (root != null)
|
||||||
|
{
|
||||||
|
int cnt = root.childCount;
|
||||||
|
for (int i = 0; i < cnt; i++)
|
||||||
|
{
|
||||||
|
var ch = root.GetChild(i);
|
||||||
|
if (ch == null) continue;
|
||||||
|
added += TryAttachIfNew(ch, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Transform PcTransform()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying) return null;
|
||||||
|
var pc = MyValue.MyPC;
|
||||||
|
if (DSUtil.CheckNull(pc)) return null;
|
||||||
|
return pc.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Transform MobRoot()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying) return null;
|
||||||
|
var info = InGameInfo.Ins;
|
||||||
|
if (DSUtil.CheckNull(info)) return null;
|
||||||
|
return info.tf_Objs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int TryAttachIfNew(Transform t, WLSpriteLookSettings c)
|
||||||
|
{
|
||||||
|
if (t == null) return 0;
|
||||||
|
int id = t.GetInstanceID();
|
||||||
|
if (s_seen.Contains(id)) return 0;
|
||||||
|
s_seen.Add(id);
|
||||||
|
if (s_entries.Count >= Mathf.Max(1, c.maxActors)) return 0;
|
||||||
|
var set = MatchSet(t.name, c);
|
||||||
|
if (set == null) return 0;
|
||||||
|
return TryAttach(t.gameObject, set) != null ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>액터 이름 → 시트 SO(SO 목록의 actorNamePrefix 로 매칭). 없으면 null.</summary>
|
||||||
|
public static WLSpriteSet MatchSet(string actorName, WLSpriteLookSettings c)
|
||||||
|
{
|
||||||
|
if (c == null || c.sets == null || string.IsNullOrEmpty(actorName)) return null;
|
||||||
|
for (int i = 0; i < c.sets.Length; i++)
|
||||||
|
{
|
||||||
|
var s = c.sets[i];
|
||||||
|
if (s == null || !s.IsUsable || string.IsNullOrEmpty(s.actorNamePrefix)) continue;
|
||||||
|
if (actorName.StartsWith(s.actorNamePrefix, System.StringComparison.Ordinal)) return s;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 부착 / 떼기 (에디트 모드에서도 동작 — 캡처가 이 경로를 쓴다)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>액터 1명을 스프라이트로 바꾼다. 이미 바뀌었으면 기존 Entry 를 준다.</summary>
|
||||||
|
public static Entry TryAttach(GameObject actor, WLSpriteSet set)
|
||||||
|
{
|
||||||
|
if (actor == null || set == null || !set.IsUsable) return null;
|
||||||
|
for (int i = 0; i < s_entries.Count; i++) if (s_entries[i].actor == actor.transform) return s_entries[i];
|
||||||
|
|
||||||
|
var c = Cfg;
|
||||||
|
var e = new Entry();
|
||||||
|
e.actor = actor.transform;
|
||||||
|
e.anim = actor.GetComponentInChildren<Animator>(true);
|
||||||
|
e.set = set;
|
||||||
|
|
||||||
|
// 원본 렌더러 끄기(파괴 0 · 원값 기억)
|
||||||
|
e.smrs = actor.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
e.smrWas = new bool[e.smrs.Length];
|
||||||
|
for (int i = 0; i < e.smrs.Length; i++)
|
||||||
|
{
|
||||||
|
if (e.smrs[i] == null) continue;
|
||||||
|
e.smrWas[i] = e.smrs[i].enabled;
|
||||||
|
e.smrs[i].enabled = false;
|
||||||
|
}
|
||||||
|
if (c == null || c.alsoHideMeshRenderers)
|
||||||
|
{
|
||||||
|
var all = actor.GetComponentsInChildren<MeshRenderer>(true);
|
||||||
|
var keep = new List<MeshRenderer>(all.Length);
|
||||||
|
for (int i = 0; i < all.Length; i++)
|
||||||
|
if (all[i] != null && all[i].GetComponent<ParticleSystem>() == null) keep.Add(all[i]);
|
||||||
|
e.mrs = keep.ToArray();
|
||||||
|
e.mrWas = new bool[e.mrs.Length];
|
||||||
|
for (int i = 0; i < e.mrs.Length; i++) { e.mrWas[i] = e.mrs[i].enabled; e.mrs[i].enabled = false; }
|
||||||
|
}
|
||||||
|
else { e.mrs = new MeshRenderer[0]; e.mrWas = new bool[0]; }
|
||||||
|
|
||||||
|
// 빌보드 쿼드
|
||||||
|
e.quad = new GameObject("[WL814k] SpriteQuad");
|
||||||
|
e.quad.hideFlags = HideFlags.DontSave;
|
||||||
|
e.quadTf = e.quad.transform;
|
||||||
|
e.quadTf.SetParent(actor.transform, false);
|
||||||
|
int layer = (c != null && c.quadLayer >= 0) ? c.quadLayer : actor.layer;
|
||||||
|
e.quad.layer = layer;
|
||||||
|
var mf = e.quad.AddComponent<MeshFilter>();
|
||||||
|
mf.sharedMesh = QuadMesh();
|
||||||
|
e.quadMr = e.quad.AddComponent<MeshRenderer>();
|
||||||
|
e.quadMr.sharedMaterial = SheetMaterial(set.sheet); // 시트별 1개(액터끼리 공유 · 시트가 섞여도 안 덮인다)
|
||||||
|
e.quadMr.shadowCastingMode = (c != null && c.castShadows)
|
||||||
|
? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
e.quadMr.receiveShadows = false;
|
||||||
|
e.quadMr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||||||
|
e.quadMr.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
|
||||||
|
e.mpb = new MaterialPropertyBlock();
|
||||||
|
|
||||||
|
// 스케일 보정(액터가 구울 때와 다른 크기로 스폰될 수 있다)
|
||||||
|
float bs = set.bakeScale > 0.0001f ? set.bakeScale : 1f;
|
||||||
|
e.scaleK = Mathf.Abs(actor.transform.lossyScale.y) / bs;
|
||||||
|
if (e.scaleK < 0.0001f) e.scaleK = 1f;
|
||||||
|
|
||||||
|
s_entries.Add(e);
|
||||||
|
Attached++;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DetachAt(int i)
|
||||||
|
{
|
||||||
|
var e = s_entries[i];
|
||||||
|
if (e == null) return;
|
||||||
|
if (e.smrs != null)
|
||||||
|
for (int k = 0; k < e.smrs.Length; k++) if (e.smrs[k] != null) e.smrs[k].enabled = e.smrWas[k];
|
||||||
|
if (e.mrs != null)
|
||||||
|
for (int k = 0; k < e.mrs.Length; k++) if (e.mrs[k] != null) e.mrs[k].enabled = e.mrWas[k];
|
||||||
|
if (e.quad != null)
|
||||||
|
{
|
||||||
|
if (Application.isPlaying) Object.Destroy(e.quad);
|
||||||
|
else Object.DestroyImmediate(e.quad);
|
||||||
|
}
|
||||||
|
s_entries.RemoveAt(i);
|
||||||
|
Detached++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>액터 1명만 원복한다(프로브용).</summary>
|
||||||
|
public static bool Detach(GameObject actor)
|
||||||
|
{
|
||||||
|
if (actor == null) return false;
|
||||||
|
for (int i = 0; i < s_entries.Count; i++)
|
||||||
|
if (s_entries[i].actor == actor.transform) { DetachAt(i); s_seen.Remove(actor.transform.GetInstanceID()); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 프레임 · 방향 · 자세 갱신 (프레임당 할당 0)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>프로브/캡처가 프레임을 직접 지정한다(Animator 폴링 대신).</summary>
|
||||||
|
public static bool SetManualFrame(GameObject actor, string actionKey, int dir, int frame)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < s_entries.Count; i++)
|
||||||
|
{
|
||||||
|
var e = s_entries[i];
|
||||||
|
if (e.actor != actor.transform) continue;
|
||||||
|
var a = e.set.FindByKey(actionKey);
|
||||||
|
if (a == null) return false;
|
||||||
|
e.manual = true; e.action = a;
|
||||||
|
e.dir = Mathf.Clamp(dir, 0, e.set.dirCount - 1);
|
||||||
|
e.frame = Mathf.Clamp(frame, 0, a.frameCount - 1);
|
||||||
|
e.lastIndex = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>전체 갱신(러너가 매 프레임 부른다). cam 이 null 이면 Camera.main.</summary>
|
||||||
|
public static void UpdateAll(Camera cam)
|
||||||
|
{
|
||||||
|
if (cam == null) cam = Camera.main;
|
||||||
|
if (cam == null) return;
|
||||||
|
var c = Cfg;
|
||||||
|
if (c == null) return;
|
||||||
|
|
||||||
|
Vector3 camFwd = cam.transform.forward;
|
||||||
|
Vector3 camUp = cam.transform.up;
|
||||||
|
Vector3 camRight = cam.transform.right;
|
||||||
|
Quaternion camRot = cam.transform.rotation;
|
||||||
|
|
||||||
|
// yAxisOnly 용 수평 전방 · 눌림 보정
|
||||||
|
Vector3 flatFwd = camFwd; flatFwd.y = 0f;
|
||||||
|
if (flatFwd.sqrMagnitude < 1e-6f) flatFwd = Vector3.forward; else flatFwd.Normalize();
|
||||||
|
float comp = 1f;
|
||||||
|
if (c.yAxisOnly && c.yAxisOnlyCompensate)
|
||||||
|
{
|
||||||
|
float cosP = Mathf.Cos(Mathf.Abs(Mathf.Asin(Mathf.Clamp(-camFwd.y, -1f, 1f))));
|
||||||
|
comp = cosP > 0.05f ? 1f / cosP : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = s_entries.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var e = s_entries[i];
|
||||||
|
if (e == null || e.actor == null || e.quadTf == null) { if (e != null && e.actor != null) s_seen.Remove(e.actor.GetInstanceID()); s_entries.RemoveAt(i); continue; }
|
||||||
|
UpdateEntry(e, c, camFwd, camUp, camRight, camRot, flatFwd, comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpdateEntry(Entry e, WLSpriteLookSettings c,
|
||||||
|
Vector3 camFwd, Vector3 camUp, Vector3 camRight, Quaternion camRot,
|
||||||
|
Vector3 flatFwd, float comp)
|
||||||
|
{
|
||||||
|
var set = e.set;
|
||||||
|
|
||||||
|
// ── 동작 · 프레임 (Animator 폴링 · 원본 코드 수정 0) ──────────────
|
||||||
|
if (!e.manual && e.anim != null && e.anim.runtimeAnimatorController != null)
|
||||||
|
{
|
||||||
|
var st = e.anim.GetCurrentAnimatorStateInfo(0);
|
||||||
|
int hash = st.shortNameHash;
|
||||||
|
float nt = st.normalizedTime;
|
||||||
|
if (e.anim.IsInTransition(0))
|
||||||
|
{
|
||||||
|
var nx = e.anim.GetNextAnimatorStateInfo(0);
|
||||||
|
if (set.FindByStateHash(nx.shortNameHash) != null) { hash = nx.shortNameHash; nt = nx.normalizedTime; }
|
||||||
|
}
|
||||||
|
var act = set.FindByStateHash(hash);
|
||||||
|
if (act == null) act = set.FindByKey(c.fallbackActionKey);
|
||||||
|
if (act == null && set.actions.Length > 0) act = set.actions[0];
|
||||||
|
if (act != null)
|
||||||
|
{
|
||||||
|
e.action = act;
|
||||||
|
int fc = Mathf.Max(1, act.frameCount);
|
||||||
|
int f = act.loop
|
||||||
|
? Mathf.FloorToInt(Mathf.Repeat(nt, 1f) * fc)
|
||||||
|
: Mathf.FloorToInt(Mathf.Clamp01(nt) * fc);
|
||||||
|
e.frame = Mathf.Clamp(f, 0, fc - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.action == null && set.actions.Length > 0) { e.action = set.actions[0]; e.frame = 0; }
|
||||||
|
if (e.action == null) return;
|
||||||
|
|
||||||
|
// ── 방향 (캐릭터 forward 와 카메라 forward 의 상대 yaw) ───────────
|
||||||
|
if (!e.manual)
|
||||||
|
{
|
||||||
|
Vector3 af = e.actor.forward; af.y = 0f;
|
||||||
|
if (af.sqrMagnitude > 1e-6f)
|
||||||
|
{
|
||||||
|
float rel = Vector3.SignedAngle(flatFwd, af, Vector3.up);
|
||||||
|
float step = 360f / Mathf.Max(1, set.dirCount);
|
||||||
|
int d = Mathf.RoundToInt(Mathf.Repeat(rel, 360f) / step);
|
||||||
|
if (d >= set.dirCount) d -= set.dirCount;
|
||||||
|
e.dir = d;
|
||||||
|
}
|
||||||
|
else if (e.dir < 0) e.dir = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UV (바뀔 때만 MPB 를 다시 넣는다) ─────────────────────────────
|
||||||
|
int index = e.action.startIndex + e.dir * e.action.frameCount + e.frame;
|
||||||
|
if (index != e.lastIndex)
|
||||||
|
{
|
||||||
|
e.lastIndex = index;
|
||||||
|
FrameChanges++;
|
||||||
|
Vector4 st4 = set.UvRect(index);
|
||||||
|
e.mpb.SetVector(IdBaseMapST, st4);
|
||||||
|
e.mpb.SetVector(IdMainTexST, st4);
|
||||||
|
e.quadMr.SetPropertyBlock(e.mpb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 자세 · 크기 (발밑 정렬 · 픽셀 격자) ───────────────────────────
|
||||||
|
float wpp = set.worldPerPixel * e.scaleK;
|
||||||
|
float w = set.frameWidth * wpp;
|
||||||
|
float h = set.frameHeight * wpp;
|
||||||
|
Vector3 foot = e.actor.position;
|
||||||
|
|
||||||
|
// 지면 파묻힘 방지 — 쿼드의 발밑 아래쪽이 지면 밑으로 내려간 만큼 시선 방향으로 당긴다.
|
||||||
|
// 직교 카메라에서는 시선 방향 이동이 화면 위치를 바꾸지 않으므로 그림은 그대로다.
|
||||||
|
float bias = c.depthBias;
|
||||||
|
if (c.autoDepthBias)
|
||||||
|
{
|
||||||
|
float sinP = Mathf.Clamp(-camFwd.y, 0.02f, 1f);
|
||||||
|
float cosP = Mathf.Sqrt(Mathf.Max(0f, 1f - sinP * sinP));
|
||||||
|
bias += (set.footPixelY * wpp) * cosP / sinP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.yAxisOnly)
|
||||||
|
{
|
||||||
|
Vector3 center = foot
|
||||||
|
+ camRight * ((set.frameWidth * 0.5f - set.footPixelX) * wpp)
|
||||||
|
+ camUp * ((set.frameHeight * 0.5f - set.footPixelY) * wpp)
|
||||||
|
- camFwd * bias;
|
||||||
|
e.quadTf.SetPositionAndRotation(center, camRot);
|
||||||
|
e.quadTf.localScale = new Vector3(w / SafeScale(e.actor.lossyScale.x),
|
||||||
|
h / SafeScale(e.actor.lossyScale.y),
|
||||||
|
1f / SafeScale(e.actor.lossyScale.z));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float hh = h * comp;
|
||||||
|
Quaternion rot = Quaternion.LookRotation(flatFwd, Vector3.up);
|
||||||
|
Vector3 right = rot * Vector3.right;
|
||||||
|
Vector3 center = foot
|
||||||
|
+ right * ((set.frameWidth * 0.5f - set.footPixelX) * wpp)
|
||||||
|
+ Vector3.up * ((set.frameHeight * 0.5f - set.footPixelY) * wpp * comp)
|
||||||
|
- flatFwd * c.depthBias; // 수직 쿼드는 지면에 안 파묻힌다 → 기본 여유분만
|
||||||
|
e.quadTf.SetPositionAndRotation(center, rot);
|
||||||
|
e.quadTf.localScale = new Vector3(w / SafeScale(e.actor.lossyScale.x),
|
||||||
|
hh / SafeScale(e.actor.lossyScale.y),
|
||||||
|
1f / SafeScale(e.actor.lossyScale.z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float SafeScale(float v) { v = Mathf.Abs(v); return v < 0.0001f ? 1f : v; }
|
||||||
|
|
||||||
|
static readonly int IdBaseMapST = Shader.PropertyToID("_BaseMap_ST");
|
||||||
|
static readonly int IdMainTexST = Shader.PropertyToID("_MainTex_ST");
|
||||||
|
static readonly int IdBaseMap = Shader.PropertyToID("_BaseMap");
|
||||||
|
static readonly int IdMainTex = Shader.PropertyToID("_MainTex");
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 쿼드 메시 · 머티리얼 (런타임 인스턴스 · 디스크 에셋 0)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public static Mesh QuadMesh()
|
||||||
|
{
|
||||||
|
if (s_mesh != null) return s_mesh;
|
||||||
|
s_mesh = new Mesh();
|
||||||
|
s_mesh.name = "[WL814k] BillboardQuad";
|
||||||
|
s_mesh.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
s_mesh.vertices = new[]
|
||||||
|
{
|
||||||
|
new Vector3(-0.5f, -0.5f, 0f), new Vector3(0.5f, -0.5f, 0f),
|
||||||
|
new Vector3(-0.5f, 0.5f, 0f), new Vector3(0.5f, 0.5f, 0f),
|
||||||
|
};
|
||||||
|
s_mesh.uv = new[] { new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) };
|
||||||
|
s_mesh.normals = new[] { -Vector3.forward, -Vector3.forward, -Vector3.forward, -Vector3.forward };
|
||||||
|
s_mesh.triangles = new[] { 0, 1, 2, 2, 1, 3 };
|
||||||
|
s_mesh.RecalculateBounds();
|
||||||
|
return s_mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material QuadMaterial(Texture2D sheet)
|
||||||
|
{
|
||||||
|
var c = Cfg;
|
||||||
|
if (s_mat == null)
|
||||||
|
{
|
||||||
|
var sh = Shader.Find("Universal Render Pipeline/Unlit");
|
||||||
|
if (sh == null) sh = Shader.Find("Unlit/Transparent");
|
||||||
|
s_mat = new Material(sh);
|
||||||
|
s_mat.name = "[WL814k] SpriteBillboard (런타임 인스턴스)";
|
||||||
|
s_mat.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
s_mat.SetFloat("_Surface", 0f); // Opaque + AlphaClip → 깊이 정상
|
||||||
|
s_mat.SetFloat("_AlphaClip", 1f);
|
||||||
|
s_mat.SetFloat("_Cutoff", c != null ? c.alphaCutoff : 0.5f);
|
||||||
|
s_mat.SetFloat("_Cull", 0f); // 양면 — 감기 방향 신경 안 쓴다
|
||||||
|
s_mat.SetFloat("_ZWrite", 1f);
|
||||||
|
s_mat.EnableKeyword("_ALPHATEST_ON");
|
||||||
|
s_mat.renderQueue = 2450; // AlphaTest
|
||||||
|
}
|
||||||
|
if (sheet != null)
|
||||||
|
{
|
||||||
|
s_mat.SetTexture(IdBaseMap, sheet);
|
||||||
|
s_mat.SetTexture(IdMainTex, sheet);
|
||||||
|
}
|
||||||
|
return s_mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly Dictionary<Texture2D, Material> s_sheetMats = new Dictionary<Texture2D, Material>();
|
||||||
|
|
||||||
|
/// <summary>시트 1장당 머티리얼 1개(런타임 인스턴스 · 디스크 에셋 0). 시트가 섞여도 서로 안 덮는다.</summary>
|
||||||
|
public static Material SheetMaterial(Texture2D sheet)
|
||||||
|
{
|
||||||
|
if (sheet == null) return QuadMaterial(null);
|
||||||
|
Material m;
|
||||||
|
if (s_sheetMats.TryGetValue(sheet, out m) && m != null) return m;
|
||||||
|
m = new Material(QuadMaterial(null));
|
||||||
|
m.name = "[WL814k] Sprite " + sheet.name;
|
||||||
|
m.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
m.SetTexture(IdBaseMap, sheet);
|
||||||
|
m.SetTexture(IdMainTex, sheet);
|
||||||
|
s_sheetMats[sheet] = m;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// 틱 · 허브 · 러너
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public static void TickForProbe() { Tick(); }
|
||||||
|
public static void EnsureRunnerForProbe() { EnsureRunner(); }
|
||||||
|
|
||||||
|
internal static void Tick()
|
||||||
|
{
|
||||||
|
if (s_faulted || !WLSpriteLookSettings.Enabled) return;
|
||||||
|
var c = Cfg;
|
||||||
|
if (c == null) return;
|
||||||
|
|
||||||
|
bool want = ShouldBeOn;
|
||||||
|
if (want != s_applied)
|
||||||
|
{
|
||||||
|
try { if (want) ApplyNow("모드"); else RestoreNow("모드"); }
|
||||||
|
catch (System.Exception ex) { s_faulted = true; LastLog = "중단(예외) — " + ex.Message; RestoreNow("fault"); return; }
|
||||||
|
}
|
||||||
|
if (!s_applied) return;
|
||||||
|
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (c.sweepSeconds > 0f && now >= s_nextSweep)
|
||||||
|
{
|
||||||
|
s_nextSweep = now + Mathf.Max(0.1f, c.sweepSeconds);
|
||||||
|
try { Sweep(); }
|
||||||
|
catch (System.Exception ex) { s_faulted = true; LastLog = "중단(예외·스윕) — " + ex.Message; RestoreNow("fault"); return; }
|
||||||
|
}
|
||||||
|
UpdateAll(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void HookHub()
|
||||||
|
{
|
||||||
|
if (s_hooked) return;
|
||||||
|
s_hooked = true;
|
||||||
|
LookModeHub.CameraModeChanged += OnCameraMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UnhookHub()
|
||||||
|
{
|
||||||
|
if (!s_hooked) return;
|
||||||
|
s_hooked = false;
|
||||||
|
LookModeHub.CameraModeChanged -= OnCameraMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnCameraMode(int mode)
|
||||||
|
{
|
||||||
|
HubEvents++;
|
||||||
|
// 실제 적용은 Tick 이 ShouldBeOn 으로 판정한다(모드 전환 타이밍과 카메라 조립 순서를 안 탄다).
|
||||||
|
}
|
||||||
|
|
||||||
|
static SpriteBillboardRunner s_runner;
|
||||||
|
|
||||||
|
internal static void EnsureRunner()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying || s_runner != null) return;
|
||||||
|
var go = new GameObject("[WL814k] SpriteBillboardRunner");
|
||||||
|
go.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
s_runner = go.AddComponent<SpriteBillboardRunner>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
|
static void Boot()
|
||||||
|
{
|
||||||
|
if (!WLSpriteLookSettings.Enabled) return;
|
||||||
|
EnsureRunner();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브용 — 카운터 초기화(적용 상태는 유지).</summary>
|
||||||
|
public static void ResetDiagnostics()
|
||||||
|
{
|
||||||
|
Applies = Restores = Attached = Detached = HubEvents = Sweeps = FrameChanges = 0;
|
||||||
|
LastLog = "";
|
||||||
|
s_faulted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>스프라이트 교체의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||||||
|
internal sealed class SpriteBillboardRunner : MonoBehaviour
|
||||||
|
{
|
||||||
|
void Update() { SpriteBillboard.Tick(); }
|
||||||
|
void OnEnable() { SpriteBillboard.HookHub(); }
|
||||||
|
void OnDisable() { SpriteBillboard.UnhookHub(); SpriteBillboard.RestoreNow("runner-disable"); }
|
||||||
|
void OnApplicationQuit() { SpriteBillboard.RestoreNow("app-quit"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>🔴 에디터 전용 안전망 — 플레이모드를 벗어날 때 반드시 원복한다.</summary>
|
||||||
|
internal static class SpriteEditorSafetyNet
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
[UnityEditor.InitializeOnLoadMethod]
|
||||||
|
static void Hook()
|
||||||
|
{
|
||||||
|
UnityEditor.EditorApplication.playModeStateChanged -= OnPlayMode;
|
||||||
|
UnityEditor.EditorApplication.playModeStateChanged += OnPlayMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnPlayMode(UnityEditor.PlayModeStateChange s)
|
||||||
|
{
|
||||||
|
if (s == UnityEditor.PlayModeStateChange.ExitingPlayMode) SpriteBillboard.RestoreNow("exit-playmode");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d2ca3b74c48b2f34fbd64fa57133fb66
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLSpriteBakeSettings.cs — 「3D → 도트 스프라이트」 굽기 값의 단일 출처 (WL-814k · #814)
|
||||||
|
//
|
||||||
|
// 🔴 이 에셋에는 **특정 캐릭터 이름이 하나도 없다**. 굽는 대상 프리팹은 **인자**로 받는다
|
||||||
|
// (PD 가 캐릭터 에셋 교체를 검토 중 → 프리팹에 하드코딩하면 툴이 통째로 버려진다).
|
||||||
|
// 여기 있는 값은 「어떤 프리팹이든 이렇게 굽는다」는 규칙뿐이다.
|
||||||
|
//
|
||||||
|
// ■ 값을 코드 상수로 두지 않는다(C45) — 방향 수 · 프레임 수 · 해상도 · 팔레트 단계 · 외곽선 색 전부 에셋.
|
||||||
|
// ■ 동작 표는 **이름 규약**으로만 적는다 — Animator 상태 이름 후보 목록(첫 번째로 찾은 것을 쓴다).
|
||||||
|
// 실측(814k): PC `pcanim_mage` 와 잡몹 `Batty_Animations_Check` 가 **같은 상태 이름**을 쓴다
|
||||||
|
// (idle · run · attack · hit · die) → 이 표 하나로 PC·몹이 동시에 걸린다.
|
||||||
|
//
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake
|
||||||
|
{
|
||||||
|
/// <summary>구울 동작 1줄 — 「이 이름의 상태를 찾아 이만큼 프레임을 뽑아라」.</summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public sealed class WLSpriteBakeAction
|
||||||
|
{
|
||||||
|
[Tooltip("동작 키(시트 메타에 그대로 들어간다).")]
|
||||||
|
public string key = "idle";
|
||||||
|
|
||||||
|
[Tooltip("Animator 상태 이름 후보 — 위에서부터 찾아 처음 맞는 것을 굽는다.")]
|
||||||
|
public string[] stateNames = new string[] { "idle" };
|
||||||
|
|
||||||
|
[Tooltip("굽지는 않지만 런타임에서 이 동작으로 **볼** 상태 이름들(예: 스킬 상태를 공격 그림으로). " +
|
||||||
|
"컨트롤러에 실제로 있는 것만 시트 메타에 들어간다.")]
|
||||||
|
public string[] alsoMapStateNames = new string[0];
|
||||||
|
|
||||||
|
[Tooltip("이 동작을 몇 프레임으로 구울지(클립 길이를 균등 분할).")]
|
||||||
|
public int frames = 4;
|
||||||
|
|
||||||
|
[Tooltip("루프 동작인가(사망은 끄면 마지막 프레임에서 멈춘다).")]
|
||||||
|
public bool loop = true;
|
||||||
|
|
||||||
|
[Tooltip("이 동작을 못 찾으면 굽기를 실패로 볼지(끄면 「미확인」으로 건너뛴다).")]
|
||||||
|
public bool required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CreateAssetMenu(fileName = "WLSpriteBakeSettings", menuName = "WL/Sprite Bake Settings (도트 굽기)", order = 37)]
|
||||||
|
public sealed class WLSpriteBakeSettings : ScriptableObject
|
||||||
|
{
|
||||||
|
public const string ResourcesPath = "WL/WLSpriteBakeSettings";
|
||||||
|
|
||||||
|
static WLSpriteBakeSettings s_instance;
|
||||||
|
static bool s_tried;
|
||||||
|
|
||||||
|
/// <summary>에셋이 없으면 null. 에디터 툴만 쓴다.</summary>
|
||||||
|
public static WLSpriteBakeSettings Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load<WLSpriteBakeSettings>(ResourcesPath); }
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearCache() { s_instance = null; s_tried = false; }
|
||||||
|
|
||||||
|
// ── 카메라 (인게임 도트 카메라와 같게) ────────────────────────────────
|
||||||
|
[Header("카메라 — 인게임 도트 모드와 같은 구도로 굽는다")]
|
||||||
|
[Tooltip("내림각(도). 0 이면 아래 3값(811o 궤도 파라미터)에서 자동 산출한다. 실측 기본 = 43.609°.")]
|
||||||
|
public float pitchDegOverride = 0f;
|
||||||
|
|
||||||
|
[Tooltip("811o 궤도 파라미터 cameraHeight — pitchDegOverride 가 0 일 때 여기서 내림각을 만든다.")]
|
||||||
|
public float orbitCameraHeight = 80f;
|
||||||
|
public float orbitDistance = 3f;
|
||||||
|
public float orbitDistanceUp = 3f;
|
||||||
|
|
||||||
|
[Tooltip("방향 수 — 카메라 기준 상대 yaw 를 이 수로 나눈다. 8 = 45° 간격.")]
|
||||||
|
public int dirCount = 8;
|
||||||
|
|
||||||
|
// ── 픽셀 크기 ────────────────────────────────────────────────────────
|
||||||
|
[Header("픽셀 크기 — 도트 모드 화면 픽셀과 1:1 로 맞춘다")]
|
||||||
|
[Tooltip("픽셀 1개의 월드 크기(m). 0 이면 아래 두 값(도트 프리셋)에서 (2×orthoSize)/pixelHeight 로 산출. 실측 도트A = 0.0283333.")]
|
||||||
|
public float worldPerPixelOverride = 0f;
|
||||||
|
|
||||||
|
[Tooltip("도트 프리셋 orthoSize(직교 반높이 m) — worldPerPixelOverride 가 0 일 때 쓴다.")]
|
||||||
|
public float refOrthoSize = 3.4f;
|
||||||
|
|
||||||
|
[Tooltip("도트 프리셋 세로 픽셀 수 — worldPerPixelOverride 가 0 일 때 쓴다.")]
|
||||||
|
public int refPixelHeight = 240;
|
||||||
|
|
||||||
|
[Header("프레임 크기")]
|
||||||
|
[Tooltip("기본 프레임 해상도(px). autoFrameSize 가 꺼져 있으면 이 값을 그대로 쓴다.")]
|
||||||
|
public int frameSize = 64;
|
||||||
|
|
||||||
|
[Tooltip("켜면 실제 실루엣을 재서 프레임을 8의 배수로 자동 조정한다(꽉 차게 · 아래 min/max 안에서).")]
|
||||||
|
public bool autoFrameSize = true;
|
||||||
|
public int frameSizeMin = 16;
|
||||||
|
public int frameSizeMax = 128;
|
||||||
|
|
||||||
|
[Tooltip("실루엣 둘레에 남길 여백(px) — 외곽선 1px 이 잘리지 않게 최소 2 권장.")]
|
||||||
|
public int marginPx = 2;
|
||||||
|
|
||||||
|
[Tooltip("실루엣을 재는 1차 렌더 해상도(px). 캐릭터가 이보다 크면 잘린다 — 넉넉히.")]
|
||||||
|
public int probeSize = 192;
|
||||||
|
|
||||||
|
[Header("샘플링 경로")]
|
||||||
|
[Tooltip("🔴 기본(끔) = **진짜 Animator 구동**(anim.Play + anim.Update · 게임과 같은 컨트롤러·리타깃 · applyRootMotion=false). " +
|
||||||
|
"켜면 AnimationMode.SampleAnimationClip 로 굽는다 — 실측(814k)에서 이 경로는 **루트 모션을 포즈에 얹어** " +
|
||||||
|
"Ai01 Run 이 2.0 m, death 가 3.0 m 밀려 프레임 밖으로 나갔다(게임에는 안 나오는 그림). 진단용으로만 켠다.")]
|
||||||
|
public bool sampleWithAnimationMode = false;
|
||||||
|
|
||||||
|
// ── 도트화 (굽는 시점에 강제) ────────────────────────────────────────
|
||||||
|
[Header("도트화 — 굽는 시점에 강제한다")]
|
||||||
|
[Tooltip("외곽선 1px 을 알파 경계 바깥에 두른다.")]
|
||||||
|
public bool outline = true;
|
||||||
|
public Color outlineColor = new Color(0.055f, 0.043f, 0.067f, 1f);
|
||||||
|
|
||||||
|
[Tooltip("외곽선을 대각선 이웃까지 볼지(켜면 모서리가 더 두껍다).")]
|
||||||
|
public bool outlineDiagonal = false;
|
||||||
|
|
||||||
|
[Tooltip("팔레트 양자화 — 단계 수(2~256 · 6 = 도트 느낌). 0/1 이면 끈다.")]
|
||||||
|
public int paletteLevels = 6;
|
||||||
|
|
||||||
|
[Tooltip("🔴 켜면 **밝기만** 단계로 끊고 색상·채도는 유지한다(명암이 도트처럼 평평해진다). " +
|
||||||
|
"끄면 R·G·B 를 각각 끊는다(발주서 문구 그대로) — 실측(814k): 어두운 색이 채널마다 다른 쪽으로 " +
|
||||||
|
"반올림돼 **없던 붉은/초록 얼룩**이 생겼다. 그래서 기본을 밝기 단계로 뒀다.")]
|
||||||
|
public bool paletteValueBands = true;
|
||||||
|
|
||||||
|
[Tooltip("알파 이진화 임계(0~1). 이 값 이상만 남기고 나머지는 완전 투명 → 가장자리가 또렷해진다.")]
|
||||||
|
public float alphaCutoff = 0.5f;
|
||||||
|
|
||||||
|
// ── 렌더 ─────────────────────────────────────────────────────────────
|
||||||
|
[Header("렌더")]
|
||||||
|
[Tooltip("814c Toon 머티리얼(<이름>_Toon.mat)이 있으면 그것으로 바꿔 굽는다(원본 .mat 수정 0 · 인스턴스에만).")]
|
||||||
|
public bool useToonMaterials = true;
|
||||||
|
public string toonMaterialDir = "Assets/WL/Look/Env/Materials";
|
||||||
|
public string toonMaterialSuffix = "_Toon";
|
||||||
|
|
||||||
|
[Tooltip("주 방향광 — 게임 맵(WL_Nature) Sun 실측값.")]
|
||||||
|
public Vector3 sunEuler = new Vector3(26.4f, 238.6f, 0f);
|
||||||
|
public Color sunColor = new Color(1f, 0.92f, 0.703f, 1f);
|
||||||
|
public float sunIntensity = 1.2f;
|
||||||
|
|
||||||
|
[Tooltip("보조 방향광(그림자 없음) — 게임 맵 Sun_Ambient 실측값. intensity 0 이면 안 만든다.")]
|
||||||
|
public Vector3 fillEuler = new Vector3(36.1f, 116.1f, 0f);
|
||||||
|
public Color fillColor = new Color(0.596f, 0.703f, 0.839f, 1f);
|
||||||
|
public float fillIntensity = 0.3f;
|
||||||
|
|
||||||
|
[Tooltip("🔴 켜면 인게임과 같은 **Skybox 환경광**(Ingame.unity 실측 = m_AmbientMode 0 + 기본 스카이박스). " +
|
||||||
|
"끄면 아래 단색으로 굽는다(더 어둡다 · 재현성 우선일 때).")]
|
||||||
|
public bool useSkyboxAmbient = true;
|
||||||
|
|
||||||
|
[Tooltip("Skybox 환경광 세기(인게임 m_AmbientIntensity 실측 = 1).")]
|
||||||
|
public float ambientIntensity = 1f;
|
||||||
|
|
||||||
|
[Tooltip("useSkyboxAmbient 가 꺼졌을 때 쓰는 환경광 단색.")]
|
||||||
|
public Color ambient = new Color(0.212f, 0.227f, 0.259f, 1f);
|
||||||
|
|
||||||
|
// ── 출력 ─────────────────────────────────────────────────────────────
|
||||||
|
[Header("출력")]
|
||||||
|
[Tooltip("시트 PNG · 메타 SO 가 들어갈 폴더.")]
|
||||||
|
public string outputDir = "Assets/WL/Look/Sprite/Baked";
|
||||||
|
|
||||||
|
[Tooltip("시트 가로 칸 수. 0 이면 정사각에 가깝게 자동.")]
|
||||||
|
public int sheetColumns = 16;
|
||||||
|
|
||||||
|
[Tooltip("시트 가로 픽셀 상한(초과하면 칸 수를 줄인다).")]
|
||||||
|
public int sheetMaxWidth = 2048;
|
||||||
|
|
||||||
|
// ── 동작 표 ──────────────────────────────────────────────────────────
|
||||||
|
[Header("동작 표 — 🔴 프리팹 이름은 여기 없다(대상은 인자)")]
|
||||||
|
public WLSpriteBakeAction[] actions = new WLSpriteBakeAction[]
|
||||||
|
{
|
||||||
|
new WLSpriteBakeAction { key = "idle", stateNames = new[]{ "idle", "Idle", "wait", "IdleA", "IdleB" }, frames = 4, loop = true,
|
||||||
|
alsoMapStateNames = new[]{ "land", "getup" } },
|
||||||
|
new WLSpriteBakeAction { key = "run", stateNames = new[]{ "run", "Run", "move", "Move", "walk" }, frames = 6, loop = true,
|
||||||
|
alsoMapStateNames = new[]{ "dodge" } },
|
||||||
|
new WLSpriteBakeAction { key = "attack", stateNames = new[]{ "attack", "Attack", "atk", "ATK0", "ATK1" }, frames = 6, loop = true,
|
||||||
|
alsoMapStateNames = new[]{ "ATK2", "ATK3", "ATK4", "Shoot", "attack2", "skill2", "skill3", "skill4", "skill5", "casting", "casting_end" } },
|
||||||
|
new WLSpriteBakeAction { key = "hit", stateNames = new[]{ "hit", "Hit", "damage", "Damage" }, frames = 2, loop = true,
|
||||||
|
alsoMapStateNames = new[]{ "stun", "Swoon" } },
|
||||||
|
new WLSpriteBakeAction { key = "die", stateNames = new[]{ "die", "Die", "death", "Die0", "Die1" }, frames = 6, loop = false,
|
||||||
|
alsoMapStateNames = new[]{ "lying", "Die2" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>내림각(도) — override 가 0 이면 811o 궤도 수식으로 산출.</summary>
|
||||||
|
public float PitchDeg
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (pitchDegOverride > 0.01f) return pitchDegOverride;
|
||||||
|
return WL.CameraFraming.CameraFramingOverride.DownAngleDeg(orbitCameraHeight, orbitDistance, orbitDistanceUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>픽셀 1개의 월드 크기(m).</summary>
|
||||||
|
public float WorldPerPixel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (worldPerPixelOverride > 0.000001f) return worldPerPixelOverride;
|
||||||
|
int ph = Mathf.Max(1, refPixelHeight);
|
||||||
|
return (2f * refOrthoSize) / ph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e9dcf8e4e8011b84ab5e5df43a7e0255
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLSpriteLookSettings.cs — 런타임 빌보드 교체(도트 모드 한정)의 값 단일 출처 (WL-814k · #814)
|
||||||
|
//
|
||||||
|
// ■ C8 롤백: `enabled_ = 0` → 러너를 아예 만들지 않는다 = 원본 3D 100 %.
|
||||||
|
// ■ 🔴 여기에도 특정 캐릭터 이름은 **SO 참조 목록**으로만 들어간다(코드 상수 0).
|
||||||
|
// 시트 SO(WLSpriteSet)가 `actorNamePrefix` 를 들고 있으므로, 목록에 SO 를 넣기만 하면 대상이 된다.
|
||||||
|
//
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(fileName = "WLSpriteLookSettings", menuName = "WL/Sprite Look Settings (도트 빌보드)", order = 38)]
|
||||||
|
public sealed class WLSpriteLookSettings : ScriptableObject
|
||||||
|
{
|
||||||
|
public const string ResourcesPath = "WL/WLSpriteLookSettings";
|
||||||
|
|
||||||
|
static WLSpriteLookSettings s_instance;
|
||||||
|
static bool s_tried;
|
||||||
|
|
||||||
|
/// <summary>에셋이 없으면 null(호출부는 전부 건너뛴다 = C8).</summary>
|
||||||
|
public static WLSpriteLookSettings Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load<WLSpriteLookSettings>(ResourcesPath); }
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearCache() { s_instance = null; s_tried = false; }
|
||||||
|
|
||||||
|
/// <summary>프로브·QA 용 런타임 스위치(에셋을 안 건드리고 끈다).</summary>
|
||||||
|
public static bool RuntimeDisabled;
|
||||||
|
|
||||||
|
/// <summary>이 축이 살아 있는가 = 에셋 존재 · enabled_ · 런타임 스위치 off.</summary>
|
||||||
|
public static bool Enabled
|
||||||
|
{
|
||||||
|
get { if (RuntimeDisabled) return false; var s = Instance; return s != null && s.enabled_; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("공통 (C8 롤백)")]
|
||||||
|
[Tooltip("끄면 러너 0 · 교체 0 = 원본 3D 100 %.")]
|
||||||
|
public bool enabled_ = true;
|
||||||
|
|
||||||
|
[Tooltip("켜면 교체/복원마다 Console 에 1줄(문자열은 이 값으로 막고 만든다 · GC 주의).")]
|
||||||
|
public bool verboseLog = false;
|
||||||
|
|
||||||
|
[Header("언제 켜는가")]
|
||||||
|
[Tooltip("🔴 도트 모드(814d LookModeHub 프리셋 pixelRig)에서만 켠다. 끄면 항상 켠다(디버그).")]
|
||||||
|
public bool onlyInPixelMode = true;
|
||||||
|
|
||||||
|
[Tooltip("새로 스폰된 액터를 찾아보는 주기(초). 0.5 = 프레임당 비용 0에 가깝다.")]
|
||||||
|
public float sweepSeconds = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("동시에 스프라이트로 바꿀 액터 수 상한(초과분은 원본 3D 그대로).")]
|
||||||
|
public int maxActors = 24;
|
||||||
|
|
||||||
|
[Header("시트 목록 — 대상 = 여기 있는 SO 의 actorNamePrefix")]
|
||||||
|
public WLSpriteSet[] sets = new WLSpriteSet[0];
|
||||||
|
|
||||||
|
[Header("빌보드")]
|
||||||
|
[Tooltip("🔴 기본(끔) = 카메라 정렬(구운 각도 그대로 · 스프라이트 1px = 화면 1px). " +
|
||||||
|
"켜면 Y축만 회전하는 수직 쿼드 — 구울 때의 내림각만큼 세로가 눌린다(cos 43.6° = 0.72).")]
|
||||||
|
public bool yAxisOnly = false;
|
||||||
|
|
||||||
|
[Tooltip("yAxisOnly 일 때 눌림을 1/cos(내림각) 로 보정한다.")]
|
||||||
|
public bool yAxisOnlyCompensate = true;
|
||||||
|
|
||||||
|
[Tooltip("쿼드를 카메라 쪽으로 살짝 당긴다(m) — z-파이팅 여유분.")]
|
||||||
|
public float depthBias = 0.02f;
|
||||||
|
|
||||||
|
[Tooltip("🔴 켜면 쿼드가 지면에 파묻히지 않도록 필요한 만큼 자동으로 더 당긴다. " +
|
||||||
|
"쿼드는 카메라와 같은 각도로 기울어 있어서 **발밑보다 아래쪽 부분이 지면 속으로 들어간다** — " +
|
||||||
|
"실측(814k): 끄면 캐릭터 아랫부분 7 px 이 지면에 잘렸다. 직교 카메라(도트 모드)에서는 " +
|
||||||
|
"시선 방향으로 당겨도 화면 위치가 1 px 도 안 움직이므로 공짜다.")]
|
||||||
|
public bool autoDepthBias = true;
|
||||||
|
|
||||||
|
[Tooltip("알파 컷 임계(구울 때 이미 이진화했으므로 0.5 고정 권장).")]
|
||||||
|
[Range(0.01f, 0.99f)] public float alphaCutoff = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("빌보드 쿼드가 그려질 레이어(-1 = 액터와 같은 레이어).")]
|
||||||
|
public int quadLayer = -1;
|
||||||
|
|
||||||
|
[Tooltip("SkinnedMeshRenderer 외에 일반 MeshRenderer(무기·장식)도 같이 끈다. " +
|
||||||
|
"구운 시트에는 그것들도 그려져 있으므로 켜 두는 것이 맞다. 파티클은 건드리지 않는다.")]
|
||||||
|
public bool alsoHideMeshRenderers = true;
|
||||||
|
|
||||||
|
[Tooltip("그림자 — 구운 스프라이트는 그림자를 못 만든다(한계). 켜면 쿼드 모양 그림자가 생겨 더 이상하다.")]
|
||||||
|
public bool castShadows = false;
|
||||||
|
|
||||||
|
[Header("동작 폴링")]
|
||||||
|
[Tooltip("Animator 상태를 몇 초마다 다시 볼지. 0 = 매 프레임(가장 정확 · 그래도 할당 0).")]
|
||||||
|
public float statePollSeconds = 0f;
|
||||||
|
|
||||||
|
[Tooltip("모르는 상태에 들어갔을 때 쓸 동작 키.")]
|
||||||
|
public string fallbackActionKey = "idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f39fe71df28ff94ebdc9f76766e84e9
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLSpriteSet.cs — 「3D 를 도트로 구운」 스프라이트 시트 1장의 메타 (WL-814k · #814)
|
||||||
|
//
|
||||||
|
// PD 지시(2026-09-12) 「치비 느낌이 나지 않아도 도트 그림으로 구워 쓰는 방식으로 해봐.」
|
||||||
|
//
|
||||||
|
// ■ 이 에셋이 들고 있는 것 — 그림(PNG) 1장 + 그 그림을 「어떻게 읽는지」의 표
|
||||||
|
// · 시트 = 방향 × 동작 × 프레임을 격자로 붙인 PNG 1장(서브 스프라이트 0 · UV 는 런타임 산술).
|
||||||
|
// · 동작표 = Animator **상태 이름** → 시트 시작 인덱스 · 프레임 수 · fps.
|
||||||
|
// 런타임(SpriteBillboard)은 원본 Animator 를 **폴링**만 한다 — 원본 코드 수정 0.
|
||||||
|
// · worldPerPixel = 구울 때 픽셀 1개가 차지한 월드 크기(m). 런타임 쿼드 크기 = 프레임 px × 이 값.
|
||||||
|
// 도트 모드의 화면 픽셀 크기와 같게 구우면 **스프라이트 1픽셀 = 화면 1픽셀**이 된다.
|
||||||
|
// · footPixelX/Y = 월드 원점(캐릭터 발밑)이 프레임 안 어느 픽셀에 찍혔는지(좌하단 원점).
|
||||||
|
// 런타임은 이 값으로 쿼드를 발밑에 맞춘다 — 동작마다 키가 달라져도 발이 안 뜬다.
|
||||||
|
//
|
||||||
|
// ■ 인덱스 규약 (시트 안 위치)
|
||||||
|
// globalIndex = action.startIndex + dir * action.frameCount + frame
|
||||||
|
// col = globalIndex % columns · row = globalIndex / columns (row 0 = 시트 **위**)
|
||||||
|
//
|
||||||
|
// 🔴 이 파일은 데이터만 든다. 굽는 쪽은 Editor/WLSpriteBaker.cs, 쓰는 쪽은 SpriteBillboard.cs.
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace WL.Look.SpriteBake
|
||||||
|
{
|
||||||
|
/// <summary>동작 1개(대기·이동·공격·피격·사망 …)가 시트 어디에 있는지.</summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public sealed class WLSpriteAction
|
||||||
|
{
|
||||||
|
[Tooltip("동작 키 — idle / run / attack / hit / die (SO 의 동작 표와 같은 문자열).")]
|
||||||
|
public string key = "idle";
|
||||||
|
|
||||||
|
[Tooltip("이 동작으로 볼 Animator 상태 이름들(레이어 0 shortNameHash 로 비교).")]
|
||||||
|
public string[] stateNames = new string[0];
|
||||||
|
|
||||||
|
[Tooltip("실제로 구운 AnimationClip 이름(보고·디버그용).")]
|
||||||
|
public string clipName = "";
|
||||||
|
|
||||||
|
[Tooltip("시트 안 시작 인덱스(방향 0 · 프레임 0).")]
|
||||||
|
public int startIndex;
|
||||||
|
|
||||||
|
[Tooltip("방향 1개당 프레임 수.")]
|
||||||
|
public int frameCount = 1;
|
||||||
|
|
||||||
|
[Tooltip("재생 fps(클립 길이 ÷ 프레임 수 로 산출).")]
|
||||||
|
public float fps = 8f;
|
||||||
|
|
||||||
|
[Tooltip("클립이 루프인가(사망은 false → 마지막 프레임에서 멈춘다).")]
|
||||||
|
public bool loop = true;
|
||||||
|
|
||||||
|
[Tooltip("원본 클립 길이(초).")]
|
||||||
|
public float clipLength = 1f;
|
||||||
|
|
||||||
|
// 런타임 캐시(직렬화 안 함 · 첫 사용 때 1회 만든다 → 이후 할당 0)
|
||||||
|
[System.NonSerialized] public int[] hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>구운 스프라이트 시트 1장 + 읽는 법. 프리팹 1종당 1개.</summary>
|
||||||
|
[CreateAssetMenu(fileName = "WLSpriteSet", menuName = "WL/Sprite Set (구운 도트)", order = 36)]
|
||||||
|
public sealed class WLSpriteSet : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("출처")]
|
||||||
|
[Tooltip("어느 프리팹을 구웠는가(경로).")]
|
||||||
|
public string sourcePrefab = "";
|
||||||
|
|
||||||
|
[Tooltip("액터 이름이 이 문자열로 시작하면 이 시트를 쓴다(예: Ai01 → 'Ai01(Clone)' 매칭).")]
|
||||||
|
public string actorNamePrefix = "";
|
||||||
|
|
||||||
|
[Tooltip("구운 시각 · 툴 버전(보고용).")]
|
||||||
|
public string bakeStamp = "";
|
||||||
|
|
||||||
|
[Header("시트")]
|
||||||
|
public Texture2D sheet;
|
||||||
|
public int frameWidth = 64;
|
||||||
|
public int frameHeight = 64;
|
||||||
|
public int columns = 16;
|
||||||
|
public int rows = 1;
|
||||||
|
|
||||||
|
[Header("굽기 파라미터")]
|
||||||
|
[Tooltip("방향 수(카메라 기준 상대 yaw 를 이 수로 나눈다).")]
|
||||||
|
public int dirCount = 8;
|
||||||
|
|
||||||
|
[Tooltip("구울 때 픽셀 1개가 차지한 월드 크기(m). 런타임 쿼드 크기 = 프레임 px × 이 값.")]
|
||||||
|
public float worldPerPixel = 0.0283333f;
|
||||||
|
|
||||||
|
[Tooltip("구울 때 루트 lossyScale.y. 런타임 액터 스케일이 다르면 이 비율로 쿼드를 키운다.")]
|
||||||
|
public float bakeScale = 1f;
|
||||||
|
|
||||||
|
[Tooltip("월드 원점(발밑)이 프레임 안 어느 픽셀에 찍혔는지 — 좌하단 원점 · 픽셀 단위.")]
|
||||||
|
public float footPixelX = 32f;
|
||||||
|
public float footPixelY = 4f;
|
||||||
|
|
||||||
|
[Tooltip("구울 때 카메라 내림각(도). 런타임 도트 카메라와 같아야 한다.")]
|
||||||
|
public float pitchDeg = 43.609f;
|
||||||
|
|
||||||
|
[Header("동작 표")]
|
||||||
|
public WLSpriteAction[] actions = new WLSpriteAction[0];
|
||||||
|
|
||||||
|
/// <summary>방향 1개 기준 총 프레임 수(= 시트 총 칸 수 ÷ 방향 수).</summary>
|
||||||
|
public int TotalFrames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
if (actions != null) for (int i = 0; i < actions.Length; i++) if (actions[i] != null) n += actions[i].frameCount;
|
||||||
|
return n * Mathf.Max(1, dirCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Animator 상태 해시(레이어 0 shortNameHash)로 동작을 찾는다. 없으면 null.</summary>
|
||||||
|
public WLSpriteAction FindByStateHash(int shortNameHash)
|
||||||
|
{
|
||||||
|
if (actions == null) return null;
|
||||||
|
for (int i = 0; i < actions.Length; i++)
|
||||||
|
{
|
||||||
|
var a = actions[i];
|
||||||
|
if (a == null) continue;
|
||||||
|
if (a.hashes == null) BuildHashes(a);
|
||||||
|
for (int h = 0; h < a.hashes.Length; h++) if (a.hashes[h] == shortNameHash) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>키로 동작을 찾는다(폴백용 · 예: "idle").</summary>
|
||||||
|
public WLSpriteAction FindByKey(string key)
|
||||||
|
{
|
||||||
|
if (actions == null || string.IsNullOrEmpty(key)) return null;
|
||||||
|
for (int i = 0; i < actions.Length; i++) if (actions[i] != null && actions[i].key == key) return actions[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BuildHashes(WLSpriteAction a)
|
||||||
|
{
|
||||||
|
var src = a.stateNames ?? new string[0];
|
||||||
|
a.hashes = new int[src.Length];
|
||||||
|
for (int i = 0; i < src.Length; i++) a.hashes[i] = Animator.StringToHash(src[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>시트가 실제로 쓸 수 있는 상태인가.</summary>
|
||||||
|
public bool IsUsable
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return sheet != null && frameWidth > 0 && frameHeight > 0 && columns > 0
|
||||||
|
&& dirCount > 0 && actions != null && actions.Length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>globalIndex → 시트 UV 사각형(x, y, w, h · 0~1). 할당 0.</summary>
|
||||||
|
public Vector4 UvRect(int globalIndex)
|
||||||
|
{
|
||||||
|
int cols = Mathf.Max(1, columns);
|
||||||
|
int col = globalIndex % cols;
|
||||||
|
int row = globalIndex / cols;
|
||||||
|
float tw = sheet != null ? sheet.width : 1f;
|
||||||
|
float th = sheet != null ? sheet.height : 1f;
|
||||||
|
float u = (col * frameWidth) / tw;
|
||||||
|
float v = (th - (row + 1) * frameHeight) / th; // row 0 = 시트 위
|
||||||
|
return new Vector4(frameWidth / tw, frameHeight / th, u, v); // (scaleX, scaleY, offsetX, offsetY) = _ST 규약
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7485ff60a4b963544b28a1b345440fb9
|
||||||
Loading…
Reference in New Issue