205 lines
12 KiB
C#
205 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
}
|