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