Postprocessing with the Depth Texture
Summary In the last tutorial I explained how to do very simple postprocessing effects. One important tool to do more advanced effects is access to the depth buffer. It’s a texture in which the distance of pixels from the camera is saved in. To understand
www.ronja-tutorials.com
Ronja 님의 허락을 받고 번역한 튜토리얼입니다. 원문은 위 링크에서 확인하실 수 있습니다.
몇몇 부분은 생략·추가하였습니다.
의역과 오역이 넘쳐날 수 있으니 편하게 봐주시고 잘못된 부분은 알려주시면 감사하겠습니다!
목차
- 개요
- 뎁스 값 읽기
- 퍼져나가는 파동 생성하기
개요
지난 튜토리얼에서는 아주 간단한 후처리 효과를 구현하는 방법을 설명했습니다. 더 발전된 효과를 만들기 위해 중요한 도구 중 하나는 바로 깊이 버퍼(depth buffer)에 접근하는 것입니다. 깊이 버퍼는 픽셀이 카메라로부터 떨어진 거리를 저장하는 텍스처입니다.
깊이 버퍼에 접근하는 후처리 효과가 어떻게 작동하는지 이해하려면, 우선 유니티에서 후처리가 전반적으로 어떻게 동작하는지를 알아두는 것이 좋습니다. 이에 대해서는 작성한 튜토리얼을 확인해주세요.

뎁스 값 읽기
우리는 이전에 만든 간단한 후처리 튜토리얼의 파일들을 기반으로 시작해 여기에 추가 작업을 해 나갈 것입니다.
가장 먼저 확장할 부분은, 우리가 만든 머티리얼을 렌더링 파이프라인에 삽입하는 C# 스크립트입니다. 이 스크립트를 확장해서, 실행될 때 자기 자신이 붙어 있는 동일한 게임 오브젝트의 카메라를 찾아내고, 그 카메라에 깊이 버퍼를 생성하도록 지시하게 만들 것입니다. 이는 DepthTextureMode 플래그를 통해 이루어집니다.
물론 단순히 깊이 버퍼를 렌더링하도록 설정할 수도 있지만, 여기서는 기존 값에 우리가 필요한 플래그를 비트 OR 연산으로 더해 주는 방식을 사용할 것입니다. 이렇게 하면 다른 스크립트들이 자신들의 효과를 렌더링하기 위해 설정한 플래그들을 덮어쓰지 않고 그대로 유지할 수 있습니다. (비트마스크가 어떻게 동작하는지 궁금하다면 따로 참고해 보셔도 좋습니다.)
private void Start()
{
Camera cam = GetComponent<Camera>();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.Depth;
}
깊이 텍스처에 접근하기 위해 C#에서 수정해야 할 부분은 모두 끝났습니다. 이제 셰이더를 작성할 수 있습니다.
깊이 텍스처에 접근하는 방법은, _CameraDepthTexture 라는 새로운 텍스처 샘플러를 만드는 것입니다. 이 샘플러는 다른 텍스처와 마찬가지로 읽을 수 있으므로, 단순히 샘플링해서 깊이 텍스처가 어떻게 보이는지 확인할 수 있습니다.
깊이 값은 단일 값이기 때문에 텍스처의 R(red) 채널에만 저장되고, 다른 색상 채널은 비어 있습니다. 따라서 우리는 R값을 그대로 가져와 사용하면 됩니다.
// 뎁스 텍스처
sampler2D _CameraDepthTexture;
// ···
// 프래그먼트 셰이더
fixed4 frag(v2f i) : SV_TARGET
{
// 뎁스 텍스처로부터 뎁스 값을 가져옵니다.
float depth = tex2D(_CameraDepthTexture, i.uv).r;
return depth;
}
이 작업을 마치고 게임을 실행해 보면, 화면이 대부분 검게 보일 가능성이 큽니다. 그 이유는 깊이 값이 선형(linear)으로 인코딩되지 않기 때문입니다. 카메라에 가까운 거리일수록 먼 거리보다 더 정밀하게 표현되는데, 이는 가까운 영역에서 더 높은 정밀도가 필요하기 때문입니다.
따라서 카메라를 오브젝트에 아주 가까이 두면 밝은 색으로 보일 것이며, 이는 해당 오브젝트가 카메라와 가깝다는 것을 의미합니다. (만약 카메라를 오브젝트에 바짝 붙였는데도 화면이 여전히 검거나 거의 검게 보인다면, Near Clipping Distance 값을 늘려 보는 것도 좋습니다.)

이를 좀 더 활용하기 쉽게 만들기 위해서는 깊이 값을 디코딩해야 합니다. 다행히 유니티는 현재의 깊이 값을 받아 이를 0에서 1 사이의 선형 깊이(linear depth)로 변환해 주는 메소드를 제공합니다. 여기서 0은 카메라 위치를, 1은 먼 거리의 클리핑 평면(Far Clipping Plane)을 의미합니다.
(만약 이 단계에서 이미지가 거의 검고 하늘 상자가 흰색으로만 보인다면, 카메라의 Far Clipping Plane 값을 낮춰서 더 다양한 그라데이션을 확인할 수 있습니다.)
// 프래그먼트 셰이더
fixed4 frag(v2f i) : SV_TARGET
{
// 뎁스 텍스처로부터 뎁스 값을 가져옵니다.
float depth = tex2D(_CameraDepthTexture, i.uv).r;
// 카메라와 먼 거리의 클리핑 평면 사이의 선형 깊이값
depth = Linear01Depth(depth);
return depth;
}

다음 단계는, 우리가 가진 깊이를 카메라 설정과 완전히 분리해서 카메라 설정을 변경하더라도 후처리 효과의 결과가 달라지지 않도록 만드는 것입니다. 이를 위해 단순히 현재의 선형 깊이(linear depth) 에 Far Clipping Plane의 거리를 곱해 주면 됩니다.
유니티는 ProjectionParams 변수를 통해 Near Clipping Plane과 Far Clipping Plane 값을 제공합니다. 이 중 Far Clipping Plane 값은 z 성분에 들어 있습니다.
// 프래그먼트 셰이더
fixed4 frag(v2f i) : SV_TARGET
{
// 뎁스 텍스처로부터 뎁스 값을 가져옵니다.
float depth = tex2D(_CameraDepthTexture, i.uv).r;
// 카메라와 먼 거리의 클리핑 평면 사이의 선형 깊이값
depth = Linear01Depth(depth);
// 카메라와의 거리를 Unit 단위로 나타낸 깊이값
depth = depth * _ProjectionParams.z;
return depth;
}

대부분의 오브젝트들은 카메라로부터 1 유닛(Unit) 이상 떨어져 있기 때문에, 화면은 다시 대부분 흰색으로 보일 것입니다. 하지만 이제 우리는 카메라의 클리핑 평면(Clipping Planes) 에 의존하지 않고, 이해할 수 있는 단위(유니티 유닛, Unity Units)로 표현된 값을 사용할 수 있게 되었습니다.
퍼져나가는 파동 생성하기
이제 이 깊이 정보를 활용해서, 마치 플레이어로부터 퍼져나가는 파동 효과를 만드는 방법을 보여드리겠습니다. 파동이 플레이어로부터 멀어지는 거리, 파동에 적용될 트레일 효과의 길이, 그리고 파동의 색상을 자유롭게 조정할 수 있게 될 것입니다.
따라서 첫 번째 단계는 이러한 변수들을 프로퍼티와 셰이더의 변수로 추가하는 것입니다. 여기서 우리는 Header 속성을 사용하여, 인스펙터에서 파동과 관련된 변수들이 모여 있는 부분 위에 “Wave”라는 글씨를 굵게 표시해줍니다. 단지 시각적으로 구분하기 위해 추가하는 것으로, 셰이더의 기능에는 전혀 영향을 주지 않습니다.
// 인스펙터 창에서 조절 가능한 값을 노출시킵니다.
Properties
{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
[Header(Wave)]
_WaveDistance ("Distance from player", float) = 10
_WaveTrail ("Length of the trail", Range(0,5)) = 1
_WaveColor ("Color", Color) = (1,0,0,1)
}
// ···
// 파동을 조작하기 위한 변수들
float _WaveDistance;
float _WaveTrail;
float4 _WaveColor;

이번 파동 예제에서는 파동의 앞 부분에는 경계가 선명하게, 그 뒤로는 부드러운 트레일 이어지도록 만들 것입니다. 먼저 거리 값을 기준으로 뚝 잘린 형태를 만들어 보겠습니다. 이를 위해 우리는 step 함수를 사용할 것인데, 이 함수는 첫 번째 값보다 두 번째 값이 더 크면 0 을, 그렇지 않으면 1 을 반환합니다.
// ···
// 파동 계산
float waveFront = step(depth, _WaveDistance);
return waveFront;
}

다음으로는 트레일 정의하기 위해 smoothstep 함수를 사용합니다. 이 함수는 step 함수와 비슷하지만, 세 번째 값을 비교할 두 개의 기준 값을 정의할 수 있다는 점이 다릅니다. 세 번째 값이 첫 번째 값보다 작으면 0을 반환하고, 두 번째 값보다 크면 1을 반환합니다. 그 사이 값들은 0과 1 사이의 값을 반환합니다. 저는 이를 역방향 선형 보간(inverse linear interpolation) 으로 상상하고는 하는데, 그 이유는 smoothstep의 결과를 동일한 최소값과 최대값을 갖는 lerp 함수에 넣으면 세 번째 인자의 값을 얻을 수 있기 때문입니다.
이 경우 우리가 비교하고 싶은 값은 깊이 값이고, 최대값은 파동의 거리(wave distance), 최소값은 파동의 거리에서 트레일 길이를 뺀 값이 됩니다.
// ···
float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
return waveTrail;
}

아마 눈치챘겠지만, 파동의 앞부분과 트레일이 서로 반대로 되어 있습니다. 이것은 간단히 고칠 수 있습니다. (clip 함수의 두 인자를 뒤집거나, smoothstep의 최소/최대 값을 뒤집으면 됩니다.) 하지만 여기서는 의도적으로 반대로 뒤집었습니다.
왜냐하면 어떤 수든 0을 곱하면 0이 되기 때문에, 파동의 앞부분과 꼬리를 서로 곱해 주면 앞쪽과 뒤쪽은 모두 0이 되고, 우리가 정의한 거리 지점에만 작은 흰색 파동이 남게 되기 때문입니다.
// ···
// 파동 계산
float waveFront = step(depth, _WaveDistance);
float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
float wave = waveFront * waveTrail;
return wave;
}

이제 파동을 정의했으니, 다시 색상을 입힐 수 있습니다. 이를 위해 먼저 소스 이미지를 다시 샘플링한 뒤, 방금 계산한 파동 파라미터를 기준으로 원본 이미지와 파동 색상 사이를 선형 보간(linear interpolation) 해 줍니다.
// ···
// 소스 컬러(카메라에 렌더된 색상)와 파동 색상을 섞습니다.
fixed4 col = lerp(source, _WaveColor, wave);
return col;

보시다시피, 이 방식에는 파동 Far Clipping Plane에 도달했을 때 아티팩트(artefact)가 발생합니다. 스카이박스는 기술적으로 Far Clipping Plane의 거리에 위치해 있지만, 우리가 원하는 것은 파동이 그 지점에 도달했을 때 보이지 않게 하는 것입니다.
이를 해결하기 위해, 깊이를 계산한 직후 소스 색상을 읽어오고, 만약 깊이가 Far Clipping Plane에 있다면 즉시 소스 색상을 반환하도록 합니다.
// 프래그먼트 셰이더
fixed4 frag(v2f i) : SV_TARGET
{
// 뎁스 텍스처로부터 뎁스 값을 가져옵니다.
float depth = tex2D(_CameraDepthTexture, i.uv).r;
// 카메라와 먼 거리의 클리핑 평면 사이의 선형 깊이값
depth = Linear01Depth(depth);
// 카메라와의 거리를 Unit 단위로 나타낸 깊이값
depth = depth * _ProjectionParams.z;
// 소스 색상을 가져옵니다.
fixed4 source = tex2D(_MainTex, i.uv);
// 만약 스카이박스까지 도달한다면 파동을 생성하지 않고 소스 색상을 결과값으로 반환합니다.
if(depth >= _ProjectionParams.z)
return source;
// 파동 계산
float waveFront = step(depth, _WaveDistance);
float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
float wave = waveFront * waveTrail;
// // 소스 컬러(카메라에 렌더된 색상)와 파동 색상을 섞습니다.
fixed4 col = lerp(source, _WaveColor, wave);
return col;
}
마지막으로 하고 싶은 것은, C# 스크립트를 확장해서 파동이 퍼져나가는 거리를 자동으로 설정하고, 그것이 천천히 플레이어로부터 멀어지도록 만드는 것입니다. 저는 파동이 이동하는 속도와 파동이 활성화(active)되어 있는지 여부를 제어하고 싶습니다. 또한 현재 파동의 거리를 기억해야 합니다.
이러한 기능들을 위해서 스크립트에 몇 가지 새로운 클래스 변수를 추가합니다.
[SerializeField] private Material postprocessMaterial;
[SerializeField] private float waveSpeed;
[SerializeField] private bool waveActive;
그다음, 유니티가 매 프레임 자동으로 호출하는 Update() 메소드를 추가합니다. 이 메소드 안에서는 파동이 활성 상태일 경우 거리를 점차 증가시키고, 비활성 상태일 경우 거리를 0으로 설정합니다. 이렇게 하면 파동이 매번 다시 플레이어로부터 시작되도록 초기화됩니다.
private void Update()
{
// 만약 파동이 활성화되어있다면 이동하게 만들고, 아니라면 초기화합니다.
if(waveActive)
{
waveDistance = waveDistance + waveSpeed * Time.deltaTime;
}
else
{
waveDistance = 0;
}
}
그리고 셰이더에서 waveDistance 변수를 사용하기 위해 값을 설정해 줍니다. 이 설정은 OnRenderImage 안에서, 해당 메소드가 실행되기 직전에 이루어지는데, 이렇게 해야 변수가 사용될 때 올바른 값이 적용되도록 보장할 수 있습니다.
// 카메라 렌더링이 끝난 뒤 유니티가 자동으로 호출하는 메소드
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 스크립트의 거리를 셰이더로 전달합니다.
postprocessMaterial.SetFloat("_WaveDistance", waveDistance);
// 소스 텍스처의 픽셀을 대상 텍스처에 그립니다.
Graphics.Blit(source, destination, postprocessMaterial);
}

이번 튜토리얼에서 작성한 셰이더와 스크립트 코드 전문은 아래와 같습니다.
Shader "Tutorial/017_Depth_Postprocessing"
{
// 인스펙터 창에서 조절 가능한 값을 노출시킵니다.
Properties
{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
[Header(Wave)]
_WaveDistance ("Distance from player", float) = 10
_WaveTrail ("Length of the trail", Range(0,5)) = 1
_WaveColor ("Color", Color) = (1,0,0,1)
}
SubShader
{
// 컬링이나 뎁스 버퍼 읽기/쓰기가 필요 없음을 지정하는 마커
// 또는 뎁스 버퍼에 비교 또는 작성
Cull Off
ZWrite Off
ZTest Always
Pass
{
CGPROGRAM
// 유용한 셰이더 함수들을 포함해줍니다.
#include "UnityCG.cginc"
// 버텍스와 프래그먼트 셰이더 정의
#pragma vertex vert
#pragma fragment frag
// 지금까지 렌더된 화면 이미지
sampler2D _MainTex;
// 뎁스 텍스처
sampler2D _CameraDepthTexture;
// 파동을 조작하기 위한 변수들
float _WaveDistance;
float _WaveTrail;
float4 _WaveColor;
// 버텍스 셰이더에 전달되는 오브젝트 데이터
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
// 프래그먼트를 생성할 때 사용되며, 프래그먼트 셰이더에서 읽을 수 있는 데이터
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
// 버텍스 셰이더
v2f vert(appdata v)
{
v2f o;
// 렌더될 수 있도록 버텍스 좌표를 오브젝트 공간에서 클립 공간으로 변환
o.position = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
// 프래그먼트 셰이더
fixed4 frag(v2f i) : SV_TARGET
{
// 뎁스 텍스처로부터 뎁스 값을 가져옵니다.
float depth = tex2D(_CameraDepthTexture, i.uv).r;
// 카메라와 먼 거리의 클리핑 평면 사이의 선형 깊이값
depth = Linear01Depth(depth);
// 카메라와의 거리를 Unit 단위로 나타낸 깊이값
depth = depth * _ProjectionParams.z;
// 소스 색상을 가져옵니다.
fixed4 source = tex2D(_MainTex, i.uv);
// 만약 스카이박스까지 도달한다면 파동을 생성하지 않고 소스 색상을 결과값으로 반환합니다.
if(depth >= _ProjectionParams.z)
return source;
// 파동 계산
float waveFront = step(depth, _WaveDistance);
float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
float wave = waveFront * waveTrail;
// // 소스 컬러(카메라에 렌더된 색상)와 파동 색상을 섞습니다.
fixed4 col = lerp(source, _WaveColor, wave);
return col;
}
ENDCG
}
}
}
using UnityEngine;
// 메인 카메라인 게임 오브젝트에 부착되어야 합니다.
public class DepthPostprocessing : MonoBehaviour
{
// 포스트프로세싱을 진행할 때 적용되는 메테리얼
[SerializeField] private Material postprocessMaterial;
[SerializeField] private float waveSpeed;
[SerializeField] private bool waveActive;
private float waveDistance;
private void Start(){
// 카메라 컴포넌트를 가져오고, 카메라에서 뎁스 텍스처를 렌더하도록 합니다.
Camera cam = GetComponent<Camera>();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.Depth;
}
private void Update()
{
// 만약 파동이 활성화되어있다면 이동하게 만들고, 아니라면 초기화합니다.
if(waveActive)
{
waveDistance = waveDistance + waveSpeed * Time.deltaTime;
}
else
{
waveDistance = 0;
}
}
// 카메라 렌더링이 끝난 뒤 유니티가 자동으로 호출하는 메소드
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 스크립트의 거리를 셰이더로 전달합니다.
postprocessMaterial.SetFloat("_WaveDistance", waveDistance);
// 소스 텍스처의 픽셀을 대상 텍스처에 그립니다.
Graphics.Blit(source, destination, postprocessMaterial);
}
}
'Graphics > Ronja's Unity Shader tutorials' 카테고리의 다른 글
| 포스트프로세싱을 사용한 외곽선 효과 (0) | 2026.02.02 |
|---|---|
| 노말 텍스처를 활용한 포스트프로세싱 (0) | 2025.09.16 |
| 포스트프로세싱 기초 (0) | 2025.08.04 |
| 버텍스 디스플레이스먼트 (0) | 2025.07.13 |
| 폴리곤 클리핑 (0) | 2025.03.22 |