사전 깊이 버퍼
개념

깊이 버퍼

gabury1고친 사람 github-actions[bot]

깊이 버퍼는 화면의 칸마다 지금 보이는 것까지의 거리를 적어 둡니다. 같은 칸을 새로 칠하겠다는 것이 나타나면 그리는 쪽은 이 숫자만 보고 덮어쓸지 버릴지 정합니다. 덕분에 물체를 아무 차례로나 그려도 가까운 것이 앞에 보입니다.

쉽고 빠른 이해

깊이 버퍼는 화면 칸마다 "지금 여기 보이는 것이 얼마나 가까운가"를 적어 둔 숫자판입니다. 가로 1920 · 세로 1080 짜리 화면이면 숫자가 200만 개쯤 들어 있습니다.

이게 없으면 물체를 먼 것부터 차례로 그려야 합니다. 그런데 두 물체가 서로를 파고들면 어느 쪽이 통째로 앞이라고 말할 수 없어서 차례가 안 정해집니다.

  1. 장면을 그리기 전에 모든 칸을 가장 먼 값으로 채웁니다
  2. 새로 칠하겠다는 것이 올 때마다 그 칸의 값과 견줘, 더 가까우면 색과 함께 이 숫자를 고칩니다
  3. 다 그리고 나면 칸마다 보이는 면까지의 거리가 남고, 다음 처리가 그 숫자를 읽어 씁니다

대가는 메모리와 오가는 값입니다. 화면 크기만 한 메모리를 하나 더 들고 있어야 합니다. 칠할 것이 올 때마다 읽기와 쓰기가 붙습니다. 한 칸에 하나만 남기 때문에 반투명은 이것만으로 안 풀립니다.

상세

이 절은 깊이 버퍼가 어떤 모양의 메모리인지, 그 안의 숫자가 한 장을 그리는 동안 어떻게 바뀌고 다 그린 뒤 어디에 쓰이는지를 봅니다.

선반 칸에 붙인 쪽지

창고 선반의 칸마다 쪽지를 한 장씩 붙여 둔다고 해 보겠습니다. 쪽지에는 지금 그 칸에 놓인 상자가 문에서 얼마나 가까운지가 적혀 있습니다.

새 상자를 든 사람은 쪽지의 숫자만 보고 자기 상자가 더 가까운지 판단합니다. 더 가까우면 상자를 바꿔 놓고 쪽지도 새 숫자로 고쳐 씁니다. 아니면 그냥 돌아갑니다.

이 방식의 이점은 아무도 기억을 안 해도 된다는 것입니다. 그 칸에 어떤 상자가 몇 번 다녀갔는지 몰라도, 쪽지 한 장이 지금까지의 결과를 전부 쥐고 있습니다.

깊이 버퍼가 이 쪽지 더미에 해당합니다. 선반 칸은 화면의 칸에, 문에서의 거리는 카메라에서의 거리에 해당합니다.

화면과 같은 크기의 숫자판

화면은 작은 칸이 격자로 늘어선 것입니다. 칸 하나가 픽셀 하나입니다. 몇 줄 몇 칸인지는 화면의 해상도가 정합니다.

화면에 보일 색을 칸마다 적어 둔 메모리가 색 버퍼입니다. 깊이 버퍼는 그 옆에 나란히 서는 같은 크기의 메모리입니다. 칸마다 색 대신 거리 하나를 담습니다.

두 메모리의 칸은 번호로 맞물립니다. 색 버퍼의 셋째 줄 다섯째 칸과 깊이 버퍼의 셋째 줄 다섯째 칸은 화면의 같은 점을 말합니다. 그 덕에 색을 쓸지 정하는 판단과 그 색을 쓰는 동작이 같은 번호로 이어집니다.

칸마다 표식을 하나 더 담는 스텐실 버퍼도 흔히 곁에 둡니다. 이렇게 화면 한 장을 이루는 메모리를 한 벌로 묶은 것이 프레임버퍼입니다.

block-beta
columns 4
  a0["색 버퍼"] a1[" "] a2["색"] a3[" "]
  b0["깊이 버퍼"] b1[" "] b2["거리"] b3[" "]
  c0["스텐실 버퍼"] c1[" "] c2["표식"] c3[" "]

줄 하나가 메모리 하나입니다. 세로로 같은 곳에 놓인 세 칸은 화면의 같은 점을 말합니다. 그 세 칸이 저마다 색 하나 · 거리 하나 · 표식 하나를 담습니다.

세 메모리는 따로 떨어져 있습니다. 이어 주는 것은 칸 번호뿐입니다. 그 덕에 깊이 버퍼만 크기를 달리 잡거나 혼자 떼어 쓰는 일이 가능합니다.

한 칸을 차지하겠다고 나선 색 하나를 아래에서는 후보라고 부르겠습니다. 그래픽스에서 부르는 이름은 프래그먼트입니다. 한 칸에 후보가 여럿 올 수 있고 화면에 남는 것은 하나입니다.

한 장을 그리는 동안

깊이 버퍼의 값은 한 장을 그리는 사이에 여러 번 바뀝니다. 아래는 그 한 장의 처음부터 끝까지입니다.

flowchart TD
    A["장면 시작 · 모든 칸을 가장 먼 값으로 채운다"] --> B["물체를 하나씩 그린다"]
    B --> C["앞엣것이 오면 그 칸의 값을 고쳐 쓴다"]
    C --> B
    B --> E["다 그렸다 · 칸마다 보이는 면까지의 거리가 남는다"]
    E --> G["남은 깊이를 그림처럼 읽어 안개·흐림·그늘의 정도를 정한다"]
    G --> F["다음 장면 시작 · 다시 가장 먼 값으로 채운다"]
    E --> F

첫 단계를 지우기라고 부릅니다. 가장 먼 값으로 채우는 까닭은 아직 아무것도 안 그린 상태라 어떤 후보가 와도 통과해야 하기 때문입니다.

앞 장의 값을 안 지우고 그리면 화면이 망가집니다. 지난 장면에서 코앞에 있던 물체의 거리가 남아 있으면, 이번 장면에서 그 칸에 와야 할 후보가 뒤에 있다는 판정을 받고 버려집니다.

지우기는 칸을 전부 건드리는 일입니다. 한 장에 한 번씩 화면 전체를 훑는 비용을 치르는 셈입니다. GPU(Graphics Processing Unit, 그래픽 처리 장치)는 이 동작만 따로 빠르게 처리하는 길을 둡니다.

위 그림의 가운데 단계를 한 후보의 눈으로 확대하면 아래가 됩니다. 후보 하나가 겪는 일은 읽기 한 번과 갈림 하나입니다.

flowchart TD
    A["후보 하나가 어느 칸에 도착한다"] --> B["그 칸의 깊이를 읽는다"]
    B --> C{"읽은 값보다 가까운가"}
    C -->|그렇다| D["색 버퍼에 색을 쓰고 깊이 버퍼에 새 값을 쓴다"]
    C -->|아니다| E["버린다 · 색도 깊이도 안 쓴다"]

값을 고쳐 쓸지 정하는 비교 자체는 깊이 테스트가 맡습니다. 버퍼는 그 판단의 재료를 대주고 결과를 받아 적는 쪽입니다.

이 갈림을 칸 하나에 여러 번 적용하면 차례가 왜 상관없는지가 보입니다. 같은 칸에 후보 셋이 오는데 그리는 차례만 뒤집어 본 것이 아래입니다.

flowchart TD
    subgraph SA["그린 차례 A · 먼 것부터"]
        A1["먼 것 · 쓴다"] --> A2["중간 · 더 가깝다 · 쓴다"]
        A2 --> A3["가까운 것 · 쓴다 → 남는 것: 가까운 것"]
    end
    subgraph SB["그린 차례 B · 가까운 것부터"]
        B1["가까운 것 · 쓴다"] --> B2["중간 · 더 멀다 · 버린다"]
        B2 --> B3["먼 것 · 버린다 → 남는 것: 가까운 것"]
    end

두 차례가 거쳐 온 길은 다릅니다. 칸에 남는 값은 같습니다. 어느 차례로 와도 가장 가까운 것이 남기 때문에 그리는 쪽은 물체를 정렬할 필요가 없습니다.

거리가 아니라 비율

깊이 버퍼에 카메라까지의 거리를 미터나 센티미터 단위로 담지는 않습니다. 정해진 범위 안의 비율로 바꿔서 담습니다.

그 범위는 면 두 장이 정합니다. 카메라 앞 어디부터 어디까지를 그릴지를 두 면으로 끊어 두는 것입니다. 가까운 쪽 면을 근평면, 먼 쪽 면을 원평면이라고 합니다. 흔히 근평면을 0, 원평면을 1 로 두는 범위를 씁니다.

범위를 고정하는 까닭은 그릇의 크기가 정해져 있기 때문입니다. 칸 하나가 쓸 수 있는 비트 수가 정해져 있으니 담을 수 있는 값의 개수도 정해집니다. 장면마다 거리의 단위가 달라도 같은 그릇에 담으려면 먼저 범위를 맞춰야 합니다.

이 비율은 고르게 매겨지지 않습니다. 멀리 있는 것을 작게 보이도록 하는 원근 투영을 거치면서 거리의 역수에 가까운 꼴로 바뀌기 때문입니다.

그 탓에 담을 수 있는 단계의 대부분이 가까운 거리에 쓰입니다. 가까운 곳은 촘촘하게 나뉘고 먼 곳은 성깁니다.

block-beta
columns 8
  t1["거리 · 가까운 구간"]:2 t2["다음 구간"]:2 t3["다음 구간"]:2 t4["거리 · 먼 구간"]:2
  b1["저장값을 이만큼 가져간다"]:4 b2["이만큼"]:2 b3["조금"]:1 b4["조금"]:1

위 줄은 카메라에서의 거리를 고르게 넷으로 자른 것입니다. 아래 줄은 그 구간마다 저장값을 얼마나 나눠 받는지입니다. 가까운 구간이 대부분을 가져가고 먼 구간에는 조금밖에 안 남습니다.

먼 곳에서 두 면이 같은 값으로 뭉개지면 화면에 얼룩덜룩한 무늬가 생깁니다. 이것을 Z 파이팅이라고 부릅니다.

이 쏠림을 조절하는 손잡이가 앞에서 말한 가까운 쪽 면, 곧 근평면입니다. 근평면은 카메라에서 얼마나 가까운 데부터 그리기 시작할지를 정하는 값입니다. 이것을 너무 작게 잡을수록 값이 가까운 쪽으로 더 쏠립니다.

칸 하나의 비트 수

칸 하나의 크기는 고를 수 있습니다. 크기가 클수록 깊이를 더 잘게 나눠 담습니다. 성기게 나눌수록 앞의 뭉개짐이 자주 보입니다.

칸 하나의 크기 깊이를 어떻게 나누나 어떤 때 고르나
16비트 6만 5천여 단계로 고르게 메모리를 아껴야 하고 장면이 얕을 때
24비트 1600만여 단계로 고르게 널리 쓰이는 크기. 스텐실 8비트를 붙여 한 덩이로 담는다
32비트 부동소수 단계가 더 많고 0 에 가까운 쪽이 훨씬 촘촘하게 가까운 데부터 먼 데까지 한 장에 담을 때

셋째 줄의 부동소수는 담는 방법이 앞의 둘과 다릅니다. 앞 절에서 본 대로 원근 투영을 거치면 먼 쪽이 성기게 나뉩니다. 그런데 부동소수는 0 에 가까운 쪽을 훨씬 촘촘하게 나눕니다.

그래서 성긴 먼 쪽이 0 에 오도록 뒤집어 담으면 두 치우침이 상쇄돼 전체가 고르게 퍼집니다. 이렇게 뒤집어 담는 방식의 이름이 역깊이 버퍼입니다.

flowchart TD
    subgraph SA["그냥 담기 · 가까운 면이 0"]
        A1["원근 투영 · 먼 쪽이 성기다"] --> A2["부동소수 · 0 쪽인 가까운 데가 촘촘하다"]
        A2 --> A3["둘이 겹쳐 먼 쪽이 더 성겨진다"]
    end
    subgraph SB["뒤집어 담기 · 먼 면이 0"]
        B1["원근 투영 · 먼 쪽이 성기다"] --> B2["부동소수 · 0 쪽인 먼 데가 촘촘하다"]
        B2 --> B3["둘이 상쇄돼 전체가 고르다"]
    end

둘째 줄처럼 깊이 24비트와 스텐실 8비트를 붙여 32비트 한 덩이로 담는 형식도 흔합니다. 비트를 이렇게 갈라 씁니다.

packet-beta
0-23: "깊이"
24-31: "스텐실"

둘을 따로 두면 칸 하나를 다룰 때 메모리 두 군데를 건드려야 합니다. 붙여 두면 한 번에 읽고 씁니다.

다 그린 뒤의 쓰임

깊이 버퍼는 사람이 볼 그림이 아닙니다. 화면에 내보내는 것은 색 버퍼입니다. 깊이 버퍼는 그 색을 고르는 동안만 쓰이다 버려지는 것처럼 보입니다.

그런데 다 그리고 난 깊이 버퍼에는 쓸모 있는 정보가 남습니다. 칸마다 "화면의 이 점에 보이는 면이 카메라에서 얼마나 떨어져 있나"가 적혀 있기 때문입니다. 이 숫자를 그림처럼 읽어 재료로 쓰는 기법이 여럿 있습니다. 한 장 흐름도에서 "다 그렸다" 다음에 갈라져 나온 가지가 이것입니다.

먼 것일수록 뿌옇게 만드는 안개, 초점에서 벗어난 것을 흐리는 처리, 구석을 어둡게 만드는 처리가 모두 이 숫자 하나로 정도를 정합니다. 이렇게 읽어 쓰도록 만든 깊이 버퍼가 깊이 텍스처입니다.

그림자도 같은 방식입니다. 빛이 있는 데 카메라를 놓고 한 번 그려서 깊이만 받아 두면, 빛에서 본 가장 가까운 면까지의 거리가 칸마다 남습니다. 이렇게 받아 둔 깊이가 그림자 맵입니다.

대가

첫째는 메모리입니다. 가로 1920 · 세로 1080 짜리 화면이면 칸이 200만 개쯤 되고, 칸마다 32비트를 쓰면 8메가바이트가 넘습니다. 화면에 안 내보내고 따로 그려 두는 렌더 타깃을 여럿 쓰면 그만큼 곱절로 듭니다.

둘째는 오가는 값입니다. 후보 하나마다 읽기가 한 번 붙고, 통과하면 쓰기가 한 번 더 붙습니다. 앞의 갈림 그림에서 읽기와 쓰기로 선 두 단계가 그것입니다. 화면을 가득 덮는 물체가 여럿이면 오가는 값이 메모리 대역폭을 상당히 먹습니다.

그것을 줄이려고 여러 칸의 깊이를 묶어 요약해 두었다가 덩어리째 버리는 계층적 Z, 값을 줄여 담는 깊이 압축 같은 기법을 함께 씁니다.

셋째는 한 칸에 하나만 남는다는 점입니다. 유리창 너머로 뒤가 비쳐 보이려면 뒤엣것의 색도 살아 있어야 합니다. 깊이 버퍼는 앞엣것 하나만 남기고 나머지를 버립니다.

그래서 반투명한 물체는 깊이 쓰기를 끈 채 먼 것부터 차례로 그려 색을 섞습니다. 깊이 쓰기를 끈다는 것은 앞의 갈림 그림에서 깊이 버퍼에 새 값을 쓰는 동작만 빼는 것입니다. 색을 섞는 이 처리의 이름이 블렌딩입니다.

관련 항목

깊이 버퍼와 한 벌로 묶이는 화면 메모리

프레임버퍼 · 색 버퍼 · 스텐실 버퍼 · 렌더 타깃 · 프레임버퍼 객체 · 누적 버퍼 · 깊이 텍스처

깊이 버퍼를 읽고 쓰는 처리 단계

깊이 테스트 · 깊이 쓰기 마스크 · 얼리 Z · Z 프리패스 · 출력 병합기 · 프래그먼트 셰이더 · 래스터화 · 그래픽스 파이프라인

깊이 버퍼에 적힐 값을 만들어 내는 좌표 변환

원근 투영 · 직교 투영 · 근평면 · 원평면 · 뷰 절두체 · 정규화 장치 좌표 · 깊이 범위

깊이 값이 성기어 생기는 화면 결함

Z 파이팅 · 깊이 정밀도 · 뎁스 바이어스 · 폴리곤 오프셋 · 역깊이 버퍼 · 깊이 클램프

깊이 버퍼를 재료로 삼는 그리기 기법

그림자 맵 · 디퍼드 셰이딩 · 화면 공간 앰비언트 오클루전 · 안개 · 피사계 심도 · 깊이 필링

깊이 버퍼가 먹는 메모리와 대역폭을 줄이는 기법

계층적 Z · 깊이 압축 · 타일 기반 렌더링 · 메모리 대역폭 · 오버드로

깊이 버퍼 없이 가림을 푸는 방법

화가 알고리즘 · 은면 제거 · 오클루전 컬링 · 백페이스 컬링 · 광선추적

깊이 버퍼를 다루는 그래픽 API

OpenGL · Vulkan · DirectX · WebGPU · Metal

다른 이름: depth buffer · z-buffer · Z 버퍼 · 뎁스 버퍼