164 lines
7.7 KiB
C#
164 lines
7.7 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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 규약
|
||
}
|
||
}
|
||
}
|