Project_WL/Assets/WL/Look/Sprite/WLSpriteBakeSettings.cs

205 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}
}