// NiloToon 캐릭터 전용 색조 보정 셰이더 (YAMO) // // Pass 구성: // Pass 0 "Copy" : 스텐실 없음 — 카메라 컬러를 임시 RT로 단순 복사 // Pass 1 "ToneAdjust" : Stencil Equal(128) — 캐릭터 픽셀에만 색조 보정 // Pass 2 "ToneAdjust_Full" : 스텐실 없음 — 전체 화면에 색조 보정 (디버그용) // Pass 3 "StencilView" : Stencil Equal(128) — 마스크 시각화 (디버그용) // Pass 4 "DebugStencilFill" : 전체 화면 stencil=128 기록 (진단용) // Pass 5 "MeshStencilFill" : 메시 기반 stencil=128 기록 (ColorMask 0) // Pass 6 "MeshMaskFill" : 메시 → 마스크=1 + stencil=128 (Auto Match용) // Pass 7 "MaskedDownsample" : 마스크 기반 가중 다운샘플 (첫 단계) // Pass 8 "WeightedDownsample": 알파 기반 가중 다운샘플 (후속 단계) Shader "YAMO/NiloToonCharToneAdjust" { Properties { } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // _MainTex는 Properties에 선언하지 않습니다. // Properties에 선언하면 Material 기본값이 Global 값보다 우선합니다. TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _CharToneAdjust_Lift; float4 _CharToneAdjust_Gamma; float4 _CharToneAdjust_Gain; float4 _CharToneAdjust_Shadows; float4 _CharToneAdjust_Midtones; float4 _CharToneAdjust_Highlights; float4 _CharToneAdjust_SMHRange; float _CharToneAdjust_Saturation; float _CharToneAdjust_PostExposure; float _CharToneAdjust_BlendAmount; #define LUMA_WEIGHTS float3(0.2126729, 0.7151522, 0.0721750) struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(uint vertexID : SV_VertexID) { Varyings output; float2 uv = float2((vertexID << 1u) & 2u, vertexID & 2u); output.positionCS = float4(uv * 2.0 - 1.0, 0.0, 1.0); #if UNITY_UV_STARTS_AT_TOP uv.y = 1.0 - uv.y; #endif output.uv = uv; return output; } float3 ApplyLiftGammaGain(float3 color, float3 lift, float3 gamma, float3 gain) { color = color * gain + lift; gamma = max(gamma, 1e-4); color = sign(color) * pow(abs(color) + 1e-5, 1.0 / gamma); return max(color, 0.0); } float3 ApplySMH(float3 color, float3 shadows, float3 midtones, float3 highlights, float shadowStart, float shadowEnd, float highlightStart, float highlightEnd) { float lum = dot(color, LUMA_WEIGHTS); float shadowW = 1.0 - smoothstep(shadowStart, shadowEnd, lum); float highlightW = smoothstep(highlightStart, highlightEnd, lum); float midtoneW = saturate(1.0 - shadowW - highlightW); color += shadows * shadowW; color += midtones * midtoneW; color += highlights * highlightW; return max(color, 0.0); } float3 ApplySaturation(float3 color, float sat) { float lum = dot(color, LUMA_WEIGHTS); return max(lerp(lum.xxx, color, sat), 0.0); } half4 ToneAdjustFrag(Varyings input) : SV_Target { half4 original = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); float3 color = original.rgb; color *= _CharToneAdjust_PostExposure; color = ApplyLiftGammaGain(color, _CharToneAdjust_Lift.rgb, _CharToneAdjust_Gamma.rgb, _CharToneAdjust_Gain.rgb); color = ApplySMH(color, _CharToneAdjust_Shadows.rgb, _CharToneAdjust_Midtones.rgb, _CharToneAdjust_Highlights.rgb, _CharToneAdjust_SMHRange.x, _CharToneAdjust_SMHRange.y, _CharToneAdjust_SMHRange.z, _CharToneAdjust_SMHRange.w); color = ApplySaturation(color, _CharToneAdjust_Saturation); color = lerp(original.rgb, color, _CharToneAdjust_BlendAmount); return half4(color, original.a); } ENDHLSL // ── Pass 0: Copy ────────────────────────────────────────────── Pass { Name "NiloToonCharToneAdjust_Copy" ZTest Always ZWrite Off Cull Off HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag half4 Frag(Varyings input) : SV_Target { return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); } ENDHLSL } // ── Pass 1: ToneAdjust (Stencil Equal 128) ─────────────────── Pass { Name "NiloToonCharToneAdjust_ToneAdjust" ZTest Always ZWrite Off Cull Off Stencil { Ref 128 ReadMask 128 WriteMask 0 Comp Equal Pass Keep Fail Keep } HLSLPROGRAM #pragma vertex Vert #pragma fragment ToneAdjustFrag ENDHLSL } // ── Pass 2: ToneAdjust_Full (스텐실 없음, 전체 화면) ───────── // 디버그 및 스텐실 마킹 우회용. // Feature의 FullScreen 모드에서 사용됩니다. Pass { Name "NiloToonCharToneAdjust_ToneAdjustFull" ZTest Always ZWrite Off Cull Off HLSLPROGRAM #pragma vertex Vert #pragma fragment ToneAdjustFrag ENDHLSL } // ── Pass 3: StencilView (스텐실 마스크 시각화) ─────────────── // 스텐실 재마킹이 정상 동작하는지 확인하는 디버그용 패스. // stencil=128 인 픽셀을 마젠타 오버레이로 표시합니다. Pass { Name "NiloToonCharToneAdjust_StencilView" ZTest Always ZWrite Off Cull Off Stencil { Ref 128 ReadMask 128 WriteMask 0 Comp Equal Pass Keep Fail Keep } HLSLPROGRAM #pragma vertex Vert #pragma fragment StencilViewFrag half4 StencilViewFrag(Varyings input) : SV_Target { half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); return half4(lerp(c.rgb, half3(1.0, 0.0, 0.3), 0.6), 1.0); } ENDHLSL } // ── Pass 4: DebugStencilFill (전체 화면 stencil=128 기록) ──── // 진단용 풀스크린 패스. DrawProcedural로 전체 화면에 stencil=128 강제 기록. Pass { Name "NiloToonCharToneAdjust_DebugStencilFill" ZTest Always ZWrite Off Cull Off ColorMask 0 Stencil { Ref 128 WriteMask 128 Comp Always Pass Replace } HLSLPROGRAM #pragma vertex Vert #pragma fragment FragEmpty half4 FragEmpty(Varyings input) : SV_Target { return 0; } ENDHLSL } // ── Pass 5: MeshStencilFill (메시 기반 stencil=128 기록) ───── // DrawRendererList + overrideMaterial로 캐릭터 메시 픽셀에 stencil=128 기록. // NiloToon 자체 스텐실 패스 대신 이 패스를 사용합니다. Pass { Name "NiloToonCharToneAdjust_MeshStencilFill" ZTest Always ZWrite Off Cull Off ColorMask 0 Stencil { Ref 128 WriteMask 128 Comp Always Pass Replace } HLSLPROGRAM #pragma vertex MeshVert #pragma fragment MeshFrag struct MeshAttributes { float4 positionOS : POSITION; }; struct MeshVaryings { float4 positionCS : SV_POSITION; }; MeshVaryings MeshVert(MeshAttributes input) { MeshVaryings o; o.positionCS = TransformObjectToHClip(input.positionOS.xyz); return o; } half4 MeshFrag(MeshVaryings input) : SV_Target { return 0; } ENDHLSL } // ── Pass 6: MeshMaskFill (메시 → 마스크 + 스텐실) ────────── // DrawRendererList + overrideMaterial로 캐릭터 메시 픽셀에 // color=1.0(마스크) + stencil=128 동시 기록. Auto Match 분석용. Pass { Name "NiloToonCharToneAdjust_MeshMaskFill" ZTest Always ZWrite Off Cull Off Stencil { Ref 128 WriteMask 128 Comp Always Pass Replace } HLSLPROGRAM #pragma vertex MeshVert2 #pragma fragment MaskFrag struct MeshAttr2 { float4 positionOS : POSITION; }; struct MeshVary2 { float4 positionCS : SV_POSITION; }; MeshVary2 MeshVert2(MeshAttr2 input) { MeshVary2 o; o.positionCS = TransformObjectToHClip(input.positionOS.xyz); return o; } half4 MaskFrag(MeshVary2 input) : SV_Target { return 1; } ENDHLSL } // ── Pass 7: MaskedDownsample (마스크 기반 가중 다운샘플) ───── // _MainTex(카메라 컬러) + _MaskTex(캐릭터 마스크)를 읽고 // 마스크 조건에 맞는 픽셀만 4x4 블록 평균. 첫 단계 축소용. // _InvertMask: 0 = 캐릭터(mask>0.5), 1 = 배경(mask<0.5) Pass { Name "NiloToonCharToneAdjust_MaskedDownsample" ZTest Always ZWrite Off Cull Off HLSLPROGRAM #pragma vertex Vert #pragma fragment MaskedDownsampleFrag TEXTURE2D(_MaskTex); SAMPLER(sampler_MaskTex); float _InvertMask; float4 _MainTex_TexelSize; half4 MaskedDownsampleFrag(Varyings input) : SV_Target { float3 colorSum = 0; float validCount = 0; float2 texelSize = _MainTex_TexelSize.xy; for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { float2 offset = (float2(x, y) - 1.5) * texelSize; float2 uv = input.uv + offset; half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv); half mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, uv).r; float valid = _InvertMask > 0.5 ? step(mask, 0.499) : step(0.5, mask); colorSum += color.rgb * valid; validCount += valid; } } if (validCount < 0.5) return half4(0, 0, 0, 0); return half4(colorSum / validCount, validCount / 16.0); } ENDHLSL } // ── Pass 8: WeightedDownsample (가중 다운샘플) ────────────── // 4x4 블록의 유효 픽셀(alpha>0)만 평균하여 1/4 크기로 축소. // alpha 채널에 유효 비율을 저장합니다. Pass { Name "NiloToonCharToneAdjust_WeightedDownsample" ZTest Always ZWrite Off Cull Off HLSLPROGRAM #pragma vertex Vert #pragma fragment WeightedDownsampleFrag float4 _MainTex_TexelSize; // (1/w, 1/h, w, h) half4 WeightedDownsampleFrag(Varyings input) : SV_Target { float3 colorSum = 0; float validCount = 0; float2 texelSize = _MainTex_TexelSize.xy; // 4x4 블록 샘플링 (현재 UV 중심으로) for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { float2 offset = (float2(x, y) - 1.5) * texelSize; half4 s = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv + offset); float valid = step(0.5, s.a); colorSum += s.rgb * valid; validCount += valid; } } if (validCount < 0.5) return half4(0, 0, 0, 0); return half4(colorSum / validCount, validCount / 16.0); } ENDHLSL } // ── Pass 9: StencilToMask (스텐실 → 마스크 변환) ──────────── // stencil=128 픽셀에 1.0 기록. Auto Match 분석용 마스크 생성. Pass { Name "NiloToonCharToneAdjust_StencilToMask" ZTest Always ZWrite Off Cull Off Stencil { Ref 128 ReadMask 128 WriteMask 0 Comp Equal Pass Keep Fail Keep } HLSLPROGRAM #pragma vertex Vert #pragma fragment StencilToMaskFrag half4 StencilToMaskFrag(Varyings input) : SV_Target { return 1; } ENDHLSL } } }