깊이 비교 오류
고친 사람 github-actions[bot]
깊이 비교 오류는 가려져야 할 면이 앞으로 나오거나 보여야 할 면이 사라지는 렌더링 오류입니다. 컴퓨터는 화면의 점마다 거리를 견줘서 가장 앞에 있는 면만 남깁니다. 그 견줌이 틀린 답을 내서 생깁니다. 오류 메시지 없이 그림만 어긋납니다.
쉽고 빠른 이해
깊이 비교 오류는 「누가 앞이냐」를 가리는 판정이 틀려서 그림이 어긋나는 현상입니다. 벽 뒤의 상자가 벽을 뚫고 보이거나, 멀쩡한 물체에 구멍이 점점이 뚫리는 것이 그 예입니다.
왜 이런 판정을 하나 — 물체를 그리는 차례는 앞뒤와 상관없이 뒤섞입니다. 그래서 컴퓨터는 점마다 지금까지 그린 것 가운데 가장 가까운 거리를 적어 둡니다. 새로 그릴 것은 그 거리와 견줘 더 가까울 때만 덧칠합니다.
어떻게 틀리나:
- 거리를 적는 눈금이 성겨서 다른 두 거리가 같은 값이 됩니다
- 한 물체를 두 번 나눠 그릴 때, 같은 면의 거리를 두 번 계산한 값이 끝 숫자에서 갈립니다
- 「작을수록 가깝다」 같은 약속을 비교 규칙과 처음 채워 두는 값이 서로 다르게 잡았습니다
- 그림자도 빛에서 본 거리를 견줘서 정합니다. 이때 면이 자기 자신과 견줘서 「내가 더 멀다」는 답이 나옵니다
무엇이 나빠지나 — 화면이 깜박이거나 줄무늬와 구멍이 생깁니다. 오류 메시지가 없어서 증상만 보고 네 갈래 가운데 어느 것인지 가려야 합니다.
상세
이 절은 깊이 테스트가 무엇과 무엇을 견주는지부터 봅니다. 그다음 그 판정이 틀리는 네 갈래를 하나씩 재현 조건과 함께 봅니다.
앞뒤를 가리는 깊이 테스트
3차원 장면을 그리면 여러 물체가 화면의 같은 픽셀을 놓고 겹칩니다. 앞의 물체가 뒤의 물체를 가려야 그림이 맞습니다. 그런데 물체를 그리는 차례는 앞뒤와 상관없이 뒤섞이기 쉽습니다. 그래서 차례에 기대지 않고 앞뒤를 가리는 장치가 필요합니다.
카메라에서 면까지의 거리를 깊이라고 부릅니다. 같은 픽셀에 겹친 두 면은 깊이를 견주면 어느 쪽이 앞인지 알 수 있습니다. 더 가까운 쪽이 앞입니다.
앞뒤를 가리는 장치는 깊이 버퍼입니다. 깊이 버퍼는 픽셀마다 깊이 하나를 적어 두는 저장 공간입니다. 그 값은 지금까지 그 픽셀에 그린 면 가운데 가장 가까운 면의 깊이입니다.
면 하나가 어느 픽셀을 덮으려 할 때, 그 면의 그 픽셀 몫을 프래그먼트라고 합니다. 픽셀이 되기 전의 후보입니다. 삼각형 하나가 픽셀 백 개를 덮으면 프래그먼트도 백 개가 생깁니다.
깊이 테스트는 프래그먼트의 깊이를 깊이 버퍼에 적힌 값과 견줍니다. 통과한 프래그먼트는 색을 칠합니다. 떨어진 프래그먼트는 버립니다.
무엇을 통과로 칠지 정하는 규칙을 비교 함수라고 부릅니다. 가장 널리 쓰는 규칙은 「작음」입니다. 깊이 값이 작을수록 가깝다고 약속해 둡니다. 들어온 값이 적힌 값보다 작을 때만 통과시킵니다.
깊이 값은 대개 0 과 1 사이의 수로 적습니다. 카메라가 그리기 시작하는 가장 가까운 경계가 근평면, 가장 먼 경계가 원평면입니다. 보통은 근평면을 0 에, 원평면을 1 에 놓습니다.
통과한 프래그먼트의 깊이를 버퍼에 새로 적을지 정하는 설정이 깊이 쓰기입니다. 켜 두면 통과할 때마다 버퍼의 값이 더 가까운 깊이로 바뀝니다. 그래서 나중에 오는 프래그먼트는 지금까지의 가장 앞 면과 견줍니다.
한 장을 그리기 전에는 깊이 버퍼를 한 값으로 지워 둡니다. 이 값을 지우는 값이라고 합니다. 「작음」 규칙이면 지우는 값은 가장 먼 1.0 입니다. 그래야 처음 오는 프래그먼트가 무엇이든 통과합니다.
판정이 맞으려면 버퍼에 적힌 깊이와 새로 들어온 깊이가 맞아야 합니다. 비교 함수와 지우는 값도 같은 약속을 따라야 합니다.
판정이 틀리는 네 갈래
네 갈래는 원인이 다릅니다. 화면에 남기는 무늬도 다릅니다. 아래 표는 갈래마다 무엇이 어긋나고 화면에 무엇이 보이는지 모았습니다.
| 갈래 | 무엇이 어긋나나 | 화면에 보이는 것 |
|---|---|---|
| 눈금이 모자람 | 다른 두 깊이가 같은 값으로 적힌다 | 두 면이 얼룩덜룩 섞여 깜박인다 |
| 같은 면의 값이 갈림 | 같은 면의 깊이가 두 계산에서 다르게 나온다 | 물체에 점점이 구멍이 뚫린다 |
| 약속이 어긋남 | 근평면·원평면을 0·1 가운데 어디에 놓나 · 비교 함수 · 지우는 값이 서로 다른 약속을 따른다 | 아무것도 안 보이거나 먼 것이 앞을 덮는다 |
| 자기 자신과 견줌 | 그림자를 정할 때 면이 자기 깊이와 견준다 | 빛 받는 면에 줄무늬 그림자가 생긴다 |
앞의 둘은 깊이 값이 틀린 경우입니다. 셋째는 값은 맞는데 견주는 방법이 틀린 경우입니다. 넷째는 그림자를 그리는 기법에서만 나는 경우입니다. 아래 소절이 이 차례로 하나씩 봅니다.
눈금이 모자라 같은 값이 된다
깊이는 무한히 잘게 적히지 않습니다. 정해진 비트 수로 적으므로 0 과 1 사이에 유한한 개수의 눈금을 긋는 셈입니다. 깊이는 그 가운데 가장 가까운 눈금으로 반올림됩니다.
두 면의 깊이 차이가 눈금 한 칸보다 작으면 둘은 같은 눈금으로 반올림됩니다. 깊이 테스트에게 두 면은 같은 거리입니다. 어느 면이 이길지는 픽셀마다 반올림 결과에 따라 갈립니다. 두 면의 색이 픽셀 단위로 엇갈리는 이 현상을 Z-파이팅이라고 부릅니다.
눈금의 간격은 거리마다 다릅니다. 멀리 있는 것을 작게 그리는 방식이 원근 투영입니다. 원근 투영은 깊이를 거리 값으로 적지 않고 거리의 역수에 가까운 값으로 바꿔 적습니다.
역수는 가까운 거리에서 크게 변하고 먼 거리에서 거의 안 변합니다. 거리가 1 에서 2 로 늘면 역수는 1 에서 0.5 로 줄어듭니다. 거리가 100 에서 101 로 늘면 역수는 0.0001 쯤밖에 안 줄어듭니다. 그래서 고르게 그은 눈금을 거리로 옮기면 대부분이 카메라 가까운 곳에 몰립니다.
먼 곳은 눈금이 성깁니다. 거리마다 다른 이 촘촘함을 깊이 정밀도라고 합니다.
Z-파이팅은 두 조건이 겹칠 때 납니다. 두 면이 같은 픽셀을 덮어야 합니다. 그리고 그 픽셀에서 깊이 차이가 눈금 한 칸 안쪽이어야 합니다.
같은 평면에 두 면을 겹쳐 두면 차이가 0 이라 언제나 성립합니다. 멀리 있는 두 면은 조금 떨어져 있어도 성립합니다. 먼 곳은 눈금이 성기기 때문입니다.
같은 면이 두 번 다른 값을 낸다
이 갈래는 한 물체를 여러 번 나눠 그리는 기법에서 납니다. 그림 한 장을 만들려고 장면을 한 번 훑어 그리는 것을 패스라고 부릅니다. 그릴 것이 많은 장면은 패스를 여럿 돌려 한 장을 만듭니다.
대표적인 예가 깊이 프리패스입니다. 첫 패스는 색을 칠하지 않고 깊이만 적습니다.
둘째 패스는 같은 물체를 다시 그립니다. 이때 비교 함수를 「같음」으로 둡니다. 그러면 픽셀마다 가장 앞의 면 하나만 통과해서 비싼 색 계산을 한 번씩만 합니다.
화면을 그리는 칩을 GPU(Graphics Processing Unit, 그래픽 처리 장치)라고 합니다. GPU 에서 도는 작은 프로그램이 셰이더입니다. 꼭짓점의 화면 위치나 픽셀의 색을 셰이더가 계산합니다.
깊이 프리패스는 두 패스가 같은 면에 대해 비트 하나까지 같은 깊이를 낸다는 데 기댑니다. 깊이는 셰이더가 계산한 꼭짓점 위치에서 나옵니다. 그런데 패스마다 다른 셰이더를 쓰는 일이 많습니다. 두 셰이더가 같은 식을 담아도 계산 차례까지 같다는 보장은 없습니다.
차례가 왜 문제인지는 소수를 담는 형식에서 나옵니다. 부동소수점은 컴퓨터가 소수를 담는 형식입니다. 정해진 개수의 숫자만 담고 나머지는 반올림해서 버립니다. 그래서 같은 수들을 더해도 차례가 바뀌면 끝 숫자가 달라질 수 있습니다.
아래 두 줄은 같은 세 수를 더하는 차례만 바꿨습니다. 자바스크립트의 수로 흉내 냈습니다. 각 줄이 내는 값은 오른쪽 주석에 적었습니다.
(0.1 + 0.2) + 0.3; // 0.6000000000000001
0.1 + (0.2 + 0.3); // 0.6
수학으로는 둘 다 0.6 입니다. 컴퓨터의 수로는 끝 숫자가 다릅니다. 셰이더의 계산도 같은 까닭으로 끝 숫자가 갈릴 수 있습니다.
아래 그림은 이 끝 숫자 하나가 둘째 패스에서 어떻게 구멍이 되는지 보입니다.
sequenceDiagram
participant P1 as 첫 패스
participant B as 깊이 버퍼
participant P2 as 둘째 패스
P1->>B: 면의 깊이 0.6 을 적는다
P2->>B: 같은 면을 0.6000000000000001 로 계산해 견준다
Note over B: 비교 함수는 「같음」
B-->>P2: 같지 않다 · 버린다
Note over P2: 그 픽셀은 색이 안 칠해진다
끝 숫자가 갈린 픽셀만 버려지므로 구멍은 물체 위에 점점이 흩어집니다. 카메라가 움직이면 갈리는 픽셀이 바뀌어 구멍도 위치를 바꾸며 반짝입니다.
이 구멍은 두 조건이 겹칠 때 생깁니다. 둘째 패스의 비교 함수가 「같음」이어야 합니다. 그리고 두 패스가 같은 면의 깊이를 서로 다른 계산으로 내야 합니다.
비교 함수를 「작거나 같음」으로 바꿔도 둘째 패스의 값이 조금 더 크게 나온 픽셀은 여전히 떨어집니다. 그래서 보통은 두 패스가 위치를 같은 코드로 계산하게 묶어서 막습니다.
약속이 어긋난다
깊이 테스트에는 서로 짝이 맞아야 하는 설정이 셋 있습니다. 근평면과 원평면을 0 과 1 가운데 어디에 놓을지, 비교 함수, 지우는 값입니다. 하나만 바꾸고 나머지를 두면 판정이 전부 틀립니다. 이 소절의 표는 이 셋에 더해 자주 하는 설정 실수 둘도 함께 다룹니다.
첫째 설정을 뒤집는 방법이 리버스 Z입니다. 근평면을 1 에, 원평면을 0 에 놓습니다. 이렇게 뒤집는 까닭은 깊이를 담는 수 형식에 있습니다.
깊이 버퍼는 깊이를 고른 간격의 정수로 담기도 합니다. 부동소수점으로 담기도 합니다. 부동소수점은 0 에 가까운 수일수록 더 잘게 나눠 담습니다. 작은 수에도 큰 수와 같은 개수의 숫자를 쓰기 때문입니다.
원근 투영은 먼 곳의 눈금을 성기게 만듭니다. 리버스 Z 는 그 먼 곳을 부동소수점이 촘촘한 0 쪽에 놓습니다. 그래서 깊이를 부동소수점으로 담으면 성기던 먼 곳의 눈금이 늘어납니다.
리버스 Z 에서는 큰 값이 가깝습니다. 그래서 비교 함수를 「큼」으로, 지우는 값을 0.0 으로 함께 뒤집어야 합니다. 아래 표는 설정 하나가 어긋났을 때 판정과 화면이 어떻게 되는지 모았습니다.
| 어긋난 설정 | 판정에서 일어나는 일 | 화면 |
|---|---|---|
| 리버스 Z 에 「큼」 규칙을 쓰면서 지우는 값을 1.0 으로 둠 | 1.0 보다 큰 깊이가 없어 모두 떨어진다 | 지운 색만 보인다 |
| 「작음」 규칙인데 지우는 값을 0.0 으로 둠 | 0.0 보다 작은 깊이가 없어 모두 떨어진다 | 지운 색만 보인다 |
| 리버스 Z 로 바꾸고 규칙은 「작음」, 지우는 값은 1.0 으로 둠 | 먼 면일수록 값이 작아서 먼 면이 이긴다 | 뒤의 물체가 앞을 덮는다 |
| 깊이 쓰기를 끔 | 버퍼가 지우는 값에 머물러 모든 면이 통과한다 | 나중에 그린 물체가 앞을 덮는다 |
| 깊이 버퍼를 안 지움 | 지난 장면의 깊이와 견준다 | 물체가 군데군데 사라진다 |
다섯 줄 모두 오류 메시지가 없습니다. 위의 두 줄은 화면이 비어서 금방 눈에 띕니다. 아래 세 줄은 그림이 그려지므로 Z-파이팅이나 물체 데이터의 잘못으로 오해하기 쉽습니다.
이 갈래는 반올림에 기대지 않습니다. 설정만 같으면 매번 같은 화면이 나옵니다. 그래서 표의 왼쪽 칸에 적힌 설정이 곧 재현 조건입니다.
그림자를 정할 때 자기 자신과 견준다
깊이 비교는 그림자를 그릴 때도 씁니다. 대표적인 방법이 그림자 매핑입니다. 이 갈래는 그 기법 안에서만 납니다.
첫 패스는 카메라 대신 빛의 위치에서 장면을 봅니다. 그리고 빛에서 가장 가까운 면의 깊이만 이미지 한 장에 적습니다. 이 이미지를 그림자 맵이라고 부릅니다.
둘째 패스는 카메라에서 장면을 그립니다. 점마다 빛에서 본 깊이를 다시 계산해 그림자 맵의 값과 견줍니다. 점의 깊이가 더 크면 빛과 점 사이에 다른 면이 있다는 뜻입니다. 그 점은 그림자로 칠합니다.
flowchart TD
subgraph 첫패스["첫 패스 · 빛에서 본다"]
L1["빛에서 가장 가까운 면의 깊이를 그림자 맵에 적는다"]
end
subgraph 둘째패스["둘째 패스 · 카메라에서 본다"]
C1["점마다 빛에서 본 깊이를 계산한다"] --> C2{"점의 깊이가 그림자 맵 값보다 큰가"}
C2 -->|크다| S["그림자로 칠한다"]
C2 -->|아니다| B["빛을 받은 색으로 칠한다"]
end
L1 --> C1
문제는 빛을 받는 면 자신입니다. 이 면의 깊이는 그림자 맵에도 적혀 있습니다. 둘째 패스에서 그 면 위의 점은 자기 자신의 기록과 견주게 됩니다. 두 값이 같아야 그림자가 안 생깁니다.
그런데 그림자 맵의 픽셀 하나는 면의 한 점이 아니라 작은 넓이를 덮습니다. 그 픽셀에는 넓이 가운데 한 점의 깊이만 적힙니다. 면이 빛을 비스듬히 받으면 그 넓이 안의 다른 점들은 적힌 값보다 조금 앞이거나 조금 뒤에 있습니다. 조금 뒤에 있는 점들은 가려졌다는 판정을 받아 그림자가 됩니다.
결과는 빛 받는 면 위에 생기는 줄무늬 그림자입니다. 이 현상이 섀도 애크니입니다. 여드름을 뜻하는 영어 acne 에서 온 이름입니다.
보통은 견주기 전에 점의 깊이를 조금 앞으로 당겨서 막습니다. 이 당기는 양을 깊이 바이어스라고 합니다. 자기 기록과 견줄 때 약간의 여유를 주는 셈입니다.
너무 많이 당기면 그림자가 져야 할 점까지 빛을 받은 것으로 판정됩니다. 그러면 물체 바로 밑의 그림자가 사라져서 그림자가 물체에서 떨어져 떠 보입니다. 이 현상이 피터 패닝입니다. 그림자가 몸에서 떨어진 동화 속 인물 피터 팬에서 온 이름입니다.
줄무늬는 세 조건이 겹칠 때 생깁니다. 그림자 매핑을 써야 합니다. 깊이 바이어스가 없거나 작아야 합니다. 면이 빛을 비스듬히 받아야 합니다.
빛을 비스듬히 받을수록 그림자 맵 픽셀 하나가 덮는 넓이 안에서 깊이 차이가 커집니다. 그래서 줄무늬가 짙어집니다.
깊이 비교 오류가 아닌 경우
판정이 맞았는데 화면이 틀리는 경우도 있습니다. 유리처럼 뒤가 비치는 물체가 그렇습니다. 깊이 테스트는 한 픽셀에 면 하나만 남기므로, 앞의 유리를 먼저 그리면 뒤의 물체가 버려집니다.
이때 깊이 테스트는 규칙대로 맞게 판정했습니다. 어긋난 것은 반투명한 물체를 그리는 차례입니다. 그래서 이 경우는 블렌딩과 그리는 차례의 문제로 따로 다룹니다.
깊이 테스트를 일부러 끄고 그리는 경우도 오류가 아닙니다. 화면 위에 늘 떠 있어야 하는 글자나 버튼이 그렇습니다. 이때는 나중에 그린 것이 덮어야 맞습니다.
관련 항목
깊이 비교 오류가 속하는 상위 분류
그래픽스 · 3차원 그래픽스 · 렌더링 · 실시간 렌더링 · 렌더 파이프라인 · 래스터화
깊이 비교 오류가 일어나는 깊이 처리 장치
깊이 버퍼 · 깊이 테스트 · 비교 함수 · 깊이 쓰기 · 프래그먼트 · 픽셀 · 프레임버퍼 · 그림자 맵
깊이 비교 오류가 화면에 드러나는 결함
Z-파이팅 · 섀도 애크니 · 피터 패닝 · 그림자 에일리어싱 · 에일리어싱
깊이 비교 오류가 잘 나는 렌더링 기법
깊이 프리패스 · 다중 패스 렌더링 · 그림자 매핑 · 지연 셰이딩 · 데칼
깊이 비교 오류를 비껴가는 기법
깊이 바이어스 · 폴리곤 오프셋 · 경사 비례 깊이 바이어스 · 리버스 Z · 로그 깊이 버퍼 · 불변 한정자
깊이 값을 만들어 내는 좌표 변환
원근 투영 · 투영 행렬 · 근평면 · 원평면 · 정규화 장치 좌표 · 보간
깊이 값을 담는 수 형식
부동소수점 · 고정소수점 · IEEE 754 · 반정밀도 부동소수점 · 정규화 정수
깊이 비교와 다른 방법으로 앞뒤를 가리는 기법
다른 이름: depth comparison error · depth test error · 깊이 테스트 오류 · 깊이 판정 오류 · 뎁스 비교 오류