using UnityEngine; // 유니티 엔진 기본 기능 // ══════════════════════════════════════════════════════════════════ // ToonCameraEffect v3 — "횃불 고딕" 스타일 후처리 // // [컨셉] Curse of the Dead Gods 풍 // 어둠 속 따뜻한 빛 + 차가운 그림자 + 강한 비네팅 + 외곽선 // ══════════════════════════════════════════════════════════════════ [ExecuteInEditMode] // 에디터 미리보기 [RequireComponent(typeof(Camera))] // Camera 필수 public class ToonCameraEffect : MonoBehaviour { // ───────────────────────────────────────────────────────────── // 런타임 자동 부착 // ───────────────────────────────────────────────────────────── [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoAttachToMainCamera() // 메인 카메라에 자동 부착 { Camera mainCam = Camera.main; // 메인 카메라 탐색 if (mainCam == null) return; // 없으면 중단 if (mainCam.GetComponent() == null) // 안 붙어있으면 { mainCam.gameObject.AddComponent(); // 부착 Debug.Log("[ToonCameraEffect] 메인 카메라에 횃불 고딕 이펙트 자동 부착!"); } } // ───────────────────────────────────────────────────────────── // 어둠 설정 // ───────────────────────────────────────────────────────────── [Header("=== 어둠 (Darkness) ===")] [Tooltip("전체 어둡게 깔기 (높을수록 어두움)")] [Range(0f, 0.3f)] [SerializeField] private float darkCrush = 0.08f; [Tooltip("명암 대비 강화")] [Range(1.0f, 2.5f)] [SerializeField] private float contrastBoost = 1.4f; [Tooltip("최소 밝기 (완전 검정 방지)")] [Range(0f, 0.15f)] [SerializeField] private float blackPoint = 0.03f; // ───────────────────────────────────────────────────────────── // 색조 분리 설정 // ───────────────────────────────────────────────────────────── [Header("=== 색조 (Color Grading) ===")] [Tooltip("밝은 영역 색상 (따뜻한 금색)")] [SerializeField] private Color warmColor = new Color(1.0f, 0.85f, 0.55f, 1f); [Tooltip("어두운 영역 색상 (차가운 남보라)")] [SerializeField] private Color coolColor = new Color(0.25f, 0.2f, 0.45f, 1f); [Tooltip("따뜻/차가운 경계점")] [Range(0.1f, 0.7f)] [SerializeField] private float colorSplit = 0.35f; [Tooltip("색조 적용 강도")] [Range(0f, 1f)] [SerializeField] private float gradingStrength = 0.5f; [Tooltip("탈색 정도 (높을수록 무채색)")] [Range(0f, 0.7f)] [SerializeField] private float desaturation = 0.3f; // ───────────────────────────────────────────────────────────── // 비네팅 설정 // ───────────────────────────────────────────────────────────── [Header("=== 비네팅 (Vignette) ===")] [Tooltip("비네팅 강도")] [Range(0f, 2f)] [SerializeField] private float vignetteIntensity = 1.2f; [Tooltip("비네팅 반경 (작을수록 좁은 시야)")] [Range(0.3f, 1.5f)] [SerializeField] private float vignetteRadius = 0.75f; [Tooltip("비네팅 부드러움")] [Range(0.1f, 1.0f)] [SerializeField] private float vignetteSoftness = 0.45f; [Tooltip("비네팅 색상 (짙은 남색)")] [SerializeField] private Color vignetteColor = new Color(0.02f, 0.01f, 0.05f, 1f); // ───────────────────────────────────────────────────────────── // 외곽선 설정 // ───────────────────────────────────────────────────────────── [Header("=== 외곽선 (Edge) ===")] [Tooltip("외곽선 색상")] [SerializeField] private Color edgeColor = new Color(0.03f, 0.01f, 0.02f, 1f); [Tooltip("외곽선 두께")] [Range(0.5f, 5.0f)] [SerializeField] private float edgeThickness = 1.8f; [Tooltip("깊이 엣지 감도")] [Range(0.0001f, 0.05f)] [SerializeField] private float depthThreshold = 0.003f; [Tooltip("노멀 엣지 감도")] [Range(0.1f, 1.5f)] [SerializeField] private float normalThreshold = 0.5f; // ───────────────────────────────────────────────────────────── // 포스터라이즈 설정 // ───────────────────────────────────────────────────────────── [Header("=== 포스터라이즈 (약하게) ===")] [Tooltip("색상 단계")] [Range(2, 20)] [SerializeField] private int colorSteps = 8; [Tooltip("밝기 단계")] [Range(2, 12)] [SerializeField] private int luminanceSteps = 5; [Tooltip("포스터라이즈 강도 (약하게 권장)")] [Range(0f, 1f)] [SerializeField] private float posterizeStrength = 0.35f; // ───────────────────────────────────────────────────────────── // 내부 변수 // ───────────────────────────────────────────────────────────── private Material _material; private Shader _shader; // 쉐이더 프로퍼티 ID 캐싱 private static readonly int PropDarkCrush = Shader.PropertyToID("_DarkCrush"); private static readonly int PropContrastBoost = Shader.PropertyToID("_ContrastBoost"); private static readonly int PropBlackPoint = Shader.PropertyToID("_BlackPoint"); private static readonly int PropWarmColor = Shader.PropertyToID("_WarmColor"); private static readonly int PropCoolColor = Shader.PropertyToID("_CoolColor"); private static readonly int PropColorSplit = Shader.PropertyToID("_ColorSplit"); private static readonly int PropGradingStrength = Shader.PropertyToID("_GradingStrength"); private static readonly int PropDesaturation = Shader.PropertyToID("_Desaturation"); private static readonly int PropVignetteIntensity = Shader.PropertyToID("_VignetteIntensity"); private static readonly int PropVignetteRadius = Shader.PropertyToID("_VignetteRadius"); private static readonly int PropVignetteSoftness = Shader.PropertyToID("_VignetteSoftness"); private static readonly int PropVignetteColor = Shader.PropertyToID("_VignetteColor"); private static readonly int PropEdgeColor = Shader.PropertyToID("_EdgeColor"); private static readonly int PropEdgeThickness = Shader.PropertyToID("_EdgeThickness"); private static readonly int PropDepthThreshold = Shader.PropertyToID("_DepthThreshold"); private static readonly int PropNormalThreshold = Shader.PropertyToID("_NormalThreshold"); private static readonly int PropColorSteps = Shader.PropertyToID("_ColorSteps"); private static readonly int PropLuminanceSteps = Shader.PropertyToID("_LuminanceSteps"); private static readonly int PropPosterizeStrength = Shader.PropertyToID("_PosterizeStrength"); // ───────────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────────── private void Awake() { Camera cam = GetComponent(); if (cam != null) { cam.depthTextureMode |= DepthTextureMode.Depth | DepthTextureMode.DepthNormals; // 깊이+노멀 활성화 } } private bool EnsureMaterial() { if (_material != null) return true; if (_shader == null) _shader = Shader.Find("Hidden/ToonPostProcess"); if (_shader == null) _shader = Resources.Load("Shaders/ToonPostProcess"); if (_shader == null || !_shader.isSupported) { Debug.LogError("[ToonCameraEffect] 쉐이더를 찾을 수 없습니다: Hidden/ToonPostProcess"); enabled = false; return false; } _material = new Material(_shader); _material.hideFlags = HideFlags.HideAndDontSave; return true; } // ───────────────────────────────────────────────────────────── // 후처리 적용 // ───────────────────────────────────────────────────────────── private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (!EnsureMaterial()) { Graphics.Blit(source, destination); return; } // 어둠 _material.SetFloat(PropDarkCrush, darkCrush); _material.SetFloat(PropContrastBoost, contrastBoost); _material.SetFloat(PropBlackPoint, blackPoint); // 색조 _material.SetColor(PropWarmColor, warmColor); _material.SetColor(PropCoolColor, coolColor); _material.SetFloat(PropColorSplit, colorSplit); _material.SetFloat(PropGradingStrength, gradingStrength); _material.SetFloat(PropDesaturation, desaturation); // 비네팅 _material.SetFloat(PropVignetteIntensity, vignetteIntensity); _material.SetFloat(PropVignetteRadius, vignetteRadius); _material.SetFloat(PropVignetteSoftness, vignetteSoftness); _material.SetColor(PropVignetteColor, vignetteColor); // 외곽선 _material.SetColor(PropEdgeColor, edgeColor); _material.SetFloat(PropEdgeThickness, edgeThickness); _material.SetFloat(PropDepthThreshold, depthThreshold); _material.SetFloat(PropNormalThreshold, normalThreshold); // 포스터라이즈 _material.SetFloat(PropColorSteps, colorSteps); _material.SetFloat(PropLuminanceSteps, luminanceSteps); _material.SetFloat(PropPosterizeStrength, posterizeStrength); Graphics.Blit(source, destination, _material); } private void OnDestroy() { if (_material != null) { if (Application.isPlaying) Destroy(_material); else DestroyImmediate(_material); } } }