// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// 구울 동작 1줄 — 「이 이름의 상태를 찾아 이만큼 프레임을 뽑아라」.
[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;
/// 에셋이 없으면 null. 에디터 툴만 쓴다.
public static WLSpriteBakeSettings Instance
{
get
{
if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load(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" } },
};
/// 내림각(도) — override 가 0 이면 811o 궤도 수식으로 산출.
public float PitchDeg
{
get
{
if (pitchDegOverride > 0.01f) return pitchDegOverride;
return WL.CameraFraming.CameraFramingOverride.DownAngleDeg(orbitCameraHeight, orbitDistance, orbitDistanceUp);
}
}
/// 픽셀 1개의 월드 크기(m).
public float WorldPerPixel
{
get
{
if (worldPerPixelOverride > 0.000001f) return worldPerPixelOverride;
int ph = Mathf.Max(1, refPixelHeight);
return (2f * refOrthoSize) / ph;
}
}
}
}