티스토리 뷰

유니티 쉐이더 공부를 하던 중 Stencil(이하 스텐실)이라는 것을 알게 되었다.

 

스텐실은 GPU의 렌더링 파이프라인에서 픽셀 단위로 그릴지/안 그릴지를 결정하는 버퍼이다.

더보기

버퍼(Buffer)란, 컴퓨터 그래픽스에서 데이터를 임시로 저장해두는 메모리 영역을 뜻한다.

렌더링에서는 컬러(Color), 깊이(Depth 또는 Z), 스텐실(Stencil) 등의 Buffer가 있다.

 

특정한 구조나 형태가 정해져 있는 것이 아니라, 단순히 해상도 크기만큼의 픽셀 단위 정보를 임시로 저장하는 공간이다.

 

예를 들어 1920×1080 해상도라면 컬러, 깊이, 스텐실 각각 1920×1080 크기의 데이터가 저장되며,

색상은 RGBA(약 8MB)

깊이는 24~32비트 값(약 6MB)

스텐실은 8비트 값(약 2MB) 정도의 메모리를 차지한다.

 

즉, 한 프레임을 그리기 위해 수십 MB의 버퍼 메모리가 사용되며,

GPU는 이 버퍼들을 읽고 쓰면서 최종 화면을 완성한다.

 

 

영상은 마스킹을 통해 특정 오브젝트만 렌더링하여 얻은 결과물이다.

 

여기에 사용된 것은 전부 쉐이더 코드이다.

코드 작성이 미숙한 관계로 GPT를 사용하여 쉐이더 코드를 만들었다.

 

중요한 건 스텐실 개념

이것을 활용하여 착시 효과 원리를 익히는 것이다.

 

 

보다 시피 여러 오브젝트가 겹쳐있지만,

시점에 따라 특정 오브젝트만 보이게 된다.

 

 

 

마치 착시와 같은 이 효과는,

각 오브젝트에 부여된 스텐실 ID 값과 픽셀마다 저장된 스텐실 버퍼 값이 일치하는 경우에만

해당 오브젝트를 화면에 그리도록 함으로써 구현된다.

 

 

여기에 필요한 설정은 총 세 가지다.

1. Renderer Feature 추가

2. 마스크용 Shader

3. 스텐실을 적용할 오브젝트용 Shader

 

순서를 거꾸로 이해하면 꽤 쉬운 개념이었다.

 

스텐실 쉐이더 작성

Shader "Custom/StencilLitTexture"
{
    Properties
    {
        _BaseMap("Base Map (RGB)", 2D) = "white" {}
        _BaseColor("Base Color", Color) = (1,1,1,1)
        [IntRange]_StencilID("Stencil ID", Range(0,255)) = 1
    }

        SubShader
        {
            Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" }

            Pass
            {
                Tags { "LightMode" = "UniversalForward" }

                HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                float2 uv           : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float3 normalWS     : TEXCOORD0;
                float3 positionWS   : TEXCOORD1;
                float2 uv           : TEXCOORD2;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            float4 _BaseMap_ST;
            float4 _BaseColor;

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.positionHCS = TransformWorldToHClip(OUT.positionWS);
                OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                float4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);

                float3 normalWS = normalize(IN.normalWS);
                half3 color = texColor.rgb * _BaseColor.rgb * SampleSH(normalWS);

                Light mainLight = GetMainLight();
                float3 lightDir = normalize(mainLight.direction);
                float NdotL = saturate(dot(normalWS, lightDir));
                color += texColor.rgb * _BaseColor.rgb * mainLight.color.rgb * NdotL;

                return half4(color, texColor.a * _BaseColor.a);
            }
            ENDHLSL

                Stencil
                {
                    Ref[_StencilID]
                    Comp Equal
                    Pass Keep
                }
            }
        }
}

 

갑자기 나타난 쉐이더 코드지만,

GPT에게 시켜서 얻은 평범한 내용이다...

연결한 텍스처를 UV에 적용해주는...

 

            ENDHLSL

                Stencil
                {
                    Ref[_StencilID]
                    Comp Equal
                    Pass Keep
                }
            }

 

중요한 부분은 ENDHLSL 뒤의 스텐실 부분이다.

스텐실 버퍼 값과 _StencilID를 비교해서 값이 같은 경우에만

화면에 픽셀을 그리도록 하는 부분이다.

 

 

이 쉐이더를 적용하면 머티리얼에 ID 변수가 나타나고

이것을 오브젝트에 대응되는 마스크 오브젝트의 ID와 일치 시키면 된다.

 

 

 

마스크 오브젝트

 

마스크 오브젝트를 설명해야 하는데,

이미지는 붉은 벽면의 오브젝트가

마스크 오브젝트에 의해

일부가 안 보이게 된 것이다.

 

 

Render Pass가 딱히 없어 화면에 그려지지 않는 이 오브젝트는

자신이 걸쳐진 픽셀에 특정 스텐실 버퍼 값을 저장하고 있다.

 

Shader "Custom/StencilMask"
{
    Properties
    {
        [IntRange] _StencilID("Stencil ID", Range(0,255)) = 1
    }
        SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" }

        Pass
        {
            ColorMask 0
            ZWrite Off
            Stencil
            {
                Ref[_StencilID]
                Comp Always
                Pass Replace
            }
        }
    }
}

 

 

그러니까 정리하자면 스텐실 버퍼 값을 그리는 녀석이 있고,

그 값을 기준으로 자신을 화면에 그릴지 말지 판단하는 녀석들이 있다는 것이다.

 

 

 

 

보이지 않는 마스크(Quad 오브젝트)가 스텐실 버퍼에 자신의 ID 값을 저장하고 있다.

 

 

검 오브젝트는 지금 스텐실 버퍼 값을 신경쓰지 않고 렌더링 중이다.

 

 

 

여기서 검 오브젝트의 머티리얼을 교체해본다.

이 머티리얼은 스텐실 버퍼 값이 2인 영역에만 렌더링을 하는 것이다.

 

 

 

이제 검 오브젝트는 스텐실 버퍼 값이 2인 영역에서만 렌더링된다.

 

 

 

그런데 이 마스크 오브젝트,

백페이스 컬링(뒤집힌 Z축) 영향을 받고 있다.

오브젝트의 Z축이 뒤집어지면 스텐실 버퍼 값을 저장하지 못한다.

 

 

 

Render Feature

처음 스텐실에 대한 정보를 발견한 글에서는

렌더피처(Render Feature)를 사용하여 위와 같은 착시 효과를 구현했다.

 

 

그 방법이란,

기존 렌더러에서 효과를 적용시킬 오브젝트들의 레이어를 제외시키고

렌더 피처에서 지정한 렌더링 단계에서

스텐실 버퍼 검사를 통과한 오브젝트들만 별도로 다시 그려주는 것이었다.

 

사실 본문에선 렌더피처를 직접적으로 활용하지 않았다.

렌더 피처는 필수 요소가 아니라

순서 제어나 특별한 조건이 필요할 때 쓰이는 도구일 뿐.

 

 

 

진짜 문제

 

쉐이더 그래프는 스텐실 관련 노드를 제공해주지 않는다...!

 

그러니까 이런 효과 만들고 싶으면

쉐이더코드를 작성할 줄 알아야 한다!

...라는 교훈을 배웠다.

 

매번 GPT한테 쉐이더 코드 만들어 달라고 부탁?

할 수 있다면? 정말 좋을텐데

GPT가 원하는 것을 한 번에 뚝딱 만들어주는 그런 놈이 었던가?

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함