// ───────────────────────────────────────────────────────────────────────────── // WLCharacterBacklightSettings.cs — 캐릭터 역광(림·라이트 랩·피부 투과) + 번짐(Bloom) 값 (C45 · 코드 상수 금지) // // 2026-09-17 PD 「캐릭터가 조금 더 돋보이게」 + 팁(차가운 역광 · SSS · Bloom). // enabled_ = 0 → 아무 것도 덧붙이지 않는다(캐릭터 재질 배열 원래대로) · bloomEnabled = 0 → 카메라/볼륨 0 변경. // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Look.Character { public class WLCharacterBacklightSettings : ScriptableObject { [Header("스위치")] [Tooltip("0 = 역광 패스 전체 off(재질 배열 원래대로).")] public int enabled_ = 1; [Tooltip("1 이면 적용 내역을 로그로 남긴다.")] public int verboseLog = 0; [Header("대상")] [Tooltip("이 셰이더 이름의 재질을 가진 렌더러에만 덧붙인다(얼굴 Unlit/Transparent · 파티클 제외). 비우면 파티클 외 전부.")] public string shaderNameFilter = "Shader Graphs/Toon"; [Tooltip("재질 이름에 이 글자가 들어 있으면 피부로 보고 피부 투과(SSS)를 켠 재질을 물린다.")] public string[] skinMaterialHints = { "Skin" }; [Header("① 역광 림 — 메인 조명(따뜻함)과 반대인 차가운 색")] [ColorUsage(false, true)] public Color rimColor = new Color(0.28f, 0.62f, 1f, 1f); [Tooltip("림 밝기 배수(1 초과 = HDR · Bloom 이 집어낸다).")] public float rimStrength = 1.45f; [Header("①-b 화면 공간 깊이 림(기본) — 실루엣 바깥쪽에만 · 화면 픽셀 굵기 일정")] [Tooltip("1 = 깊이 림 사용(URP 에셋의 Depth Texture 가 꺼져 있으면 자동으로 프레넬만 쓴다). 실측: 프레넬만 쓰면 머리카락 안쪽 가닥마다 줄이 생긴다.")] public int rimUseDepth = 1; [Tooltip("띠 굵기 = 화면 짧은 변 × 이 값(0.0045 → 1080 기준 ≈ 5 px).")] [Range(0f, 0.02f)] public float rimWidth = 0.0045f; [Tooltip("역광 쪽 이웃 픽셀이 이만큼(m) 더 멀면 실루엣으로 본다(작으면 머리카락 가닥에도 생긴다).")] public float rimDepthThreshold = 0.12f; [Tooltip("깊이 림에 섞는 프레넬 림의 양 — 가까울 때(두께감).")] [Range(0f, 1f)] public float rimFresnelNear = 0.6f; [Tooltip("깊이 림에 섞는 프레넬 림의 양 — 멀 때(0 = 깊이 림만 · 가닥 줄 없음).")] [Range(0f, 1f)] public float rimFresnelFar = 0f; [Header("①-a 프레넬 림(보조 · 깊이 텍스처가 없을 때는 이것만)")] [Tooltip("가까울 때(rimNearDistance 이하)의 띠 굵기 — 클수록 가는 띠(0.4 = 굵게 · 0.7 = 가늘게).")] [Range(0f, 1f)] public float rimThreshold = 0.52f; [Tooltip("멀 때(rimFarDistance 이상)의 띠 굵기 — 작게 줘서 굵게. 게임 카메라(섬 12 m · 던전 11 m)에서 캐릭터가 ~70 px 라 가는 띠는 1 px 로 사라진다(실측).")] [Range(0f, 1f)] public float rimThresholdFar = 0.36f; [Tooltip("이 거리(m) 이하 = 가까움.")] public float rimNearDistance = 3f; [Tooltip("이 거리(m) 이상 = 멂.")] public float rimFarDistance = 12f; [Tooltip("띠 가장자리 부드러움(작을수록 셀 느낌).")] [Range(0.001f, 0.5f)] public float rimSoftness = 0.05f; [Tooltip("0 = 둘레 전부 · 1 = 가상 역광 쪽 윤곽만.")] [Range(0f, 1f)] public float rimDirectional = 0.85f; [Tooltip("림이 바탕을 덮는 정도(1 = 림 색 그대로 · 0 = 순수 가산). 실측: 순수 가산은 노란 머리·살색 위에서 흰 테가 돼 차가운 색이 안 보였다.")] [Range(0f, 1f)] public float rimOpacity = 0.85f; [Header("가상 역광 위치(카메라 기준) — 캐릭터 뒤 + 위 + 옆")] [Tooltip("위쪽 성분(클수록 머리·어깨 윗선에 맺힌다).")] public float backlightUp = 0.7f; [Tooltip("옆 성분(+ = 화면 오른쪽 · − = 왼쪽). 메인 조명이 왼쪽 앞에서 오므로 오른쪽 뒤가 자연스럽다.")] public float backlightSide = 0.8f; [Header("② 라이트 랩 — 빛이 안쪽으로 스며드는 옅은 번짐")] [ColorUsage(false, true)] public Color wrapColor = new Color(0.75f, 0.85f, 1f, 1f); public float wrapStrength = 0.12f; public float wrapPower = 1.6f; [Header("③ 피부 투과(SSS 흉내) — 피부 재질에만")] [ColorUsage(false, true)] public Color sssColor = new Color(1f, 0.32f, 0.22f, 1f); public float sssStrength = 1.5f; public float sssPower = 1.7f; [Header("④ 번짐(Bloom) — 역광이 화사하게 번지게(전체 화면 패스 · 모바일 비용 있음)")] [Tooltip("0 = 카메라·볼륨을 건드리지 않는다.")] public int bloomEnabled = 1; [Tooltip("이 씬이 떠 있을 때만 켠다(전투 맵의 기존 후처리는 그대로).")] public string[] bloomScenes = { "Level01" }; [Tooltip("이 밝기 이상만 번진다(HDR 1 초과 = 림·반짝임만).")] public float bloomThreshold = 1.05f; public float bloomIntensity = 0.55f; [Range(0f, 1f)] public float bloomScatter = 0.55f; [Tooltip("1 = 카메라 HDR 을 켠다(림만 골라 번지게 · 끄면 흰 거품·밝은 모래도 같이 번진다).")] public int bloomHdr = 1; [Tooltip("1 = 1/4 해상도에서 시작(모바일 권장).")] public int bloomQuarter = 1; [Tooltip("블러 단계 수(작을수록 쌈).")] public int bloomMaxIterations = 4; // ───────────────────────────────────────── 싱글턴(Resources) public const string ResourcePath = "WL/WLCharacterBacklightSettings"; static WLCharacterBacklightSettings s_inst; static bool s_tried; public static WLCharacterBacklightSettings Instance { get { if (s_inst == null && !s_tried) { s_tried = true; s_inst = Resources.Load(ResourcePath); } return s_inst; } } public static void Invalidate() { s_inst = null; s_tried = false; } } }