깊이 테스트
고친 사람 github-actions[bot]
깊이 테스트는 화면의 같은 칸을 두고 다투는 후보 중에서 앞에 있는 것만 남기는 검사입니다. 뒤에 있는 것이 나중에 그려져도 앞엣것을 덮어쓰지 못하게 막습니다. 그래서 그리는 차례를 맞추지 않아도 가까운 물체가 앞에 보입니다.
쉽고 빠른 이해
깊이 테스트는 화면 칸마다 지금까지 가장 가까웠던 거리를 적어 둡니다. 새로 온 후보가 그보다 가까울 때만 그 칸을 넘겨주는 검사입니다. 벽 앞에 사람이 서 있는 장면이라면, 벽을 나중에 그려도 사람이 차지한 칸은 벽이 가져가지 못합니다.
이게 없으면 나중에 그린 것이 언제나 앞에 옵니다. 그리는 쪽이 먼 물체부터 차례로 그려야 합니다. 두 물체가 서로 파고들면 그런 차례가 아예 없습니다.
어떻게 도나:
- 화면 칸마다 거리 값을 담는 메모리를 하나 더 둡니다
- 후보가 올 때마다 그 후보의 거리와 칸에 적힌 거리를 비교합니다
- 후보가 더 가까우면 칸에 적힌 거리를 고치고 후보를 다음 처리로 넘깁니다. 아니면 후보를 버립니다
대가는 메모리입니다. 화면 칸 수만큼 거리 값을 따로 들고 있어야 합니다. 반투명한 물체는 뒤엣것도 비쳐 보여야 해서 이 검사만으로는 안 풀립니다.
상세
달리기 기록판에는 종목마다 지금까지 가장 짧은 시간과 그 시간을 낸 사람이 적혀 있습니다. 새로 잰 시간이 더 짧을 때만 이름과 시간을 함께 고쳐 적습니다. 누가 먼저 뛰었든 마지막에 판에 남는 것은 가장 짧은 시간입니다.
프래그먼트와 깊이 버퍼
화면은 칸으로 나뉩니다. 도형 하나가 칸 여럿을 덮으면 칸마다 후보가 하나씩 생깁니다. 이렇게 칸 하나를 차지하겠다고 나선 후보를 프래그먼트라고 부릅니다. 한 칸에는 여러 도형에서 온 프래그먼트가 겹쳐 올 수 있습니다.
프래그먼트마다 깊이 값이 붙어 옵니다. 그 점이 카메라에서 얼마나 먼지를 나타내는 값입니다. 깊이 값은 흔히 z 로 적습니다.
칸마다 지금까지 남은 깊이 값을 적어 두는 메모리가 깊이 버퍼입니다. 화면의 색을 칸마다 적어 두는 색 버퍼와 칸 수가 같습니다. 색 대신 깊이를 담는다는 것만 다릅니다. 같은 까닭으로 깊이 버퍼를 Z 버퍼, 깊이 테스트를 Z 테스트라고도 합니다.
앞의 기록판이 깊이 버퍼에 해당합니다. 종목은 화면 칸입니다. 판에 적힌 가장 짧은 시간은 그 칸의 가장 가까운 깊이입니다.
검사의 정의
깊이 테스트는 들어온 프래그먼트의 깊이 값을, 깊이 버퍼의 같은 칸에 적힌 값과 비교하는 검사입니다. 비교가 실패하면 그 프래그먼트를 버립니다. 통과한 프래그먼트는 다음 처리로 넘어갑니다. 깊이 쓰기가 켜져 있으면 이때 깊이 버퍼의 그 칸도 새 깊이 값으로 바뀝니다.
깊이 버퍼가 없으면 검사는 언제나 통과한 것으로 칩니다. 검사를 꺼 둔 것과 같습니다.
비교 규칙 여덟
무엇을 통과로 칠지는 고를 수 있습니다. 들어온 값과 적힌 값을 두고 고를 수 있는 규칙은 여덟입니다.
| 규칙 | 통과하는 경우 |
|---|---|
| 언제나 버림 | 통과하지 않는다 |
| 작음 | 들어온 값이 적힌 값보다 작을 때 |
| 작거나 같음 | 들어온 값이 적힌 값보다 작거나 같을 때 |
| 같음 | 두 값이 같을 때 |
| 다름 | 두 값이 다를 때 |
| 큼 | 들어온 값이 적힌 값보다 클 때 |
| 크거나 같음 | 들어온 값이 적힌 값보다 크거나 같을 때 |
| 언제나 통과 | 언제나 통과한다 |
기본 규칙은 「작음」입니다. 기본 설정에서는 깊이 값이 작을수록 가깝습니다. 그래서 이 규칙은 더 가까운 프래그먼트만 남깁니다.
한 칸을 따라가 보기
한 장을 그리기 전에 깊이 버퍼를 한 값으로 지워 둡니다. 그 값은 그리는 쪽이 정합니다. 값은 0 에서 1 사이로 묶입니다. 처음 오는 프래그먼트가 통과하려면 이 값이 어느 프래그먼트보다도 멀어야 합니다.
아래 표는 지우는 값을 가장 먼 1.0 으로 둔 경우입니다. 규칙은 기본인 「작음」입니다. 한 칸에 프래그먼트 셋이 차례로 옵니다.
| 그린 차례 | 들어온 깊이 | 검사 전 칸의 깊이 | 판정 | 검사 뒤 칸의 깊이 |
|---|---|---|---|---|
| 첫째 | 0.8 | 1.0 | 통과 | 0.8 |
| 둘째 | 0.3 | 0.8 | 통과 | 0.3 |
| 셋째 | 0.6 | 0.3 | 버림 | 0.3 |
셋째는 둘째보다 멀어서 버려졌습니다. 셋을 어떤 차례로 보내도 마지막에 남는 것은 깊이 0.3 인 둘째입니다.
앞에서 무엇을 그렸는지는 볼 필요가 없습니다. 칸에 적힌 값 하나가 지금까지의 결과를 전부 쥐고 있기 때문입니다. 그리는 차례를 안 맞춰도 되는 까닭이 이것입니다.
배경
3차원 장면을 그리려면 어느 면이 보이고 어느 면이 가려지는지를 정해야 합니다. 이 문제를 은면 제거라고 부릅니다. 한 가지 방법은 다각형을 깊이 순으로 정렬해 눈에서 가장 먼 것부터 칠하는 화가 알고리즘입니다. 그런데 두 다각형이 서로 파고들거나 순서를 정하기 어렵게 놓이면 깊이 순으로 정렬하기 어렵습니다. 이 방식을 다듬은 Newell·Newell·Sancha 의 알고리즘은 이럴 때 정렬이 될 때까지 다각형을 작은 조각으로 자릅니다.
다른 방법은 화면 칸마다 밝기, 곧 그 칸의 색 값과 함께 깊이 값을 저장해 두는 것입니다. 새 점이 오면 이미 있는 점의 깊이와 비교합니다. 새 점이 뒤에 있으면 버립니다. 앞에 있으면 옛 값을 바꿉니다.
서로 파고드는 면도 칸마다 따로 가려지므로 자를 필요가 없습니다. 실패 절에서 볼 예외를 빼면 면을 아무 차례로나 그려도 됩니다.
이 방법은 1974년에 z 버퍼라는 이름으로 정리됐습니다. 걸림돌은 메모리였습니다. 512×512 화면에 밝기 8비트와 깊이 20비트를 두면 28비트짜리 값이 약 25만 개 듭니다. 당시 메모리 값으로는 비싼 구현이었습니다.
예시
이 검사를 켜고 설정하는 쪽은 그래픽 API(Application Programming Interface, 응용 프로그램 인터페이스)입니다. 설정은 대개 셋입니다. 검사를 켜는 스위치, 비교 규칙, 깊이 버퍼에 쓸지 말지를 정하는 스위치입니다. WebGPU 에는 검사를 켜는 스위치가 따로 없습니다. 그래서 WebGPU 의 설정은 둘입니다.
| API | 검사 켜기 | 비교 규칙 | 깊이 쓰기 |
|---|---|---|---|
| OpenGL | glEnable(GL_DEPTH_TEST) |
glDepthFunc |
glDepthMask |
| Vulkan | depthTestEnable |
depthCompareOp |
depthWriteEnable |
| WebGPU | 따로 없음 | depthCompare |
depthWriteEnabled |
| Direct3D 11 | DepthEnable |
DepthFunc |
DepthWriteMask |
OpenGL
glEnable(GL_DEPTH_TEST); // 처음에는 꺼져 있다
glDepthFunc(GL_LESS); // 초깃값과 같다
glDepthMask(GL_TRUE); // 처음에는 쓰기가 켜져 있다
OpenGL 에서 깊이 테스트는 처음에 꺼져 있습니다. 그래서 glEnable(GL_DEPTH_TEST) 로 켜야 검사가
돕니다. 규칙의 초깃값은 GL_LESS 입니다. 깊이 쓰기는 처음부터 켜져 있습니다.
검사를 끄면 쓰기 스위치가 켜져 있어도 깊이 버퍼를 고치지 않습니다. 깊이 버퍼에 무조건 쓰고 싶으면
검사를 켠 채 규칙을 GL_ALWAYS 로 둡니다.
Vulkan
Vulkan 은 파이프라인을 만들 때 VkPipelineDepthStencilStateCreateInfo 의 세 필드로 정합니다.
depthCompareOp 에는 VkCompareOp 값을 넣습니다. depthTestEnable 이 VK_FALSE 면
depthWriteEnable 이 무엇이든 쓰기도 꺼집니다.
파이프라인을 만든 뒤 명령으로 바꾸는 길도 있습니다. vkCmdSetDepthTestEnable ·
vkCmdSetDepthCompareOp · vkCmdSetDepthWriteEnable 이 각각 같은 값을 정합니다.
WebGPU
WebGPU 는 GPUDepthStencilState 의 두 멤버로 정합니다. 검사를 켜는 필드는 따로 없습니다.
dictionary GPUDepthStencilState {
required GPUTextureFormat format;
boolean depthWriteEnabled;
GPUCompareFunction depthCompare;
...
};
format 이 깊이 값을 담는 형식이면 depthWriteEnabled 를 반드시 적어야 합니다. depthWriteEnabled 가
true 인 경우 등에는 depthCompare 도 반드시 적어야 합니다. 검사를 따로 끄는 길이 있는지는 자료에
없습니다(미확인).
Direct3D 11
Direct3D 11 은 D3D11_DEPTH_STENCIL_DESC 구조체의 세 멤버로 정합니다. 이 상태를 읽고 검사를 하는
단계는 출력 병합기입니다.
| 멤버 | 기본값 |
|---|---|
DepthEnable |
TRUE |
DepthWriteMask |
D3D11_DEPTH_WRITE_MASK_ALL |
DepthFunc |
D3D11_COMPARISON_LESS |
OpenGL 과 달리 기본값에서 검사가 켜져 있습니다. 기본 규칙이 「작음」인 것은 둘이 같습니다.
규칙 값 여덟의 이름
상세의 규칙 여덟은 API 마다 이름만 다르게 붙습니다.
| 규칙 | OpenGL | WebGPU |
|---|---|---|
| 언제나 버림 | GL_NEVER |
"never" |
| 작음 | GL_LESS |
"less" |
| 작거나 같음 | GL_LEQUAL |
"less-equal" |
| 같음 | GL_EQUAL |
"equal" |
| 다름 | GL_NOTEQUAL |
"not-equal" |
| 큼 | GL_GREATER |
"greater" |
| 크거나 같음 | GL_GEQUAL |
"greater-equal" |
| 언제나 통과 | GL_ALWAYS |
"always" |
동작
프래그먼트 하나가 깊이 테스트를 지나는 과정은 세 단계로 나뉩니다. 단계마다 무엇이 켜져 있느냐에 따라 프래그먼트가 버려지거나 깊이 버퍼가 바뀝니다.
프래그먼트 셰이더는 프래그먼트마다 한 번씩 도는 프로그램입니다. 그 칸의 색을 계산합니다. 깊이 테스트 전체가 이 프로그램 앞에서 도느냐 뒤에서 도느냐도 갈립니다.
검사 한 번의 세 단계
뷰포트는 화면에서 그림을 그려 넣을 구역입니다. 뷰포트마다 깊이의 가까운 끝 n 과 먼 끝 f 를 정해 둡니다. 이 둘이 그 뷰포트의 깊이 범위입니다.
깊이 고정은 프래그먼트의 깊이를 이 범위 안으로 잘라 넣는 처리입니다. 범위의 아래 끝은 n 과 f 중 작은 값입니다. 위 끝은 큰 값입니다. 깊이 고정은 켜져 있을 때만 돕니다.
| 단계 | 하는 일 |
|---|---|
| 깊이 고정 | 깊이 고정이 켜져 있으면 프래그먼트의 깊이를 뷰포트의 깊이 범위 안으로 묶는다 |
| 비교 | 검사가 켜져 있으면 고른 규칙으로 들어온 깊이와 적힌 깊이를 비교한다. 거짓이면 프래그먼트를 버린다 |
| 깊이 쓰기 | 비교가 참이었을 때만 그 칸의 깊이를 들어온 깊이로 바꾼다. 쓰기가 꺼져 있으면 바꾸지 않는다 |
깊이 버퍼는 값을 고정소수점 형식으로 적기도 합니다. 0 에서 1 사이를 정해진 개수의 눈금으로 나눠 그중 하나로 적는 방식입니다. 이 형식의 깊이 버퍼라면, 첫 단계를 지난 깊이가 0 에서 1 밖에 있을 때 그 깊이 값은 정의되지 않습니다. 깊이 고정이 꺼져 있으면 첫 단계는 깊이를 묶지 않습니다.
flowchart TD
B["깊이 고정"] --> T{"검사가 켜져 있나"}
T -->|꺼져 있다| P["다음 처리로 넘긴다"]
T -->|켜져 있다| C{"규칙으로 비교한 결과가 참인가"}
C -->|거짓| X["프래그먼트를 버린다"]
C -->|참| W{"깊이 쓰기가 켜져 있나"}
W -->|켜져 있다| U["그 칸의 깊이를 들어온 깊이로 바꾼다"]
W -->|꺼져 있다| P
U --> P
검사가 꺼져 있으면 비교도 쓰기도 건너뜁니다. 프래그먼트는 깊이 버퍼를 건드리지 않고 다음 처리로 넘어갑니다.
검사가 켜져 있으면 프래그먼트 하나마다 깊이 버퍼를 한 번 읽습니다. 통과한 데다 쓰기가 켜져 있으면 쓰기가 한 번 더 붙습니다.
셰이더 앞에서 도는 이른 검사
기본 순서에서 깊이 테스트는 프래그먼트 셰이더가 끝난 뒤에 돕니다. 프래그먼트 셰이더 코드에 이른 검사를 켜는 한정자를 달면 순서가 뒤집힙니다. 한정자는 셰이더 코드 안에 적는 선언입니다. 그러면 깊이 테스트가 셰이더보다 먼저 돕니다. 셰이더가 끝난 뒤에는 다시 돌지 않습니다.
아래 그림의 래스터화는 도형이 덮는 칸을 골라 프래그먼트를 만드는 단계입니다. 깊이 테스트보다 언제나 앞에 섭니다.
flowchart TD
R["래스터화 · 프래그먼트를 만든다"] --> Q{"프래그먼트 셰이더가 이른 검사를 켰나"}
Q -->|켰다| E["깊이 테스트"]
E --> S1["프래그먼트 셰이더"]
Q -->|안 켰다| S2["프래그먼트 셰이더"]
S2 --> L["깊이 테스트"]
얻는 것은 셰이더 실행입니다. 이른 검사에서 버려진 프래그먼트는 그 뒤 어느 단계도 거치지 않습니다. 프래그먼트 셰이더도 그 프래그먼트에 대해서는 돌지 않습니다.
잃는 것도 둘 있습니다. 첫째는 셰이더가 내놓는 깊이입니다. 프래그먼트 셰이더는 색만이 아니라 그 프래그먼트의 깊이 값을 새로 계산해 내놓을 수도 있습니다. 이른 검사를 켜면 그 깊이 값은 아무 효과가 없습니다. 검사가 이미 끝났기 때문입니다.
둘째는 셰이더 뒤에 남은 검사입니다. 알파 투 커버리지 검사처럼 셰이더가 끝난 뒤에 프래그먼트를 버리는 프래그먼트별 검사가 더 있습니다. 거기서 버려질 프래그먼트도 깊이 버퍼를 이미 고쳐 놓았을 수 있습니다. 그러면 화면에 남지 않는 프래그먼트의 깊이가 칸에 적혀 남습니다.
아래 그림은 이른 검사를 켰을 때 프래그먼트 하나에 일어나는 일을 시간 순서로 보입니다.
sequenceDiagram
participant 검사 as 깊이 테스트
participant 버퍼 as 깊이 버퍼
participant 셰이더 as 프래그먼트 셰이더
participant 뒤 as 셰이더 뒤의 검사
검사->>버퍼: 통과한 깊이를 적는다
검사->>셰이더: 프래그먼트를 넘긴다
Note over 셰이더: 새로 계산한 깊이는 효과가 없다
셰이더->>뒤: 프래그먼트를 넘긴다
Note over 뒤: 여기서 버려질 수 있다
Note over 버퍼: 적힌 깊이는 그대로 남는다
실패
검사가 규칙대로 끝나도 화면이 틀리는 경우가 둘 있습니다. 대상은 먼 곳에 붙어 있는 두 면과 반투명한 물체입니다. 두 경우 모두 검사 자체에는 잘못이 없습니다.
같은 값으로 뭉개지는 먼 깊이
깊이 버퍼는 값을 한정된 비트로 적습니다. 고정소수점 형식이 m 비트면 0 에서 1 까지를 고르게 나눈 2^m 개의 눈금만 적을 수 있습니다. 눈금 사이에 떨어지는 깊이는 이 눈금 중 하나로 바뀝니다.
원근 투영은 멀리 있는 것을 작게 보이게 하는 변환입니다. 원근 투영을 거친 깊이 값은 거리에 비례하지 않습니다. 거리의 역수에 가까운 꼴이 됩니다. 그래서 적을 수 있는 값의 대부분을 가까운 쪽이 씁니다. 먼 곳은 값 사이가 성깁니다.
먼 곳에서 조금 떨어진 두 면은 같은 깊이 값이 될 수 있습니다. 그러면 어느 면이 남을지가 그리는 차례와 규칙에 따라 칸마다 갈립니다. 화면에는 두 면이 얼룩덜룩 섞인 무늬가 생깁니다. 이 현상을 Z 파이팅이라고 부릅니다.
아래 그림은 가까운 쪽에 몰린 눈금과, 먼 두 면이 한 눈금에 겹치는 모양을 보입니다.
flowchart TD
subgraph 가까운쪽["가까운 쪽 · 눈금이 촘촘하다"]
N1["눈금"] ~~~ N2["눈금"] ~~~ N3["눈금"] ~~~ N4["눈금"]
end
subgraph 먼쪽["먼 쪽 · 눈금이 성기다"]
F1["눈금 하나"]
end
가까운쪽 ~~~ 먼쪽
S1["먼 곳의 면 하나"] --> F1
S2["조금 더 뒤의 면"] --> F1
조절할 수 있는 값 하나는 근평면입니다. 카메라에서 얼마나 가까운 데부터 그릴지를 정하는 값입니다. 근평면을 카메라에 가깝게 잡을수록 값이 가까운 쪽으로 더 쏠립니다. 멀리까지 그려야 하는 장면이면 근평면을 카메라에서 더 멀리 두는 쪽이 먼 곳의 뭉개짐을 줄입니다.
반투명한 물체
깊이 테스트는 한 칸에 하나만 남깁니다. 유리창 너머가 비쳐 보이려면 뒤엣것의 색도 살아 있어야 합니다. 새 색과 이미 쓰여 있던 색을 섞는 처리가 따로 있습니다. 이 처리가 블렌딩입니다.
반투명한 물체를 아무 차례로나 그리면 섞을 재료가 사라집니다. 앞의 유리를 먼저 그린 칸에서는, 나중에 온 뒤 물체가 깊이 테스트에 걸려 버려집니다. 결국 섞인 칸과 안 섞인 칸이 뒤섞여 화면이 뒤죽박죽이 됩니다.
그래서 반투명한 물체는 이렇게 그립니다. 불투명한 물체를 먼저 다 그려 깊이 버퍼를 채웁니다. 반투명한 물체는 깊이 테스트를 켠 채 깊이 쓰기만 끕니다. 그리고 먼 것부터 차례로 그려 섞습니다.
flowchart TD
subgraph 먼저["먼저 · 불투명한 물체"]
O1["깊이 테스트 켬 · 깊이 쓰기 켬"] --> O2["다 그려 깊이 버퍼를 채운다"]
end
subgraph 나중["나중 · 반투명한 물체"]
T1["먼 것부터 차례를 맞춘다"] --> T2["깊이 테스트 켬 · 깊이 쓰기 끔"] --> T3["블렌딩으로 섞는다"]
end
먼저 --> 나중
검사가 켜져 있어 불투명한 물체 뒤의 유리는 가려집니다. 쓰기를 꺼서 유리가 뒤에 오는 유리를 막지 않습니다. 차례를 다시 맞춰야 한다는 점에서 화가 알고리즘의 문제가 반투명에는 남아 있습니다.
물체 가장자리의 계단 무늬를 없애는 일을 안티에일리어싱이라고 합니다. 가장자리 칸의 색은 그 물체와 뒤에 가려진 물체, 적어도 둘의 색을 섞어 만듭니다. 물체를 아무 차례로 그리면 엉뚱한 물체끼리 섞여 그 근처가 틀릴 수 있습니다. 그래서 이때도 물체를 정렬해야 할 때가 있습니다.
관련 항목
깊이 테스트가 놓이는 처리 단계
그래픽스 파이프라인 · 래스터화 · 프래그먼트 셰이더 · 프래그먼트별 테스트 · 출력 병합기 · 프래그먼트 · 이른 프래그먼트 테스트
깊이 테스트와 나란히 도는 다른 검사
스텐실 테스트 · 알파 테스트 · 시저 테스트 · 깊이 경계 테스트 · 픽셀 소유권 테스트 · 컬링
깊이 테스트가 읽고 쓰는 저장소
깊이 버퍼 · 색 버퍼 · 스텐실 버퍼 · 프레임버퍼 · 렌더 타깃 · 깊이 텍스처
깊이 테스트를 조정하는 설정
비교 함수 · 깊이 쓰기 · 깊이 고정 · 깊이 범위 · 뷰포트
깊이 값을 만들어 내는 좌표 변환
원근 투영 · 투영 행렬 · 근평면 · 원평면 · 뷰 절두체 · 정규화 장치 좌표 · 직교 투영
깊이 테스트에서 비롯되는 화면 결함
Z 파이팅 · 깊이 정밀도 · 뎁스 바이어스 · 폴리곤 오프셋 · 고정소수점
깊이 테스트를 앞당기거나 건너뛰는 최적화
얼리 Z · 계층적 Z · Z 프리패스 · 오버드로 · 오클루전 컬링 · 오클루전 쿼리 · 디퍼드 셰이딩
깊이 테스트가 샘플 단위로 돌 때 만나는 기법
멀티샘플링 · 안티에일리어싱 · 알파 투 커버리지 · 샘플 마스크
깊이 테스트로는 못 푸는 반투명 처리
블렌딩 · 알파 블렌딩 · 알파 채널 · 순서 독립 투명
깊이 테스트 대신 앞뒤를 가리는 방법
화가 알고리즘 · Z 버퍼 알고리즘 · 레이 트레이싱 · BSP 트리 · 깊이 정렬
깊이 테스트를 제공하는 그래픽 API
OpenGL · WebGL · Vulkan · Direct3D · Direct3D 11 · Metal · WebGPU
깊이 테스트가 속하는 상위 분류
다른 이름: depth test · 뎁스 테스트 · Z 테스트 · z-test · depth buffer test · 깊이 버퍼 테스트