Game Dev/Unity Shader

08. Depth Buffer 심화 (2) - 다니엘 릿 쉐이더 프로젝트

Septentrions 2025. 10. 16. 03:52

01. Scene Intersections

Scene Intersection 이펙트는 겹쳐지는 오브젝트의 부분을 하이라이트하는 유용한 이펙트이다.

바닷가의 해변이라던가, 화려한 에너지 등 다양한 곳에서 사용된다.

이런 이펙트들은 모두 오브젝트간에 감지하는 기술을 필요로 한다.

 

쉐이더에서는 픽셀마다 현재 Depth Texture에 저장된 카메라와의 거리를 계산하도록 해야한다.

 

Shader "Lucid-Boundary/DepthBuffer_SceneIntersection"
{
    Properties
    {
        _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        _IntersectColor("Intersect Color", Color) = (1, 1, 1, 1)
        _IntersectPower("Intersect Power", Range(0.01, 100)) = 1
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
            "RenderPipeline" = "UniversalPipeline"
        }
        
        Pass
        {
            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/DeclareDepthTexture.hlsl"

            // texture2D _CameraDepthTexture;
            // SamplerState sampler_CameraDepthTexture;

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                float4 _IntersectColor;
                float _IntersectPower;
            CBUFFER_END

            struct appdata
            {
                float4 positionOS : Position;
            };

            struct v2f
            {
                float4 positionCS : SV_Position;    
                float4 positionSS : TEXCOORD0;    
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                o.positionSS = ComputeScreenPos(o.positionCS);


                return o;
            }

            float4 frag (v2f i): SV_Target
            {
                float2 screenUVs = i.positionSS.xy / i.positionSS.w;
                float rawDepth = SampleSceneDepth(screenUVs);
                float sceneEyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

                float screenPosW = i.positionSS.w;
                float intersectAmount = sceneEyeDepth - screenPosW;

                intersectAmount = saturate(1.0f - intersectAmount);
                intersectAmount = pow(intersectAmount, _IntersectPower);

                float outputColor = lerp(_BaseColor, _IntersectColor, intersectAmount);

                return outputColor;    
            }

            ENDHLSL
               
        }
    }
}

HLSL 코드 리뷰

해당 코드는 이전 포스팅의 실루엣과 거의 유사하므로

변화가 있는 Fragment 부분만 보겠다.

 

screenEyeDepth로 해당 픽셀과 카메라 사이의 거리를 유니티 유닛 단위의 값으로 얻을 수 있다.

intersectAmount = 현재 픽셀의 Depth Value - 현재 오브젝트의 z 정보라고 생각하자.

 

saturate는 ln에 0에서 1 사이로 값을 준다.

여기에 pow 를 통해 ln을 벗겨내면서, intersection 강도를 조절하는 역할을 한다.


02. Shader Graph로 Scene Intersection 만들기

얼핏 보면, 복잡해보이지만 그렇지 않다.

HLSL의 코드를 그대로 그래프로 담았을 뿐이다.

 


03. The Stencil Buffer

stencil buffer란 스크린상에 각 픽셀에 Integer 값을 저장하는 2D 버퍼이다.

스텐실 버퍼는 일종의 저장소 역할로 렌더링 되었던 오브젝트들을 스텐실 값을 변경한 뒤에, 버퍼 값을 읽어 우리가 원하는대로 커스터마이징하여 렌더링 할 수 있는 버퍼이다.

 

예를 들면, 화면의 특정 부분을 다른 오브젝트가 나오도록 마스크 할 수 있게 해줄 수 있다.

 

스텐실 버퍼는 Depth Buffer의 연장선으로써 구현된다.

Integer 값이기 때문에 픽셀당 24bit 되는 Depth Buffer보다 적은 8bit 여도 0 에서 255까지 값을 저장 할 수 있다.

또한, 스텐실 버퍼도 Stencil Testing 과정을 이용해 렌더링을 할 지 결정 할 수 있다.

 

*스텐실 버퍼는 쉐이더 그래프에서 지원하지 않는 기능이다. 코드 기반으로 읽고 써야 한다.

 

04. Stencil Testing in HLSL

Depth Test랑 비슷한 Stencil Test는 픽셀을 렌더링 할 지 결정할 수 있다.

Stencil Test가 통과 되면, 유니티는 즉시 Depth Test를 수행하고, 두 테스트의 결과에 따라 렌더링 할 지를 결정할 것이다.

물론, 실패하면 렌더링 되지 않는다.

 

Stencil Testing은 Depth Test와는 달리,  스텐실 테스트가 실패하면 어떻게 동작 시킬지 세밀하게 제어 할 수 있다.

 

Stencil Buffer를 사용하기 위해서는 2개의 Shader가 필요하다.

하나는 stencil buffer에 값을 쓰는 쉐이더, 다른 하나는 버퍼로부터 값을 읽을 수 있는 쉐이더이다.

 

Stencil Mask Shader

Shader "Unlit/DepthBuffer_StencilMask"
{
    Properties
    {
        [IntRange] _StencilRef("Stencil Ref", Range(0, 255)) = 1
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque"
            "Queue" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"    
        }

        Pass
        {
            Blend Zero One
            Stencil
            {
                Ref[_StencilRef]
                Comp Always
                Pass Replace
                Fail Keep
            }

            ZWrite Off
           
        }
    }
    Fallback Off
}

 

Stencil Texture Shader

Shader "Unlit/DepthBuffer_StencilTexture"
{
    Properties
    {
        _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        _BaseTex("Base Texture", 2D) = "White" {}
        [IntRange] _StencilRef("Stencil Ref", Range(0, 255)) = 1
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque"
            "Queue" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"    
        }

        Pass
        {
            Stencil
            {
                Ref[_StencilRef]
                Comp Equal
                Pass Keep
                Fail Keep
            }

            Tags
            {
                "LightMode" = "UniversalForward"    
            }


            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            texture2D _BaseTex;
            SamplerState sampler_BaseTex;

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                float4 _BaseTex_ST;
            CBUFFER_END

            struct appdata
            {
                float4 posiionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 PositionCS : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.PositionCS = TransformObjectToHClip(v.posiionOS.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _BaseTex);
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                float4 textureSample = SAMPLE_TEXTURE2D(_BaseTex,  sampler_BaseTex, i.uv);
                float4 outputColor = textureSample * _BaseColor;
                return outputColor;
            }
            ENDHLSL
        }
    }
    Fallback Off
}

 

스텐실 버퍼를 사용하는 방법은 Shader 내부에 Stencil 블록을 추가하는 것 뿐이다.

Stencil
{
	...
}

 

Stencil 안에 Ref, Comp, Pass, Fail 등

Stencil Test를 위한 파라미터들을 명시 해줘야 한다.

 

Ref 레퍼런스 밸류를 인테저로 지정한다.
그 의미는 stencil test를 진행 할 때, 이 쉐이더의 값은 Ref 값이라는 뜻이다.
Comp Stencil Test를 진행 할 때, Ref 값을 비교하기 위한
조건 문에 해당한다.
Depth Buffer처럼 LEqual / Less 등으로 정의한다.
Pass Stencil Test를 통과 했을 때, 처리 방법을 정의한다.
Fail Stencil Test를 실패 했을 때, 처리 방법을 정의한다.
그 외에 더 있으니 Docs를 확인 할 것  

 

Keep Stencil Buffer를 변경하지 않는다.
Zero Buffer Value를 0으로
Replace 해당 쉐이더의 Ref 값으로 버퍼를 덮어쓴다.
IncrSat 버퍼의 값을 1만큼 올린다. (0 ~ 255)
DecrSat 버퍼의 값을 1만큼 줄인다. (0 ~ 255)
Invert 버퍼 값의 비트를 뒤집는다. (8비트를 뒤집는다.)
IncrWrap 버퍼의 값을 1만큼 올리되, 최대 255까지 이후로 0으로 바꾼다.
DecrWrap 버퍼의 값을 1만큼 줄이되, 최소 0까지 이후로 255로 바꾼다.

 

위의 표를 참고 했을 때,

코드를 살펴보자

 

렌더링 하고자 하는 Stencil Texture 쉐이더는 앞서 다루었던 Basic Texture Shader에 Stencil을 추가한 것 뿐이다.

Stencil Texture의 Comp 값은 Equal,

Stencil Mask의 차이는 Comp 값이 Always일 뿐이다.

 

이 의미에 대해서 생각해보자!

Stencil Buffer가 통과 되면, Depth Buffer도 테스트를 시작할 것이다.

Stencil Texture의 Stencil Buffer의 조건은 Equal..

Refence Value 값이 정확히 같아야지만 버퍼에 통과한다.

 

Stencil Mask의 Stencil Buffer의 조건은 Always..

Refence Value 값, Stencil Buffer의 Value는 항상 Mask 를 따라갈 것이다.

 

여기서! Stencil Mask의 Refence Value 값이 Stencil Texture의 Ref 값과 동일하게 된다면,

Stencil Texture 은 비로소 Stencil Testing 에 성공하며, 별일이 없다면 Depth Test까지 통과하는 것이다.

 

그 결과는 다음과 같다.

 

Stencil Mask와 겹쳐진 부분의 Ref 값이 Texture와 같아지며 Test를 통과하는 모습

Stencil 조건을 바꿔 다르게 나오게 할 수도 있다.

이번엔 Texture의 Comp를 Greater로 변경 해보았다.

 

드디어 Stencil Buffer에 깨달음을 얻은 것 같다!

그리고.. 굉장히 Depth Buffer에 대한 이해까지 요구하여 난해한 부분이였다..

하지만... 아직 또 한발 남았다.

 


05. URP Renderer Features

유니티로 이것저것 개발하면서도.. 내가 이걸 건들 수 있을까? 도대체 뭣하는 물건이지 했던게..

Depth Buffer와 연관되어 있었구나.

 

URP Renderer Feature들은 전체 스크린에 커스텀한 렌더 패스를 추가 할 수 있는 기능이다.

 

지금까지 Depth니 Stencil이니 복잡하고 현학적인 내용들을 다루었는데.. 사실 이런걸 굳이 HLSL에다가 열심히 코딩해가며 만들 필요가 없다.

URP에는 Render Object가 존재하는데, 오브젝트의 레이어에서 Depth와 Stencil을 설정 할 수 있는 기능을 포함하고 있다.

 

Render Object는 Add Render Feature를 눌러서 추가 할 수 있다.

 

Event

 

Renderling Loop 안에서 이 피처가 언제 실행 할 지 조정할 수 있는 옵션이다.

눌러보면, 굉장히 많은 항목이 나오니 원하는 렌더링 루프 때, 실행 할 수 있게 해주자.

Default는 AfterRenderingOpaque 이다.

*AfterRenderingOpaque면, opaque 오브젝트들의 렌더링이 모두 끝난 후를 뜻한다.

 

Filters

 

렌더 오브젝트 피쳐가 실행할 때, 제약을 거는 용도이다.

 

Overrides

 

Depth와 Stencil 테스트를 우리가 원하는 방식으로 수정 할 수 있다.

 

Shader 내부의 세팅을 쓰지 않고, 우리가 필터링한 특정 대상들에 대하여 Depth / Stencil 테스트를 원하는 방식으로 할 수 있게 해준다는 뜻이다.

 


06. 마치며

Depth Buffer 파트 2번째 포스팅을 끝냈다.

다음 포스팅을 마지막으로 Depth Buffer 파트가 끝날 것이다.

힘내자!