Game Dev/Unity Shader

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

Septentrions 2025. 10. 15. 10:43

01. Depth Testing And Writing

 

Depth Testing 하는 방식은 HLSL에서 커스터마이징 가능하다.

여러가지 소개 전에 스탠다드한 "LessEqual" Depth Test에 대해 알아보려 한다.

 

ZTest Keyword

HLSL에서는 ZTest 라는 키워드를 지원하며 Default 는 LEqual 이다.

Pass
{
	ZTest LEqual
    ...
}

우리가 따로 명시하지 않는다면, LEqual이 기본 세팅이다.

Less 확실히 작아야 Depth Test가 통과
GEqual Depth Value가 더 크거나 같은 쪽이 Depth Test 통과
Greather Depth Value가 더 큰 쪽이 Depth Test 통과
Equal Depth가 같아야만, Depth Test 통과
NotEqual 다르기만 해도 Depth Test 통과
Always 오브젝트는 항상 렌더링
Never 항상 실패하여 렌더링하지 않음

 

렌더링이야 당연히 Less가 맞는 방법이지만, 잘 생각해보면 다른 용도로 사용 가능하다.

예를 들면, 퀘스트 마커처럼 멀리 있어도 보이도록 한다던가.

다양하게 쓰일 일이 있을 것이다.

 

ZWrite Keyword

ZWrite 키워드는 Depth Test 통과후에 실제로 렌더링을 할 지 안 할 지 선택할 수 있는 키워드이다.

이런걸 쉐이더로 쓰는게 정말 필요한 행동인지는 모르나..

Pass
{
	ZTest LEqual
    ZWrite On
    ...
}

 

Zwrite는 심플하게 Zwrite On / Zwrite Off 로 구분한다.

 

Shader Graph에서는 심플하게 선택 가능하다.

 

Early Depth Testing

Depth Testing 과정을 보면 알 수 있듯이, 유니티는 모든 쉐이더를 실행 시킬 필요가 있어 보인다.

하지만, 대부분의 쉐이더들은 픽셀에 영향을 주거나 Depth Value가 버러진다던지 하는 일이 발생하지 않고,

Depth Compare 할 이유가 없다.

 

대부분의 GPU들은 Early Depth Testing을 지원한다. 

유니티는 오브젝트의 위치를 결정하는 Vertext Shader를 가지고와서, 특별한 Depth-only rendering pass를 통과시켜 Depth Testing 할 필요 없는 오브젝트들을 감춘다. (Cull)

이 렌더링 패스를 통과한 오브젝트들은 Fragment Shader까지 고려된 다음 Depth Testing이 진행된다.

 

Depth-only rendering pass  부분들은 유니티가 잘 되어있어서 왠만하면 만질 일은 없어보이지만

그래도 이런게 있다는 사실을 아는 걸로 만족하자.

 

Z-Fighting

Z-Fighting은 Depth Testing 과정에서 생기는 불확실함으로 발생하는 현상이다.

두 surface가 겹쳐지거나 너무 가까워지면, 유니티는 확실하게 누군가를 선택해서 렌더링하지 못한다.

*자글자글 거리는 현상을 다들 겪어보았을 것이다.

*주로 Stripy pattern 이라는 패턴처럼 보이는 걸 알 것이다.

Z-Fighting 현상

 

Z-Fighting 현상을 막을 방법은 어떤게 있을까?

아주~~ 심플하게 오브젝트끼리 서로 겹쳐지지 않게 배치하는게 최고의 정답이다.

하지만, 이를 모두 고려하는건 쉽지 않을 것이다.

 

Depth Buffer의 주요한 특징중 하나는 카메라의 near and clip planes에 의해 표현되게 하는 것이다.

clip distance를 near는 최대한 작게, far는 최대한 크게하면 이 현상을 피할 수 있다.

*유니티는 기본 값이 .near는 0.3, far는 1000이다.

(솔직히 조금 무책임하게 막는 감이 없지 않다.. 사실 겹치게 한 개발자 잘못이다.)

 

이 값은 카메라 인스펙터에서 바꿀 수 있다.

 


02. Shader Effects Using Depth

Silhouette 실루엣

실루엣 이펙트란 앞에 놓여진 오브젝트들을 검정색으로 바꾸고, 뒤에 놓여진 요소들은 밝은 색으로 하는 기법이다.

Edge 정보외에는 모든 디테일 요소들이 사라지는거나 다름없지만, 시각적으로 재밌는 요소로 쓰일 수 있다.

 

Silhouette Effect In HLSL

HLSL에서 Depth Value를 다루려면 Depth Texture를 이용해야 한다.

URP 세팅에 Depth Texture 체크 표시를 확인하자.

 

Camera Depth Texture

 

Opaque 오브젝트들이 렌더링 될 때는, 기본적으로 각 픽셀들의 Depth Value를 이용하지만

모든 Opaque 오브젝트들의 렌더링이 끝난 후에 그려지는 Transparent 오브젝트들의 경우에는 Depth Buffer를 위한 Depth 정보를 가지고 있지 않다.

 

그러므로, 현재 프레임의 완전한 Depth 정보를 얻고 싶다면, Opaque 오브젝트들이 모두 그려진 다음에 접근해야 한다.

*Transparent 관련 포스팅은 차후에 진행하 예정이다.

 

하지만, Fragment 쉐이더 단계에서 Depth Buffer에 직접 접근할 방법이 없다.

대신에, 유니티는 추가로 Depth-only pass를 사용한다. (모든 Opaque 오브젝트들이 통과하는 Pass이다.)

이 Pass는 이러한 Depth 값들을 특별한 텍스처인 "_CameraDepthTexture" 에 저장한다.

_CameraDepthTexture는 우리의 쉐이더에서 접근 가능하다.

 

Depth Buffer와 Camera Depth Texture는 서로 같지 않지만, 비슷한 정보를 가지고 있다.

 

Depth Buffer는 내부적으로 Depth Testing 과 Culling을 위해, 그래픽스 파이프라인에 의해 사용된다. 

Depth Texture는 Depth-Based Effects를 위해, 쉐이더 내부에서 사용된다.

 


02. Depth Values에 접근하기

_CameraDepthTexture는 Texture2D와 비슷하다.

마찬가지로, 이것의 UV는 특정 포지션을 샘플하는 역할을 한다.

차이점은 이 UV들의 포지션은 Screen Space의 포지션에 해당한다. (Depth 정보를 얻기 위함이다.)

 

이제 드디어 HLSL 코드 예제를 살펴보자.

Shader "Custom/DepthBuffer_Silhouette"
{
    Properties
    {
        _ForegroundColor ("FG Color", Color) = (1, 1, 1, 1)
        _BackgroundColor ("BG Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags
        {
                "RenderType" = "Transparent"
                "Queue" = "Transparent"
                "RenderPipeline" = "UniversalPipeline"
        }
        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/DeclareDepthTexture.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float4 _ForegroundColor;
                float4 _BackgroundColor;
            CBUFFER_END
            
            SamplerState sampler_PointLinear;

            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 = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_PointLinear, screenUVs).r;
                float rawDepth = SampleSceneDepth(screenUVs);
                float screen01Depth = Linear01Depth(rawDepth, _ZBufferParams);
                float4 outputColor = lerp(_ForegroundColor, _BackgroundColor, screen01Depth);

                return outputColor;
            }

            ENDHLSL
        }

    }
    FallBack "Diffuse"
}

 


코드 리뷰 구간

Tags

Depth Texture를 다루어야 하므로 Opaque 뿐만 아니라 Transparent 오브젝트도 필요하다.

RenderType / Queue를 각각 Transparent 모드로 했다.

 

Precompile

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

기존 HLSL 코드와 달리 해당 패키지를 추가한다.

 

v2f

positionCS 는 Clip Space에 해당 하는 위치

positionSS 는 Screen Space에 해당하는 위치이다.

 

ComputeScreenPos 함수를 통해, Clip Position을 Screen Position으로 바꾸면서, UV 자체가 Screen을 타겟하도록 바꾼다.

 

screenUVs = i.positionSS.xy / i.positionSS.w; 에서 보통 1로 디폴트값이 들어가 있는 w이지만, 아닐 경우 전체적으로 0 에서 1 사이로 노말라이즈 하기 위함이다.

 

SampleSceneDepth 함수는 해당 픽셀의 z 좌표를 샘플링하는 함수이다.

 

Converting Depth Values ( linear01depth  함수의 의미는?)

Depth Value들은 몇몇 포맷으로 변환 가능하다.

실제 Depth Texture에 있는 값들은 NonLinear한 값들이기 때문에.. 자체적으로 의미를 가지기 힘든데 이를 변환해주도록 포맷 변환을 해야한다.

 

*Shader에는 각 픽셀들이 함수를 통과하며 계산되는데.. 이 때 얻어진 Depth Value들은 픽셀마다 서로 의미가 다르다는 의미로 nonlinear하다라고 표현한다. 서로 픽셀의 상관관계까지 고려하여 전체적으로 의미있게 값을 바꿔줘야 한다.

 

(1) raw depth 

0과 1사이의 값으로 nonlinear한 벨류로써, 카메라와 거리를 표현한 값이다.

0.5라고 해서 절반 사이의 거리란 뜻이 아니다. 꼭 명심하자.

 

(2) eye depth

linear한 벨류로써 카메라로부터 Unity Unit (meter)만큼 떨어진 픽셀의 Depth를 표현한 값이다.

이 값을 이용하여 World Space에서 의미있게 값을 사용 할 수 있다.

 

raw값을 eye로 바꾸기 위한 함수는 2가지가 있다.

linear01depth 함수와 lineareyedepth 함수이다.

함수명에서 알 수 있듯이 linear01depth는 0과 1사이로 정규화해주는 함수이고,

lineareyedepth는 Unity Unit으로 변환해주는 함수이니 차이점을 알길 바란다.

 

URP에서는 이 함수들에 _ZBufferParams 라는 키워드를 2번째 파라미터를 요구한다.

_ZBufferParams 는 Depth Texture를 선형화하기 위해 필요한 파라미터들로 구성된 float4 변수이다.

_ZBufferParams

 

코드 마지막에는 lerp 함수가 쓰였는데,

0에서 1 사이로 노말라이즈 된 Depth Value로 색상을 정하게 만드는 꽤 적합한 함수를 사용했다.

 


실루엣 적용 모습 Lit 오브젝트들이 실루엣 매트리얼을 가진 오브젝트의 앞 뒤에 배치 했을 때, 모습이다.


03. Shader Graph로 실루엣 만들어보기

Graph Inspector : Surface Type을 Transparent로 하였다.

 

좀 어처구니없이 쉽다.

UV를 Scene Depth에 넣고, Lerp 값에 넣기만 하면 똑딱이다..

허허...

 


 

04.Depth-Only Pass를 이용해서 Depth Texture 쓰기 (Writing)

위에서 언급한 Camera Depth Texture에 대한 추가 이야기이다.

유니티는 opaque 오브젝트들을 depth information만을 렌더링 하는 특별한 pass 안에서 Opaque 오브젝트들을 렌더링 한다.

 

CG에서는 ShadowCaster라는 이름으로 추가 Pass를 만들면 되고,

HLSL에서는 DepthOnly 라는 이름으로 추가 Pass를 만들면 된다.

 

이 Pass들은 Unlit 쉐이더들에 넣어주면, Unlit 쉐이더라 할지라도 실루엣 같은 Depth 이펙트에 영향을 받게 된다.

URP에서 기본 제공되는 Unlit에 DepthOnlyPass를 이용하는 코드가 이미  있어서, 실루엣에 바로 영향을 받게 된다.

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly"}

            ZWrite On
            HLSLPROGRAM

            #pragma vertex DepthOnlyVertex;
            #pragma fragment DepthOnlyFragment;

            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"

            #pragma multi_compile_instancing
            #pragma multi_compile_DOTS_INSTANCING_ON

            ENDHLSL
        }

 


 

05. 마치며

 

이번 챕터를 통해.. 쉐이더를 알고 쓰는 것과 모르고 쓰는 것의 천지차이를 통감하게 되었다.

왜냐하면..

내가 이전에 만들던 ECS 게임에서 쓰이는 쉐이더가 무슨 짓을 해도 적용이 안되는 문제가 있었기 때문이다.

게임의 컨셉상 중요한 쉐이더였는데, 답답해 미칠 노릇이였다.

HLSL과 호환이 안되는 ShadowCaster 부분이 문제였던 것 같은데..

이런 Depth Buffer 같은 기본 개념조차 없던 상태에서 해결하는건 불가능 했다는걸 깨닫게 되었다..

 

Depth 정보를 기반으로 전통화 느낌이 나도록 하려고 했던 ECS 게임

 

 

Depth Buffer 포스팅은 아직 끝나지 않았다..

책에서도 거의 100장 가까이 할애한 챕터이기 때문에.. 마라톤이 될 것이다.