Projext/Assets/02_Scripts/Camera/Effects/ToonCameraEffect.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

241 lines
12 KiB
C#

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<ToonCameraEffect>() == null) // 안 붙어있으면
{
mainCam.gameObject.AddComponent<ToonCameraEffect>(); // 부착
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<Camera>();
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<Shader>("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);
}
}
}