Unity HDRP 커스텀 Depth 셰이더 가이드

Mazeline·2일 전
0

TA 가이드

목록 보기
5/6
post-thumbnail

고객사에서 새로운 일감을 받았습니다. Uber Particle 이라는 URP 전용 이펙트 개발 라이브러리를 HDRP 에서도 동작하도록 포팅 해 달라는 것이었습니다. 그 과정에서 뎁스 텍스처 관련 된 내용에서 많은 차이가 있었기 때문에 기록용으로 velog 에 정리해서 남겨놔야겠다는 생각이 들어 정리해 봤습니다.

Unity HDRP에서 커스텀 Depth 셰이더 다루기

HDRP는 깊이 텍스처에 접근하는 방식이 Built-in이나 URP 파이프라인과 완전히 다릅니다. _CameraDepthTexture를 직접 샘플링하는 대신 LoadCameraDepth()나 SampleCameraDepth() 함수를 사용해야 하는데, 이는 HDRP가 깊이를 계층적 밉맵 피라미드 구조로 저장하기 때문입니다. LinearEyeDepth() 함수는 명시적으로 _ZBufferParams 파라미터가 필요하고, 모든 셰이더는 레거시 CGPROGRAM 문법 대신 HLSLPROGRAM 블록과 패키지 기반 include를 사용해야 합니다. 이런 변화들은 하이엔드 그래픽에 최적화된 HDRP의 모던 렌더링 아키텍처를 반영한 것입니다.

이런 차이점을 이해하면 가장 흔한 함정을 피할 수 있습니다. Built-in 파이프라인의 깊이 접근 패턴을 그대로 사용하려고 하면 컴파일 에러가 나거나 잘못된 값이 반환됩니다. HDRP의 접근 방식은 depth pyramid 시스템을 통해 더 나은 성능을 제공하며, ambient occlusion이나 reflection 같은 스크린 스페이스 이펙트를 효율적으로 구현할 수 있지만, 그만큼 정해진 API를 따라야 합니다.

HDRP 깊이 접근을 위한 필수 설정과 include

깊이에 접근하는 모든 HDRP 셰이더는 Unity의 Scriptable Render Pipeline 패키지에서 특정 include 파일들을 필요로 합니다. 필수적으로 포함해야 하는 두 가지는 Common.hlsl과 ShaderVariables.hlsl인데, 이들은 깊이 샘플링 함수와 전역 셰이더 변수들(_ZBufferParams, _ScreenSize 등)을 제공합니다.

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

Custom Pass 셰이더의 경우, 카메라 컬러와 커스텀 버퍼를 로드하는 헬퍼 함수들이 포함된 CustomPassCommon.hlsl을 추가로 포함해야 합니다:

#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl"

깊이와 함께 노말을 다루는 경우, HDRP가 depth prepass 중에 생성하는 노말 버퍼에 접근하기 위해 NormalBuffer.hlsl을 포함하시기 바랍니다.

모든 HDRP 셰이더는 CGPROGRAM/ENDCG 대신 HLSLPROGRAM/ENDHLSL 블록을 사용해야 하고, 셰이더 모델 4.5 이상을 타겟으로 하며, 지원하는 플랫폼을 명시적으로 지정해야 합니다. pragma 지시자는 다음과 같이 작성하시면 됩니다:

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment CustomPostProcess
#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone vulkan metal switch

UnityCG.cginc 같은 레거시 Built-in 파이프라인 include는 HDRP와 호환되지 않아서 컴파일 에러를 일으킵니다. 패키지 기반 include로의 전환은 각 파이프라인이 자체 패키지와 파이프라인별 셰이더 라이브러리를 가지는 Unity의 모듈식 렌더링 아키텍처를 반영한 것입니다.

HDRP의 핵심 깊이 샘플링 함수들

HDRP는 깊이 접근을 위한 두 가지 주요 함수를 제공하는데, 각각 다른 좌표 시스템에 맞춰져 있습니다. LoadCameraDepth()는 정수형 픽셀 좌표(uint2)를 받아서 필터링 없이 depth pyramid에서 직접 로드합니다. 스크린 해상도에서 작동하는 포스트 프로세싱 이펙트에 이상적입니다:

uint2 positionSS = input.texcoord * _ScreenSize.xy;
float rawDepth = LoadCameraDepth(positionSS);

SampleCameraDepth()는 정규화된 UV 좌표(0-1 범위의 float2)를 받아서 바이리니어 필터링과 함께 샘플링합니다. UV로 작업하거나 필터링된 깊이 값이 필요할 때 유용합니다:

float2 uv = input.texcoord;
float rawDepth = SampleCameraDepth(uv);

두 함수 모두 HDRP의 depth pyramid 구조를 자동으로 처리합니다. 이는 각 레벨이 최소값(가장 가까운)을 사용해 다운샘플링된 깊이를 저장하는 멀티레벨 밉맵 아틀라스입니다. 이 피라미드 덕분에 screen-space ambient occlusion 같은 계층적 Z-buffer 알고리즘이 먼 픽셀에 대해 더 거친 깊이 레벨을 샘플링할 수 있어 효율적입니다.

반환되는 값은 깊이 버퍼에 저장된 raw non-linear depth입니다. 일반적으로 DirectX 계열 플랫폼에서는 reversed-Z 포맷으로, 1.0이 near plane을, 0.0이 far plane을 나타냅니다. 이를 사용 가능한 선형 값으로 변환하려면 전용 변환 함수를 사용해야 합니다.

Custom Pass 셰이더의 경우, 특정 오브젝트를 렌더링할 수 있는 커스텀 깊이 버퍼에 접근하는 추가 함수들이 있습니다:

float customDepth = LoadCustomDepth(uint2 pixelCoords);
float customDepth = SampleCustomDepth(float2 uv);

이런 커스텀 버퍼를 사용하면 X-ray 비전, 선택 아웃라인, 또는 특정 지오메트리의 깊이를 메인 씬 깊이와 비교하는 등의 고급 이펙트를 구현할 수 있습니다.

Raw 깊이를 선형 공간으로 변환하기

Raw 깊이 버퍼 값은 디테일이 중요한 카메라 근처에서 최대한의 정밀도를 확보하기 위해 비선형 분포를 사용합니다. LinearEyeDepth()는 raw 깊이를 카메라 평면으로부터의 월드 단위 선형 거리로 변환하는데, 일관된 감쇠가 필요한 안개나 깊이 기반 페이딩 같은 이펙트에 필수적입니다:

float rawDepth = LoadCameraDepth(positionSS);
float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
// eyeDepth는 이제 카메라로부터의 월드 단위(미터) 거리입니다

Linear01Depth()는 깊이를 0-1 범위로 정규화하는데, 0이 near plane이고 1이 far plane입니다. 시각화나 카메라 far plane 거리와 무관하게 일관된 깊이가 필요할 때 유용합니다:

float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
// linear01은 near plane에서 0.0, far plane에서 1.0

두 함수 모두 _ZBufferParams가 필요한데, 이는 카메라 프로젝션 설정에 기반해 HDRP가 자동으로 채우는 깊이 재구성 파라미터들을 담은 float4입니다. 정확한 공식은 reversed-Z 구성을 고려하고 원근/직교 카메라 모두를 올바르게 처리합니다.

차이점을 이해하는 것이 중요합니다: LinearEyeDepth는 계산에 사용할 수 있는 실제 거리를 제공하고("카메라로부터 10미터" 같은), Linear01Depth는 그라데이션이나 시각화에 유용한 정규화된 깊이를 제공합니다. 5미터에서 20미터로 페이드되는 안개 이펙트라면 LinearEyeDepth를 사용하고, 깊이 텍스처 시각화라면 Linear01Depth를 사용하시면 됩니다.

플랫폼별 깊이 버퍼 포맷 차이는 자동으로 처리됩니다. DirectX는 더 나은 부동소수점 정밀도 분포를 위해 reversed-Z(near에서 1.0)를 사용하고, OpenGL은 전통적으로 near에서 0.0을 사용했습니다. 변환 함수들이 이런 차이를 추상화해서 플랫폼 간에 일관된 동작을 보장합니다.

완전한 포스트 프로세싱 셰이더 예제

깊이를 로드하고 선형 공간으로 변환한 다음, 깊이 기반 이펙트를 만드는 실전용 커스텀 포스트 프로세싱 셰이더를 살펴보겠습니다. 이 예제는 거리에 따른 circle of confusion 값을 계산하는데, depth-of-field나 거리 기반 블러 효과에 유용합니다:

Shader "Hidden/Shader/DepthExample"
{
    SubShader
    {
        Pass
        {
            Name "DepthExample"
            ZWrite Off
            ZTest Always
            Blend Off
            Cull Off

            HLSLPROGRAM
            #pragma fragment CustomPostProcess
            #pragma vertex Vert
            #pragma target 4.5
            #pragma only_renderers d3d11 playstation xboxone vulkan metal switch

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

            struct Attributes
            {
                uint vertexID : SV_VertexID;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings Vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
                output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
                output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);
                return output;
            }

            float4 _Params;
            #define _Distance _Params.w

            TEXTURE2D_X(_InputTexture);

            float4 CustomPostProcess(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                uint2 positionSS = input.texcoord * _ScreenSize.xy;
                float3 inColor = LOAD_TEXTURE2D_X(_InputTexture, positionSS).xyz;

                // 깊이 로드 및 선형화
                float depth = LoadCameraDepth(positionSS);
                float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);
                float coc = saturate(_Distance / linearEyeDepth);

                // 깊이 기반 블렌딩
                return float4(lerp(inColor, float3(0, 0, 1), coc), 1.0);
            }
            ENDHLSL
        }
    }
}

이 셰이더를 HDRP의 포스트 프로세싱 스택에 통합하는 C# 볼륨 컴포넌트는 다음과 같습니다:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System;

[Serializable, VolumeComponentMenu("Post-processing/Custom/DepthExample")]
public sealed class DepthExample : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter depthDistance = new ClampedFloatParameter(10f, 0f, 100f);
    Material m_Material;

    public bool IsActive() => m_Material != null && depthDistance.value > 0f;

    public override CustomPostProcessInjectionPoint injectionPoint =>
        CustomPostProcessInjectionPoint.AfterPostProcess;

    public override void Setup()
    {
        if (Shader.Find("Hidden/Shader/DepthExample") != null)
            m_Material = new Material(Shader.Find("Hidden/Shader/DepthExample"));
    }

    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
    {
        if (m_Material == null) return;
        m_Material.SetVector("_Params", new Vector4(0, 0, 0, depthDistance.value));
        m_Material.SetTexture("_InputTexture", source);
        HDUtils.DrawFullScreen(cmd, m_Material, destination);
    }

    public override void Cleanup() => CoreUtils.Destroy(m_Material);
}

이 셰이더는 Resources 폴더에 배치해야 하고, C# 스크립트는 Project Settings → Graphics → HDRP Default Settings의 Custom Post Process Orders 리스트에 추가해야 합니다. injection point는 이펙트가 언제 실행될지를 결정하는데—AfterPostProcess는 가장 마지막에 실행되고, BeforePostProcess는 tonemapping과 color grading 이전에 실행됩니다.

Custom Pass 풀스크린 셰이더 템플릿

Custom Pass는 포스트 프로세싱 이펙트보다 더 유연한데, HDRP 프레임의 특정 지점에 커스텀 렌더링을 주입할 수 있습니다. 깊이 접근이 포함된 완전한 Custom Pass 셰이더 템플릿을 보여드리겠습니다:

Shader "FullScreen/DepthEffect"
{
    HLSLINCLUDE
    #pragma vertex Vert
    #pragma target 4.5
    #pragma only_renderers d3d11 ps4 xboxone vulkan metal switch

    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl"

    float4 FullScreenPass(Varyings varyings) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(varyings);

        // 깊이 로드 및 위치 재구성
        float depth = LoadCameraDepth(varyings.positionCS.xy);
        PositionInputs posInput = GetPositionInput(
            varyings.positionCS.xy,
            _[ScreenSize.zw](http://ScreenSize.zw),
            depth,
            UNITY_MATRIX_I_VP,
            UNITY_MATRIX_V
        );

        float3 worldPos = posInput.positionWS;
        float3 viewDir = GetWorldSpaceNormalizeViewDir(posInput.positionWS);
        float eyeDepth = LinearEyeDepth(depth, _ZBufferParams);

        // 현재 카메라 컬러 로드
        float4 color = float4(CustomPassLoadCameraColor(varyings.positionCS.xy, 0), 1);

        // 깊이 기반 이펙트 적용
        float depthFade = saturate(eyeDepth / 20.0);
        color.rgb = lerp(color.rgb, float3(0.5, 0.7, 1.0), depthFade * 0.3);

        return color;
    }
    ENDHLSL

    SubShader
    {
        Pass
        {
            Name "Custom Pass"
            ZWrite Off
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            HLSLPROGRAM
            #pragma fragment FullScreenPass
            ENDHLSL
        }
    }
    Fallback Off
}

PositionInputs 구조체는 특히 강력한데—월드 공간 위치, 뷰 공간 위치를 재구성하고 라이팅 계산을 위한 헬퍼 데이터를 제공합니다. 이 덕분에 Custom Pass는 커스텀 ambient occlusion이나 특수한 안개 볼륨 같은, 전체 씬 컨텍스트가 필요한 스크린 스페이스 이펙트에 이상적입니다.

깊이로부터 월드 공간 위치 재구성하기

스크린 공간 좌표와 깊이를 월드 공간 위치로 되돌리면 레이 마칭, 볼륨메트릭 계산, 또는 씬 깊이와 커스텀 지오메트리를 비교하는 등의 이펙트가 가능해집니다. HDRP는 완전한 재구성을 수행하는 GetPositionInput() 헬퍼를 제공합니다:

float depth = LoadCameraDepth(varyings.positionCS.xy);
PositionInputs posInput = GetPositionInput(
    varyings.positionCS.xy,      // 스크린 공간 픽셀 좌표
    _[ScreenSize.zw](http://ScreenSize.zw),              // 역 스크린 크기 (1/width, 1/height)
    depth,                       // Raw 깊이 값
    UNITY_MATRIX_I_VP,           // 역 뷰-프로젝션 행렬
    UNITY_MATRIX_V               // 뷰 행렬
);

float3 worldPos = posInput.positionWS;  // 카메라 상대 월드 위치
float3 viewPos = posInput.positionVS;   // 뷰 공간 위치

중요: HDRP에서 positionWS는 큰 월드 좌표에서 부동소수점 정밀도를 향상시키기 위해 카메라 상대 좌표입니다. 절대 월드 위치를 얻으려면 다음을 사용하십시오:

float3 absoluteWorldPos = GetAbsolutePositionWS(posInput.positionWS);

Camera-Relative World Position 에 대하여 잠깐 더 살펴보겠습니다…

Camera-Relative World Position 이해하기

왜 Camera-Relative 좌표가 필요한가?

float는 32비트로 약 7자리 정도의 정밀도만 보장합니다. 그래서 원점에서 매우 먼 곳에 있는 오브젝트를 다룰 때 문제가 발생합니다.

// 문제 상황
float3 worldPos = float3(10000000.0, 0.0, 10000000.0);  // 원점에서 10km 떨어진 위치
float3 offset = float3(0.001, 0.0, 0.001);              // 1mm만 이동하고 싶음

float3 newPos = worldPos + offset;
// 결과: 변화 없음! offset이 무시됨

큰 숫자에 작은 숫자를 더하면 부동소수점 정밀도 때문에 작은 숫자가 손실됩니다. HDRP는 이 문제를 카메라를 원점으로 하는 상대 좌표계로 해결합니다.

HDRP의 해결 방법

// 카메라를 원점(0,0,0)으로 보는 좌표계
// 실제 월드 위치: (10000000.0, 100.0, 10000000.0)
// 카메라 위치: (10000000.0, 100.0, 9999990.0)

// Camera-relative position
float3 positionWS = float3(0.0, 0.0, 10.0);  // 카메라 앞 10m
// 이제 작은 변화도 정확하게 표현 가능합니다!

실전 코드 예제

Shader "HDRP/CameraRelativeExample"
{
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;  // Camera-relative world position
            };

            Varyings Vert(Attributes input)
            {
                Varyings output;
                float3 positionWS = TransformObjectToWorld([input.positionOS.xyz](http://input.positionOS.xyz));
                // 이 positionWS는 이미 camera-relative입니다!
                
                output.positionWS = positionWS;
                output.positionCS = TransformWorldToHClip(positionWS);
                return output;
            }

            float4 Frag(Varyings input) : SV_Target
            {
                // 케이스 1: 카메라와의 거리 (camera-relative 그대로 사용)
                float distanceToCamera = length(input.positionWS);
                // 카메라가 원점이라 길이가 곧 거리입니다

                // 케이스 2: 절대 월드 좌표가 필요할 때
                float3 absoluteWorldPos = GetAbsolutePositionWS(input.positionWS);

                // 케이스 3: 두 오브젝트 사이 거리
                float3 otherObjectPos = float3(100, 0, 100);
                float distance = length(input.positionWS - otherObjectPos);
                // 둘 다 camera-relative면 바로 계산 가능합니다

                // 케이스 4: 특정 월드 좌표와 비교
                float3 worldTarget = float3(5000, 0, 5000);  // 절대 좌표
                float3 cameraRelativeTarget = worldTarget - _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);
                float distanceToTarget = length(input.positionWS - cameraRelativeTarget);

                return float4(distanceToCamera / 100.0, 0, 0, 1);
            }
            ENDHLSL
        }
    }
}

상황별 사용 예제

안개(Fog) 효과

// Camera-relative 그대로 사용 - 간단하고 효율적입니다!
float fogDistance = length(positionWS);
float fogFactor = saturate(fogDistance / _FogMaxDistance);

월드 노이즈 샘플링

// 절대 좌표 필요 - 카메라가 움직여도 패턴 고정
float3 absolutePos = GetAbsolutePositionWS(positionWS);
float noise = SampleNoise3D(absolutePos * _NoiseScale);

라이트 거리 계산

// 라이트 위치도 camera-relative로 제공됩니다
float3 lightPos = _[LightPositionWS.xyz](http://LightPositionWS.xyz);
float distanceToLight = length(positionWS - lightPos);

특정 위치 마커

// 월드 (1000, 0, 1000) 위치에 마커
float3 markerWorldPos = float3(1000, 0, 1000);
float3 markerCameraRelative = markerWorldPos - _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);
float distanceToMarker = length(positionWS - markerCameraRelative);

if (distanceToMarker < 1.0)  // 1m 이내면 빨간색
    return float4(1, 0, 0, 1);

GetPositionInput 활용하기

// HDRP의 PositionInputs 구조체
PositionInputs posInput = GetPositionInput(
    varyings.positionCS.xy,
    _[ScreenSize.zw](http://ScreenSize.zw),
    depth,
    UNITY_MATRIX_I_VP,
    UNITY_MATRIX_V
);

// 제공되는 좌표들
float3 positionWS = posInput.positionWS;      // Camera-relative
float3 positionVS = posInput.positionVS;      // View space
float2 positionNDC = posInput.positionNDC;    // NDC
float2 positionSS = posInput.positionSS;      // Screen space

// 절대 좌표가 필요하면
float3 absoluteWS = GetAbsolutePositionWS(posInput.positionWS);

언제 어떤 좌표를 사용해야 하는가?

용도좌표 타입이유
안개, DOFCamera-relative카메라 거리만 필요
월드 텍스처 매핑Absolute카메라가 움직여도 고정
프로시저럴 생성Absolute패턴이 월드에 고정
오브젝트 간 거리Camera-relative둘 다 relative면 가능
라이팅Camera-relative라이트도 relative
물리 시뮬레이션Absolute월드 물리 법칙

디버깅 예제

// 좌표계 확인용
float4 DebugCoordinates(Varyings input) : SV_Target
{
    float3 camRelative = input.positionWS;
    float3 absolute = GetAbsolutePositionWS(camRelative);
    float3 cameraWorldPos = _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);
    
    // 검증: absolute = camRelative + cameraWorldPos
    float3 reconstructed = camRelative + cameraWorldPos;
    float error = length(absolute - reconstructed);
    
    if (error > 0.001)
        return float4(1, 0, 0, 1);  // 오류!
    
    // 거리별 색상
    float dist = length(camRelative);
    if (dist < 10)
        return float4(0, 1, 0, 1);      // 10m 이내
    else if (dist < 100)
        return float4(1, 1, 0, 1);      // 100m 이내
    else
        return float4(0, 0, 1, 1);      // 100m 이상
}

주의할 점

이 시스템을 사용할 때 몇 가지 알아두어야 할 점들이 있습니다. 먼저 성능 면에서 camera-relative 연산이 더 빠릅니다. 절대 좌표로 변환할 필요가 없어 추가 계산이 줄어듭니다. 그리고 큰 월드(10km 이상)에서는 정밀도를 유지하기 위해 이 방식이 필수적입니다. 부동소수점 정밀도 문제를 근본적으로 해결하기 때문입니다. 계산할 때는 항상 같은 좌표계끼리만 계산해야 한다는 점도 중요합니다. Camera-relative 좌표와 절대 좌표를 섞어 사용하면 잘못된 결과가 나옵니다. 마지막으로 GetAbsolutePositionWS() 함수를 호출하면 추가 연산 비용이 발생한다는 점을 기억하시기 바랍니다. 정말 필요한 경우에만 사용하는 것이 좋습니다.

// x 잘못된 예: 좌표계 섞어 사용
float3 worldPos = GetAbsolutePositionWS(positionWS);
float3 lightPos = _[LightPositionWS.xyz](http://LightPositionWS.xyz);  // camera-relative!
float distance = length(worldPos - lightPos);  // 잘못되었습니다!

// o 올바른 예: 같은 좌표계
float distance = length(positionWS - _[LightPositionWS.xyz](http://LightPositionWS.xyz));

이 시스템은 오픈월드나 우주 같은 큰 스케일 프로젝트에서 특히 중요합니다. 원점에서 아무리 멀어져도 밀리미터 단위 정밀도를 유지할 수 있습니다.

다시 본론으로 돌아와서...

투명 셰이더에서 월드 포지션 복원하기

투명 셰이더에서 씬 깊이와 프래그먼트 깊이를 비교할 때는 뷰 벡터를 이용해서 씬의 월드 포지션을 직접 복원할 수 있습니다:

// 버텍스 셰이더에서 월드 포지션을 프래그먼트로 전달
float3 positionWS = TransformObjectToWorld(input.positionOS);
float3 viewVector = _WorldSpaceCameraPos - positionWS;

// 프래그먼트 셰이더에서
float2 screenUV = i.screenPos.xy / i.screenPos.w;
float sceneDepth = LoadCameraDepth(screenUV * _ScreenSize.xy);
float sceneEyeDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);
float fragmentEyeDepth = -TransformWorldToView(positionWS).z;

// 씬 월드 포지션 복원
float3 sceneWorldPos = _WorldSpaceCameraPos - (normalize(viewVector) * sceneEyeDepth);

이러한 수동 방식은 서피스 셰이더에서 Custom Pass 헬퍼 함수들을 사용할 수 없을 때 필요합니다. 핵심 아이디어는 뷰 벡터가 카메라에서 프래그먼트까지의 방향을 제공하고, 이를 씬 깊이로 스케일하면 화면 픽셀에서 씬 표면까지의 벡터를 얻을 수 있다는 것입니다.

깊이 기반 포그 구현하기

깊이 포그는 가장 많이 사용되는 깊이 텍스처 활용법 중 하나로, 씬 지오메트리를 고려한 대기 효과를 만들 수 있습니다. 다음은 near와 far 페이드 거리를 가진 포그 구현입니다:

float4 DepthFog(Varyings input) : SV_Target
{
    uint2 positionSS = input.texcoord * _ScreenSize.xy;
    float3 sceneColor = LOAD_TEXTURE2D_X(_InputTexture, positionSS).xyz;

    // 선형 깊이 가져오기
    float depth = LoadCameraDepth(positionSS);
    float eyeDepth = LinearEyeDepth(depth, _ZBufferParams);

    // near와 far 거리로 포그 팩터 계산
    float fogFactor = saturate((eyeDepth - _FogNear) / (_FogFar - _FogNear));
    fogFactor = pow(fogFactor, _FogDensity);

    // 씬 컬러와 포그 컬러 블렌딩
    float3 finalColor = lerp(sceneColor, _FogColor.rgb, fogFactor);

    return float4(finalColor, 1.0);
}

소프트 파티클 이펙트를 만들 때는 지오메트리와 교차할 때 페이드 아웃되도록 씬 깊이와 파티클 깊이를 비교하시면 됩니다:

// 파티클 셰이더 프래그먼트 함수에서
float2 screenUV = i.screenPos.xy / i.screenPos.w;
float sceneEyeDepth = LinearEyeDepth(
    LoadCameraDepth(screenUV * _ScreenSize.xy),
    _ZBufferParams
);
float particleEyeDepth = LinearEyeDepth(i.screenPos.z / i.screenPos.w, _ZBufferParams);

// 지오메트리와 가까워질 때 페이드
float depthDiff = sceneEyeDepth - particleEyeDepth;
float fade = saturate(depthDiff / _FadeDistance);

// 알파에 적용
return particleColor * fade;

이렇게 하면 파티클 쿼드가 솔리드 지오메트리를 뚫고 나가는 날카로운 교차점을 방지하고, 물보라나 마법 파티클처럼 표면에 자연스럽게 블렌딩되는 효과를 만들 수 있습니다.

깊이 비교를 이용한 엣지 검출

깊이 기반 엣지 검출은 인접한 픽셀의 깊이를 비교해서 지오메트리 경계를 찾아내는 기법입니다. 아웃라인이나 테크니컬 드로잉 스타일, 가려지는 엣지 강조 같은 데 유용합니다:

float DepthEdgeDetection(uint2 positionSS, float threshold)
{
    float centerDepth = LoadCameraDepth(positionSS);
    float edgeStrength = 0;

    // 소벨 커널 오프셋
    int2 offsets[8] = {
        int2(-1, -1), int2(0, -1), int2(1, -1),
        int2(-1,  0),              int2(1,  0),
        int2(-1,  1), int2(0,  1), int2(1,  1)
    };

    float weights[8] = { 1, 2, 1, 2, 2, 1, 2, 1 };

    for (int i = 0; i < 8; i++)
    {
        uint2 samplePos = positionSS + offsets[i];
        float sampleDepth = LoadCameraDepth(samplePos);
        float depthDiff = abs(centerDepth - sampleDepth);
        edgeStrength += depthDiff * weights[i];
    }

    return step(threshold, edgeStrength);
}

더 나은 결과를 얻으려면 비교 전에 선형 깊이로 변환하는 것이 좋습니다. 그래야 비선형 깊이 분포가 뷰 전체에 걸쳐 엣지 강도에 일관성 없게 영향을 주는 것을 막을 수 있습니다:

float centerLinear = LinearEyeDepth(centerDepth, _ZBufferParams);
float sampleLinear = LinearEyeDepth(sampleDepth, _ZBufferParams);
float depthDiff = abs(centerLinear - sampleLinear);

깊이 엣지와 노말 엣지를 결합하면 실루엣과 표면 디테일 변화 모두에서 작동하는 강력한 아웃라인 검출을 만들 수 있습니다.

오브젝트 오클루전과 선택 효과

Custom Pass를 사용하면 특정 오브젝트를 커스텀 깊이 버퍼에 렌더링한 다음 메인 씬 깊이와 비교해서 정교한 선택 및 오클루전 효과를 만들 수 있습니다. 다음은 X-ray 선택 하이라이트용 셰이더입니다:

float4 SelectionHighlight(Varyings varyings) : SV_Target
{
    uint2 positionSS = varyings.positionCS.xy;

    // 양쪽 깊이 버퍼 로드
    float sceneDepth = LoadCameraDepth(positionSS);
    float customDepth = LoadCustomDepth(positionSS);

    // 비교 가능한 eye depth로 변환
    float sceneEye = LinearEyeDepth(sceneDepth, _ZBufferParams);
    float customEye = LinearEyeDepth(customDepth, _ZBufferParams);

    // 씬 컬러 로드
    float4 color = float4(CustomPassLoadCameraColor(positionSS, 0), 1);

    // 선택된 것이 커스텀 깊이에 보임
    if (customEye < 100000.0) // 유효한 깊이가 쓰여짐
    {
        if (sceneEye < customEye) // 선택된 오브젝트가 씬 뒤에
        {
            // 오브젝트가 가려짐 - x-ray 효과 적용
            color.rgb = lerp(color.rgb, _OccludedColor.rgb, 0.5);
        }
        else // 선택된 오브젝트 보임
        {
            // 하이라이트 적용
            color.rgb += _HighlightColor.rgb * 0.3;
        }
    }

    return color;
}

Custom Pass C# 컴포넌트는 이 셰이더를 실행하기 전에 선택된 오브젝트를 커스텀 깊이 버퍼에 렌더링합니다:

public class SelectionPass : CustomPass
{
    public LayerMask selectionLayer;

    protected override void Execute(CustomPassContext ctx)
    {
        // 선택 레이어를 커스텀 깊이에 렌더링
        CoreUtils.SetRenderTarget(ctx.cmd, ctx.customDepthBuffer, ClearFlag.All);
        CustomPassUtils.DrawRenderers(ctx, selectionLayer);

        // 풀스크린 셰이더 실행
        CoreUtils.SetRenderTarget(ctx.cmd, ctx.cameraColorBuffer, ctx.cameraDepthBuffer);
        CoreUtils.DrawFullScreen(ctx.cmd, passMaterial);
    }
}

이 테크닉은 다양한 효과로 확장할 수 있습니다. 깊이 불연속성 기반 림 라이팅, 근접 하이라이트, 깊이 기반 그림자, 또는 특정 오브젝트를 씬의 나머지 부분과 구별해야 하는 모든 효과에 적용할 수 있습니다.

Built-in과 URP와의 중요한 차이점

HDRP에서 작동하지 않는 것들을 이해하면 디버깅 시간을 몇 시간이나 절약할 수 있습니다. 레거시 매크로는 완전히 제거되었습니다. UNITY_DECLARE_DEPTH_TEXTURE, SAMPLE_DEPTH_TEXTURE를 사용할 수 없고, _CameraDepthTexture에 직접 접근할 수도 없습니다. 이것들은 단일 텍스처 깊이 버퍼용으로 설계된 것이고, HDRP의 깊이 피라미드는 특별한 샘플링 함수가 필요합니다.

인클루드 파일 변경은 필수입니다. UnityCG.cginc는 HDRP에 존재하지 않습니다. 패키지 기반 인클루드를 사용해야 합니다. 매핑은 다음과 같습니다:

  • Built-in: #include "UnityCG.cginc"
  • URP: #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
  • HDRP: #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

텍스처 선언 매크로가 UNITYDECLARE*에서 TEXTURE2D_X로 변경되었습니다. _X 변형은 스테레오 렌더링(VR)을 자동으로 처리해서 각 눈을 위한 텍스처 배열을 관리합니다. 마찬가지로 LOAD_TEXTURE2D_X가 로딩용 tex2D를 대체합니다:

// Built-in/URP
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);

// HDRP
// 선언 필요 없음, 함수 바로 사용
float depth = LoadCameraDepth(positionSS);

함수 시그니처에 명시적 파라미터가 필요합니다. LinearEyeDepth()와 Linear01Depth()는 _ZBufferParams를 명시적으로 전달받아야 합니다. HDRP는 Built-in만큼 암시적 전역 상태에 의존하지 않습니다:

// Built-in
float linearDepth = LinearEyeDepth(rawDepth);

// HDRP
float linearDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

셰이더 패스 요구사항이 크게 달라집니다. HDRP는 깊이 프리패스용 "DepthForwardOnly" 같은 특정 LightMode 태그를 사용하고, Custom Pass는 정의된 지점(BeforeRendering, BeforeTransparent, AfterOpaqueDepthAndNormal 등)에 주입됩니다. 리플레이스먼트 셰이더나 카메라 콜백 대신 말입니다.

포스트 프로세싱 통합이 Post Processing Stack v2에서 HDRP의 내장 Custom Post Processing으로 완전히 변경되었습니다. 포스트 프로세스 레이어 컴포넌트 대신 새로운 볼륨 컴포넌트 스크립트와 HDRP Global Settings에 등록이 필요합니다.

Shader Graph 깊이 노드 사용법

아티스트나 빠른 프로토타이핑용으로는 Shader Graph에서 세 가지 출력 모드를 가진 HD Scene Depth 노드를 제공합니다. Eye 모드는 월드 유닛 단위의 선형 깊이를 출력해서 LinearEyeDepth()와 동등합니다. Linear01 모드는 정규화된 0-1 깊이를 출력합니다. Raw 모드는 변환 전 원시 버퍼 값을 출력합니다.

Scene Depth 노드는 Screen Position 입력(정규화된 0-1 좌표)이 필요하고 투명 서페이스의 Fragment 스테이지에서만 작동합니다. 불투명 셰이더는 깊이를 샘플링할 수 없습니다. 자기 자신이 깊이 버퍼에 쓰고 있기 때문에 샘플링하면 순환 의존성이 발생합니다.

Shader Graph에서 간단한 깊이 포그 만들기:

  1. Screen Position 노드 추가 (기본 출력)
  2. Scene Depth 노드 추가 (Eye 모드)
  3. Screen Position 노드를 Raw로 설정, RGBA 분할해서 W 컴포넌트를 프래그먼트 깊이로 사용
  4. 씬 깊이에서 프래그먼트 깊이를 빼기
  5. 포그 거리 파라미터로 나누기
  6. 결과를 Saturate
  7. 페이드 효과를 위해 Alpha에 연결

Shader Graph에서 Custom Depth에 접근하려면 다음 코드로 Custom Function 노드를 사용하십시오:

void GetCustomDepth_float(float2 UV, out float Depth)
{
    Depth = SampleCustomDepth(UV);
}

Shader Graph는 프로토타이핑이나 아티스트 친화적인 워크플로우에 좋지만, 손으로 작성한 코드가 더 나은 성능과 복잡한 깊이 기반 알고리즘에 대한 완전한 제어를 제공합니다. Master Node를 우클릭해서 "Copy Shader"를 선택하면 Shader Graph를 코드로 내보낼 수 있고, 생성된 HLSL을 최적화할 수 있습니다.

깊이 접근 문제 해결하기

깊이가 0이나 유효하지 않은 값을 반환합니다: 깊이가 쓰여진 후에 샘플링하고 있는지 확인하십시오. 포스트 프로세싱 효과는 주입 지점이 AfterOpaqueDepthAndNormal 이상이어야 합니다. 깊이 렌더링 전 Custom Pass는 비어있거나 이전 프레임의 깊이를 읽게 됩니다. Frame Settings에서 깊이 텍스처 생성이 활성화되어 있는지 확인하십시오. 일부 커스텀 카메라 설정은 성능을 위해 비활성화합니다.

밉맵이나 샘플링 아티팩트: LoadCameraDepth()나 SampleCameraDepth()만 사용하십시오. LOAD_TEXTURE2D_X로 깊이 텍스처를 직접 샘플링하면 피라미드 샘플링 로직을 우회해서 잘못된 결과가 나옵니다. HDRP의 깊이 피라미드는 헬퍼 함수들이 내부적으로 관리하는 특별한 텍스처 좌표가 필요합니다.

투명 오브젝트가 깊이에서 누락됩니다: 기본적으로 투명 머티리얼은 깊이를 쓰지 않습니다. 머티리얼 설정에서 Transparent Depth Prepass나 Transparent Depth Postpass를 활성화하십시오. Transparent 렌더 큐의 오브젝트는 별도 패스에서 깊이를 쓰도록 명시적으로 설정하지 않으면 깊이 텍스처에 나타나지 않습니다.

커스텀 셰이더는 렌더링되는데 오브젝트가 깊이에서 보이지 않습니다: 커스텀 셰이더에 DepthForwardOnly 패스를 추가하십시오:

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

    ZWrite On
    ColorMask 0

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderLibrary/ShaderVariables.hlsl"

    struct Attributes { float4 positionOS : POSITION; };
    struct Varyings { float4 positionCS : SV_POSITION; };

    Varyings vert(Attributes input)
    {
        Varyings output;
        output.positionCS = TransformObjectToHClip(input.positionOS);
        return output;
    }

    float4 frag() : SV_Target { return 0; }
    ENDHLSL
}

포스트 프로세싱 효과가 나타나지 않습니다: 셰이더가 Resources 폴더에 있는지, C# 컴포넌트가 CustomPostProcessVolumeComponent를 상속하는지, IsActive()가 true를 반환하는지, 컴포넌트가 Edit → Project Settings → HDRP → Custom Post Process Orders의 Custom Post Process Orders에 추가되어 있는지 확인하십시오. 씬의 Volume에 충분한 우선순위로 효과가 활성화되어 있는지도 확인하십시오.

플랫폼별 이슈: Reversed-Z 동작이 플랫폼마다 다릅니다. DirectX는 reversed-Z(near에서 1)를 사용하는데 일부 OpenGL 구현은 전통적인 Z(near에서 0)를 사용합니다. 변환 함수들이 자동으로 처리하지만, 원시 깊이를 수동으로 처리한다면 UNITY_REVERSED_Z define을 확인하십시오. 에디터에서는 작동하는데 빌드에서 실패한다면 HDRP Asset Lit Shader Mode가 "Deferred Only"가 아닌 "Both"인지 확인하십시오. 셰이더 배리언트를 제한합니다.

성능 최적화 전략

깊이 텍스처에 접근하는 것은 메모리 대역폭을 많이 소비하는 작업입니다. 매번 샘플링할 때마다 VRAM에서 데이터를 읽어오기 때문입니다. 같은 픽셀의 깊이 값을 여러 번 사용한다면 LoadCameraDepth()를 세 번 호출하는 대신 로컬 변수에 저장해두는 것이 훨씬 효율적입니다. 이렇게 중복 샘플링을 최소화하는 것만으로도 성능이 눈에 띄게 개선됩니다.

인젝션 포인트도 신중하게 선택해야 합니다. 불투명 오브젝트의 깊이만 필요한 포스트 프로세싱 효과라면 AfterPostProcess 대신 AfterOpaqueAndSky에 주입하는 것이 좋습니다. 불필요한 재계산을 피할 수 있기 때문입니다. 각 인젝션 포인트마다 오버헤드가 있으므로 가능하면 여러 효과를 하나로 합치는 것도 방법입니다.

ambient occlusion 같은 무거운 깊이 기반 효과는 절반 해상도로 처리하는 것을 고려해보십시오. 화면 해상도를 절반으로 줄이면 샘플 수가 4분의 1로 줄어들면서 성능이 4배 향상되는데, 블러 효과에서는 품질 손실이 거의 눈에 띄지 않습니다. HDUtils.RTHandles로 스케일링을 적용하시면 됩니다:

RTHandle halfResDepth = RTHandles.Alloc(
    [Vector2.one](http://Vector2.one) * 0.5f,
    depthBufferBits: DepthBits.None,
    filterMode: FilterMode.Bilinear);

셰이더 복잡도도 깊이 샘플링 주변에서 최적화해야 합니다. 깊이 의존적인 계산들을 그룹화해서 프래그먼트 셰이더의 분기를 최소화하는 것이 좋습니다. discard나 clip 연산은 early-Z 최적화를 비활성화하기 때문에 깊이를 읽는 셰이더에서는 가급적 피하는 것이 좋습니다.

플랫폼별로도 고려할 점이 있습니다. 콘솔은 압축된 깊이 버퍼를 사용하는데, 읽기 자체는 빠르지만(early-Z 테스트) 전체 화면 압축 해제에 약 0.7ms 정도 소요됩니다. 그래서 성능이 중요한 경로에서는 깊이 텍스처 읽기를 최소화해야 합니다. 모바일에서는 깊이 텍스처 접근이 특히 비싸므로 더 낮은 해상도나 단순화된 알고리즘을 고려하는 것이 좋습니다.

메모리 사용량은 해상도에 비례해서 늘어납니다. 1080p 24비트 깊이는 약 2.5MB인데, 4K 32비트는 약 32MB나 차지합니다. 깊이 피라미드는 밉 레벨 때문에 대략 33% 정도의 추가 오버헤드가 있습니다. VRAM이 제한된 콘솔에서는 메모리 예산을 항상 모니터링해야 합니다.

일반적인 성능 목표치를 말씀드리면, 1080p에서 깊이 텍스처 생성은 1ms 미만, 깊이를 사용하는 포스트 프로세싱 효과 전체는 2ms 미만, 개별 Custom Pass는 0.5ms 미만이 적당합니다. Unity의 Frame Debugger와 플랫폼별 프로파일러로 병목 지점을 찾아내는 것이 중요합니다.

프로젝트 설정 필수 체크리스트

먼저 HDRP Asset 설정부터 확인해보겠습니다 (Project Settings → Graphics → HDRP Asset).

Lit Shader Mode는 "Both"로 설정해야 합니다. "Deferred Only"로 하면 Custom Pass를 지원하지 않습니다. Custom Pass는 Rendering 섹션에서 활성화하시고, Custom Buffer Format은 커스텀 버퍼를 사용한다면 적절한 포맷으로 설정하시면 되는데, 기본값은 R8G8B8A8입니다. Depth Pyramid 설정은 기본적으로 활성화되어 있고 깊이 샘플링 시 자동으로 사용됩니다.

다음으로 Frame Settings를 살펴보겠습니다. 카메라별로 또는 프로젝트 전체에 적용할 수 있습니다.

Opaque Objects의 Depth Prepass는 활성화되어 있어야 하는데, 보통 기본값으로 되어 있습니다. Custom Pass를 사용한다면 이것도 활성화해야 합니다. 특정 카메라가 깊이를 비활성화하는 오버라이드 설정을 하지 않았는지도 확인해보십시오.

Custom Post-Processing 설정도 중요합니다.

셰이더는 반드시 Resources 폴더 안에 있어야 합니다. 하위 폴더는 상관없습니다 (예: Resources/Shaders/DepthEffect.shader). C# 스크립트는 프로젝트 어디에나 있어도 되는데 CustomPostProcessVolumeComponent를 상속받아야 합니다. 이 스크립트를 Project Settings → Graphics → HDRP Default Settings → Custom Post Process Orders의 목록에 추가해야 합니다. 씬에 Volume이 있어야 하고 거기에 컴포넌트가 활성화되어 있어야 하며, 올바른 인젝션 포인트(AfterPostProcess, BeforePostProcess 등)를 선택해야 합니다.

Custom Pass 설정도 확인해보겠습니다.

Custom Pass Volume 컴포넌트가 있는 GameObject가 필요합니다. Custom Pass 스크립트가 연결되어 있거나 Fullscreen pass가 구성되어 있어야 합니다. Injection Point는 깊이가 필요한 시점에 맞게 설정해야 하며, Target ColorBuffer/DepthBuffer는 보통 둘 다 Camera로 설정하시면 됩니다.

Material/Shader 요구사항도 있습니다.

투명 머티리얼이 깊이를 읽으려면 설정이 필요합니다 (불투명 오브젝트는 자기 자신의 깊이를 읽습니다). 커스텀 셰이더에는 DepthForwardOnly 패스가 있어야 오브젝트가 깊이에 나타납니다. RenderType과 Queue 태그도 올바르게 설정되어 있어야 합니다.

설정을 확인하려면 Frame Debugger를 활성화해보십시오 (Window → Analysis → Frame Debugger). 깊이 텍스처 생성 패스가 나타나는지, 그리고 커스텀 패스나 포스트 프로세싱이 예상한 인젝션 포인트에서 실행되는지 확인할 수 있습니다.

스테레오 렌더링과 VR 작업하기

HDRP의 깊이 시스템은 _X 텍스처 매크로를 통해 스테레오 렌더링을 자동으로 처리합니다. TEXTURE2D_X는 내부적으로 텍스처 배열이 되는데, 왼쪽 눈과 오른쪽 눈용 텍스처가 따로 있습니다. LOAD_TEXTURE2D_X는 스테레오 eye 인덱스를 사용해서 올바른 배열 슬라이스를 샘플링합니다.

vertex와 fragment 함수에서는 항상 스테레오 설정 매크로를 포함해야 합니다:

// Vertex shader
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

// Fragment shader
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

이 매크로들은 스테레오가 아닌 빌드에서는 아무 일도 하지 않지만, VR에서는 정확한 동작을 위해 반드시 필요합니다. 이것이 없으면 양쪽 눈 모두 잘못된 눈의 깊이 텍스처로 렌더링되어서 심각한 시각적 오류가 발생할 수 있습니다.

_ScreenSize 변수는 VR에서 눈별 렌더 타겟 크기를 자동으로 반영하기 때문에 좌표 계산이 그대로 맞아떨어집니다. 마찬가지로 뷰-프로젝션 행렬(UNITY_MATRIX_VP, UNITY_MATRIX_I_VP)도 눈별로 설정됩니다. 제공된 매크로만 제대로 사용하면 깊이 재구성 코드는 VR에서도 수정 없이 그대로 작동합니다.

VR에서 Custom Passes를 사용할 때는 양쪽 눈이 적절한 상태로 순차적으로 패스를 실행합니다. 커스텀 버퍼를 할당한다면 RTHandle에 XR 레이아웃을 사용해서 눈별 텍스처를 만들어야 합니다:

RTHandle customBuffer = RTHandles.Alloc(
    [Vector2.one](http://Vector2.one),
    TextureXR.slices,  // 싱글/스테레오를 자동으로 처리
    colorFormat: GraphicsFormat.R32G32B32A32_SFloat);

고급 깊이 피라미드 활용

LoadCameraDepth()와 SampleCameraDepth()는 자동으로 깊이 피라미드를 사용하지만, 고급 기법을 위해 특정 밉 레벨을 직접 샘플링할 수도 있습니다. 낮은 밉 레벨에는 min-filtered 깊이(4x4 픽셀 블록에서 가장 가까운 값)가 들어있는데, 이것이 계층적 Z-버퍼 알고리즘에 유용합니다.

스크린 스페이스 ambient occlusion은 먼 샘플 포인트를 체크할 때 거친 밉 레벨을 샘플링하면 좋습니다. 정확도를 조금 포기하는 대신 상당한 성능 향상을 얻을 수 있습니다. 32픽셀 떨어진 샘플을 읽을 때 밉 레벨 0(원본) 대신 밉 레벨 2(4배 다운샘플)를 읽으면 메모리 대역폭이 16배나 줄어듭니다.

깊이 인식 업샘플링도 깊이 피라미드를 활용해서 bilateral filtering을 가이드할 수 있습니다. 여러 밉 레벨을 체크해서 전체 해상도로 모든 픽셀을 읽지 않고도 깊이 불연속성을 빠르게 찾아낼 수 있습니다.

깊이 피라미드는 불투명 렌더링 이후 AfterOpaqueDepthAndNormal 인젝션 포인트에서 자동으로 생성되기 때문에, 그 이후에 주입되는 포스트 프로세싱과 Custom Passes에서 사용할 수 있습니다. 각 밉은 min filtering을 사용하는데, 피라미드가 각 레벨에서 가장 가까운 깊이를 저장해서 날카로운 경계를 유지하고 보수적인 occlusion 테스트를 보장합니다.

밉별 접근이 필요한 커스텀 효과를 만들려면 깊이 피라미드 텍스처를 수동으로 선언해야 하는데, 대부분의 경우에는 제공된 함수들이 복잡성을 효과적으로 추상화해주기 때문에 거의 필요 없습니다.

완전한 워크플로우: 깊이 기반 물 거품

실전 예제로 실용적인 효과를 만들어보겠습니다. 물이 지형과 만나는 해안선에 거품이 나타나는 효과입니다. 투명 셰이더에서 깊이 비교를 하는데, 지금까지 다룬 모든 기법이 들어가 있습니다:

Shader "Custom/WaterFoam"
{
    Properties
    {
        _FoamColor ("Foam Color", Color) = (1,1,1,1)
        _FoamDistance ("Foam Distance", Float) = 0.5
        _WaterColor ("Water Color", Color) = (0,0.4,0.8,0.7)
    }

    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 4.5

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float4 screenPos : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float4 _FoamColor;
            float4 _WaterColor;
            float _FoamDistance;

            Varyings vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                output.positionWS = TransformObjectToWorld([input.positionOS.xyz](http://input.positionOS.xyz));
                output.positionCS = TransformWorldToHClip(output.positionWS);
                output.screenPos = ComputeScreenPos(output.positionCS);

                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                // 스크린 UV 가져오기
                float2 screenUV = input.screenPos.xy / input.screenPos.w;
                uint2 positionSS = screenUV * _ScreenSize.xy;

                // 씬 깊이 로드
                float sceneDepth = LoadCameraDepth(positionSS);
                float sceneEyeDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);

                // 프래그먼트 깊이 계산
                float3 viewPos = TransformWorldToView(input.positionWS);
                float fragmentEyeDepth = -viewPos.z;

                // 깊이 차이 - 지형이 물 아래 얼마나 깊이 있는지
                float depthDiff = sceneEyeDepth - fragmentEyeDepth;

                // 거품 팩터 - 교차점에서 1, 거리에서 0으로 페이드
                float foamFactor = 1.0 - saturate(depthDiff / _FoamDistance);
                foamFactor = pow(foamFactor, 2.0); // 폴오프 날카롭게

                // 물과 거품 블렌딩
                float4 finalColor = lerp(_WaterColor, _FoamColor, foamFactor);

                return finalColor;
            }
            ENDHLSL
        }
    }
}

이 셰이더는 물 표면과 아래 지형 사이의 깊이 차이를 계산해서 자연스러운 거품을 만듭니다. 깊이 차이가 줄어들수록(물이 해안선에 가까워질수록) 거품 강도가 증가합니다. 이 효과는 어떤 지오메트리든 자동으로 적응합니다. 해변이든 바위든 얕은 웅덩이든 수동으로 페인팅할 필요 없이 말입니다.

이 기법은 다양한 파티클과 투명 효과로 확장할 수 있습니다. 빗물 웅덩이, 충격에 반응하는 에너지 쉴드, 포스 필드 교차점, 또는 투명 표면이 실제 씬 깊이를 기반으로 단단한 지오메트리와의 근접도에 반응해야 하는 모든 효과에 활용할 수 있습니다.


그래픽스 프로그래밍 용어 레퍼런스

몇 가지 그래픽스 프로그래밍 용어들을 설명합니다.

깊이(Depth) 관련

  • Depth Buffer (Z-Buffer): 각 픽셀의 카메라로부터의 거리 정보를 저장하는 버퍼로, 어떤 오브젝트가 앞에 있는지 판단하는 데 사용됩니다. Depth Testing 설명
  • Depth Texture: 깊이 버퍼의 내용을 텍스처로 저장한 것으로, 셰이더에서 씬의 깊이 정보를 읽을 수 있게 합니다.
  • LinearEyeDepth: 카메라 공간에서의 선형 깊이 값으로, 실제 월드 단위로 카메라로부터의 거리를 나타냅니다.
  • 깊이 피라미드 (Depth Pyramid): 원본 깊이 텍스처를 여러 해상도로 다운샘플링한 밉맵 체인으로, 효율적인 깊이 쿼리를 가능하게 합니다.
  • Hierarchical Z-Buffer: 여러 레벨의 깊이 정보를 계층적으로 저장하여 대규모 오클루전 테스트를 최적화하는 기법입니다.

렌더링 기법

  • Custom Pass: HDRP에서 렌더링 파이프라인의 특정 지점에 사용자 정의 렌더링 로직을 삽입하는 기능입니다. Custom Pass 문서
  • Injection Point: 커스텀 패스가 실행되는 렌더링 파이프라인의 특정 단계입니다.
  • RTHandle (Render Texture Handle): HDRP에서 해상도 독립적인 렌더 타겟을 관리하는 시스템으로, 동적 해상도를 지원합니다.
  • Stencil Buffer: 픽셀 단위로 마스킹 정보를 저장하는 버퍼로, 특정 영역만 렌더링하거나 효과를 적용할 때 사용됩니다.

VR/XR 관련

  • Single-Pass Instanced: 양쪽 눈의 렌더링을 한 번의 드로우 콜로 처리하는 VR 최적화 기법입니다. Single-Pass Instancing 문서
  • Stereo Rendering: VR에서 좌우 눈을 위한 두 개의 별도 이미지를 렌더링하는 과정입니다.
  • Eye Index: VR에서 현재 렌더링 중인 눈(왼쪽=0, 오른쪽=1)을 식별하는 인덱스입니다.
profile
테크아트 컨설팅 전문 회사 "메이즈라인" 입니다.

0개의 댓글