오클루전 컬링
고친 사람 github-actions[bot]
오클루전 컬링은 앞의 물체에 가려 화면에 안 나올 물체를 그리기 전에 빼는 일입니다. 큰 건물 뒤에 늘어선 거리, 벽 너머 옆방의 가구가 그 대상입니다. 어차피 안 보일 것을 그리느라 쓰는 시간을 아낍니다.
쉽고 빠른 이해
오클루전 컬링은 가려진 물체를 그릴 목록에서 빼는 일입니다. 게임 속 눈앞의 벽 너머에 방이 열 개 있어도 화면에는 벽만 나옵니다. 그 방 열 개를 목록에서 빼도 화면은 같습니다.
빼지 않으면 그래픽 카드가 방 열 개를 전부 계산합니다. 벽 뒤라는 것은 그 계산이 거의 끝난 뒤에야 드러납니다. 장면이 클수록 이렇게 버리는 계산이 늘어납니다.
- 벽이나 건물처럼 크게 가리는 물체를 먼저 그려 둡니다
- 나머지 물체마다 감싸는 상자를 씌워 그 뒤에 완전히 숨는지 따집니다
- 확실히 숨는 것만 뺍니다. 애매하면 남깁니다
숨는지 따지는 데도 시간이 듭니다. 물체마다 쉬지 않고 따지면 오히려 성능을 깎을 수 있습니다. 장면이 어지간히 복잡하지 않으면 얻는 것이 별로 없습니다. 판정 답이 늦게 오면 물체가 한 박자 늦게 불쑥 나타나기도 합니다.
상세
연극 무대 안쪽에는 앞 세트에 가려 어느 객석에서도 안 보이는 뒷무대가 있습니다. 거기 놓인 소품은 칠하지 않습니다. 아낀 물감과 시간은 보이는 곳에 씁니다.
오클루전 컬링은 장면의 다른 부분에 가려진 물체를 그리지 않으려는 기법을 통틀어 부르는 이름입니다. 벽 너머 옆방의 의자는 카메라가 보는 방향에 있어도 벽에 가려 안 보입니다. 그 의자를 그리기 전에 목록에서 빼는 것이 오클루전 컬링입니다.
이 절은 화면 한 장을 그리는 과정을 먼저 봅니다. 그 과정 위에서 오클루전 컬링이 어디에 끼는지, 누가 누구를 가리는지, 판정이 틀려도 되는 방향은 어느 쪽인지를 차례로 짚습니다.
화면 한 장을 그리는 과정
게임 화면은 한 장을 그리고 끝나지 않습니다. 카메라가 움직일 때마다 새 장을 처음부터 다시 그립니다. 이 한 장을 프레임이라고 부릅니다.
화면 속 물체는 작은 삼각형 여럿을 이어 붙여 만듭니다. 이 삼각형 묶음을 메시라고 합니다. 건물 하나에 삼각형이 수천 개 들기도 합니다. 삼각형처럼 면을 이루는 낱개를 도형이라고 부릅니다.
삼각형을 화면에 칠하는 일은 GPU(Graphics Processing Unit, 그래픽 처리 장치), 곧 그래픽 카드가 합니다. 무엇을 그릴지 정해 GPU 에 넘기는 일은 CPU(Central Processing Unit, 중앙 처리 장치)에서 도는 프로그램이 합니다. CPU 가 GPU 에 물체 하나를 그려 달라고 넘기는 요청 한 번을 드로 콜이라고 부릅니다.
GPU 는 받은 삼각형을 단계별로 처리합니다. 먼저 꼭짓점 위치를 화면 좌표로 계산합니다. 다음으로 삼각형을 화면 칸 단위로 쪼갭니다. 이 단계가 래스터화입니다. 쪼갠 한 칸 몫을 프래그먼트라고 부릅니다. 마지막으로 프래그먼트마다 색을 정해 칸에 칠합니다.
꼭짓점 위치 계산과 색 계산은 GPU 에서 도는 작은 프로그램이 맡습니다. 이 프로그램이 셰이더입니다.
깊이 테스트와 은면 제거
가려진 것이 화면에 안 나오게 막는 장치는 따로 있습니다. 화면 칸마다 지금까지 칠한 것이 카메라에서 얼마나 먼지 적어 둡니다. 이 값을 깊이라고 합니다. 깊이를 칸마다 모아 둔 표가 깊이 버퍼입니다.
깊이는 Z 라고도 부릅니다. 화면의 가로를 x, 세로를 y 로 두면 화면 안쪽으로 들어가는 방향이 z 이기 때문입니다. 그래서 깊이 버퍼를 Z 버퍼라고도 부릅니다.
새 프래그먼트가 거기 적힌 깊이보다 멀면 버립니다. 이 검사가 깊이 테스트입니다. 화면에서 가려진 부분을 지우는 일을 통틀어 은면 제거(Hidden Surface Removal)라고 부릅니다. 사람이 조작하며 보는 대화형 프로그램에서는 대개 Z 버퍼가 은면 제거를 맡습니다.
은면 제거와 오클루전 컬링은 하는 일이 다릅니다. 은면 제거는 보이는 도형의 어느 부분이 보이는지까지 가려냅니다. 오클루전 컬링은 어느 도형이 안 보이는지만 가리면 됩니다. 도형 안의 어느 부분이 보이는지는 따지지 않습니다. 도형보다 잘게 쪼갠 단위를 다루는 비싼 계산이 빠집니다.
그래서 오클루전 컬링은 보이는 물체를 넉넉하게 남겨도 됩니다. 넉넉히 남긴 것은 뒤의 은면 제거가 결국 버립니다. 둘의 경계가 칼로 자르듯 갈리지는 않습니다. 은면 제거 방법이 오클루전 컬링을 제 안에 품는 경우도 있습니다. 화면 칸마다 광선을 쏘아 처음 맞는 물체를 찾는 레이 캐스팅이 그런 예입니다.
flowchart TD
A["장면 속 물체"] --> OC["오클루전 컬링 · 가려진 물체를 뺀다"]
OC --> DC["드로 콜"]
DC --> V["꼭짓점 위치 계산"]
V --> R["래스터화"]
R --> DT["깊이 테스트 · 가려진 프래그먼트를 버린다"]
DT --> P["화면"]
그림에서 오클루전 컬링은 맨 위에, 깊이 테스트는 맨 아래에 있습니다. 위에서 뺀 물체는 그 아래 네 단계를 전부 건너뜁니다. 깊이 테스트로 버려지는 물체는 드로 콜부터 래스터화까지 다 치른 뒤에야 버려집니다.
컬링 가운데 오클루전 컬링
안 보일 것을 그리기 전에 빼는 일을 통틀어 컬링이라고 합니다. 무엇 때문에 안 보이는지에 따라 이름이 갈립니다. 카메라가 보는 범위는 앞이 잘린 사각뿔 모양입니다. 이 모양을 절두체라고 부릅니다.
| 이름 | 안 보이는 까닭 |
|---|---|
| 절두체 컬링 | 카메라가 보는 범위 밖에 있다 |
| 백페이스 컬링 | 도형이 카메라를 등졌다 |
| 오클루전 컬링 | 범위 안에 있지만 장면의 다른 부분에 가렸다 |
셋 중 오클루전 컬링이 훨씬 까다롭습니다. 범위 밖인지, 카메라를 등졌는지는 도형 하나만 보면 압니다. 가렸는지는 다른 도형과의 관계라서 그 도형만 봐서는 모릅니다. 이렇게 장면 전체의 관계를 봐야 하는 성질을 전역적이라고 부릅니다.
가리는 물체와 가려지는 물체
판정에는 두 역할이 있습니다. 앞에서 가리는 물체를 오클루더라고 부릅니다. 그 뒤에 숨는지 따져 볼 물체를 오클루디라고 부릅니다.
어떤 기법은 장면의 모든 물체를 오클루더로 씁니다. 다른 기법은 대개 큰 물체 몇 개만 골라 오클루더로 씁니다. 작고 자잘한 물체는 아무것도 안 가린다고 치고 무시하기도 합니다.
오클루더 하나로는 못 가리는 물체도 있습니다. 벽 두 장이 맞붙어 서 있고 그 뒤에 긴 버스가 있다고 해 봅시다. 어느 벽 하나만으로는 버스 한쪽이 삐져나옵니다. 두 벽을 합치면 버스가 다 가려집니다.
여러 오클루더를 합쳐 판정하는 것을 오클루더 퓨전이라고 부릅니다. 작거나 떨어진 오클루더가 숲처럼 많은 장면에서 합친 오클루더는 대개 하나씩 따질 때보다 훨씬 많이 가릴 수 있습니다. 오클루더 퓨전을 하는 기법이 있습니다. 오클루더를 하나씩만 따지는 기법도 있습니다.
물체를 감싸는 상자로 하는 판정
도형을 하나하나 따지면 너무 느립니다. 그래서 물체를 감싸는 직육면체를 하나 씌우고 그 상자만 따집니다. 이 상자를 바운딩 박스라고 합니다.
대부분의 기법은 장면에 계층을 씌웁니다. 물체 여럿을 큰 상자로 묶습니다. 그 안을 다시 작은 상자로 나눕니다. 가장 아래 칸은 대개 물체 하나의 바운딩 박스입니다. 판정은 위에서 아래로 내려갑니다.
상자가 오클루더 뒤에 완전히 숨으면 안에 든 물체도 숨습니다. 상자가 조금이라도 삐져나오면 물체를 남깁니다. 그래서 물체 자체는 숨었는데 상자 모서리만 삐져나와 남는 경우도 생깁니다.
틀려도 되는 방향
판정이 틀리는 방향은 둘입니다. 둘의 결과는 전혀 다릅니다.
| 판정 | 화면에 생기는 일 |
|---|---|
| 가려졌는데 남겼다 | 쓸데없이 그립니다. 화면은 맞게 나옵니다 |
| 보이는데 뺐다 | 화면에 구멍이 나거나 물체가 깜빡입니다 |
앞쪽 틀림은 시간만 잃습니다. 뒤쪽 틀림은 화면을 망칩니다. 그래서 대부분의 기법은 가려진 물체를 보인다고 잘못 남기는 것은 허용합니다. 보이는 물체를 가려졌다고 빼는 것은 허용하지 않습니다. 이렇게 한쪽으로만 틀리게 잡는 판정을 보수적 판정이라고 부릅니다.
보수적 판정으로 남긴 물체 묶음이 잠재 가시 집합(Potentially Visible Set, PVS)입니다. 보이는 물체 전부에, 가려졌지만 남긴 물체가 조금 섞인 집합입니다. 이 집합은 보이는 집합에 되도록 가깝게, 그러면서도 쉽게 구할 수 있게 잡아야 합니다. 카메라가 선 한 지점에 대해서도, 공간의 한 구역 전체에 대해서도 정할 수 있습니다.
보수적 판정을 일부러 버리는 기법도 소수 있습니다. 광선을 몇 줄 쏘아 보거나 몇 장면을 표본으로 그려 보고 보이는 집합을 짐작합니다. 화면에 별 영향이 없을 물체를 과감하게 빼기도 합니다. 둘 다 보이는 물체를 놓칠 수 있는 대신 판정이 빠르거나 남기는 물체가 적습니다.
배경
화면에서 가려진 부분을 지우는 일을 두고 1970년대에 여러 은면 제거 방법이 나왔습니다. 이 문제는 그 뒤 대부분 풀렸다고 여겨집니다. 대화형 프로그램은 대개 Z 버퍼를 씁니다. Z 버퍼는 화면을 맞게 만듭니다. 대신 장면의 도형을 하나도 빼지 않고 받아서 하나씩 깊이를 견줍니다. 그래서 비용이 장면 전체의 크기를 따라갑니다.
3D 데이터는 점점 커졌습니다. 그러자 이 방식으로는 실시간으로 못 그리는 장면이 생겼습니다. 그런데 큰 장면에서는 보이는 프래그먼트가 입력 전체보다 훨씬 적은 경우가 많습니다. 땅 위에 선 시점에서 도시를 보면 모델의 아주 작은 부분만 보입니다. 방 안에서는 벽이 장면 대부분을 가려 그 방과 문 너머 일부만 보입니다. 어느 시점에서 봐도 장면의 작은 일부만 보이는 이런 장면을 가림이 빽빽한 장면(densely occluded environment)이라고 부릅니다.
그래서 그리는 비용을 장면 전체의 크기가 아니라 보이는 부분의 크기에 맞추려는 요구가 생겼습니다. 이상적으로는 걸리는 시간이 보이는 집합의 크기에 비례해야 합니다. 이런 성질을 출력 민감(output sensitive)이라고 부릅니다. 가려진 것을 은면 제거보다 앞에서 걸러 이 요구에 다가가려는 기법들이 오클루전 컬링입니다.
예시
오클루전 컬링은 그래픽스 API(Application Programming Interface, 응용 프로그램 인터페이스)의 호출과 게임 엔진의 설정으로 나타납니다. 아래 셋은 GPU 에 판정을 맡기는 API 입니다. 나머지 둘은 엔진입니다.
OpenGL 의 ARB_occlusion_query
이 확장은 도형이 그린 픽셀 수를 세는 질의를 정의합니다. 세는 단위는 픽셀보다 잘게 나눈 샘플입니다. 샘플은 한 픽셀 안에서 색과 깊이를 따로 재는 점입니다. 픽셀 하나에 샘플이 하나면 둘은 같습니다. 깊이 테스트 같은 칸별 검사를 통과한 샘플만 셉니다. 이 질의를 오클루전 쿼리라고 부릅니다. 아래가 그 용례입니다.
// before this point, render major occluders
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
// also disable texturing and any fancy shaders
for (i = 0; i < N; i++) {
glBeginQueryARB(GL_SAMPLES_PASSED_ARB, queries[i]);
// render bounding box for object i
glEndQueryARB(GL_SAMPLES_PASSED_ARB);
}
// ……
for (i = 0; i < N; i++) {
glGetQueryObjectuivARB(queries[i], GL_QUERY_RESULT_ARB,
&sampleCount);
if (sampleCount > 0) {
// render object i
}
}
큰 오클루더를 먼저 그려 깊이 버퍼를 채웁니다. 그다음 glColorMask 와 glDepthMask 로 색과 깊이를
안 쓰게 막습니다. 바운딩 박스가 화면과 깊이 버퍼에 흔적을 남기지 않게 하려는 것입니다. 물체마다
GL_SAMPLES_PASSED_ARB 질의 안에서 바운딩 박스를 그립니다. 나중에 결과를 읽어 sampleCount 가 0
보다 클 때만 진짜 물체를 그립니다.
질의는 여럿을 먼저 다 보내고 결과는 나중에 묻습니다. 앞선 HP_occlusion_test 확장은 하나를 묻고
답이 올 때까지 기다린 뒤에야 다음을 시작할 수 있었습니다. 그 방식은 CPU 와 GPU 가 동시에 일할 여지를
대부분 없앴습니다. GL_QUERY_RESULT_AVAILABLE_ARB 로 물으면 지금 결과를 읽을 때 기다려야 하는지를
먼저 알 수 있습니다.
Vulkan 의 VK_QUERY_TYPE_OCCLUSION
vkCmdBeginQuery 와 vkCmdEndQuery 사이의 그리기 명령들에서 칸별 검사를 통과한 샘플 수를 셉니다.
질의가 시작될 때 수는 언제나 0 입니다.
VK_QUERY_CONTROL_PRECISE_BIT 을 안 주면 통과한 샘플이 하나라도 있을 때 0 이 아닌 아무 값이나
돌려줄 수 있습니다. 어떤 구현은 0 이나 1 만 돌려줍니다. 통과한 샘플이 있었나 없었나만 알면 될 때
이렇게 씁니다. 구현에 따라 이쪽이 더 빠를 수 있습니다.
결과는 vkCmdCopyQueryPoolResults 로 GPU 쪽 버퍼에 복사하거나 vkGetQueryPoolResults 로 CPU 쪽
메모리로 받습니다. 가리는 도형을 먼저 그리지 않으면 샘플이 깊이 테스트를 통과해도 최종 화면에서는
안 보일 수 있습니다. 그리는 순서가 판정 결과를 바꿉니다.
Direct3D 12 의 프레디케이션
프레디케이션은 그리기나 복사를 하지 않기로 CPU 가 아니라 GPU 가 정하게 하는 기능입니다. 대표적인 쓰임이 가림입니다. 바운딩 박스를 그렸는데 가려졌다면 그 물체의 그리기 명령에 조건을 걸어 GPU 가 렌더링에서 뺍니다. CPU 가 결과를 읽어 올 필요가 없습니다.
장면을 GPU 에 한 차례 넘겨 그리는 일이 패스입니다. 색은 칠하지 않고 깊이 버퍼만 먼저 채우는 패스가 얼리 깊이 패스입니다.
깊이 테스트와 얼리 깊이 패스만으로는 다 못 아낍니다. 얼리 깊이 패스는 쓸데없는 픽셀은 없애지만 꼭짓점을 다루는 셰이더 단계와 래스터화는 여전히 돌릴 수 있습니다. 프레디케이션은 그리기 명령 자체의 준비 비용과 래스터화까지 뺍니다.
Direct3D 12 에서는 프레디케이션이 질의와 떨어져 있습니다. 버퍼 안의 64비트 값 하나만 보고
조건을 겁니다. 그 비교 방식은 D3D12_PREDICATION_OP 가 정합니다. 그 값을 무엇으로 채우든 상관없으므로 가림이 아닌
다른 이유로도 그리기를 건너뛸 수 있습니다.
Unreal Engine 의 r.AllowOcclusionQueries 와 r.HZBOcclusion
Unreal Engine 은 모든 프로젝트에서 기본으로 절두체 컬링과 하드웨어 오클루전 쿼리를 씁니다. 컬링
방식들은 비용 순서로 차례대로 적용됩니다. 하드웨어 오클루전 쿼리는 장면에 놓인 물체인 액터마다
프레임마다 쿼리 하나를 GPU 에 보냅니다. 결과는 한 프레임 뒤에 읽습니다. 이 쿼리를 쓸지는 엔진
설정값인 콘솔 변수 r.AllowOcclusionQueries 가 정합니다. 0 을 주면 끕니다.
다른 방식으로 계층적 Z 버퍼 오클루전이 있습니다. 장면 깊이를 밉맵으로 줄여 둔 사본으로 액터의
경계를 봅니다. 밉맵은 같은 그림을 절반씩 줄인 여러 장의 묶음입니다. 이 방식은 하드웨어 오클루전
쿼리보다 더 보수적으로 빼서 빠지는 물체가 적습니다. r.HZBOcclusion=1 로 켭니다.
미리 계산하는 방식으로 Precomputed Visibility Volume 이 있습니다. 공간을 나눈 칸이 셀입니다. 이 방식은 움직이지 않는 액터의 가시 상태를 셀에 저장합니다. 셀은 그림자를 드리우는 면 위에 놓입니다. 그림자를 드리우는 면은 빛을 가로막는 불투명한 면입니다. 왜 그 면을 기준으로 삼는지는 자료에 없습니다(미확인).
이 데이터는 조명을 미리 계산하는 라이팅 빌드 때 오프라인으로 만들어집니다. 게임의 맵 하나인 레벨이 작거나 중간 크기일 때 가장 잘 맞습니다.
Unity 의 오클루전 컬링 굽기
Unity 의 오클루전 컬링은 다른 게임 오브젝트에 완전히 가려진 게임 오브젝트의 렌더링 계산을 막습니다. 카메라는 기본으로 절두체 컬링을 합니다. 절두체 컬링은 가림을 안 봅니다. 오클루전 컬링을 켜면 카메라가 둘 다 합니다.
판정에 쓸 데이터는 에디터에서 미리 만들어 두고 실행할 때 읽습니다. 미리 만드는 과정을 굽기(baking)라고 부릅니다. 굽기는 장면을 셀로 나눕니다. 그리고 셀 안의 도형과 이웃한 셀 사이의 가시성을 적은 데이터를 만듭니다.
방과 복도처럼 단단한 오브젝트로 작은 구역들이 뚜렷이 갈린 장면에서 가장 잘 듣습니다.
갈래
갈래를 가르는 첫 축은 판정의 기준점입니다. 지금 카메라가 선 한 점에서만 맞는 답을 구하는 시점 기준이 한쪽입니다. 공간의 한 구역 안 어디서든 맞는 답을 한꺼번에 구하는 영역 기준이 다른 쪽입니다.
축은 하나씩 더 있습니다. 영역 기준 안에서는 장면이 방과 문으로 나뉘는 셀과 포털 장면인지, 일반 장면인지가 갈립니다. 시점 기준 안에서는 판정을 도형 자체로 하는 객체 정밀도와, 래스터화로 쪼갠 프래그먼트로 하는 이미지 정밀도가 갈립니다.
flowchart TD
OC["오클루전 컬링"] --> R["영역 기준 · 대개 미리 계산"]
OC --> P["시점 기준 · 프레임마다"]
R --> RC["셀과 포털 · 셀마다 잠재 가시 집합"]
R --> RG["일반 장면"]
P --> PO["객체 정밀도 · 프레임마다 따라가는 포털"]
P --> PI["이미지 정밀도"]
PI --> HZ["계층적 Z 버퍼"]
PI --> HQ["하드웨어 오클루전 쿼리"]
PI --> SW["소프트웨어 래스터화"]
아래 소절은 이 그림의 갈래를 차례로 짚습니다. 셀과 포털이 아닌 일반 장면은 「영역 기준과 잠재 가시 집합」이 함께 다룹니다. 오클루더 퓨전은 오랫동안 주로 시점 기준 이미지 정밀도 기법들이 하던 일입니다. 이 기법들은 오클루더를 깊이 정보 하나에 함께 그려 두고 판정합니다. 그래서 나란히 선 오클루더들의 가림이 판정에 함께 들어갑니다. 영역 기준 기법 가운데서도 오클루더 퓨전을 하는 것이 나왔습니다.
영역 기준과 잠재 가시 집합
영역 기준은 장면을 셀로 나누고 셀마다 잠재 가시 집합을 구해 둡니다. 셀 안 어느 지점에 서도 보일 수 있는 물체를 다 넣은 목록입니다. 계산은 실행 중에 할 수도 있지만 대개 미리 해 둡니다.
한 번 구한 답이 여러 프레임 동안 맞으므로 비용이 시간에 나뉩니다. 그리는 동안에는 카메라가 선 셀의 목록만 읽습니다. 이웃 셀의 목록도 미리 있으므로 곧 보일 도형을 디스크나 네트워크에서 먼저 가져올 수 있습니다.
셀과 포털
건물 실내는 방과 방이 문이나 창으로만 이어집니다. 방 하나를 셀이라고 부릅니다. 셀 사이의 경계에서 막히지 않은 부분, 곧 문이나 창을 포털이라고 부릅니다. 다른 방은 포털을 통해서만 보입니다.
장면을 큰 벽을 따라 셀로 나눈 뒤 셀마다 포털과 그 포털이 이어지는 이웃 셀을 적어 둡니다. 이것이 셀을 꼭짓점으로, 포털을 변으로 하는 인접 그래프입니다. 한 셀의 어떤 점과 다른 셀의 어떤 점을 잇는 시선이 하나라도 있으면 두 셀은 서로 보인다고 칩니다. 점 하나만 찾으면 되므로 판정이 넉넉한 쪽으로 기웁니다. 시선이 닿은 셀들에 든 도형을 모으면 그 셀의 잠재 가시 집합이 하나도 빠지지 않고 들어갑니다. 그리는 동안에는 카메라가 선 셀의 목록을 꺼내 카메라 시야로 한 번 더 줄입니다.
flowchart TD
R1["셀 · 방 1"] ---|문| H["셀 · 복도"]
H ---|문| R2["셀 · 방 2"]
H ---|창| R3["셀 · 방 3"]
셀과 포털은 보통의 오클루더 방식과 방향이 반대입니다. 보통은 전부 보인다고 놓고 가려진 것을 지워 나갑니다. 셀과 포털은 빈 목록에서 시작해 포털 너머로 보이는 것을 더해 나갑니다. 벽 대신 구멍을 따라가므로 여러 벽을 저절로 합쳐 가리는 셈입니다.
프레임마다 따라가는 포털
셀과 포털을 미리 계산하지 않고 프레임마다 따라가는 시점 기준 기법도 있습니다. 포털을 화면에 비춘 모양을 넉넉한 사각형으로 잡습니다. 그 사각형을 창 삼아 이웃 셀로 깊이 우선으로 내려갑니다.
flowchart TD
A["카메라가 선 셀을 그린다"] --> B["절두체 안의 포털을 찾는다"]
B --> C["포털을 화면에 비춘 사각형으로 창을 잡는다"]
C --> D["창 안에서 이웃 셀을 그린다"]
D --> E["이웃 셀의 포털을 앞의 창으로 자른다"]
E --> F{"남은 포털이 있나"}
F -->|있다| D
F -->|없다| G["끝"]
포털을 거칠 때마다 새 포털을 앞의 창으로 자르므로 창이 점점 작아집니다. 보이는 포털이 하나도 안 남으면 멈춥니다. 사각형을 넉넉하게 잡으므로 이 판정도 보수적입니다.
계층적 Z 버퍼
깊이 버퍼는 칸이 화면 픽셀 수만큼 있어서 하나씩 견주면 느립니다. 그래서 깊이 버퍼를 단계적으로 줄인 사본을 쌓아 둡니다. 가장 아래층이 원래 깊이 버퍼입니다. 위층으로 갈 때마다 가로와 세로를 절반으로 줄입니다. 아래층의 2 × 2 네 칸은 위층 한 칸이 됩니다. 이때 넷 가운데 가장 먼 깊이를 남깁니다. 맨 위에는 화면 전체에서 가장 먼 깊이 하나만 남습니다. 이 층 묶음을 Z 피라미드, 또는 깊이 피라미드라고 부릅니다.
block-beta columns 4 a1["4"] a2["4"] b1["6"] b2["5"] a3["5"] a4["4"] b3["5"] b4["6"] c1["3"] c2["4"] d1["5"] d2["6"] c3["4"] c4["3"] d3["7"] d4["6"] s1["한 층 위 · 네 칸마다 가장 먼 값"]:4 A["5"]:2 B["6"]:2 C["4"]:2 D["7"]:2 s2["맨 위 · 네 칸 가운데 가장 먼 값"]:4 T["7"]:4
그림의 수는 카메라에서 떨어진 거리입니다. 클수록 멉니다. 맨 위 네 줄이 원래 깊이 버퍼의 16 칸입니다. 왼쪽 위 네 칸 4·4·5·4 는 한 층 위에서 5 한 칸이 됩니다. 그 위층 네 칸 5·6·4·7 은 맨 위에서 7 하나가 됩니다.
판정할 때는 바운딩 박스가 화면에서 덮는 넓이를 다 덮는 가장 아래층의 칸을 고릅니다. 그 칸의 값과 상자에서 카메라에 가장 가까운 점의 깊이를 견줍니다. 상자가 왼쪽 위 네 칸만 덮고 가장 가까운 점이 8 이면, 8 은 그 칸의 5 보다 멉니다. 덮은 범위의 가장 먼 깊이보다도 뒤이므로 상자는 어디서든 이미 그린 것 뒤에 있습니다. 바로 뺍니다. 가장 가까운 점이 3 이면 가려졌다고 단정할 수 없으므로 남깁니다.
하드웨어 오클루전 쿼리
GPU 가 도형 하나를 칠할 때 깊이 버퍼가 바뀌는지, 또는 깊이 테스트를 통과한 프래그먼트가 몇 개인지를 돌려주는 기능입니다. 진짜 물체 대신 그 물체를 감싸는 단순한 도형, 대개 바운딩 박스를 먼저 그려 봅니다. 감싼 도형이 보일 때만 진짜 물체를 그립니다.
답이 GPU 에서 나오므로 CPU 가 그 답을 받기까지 시간이 걸립니다. 이 기다림이 무엇을 망가뜨리는지는 대가 절이 다룹니다.
소프트웨어 래스터화
GPU 를 거치지 않고 CPU 가 직접 오클루더를 계층적 깊이 버퍼에 그리는 기법입니다. 계층적 깊이 버퍼는 앞 소절의 계층적 Z 버퍼를 깊이 쪽 이름으로 부른 것입니다. GPU 가 하던 래스터화를 코드로 흉내 내므로 소프트웨어 래스터화라고 부릅니다. 논문 「Masked Software Occlusion Culling」에 딸려 공개된 Masked Occlusion Culling 라이브러리가 그 구현 하나입니다. 주로 CPU 의 벡터 명령인 AVX(Advanced Vector Extensions)에 맞춰 최적화되어 있습니다.
RenderTriangles() 로 오클루더 메시를 몇 개 그린 뒤 판정을 시작합니다. TestTriangles() 는 그리는
대신 판정만 하고 깊이 버퍼는 안 바꿉니다. TestRect() 는 물체의 바운딩 박스를 화면에 비춘 사각형과
깊이 하나로 빠르게 판정합니다. 답은 VISIBLE·OCCLUDED·VIEW_CULLED 셋 가운데 하나로 옵니다.
대가
오클루전 컬링은 그릴 물체를 줄이는 대신 판정이라는 일을 새로 얹습니다. 아래 넷이 그 판정 때문에 나빠지는 것입니다.
판정에 드는 시간
오클루전 쿼리는 판정 자체가 그리기 일입니다. 한 추정에 따르면 바운딩 박스로 오클루전 쿼리 하나를 하는 비용이 25 픽셀짜리 삼각형 약 190 개를 그리는 비용과 맞먹습니다. 물체마다 쉬지 않고 판정하면 오히려 성능을 깎을 수 있습니다. GPU 에서 정보를 읽어 오는 기법들은 대개 장면의 복잡도가 높은 문턱을 넘어야 효과가 납니다.
물체를 이루는 삼각형이 적을수록 아낄 것이 적습니다. 삼각형 300 개쯤의 물체에는 화면에 수백 개가 있어도 쿼리로 얻는 것이 별로 없습니다. GPU 가 빠르고 CPU 가 느리면 CPU 가 프레임 시간을 정하는 CPU 바운드가 됩니다. 이때 오클루전 판정은 별 도움이 안 됩니다.
Unity 의 내장 오클루전 컬링은 판정을 실행 중에 CPU 에서 합니다. 그 계산이 아낀 CPU 시간을 깎아 먹을 수 있습니다. 같은 칸을 여러 물체가 겹쳐 칠하는 낭비를 오버드로라고 부릅니다. Unity 에서는 GPU 가 오버드로 때문에 바쁜 GPU 바운드 프로젝트에서 성능이 오를 가능성이 가장 큽니다.
CPU 의 기다림과 팝핑
보통 CPU 와 GPU 는 나란히 일합니다. CPU 는 그리기 명령을 GPU 의 명령 줄에 넣어 두고 바로 다른 일로 넘어갑니다. GPU 는 준비되는 대로 줄에서 꺼내 그립니다.
오클루전 쿼리는 이 흐름을 끊습니다. GPU 가 몇 칸을 그렸는지 알려 주려면 먼저 그려야 합니다. 그래서 CPU 는 쿼리에 쓴 삼각형뿐 아니라 그 앞에 넘긴 삼각형까지 GPU 가 다 그릴 때까지 기다려야 합니다. 그동안 CPU 는 놉니다. 명령 줄에 쌓인 일을 GPU 가 다 비워 내야 CPU 가 다음으로 넘어갑니다. 이렇게 명령 줄을 비우는 일을 GPU 파이프라인 플러시라고 부릅니다.
그래서 흔히 지난 프레임의 결과를 씁니다. 이어진 두 프레임이 서로 닮는다는 데 기대는 것입니다. 이 성질이 시간 일관성입니다.
지난 프레임에 보였던 물체는 이번에도 보인다고 보고 그냥 그립니다. 그 사이 가려진 물체도 GPU 로 넘어갈 수 있습니다. 가려졌던 물체는 프레임마다 다시 판정해야 합니다. 이번에도 가려졌다고 그냥 두면 물체가 허공에서 불쑥 나타나는 오류가 날 수 있습니다.
sequenceDiagram
participant C as CPU
participant G as GPU
C->>G: 프레임 1 · 상자를 그려 통과한 샘플을 센다
C->>G: 프레임 1 · 지난 프레임에 보였으니 물체도 그린다
G-->>C: 프레임 1 의 답 · 통과한 샘플 0개
Note over C: 프레임 2 · 이 물체를 뺀다
한 프레임 늦은 답은 화면을 틀리게 만들기도 합니다. 모퉁이 뒤에서 방금 나타난 물체는 지난 프레임에는 가려져 있었습니다. 늦은 답을 쓰면 이 물체가 이번 프레임에서 빠질 수 있습니다. 카메라가 아주 빠르게 움직이면 이렇게 물체가 한 박자 늦게 튀어나오기도 합니다. 이 현상을 팝핑이라고 부릅니다. 앞서 본 두 틀림 가운데 「보이는데 뺐다」 쪽입니다.
판정이 한 프레임 늦는 까닭은 하나 더 있습니다. 앞의 기다림은 쿼리 답을 받으려고 CPU 가 서는 것이었습니다. 이쪽은 단계들이 프레임을 나눠 맡아서 생깁니다.
대부분의 기법은 컬링과 그리기가 같은 프레임을 함께 다룬다고 봅니다. 그런데 요즘 실시간 API 에서는 응용 프로그램이 장면을 갱신하는 장면 관리, 컬링, 그리기 세 단계가 서로 다른 세 프레임을 나란히 처리할 수 있습니다. 컬링이 이번 프레임을 다루는 동안 그리기는 아직 그 앞 프레임을 그립니다. 이번 프레임의 Z 버퍼는 아직 만들어지지 않았습니다. 컬링이 읽을 수 있는 것은 지난 프레임의 Z 버퍼입니다.
미리 계산하는 갈래의 굽기와 메모리
영역 기준 기법은 대개 오래 걸리는 사전 계산과 큰 저장 공간을 요구합니다. 움직이는 물체도 시점 기준 기법만큼 다루지 못합니다. 오클루더가 움직이면 정보를 전부 다시 계산해야 합니다. 움직이는 오클루디는 움직일 범위를 감싼 부피로 대신 다룰 수 있습니다.
Unreal Engine 의 Precomputed Visibility 를 쓰면 실행 중에 컬링 데이터를 올리는 메모리를 지켜봐야 합니다. Unity 도 구운 데이터를 실행 중에 메모리에 올립니다. Unity 에서 움직이는 게임 오브젝트는 가려질 수는 있지만 다른 오브젝트를 가리지는 못합니다. 실행 중에 장면 도형을 만들어 내는 프로젝트에는 Unity 의 내장 오클루전 컬링이 맞지 않습니다.
상자가 물체보다 비싼 장면
해상도는 높고 픽셀 셰이더가 단순하면 칸을 칠하는 속도, 곧 필레이트가 프레임 시간을 정할 수 있습니다. 이때는 바운딩 박스 전체를 그리는 데 상자 안의 물체만 그릴 때보다 훨씬 많은 픽셀이 들 수 있습니다. 판정하려고 그린 상자가 칸 단위 일을 더 늘립니다.
관련 항목
오클루전 컬링이 속하는 상위 분류
컬링 · 가시성 판정 · 은면 제거 · 렌더링 최적화
오클루전 컬링과 나란히 거는 다른 컬링
절두체 컬링 · 백페이스 컬링 · 포털 컬링 · 디테일 컬링 · 작은 삼각형 컬링 · 클러스터 컬링
가려졌는지 판정하는 기법
잠재 가시 집합 · 셀과 포털 · 오클루전 쿼리 · 조건부 렌더링 · 프레디케이션 · 계층적 Z · 깊이 피라미드 · 소프트웨어 래스터화 · 오클루더 퓨전 · 오클루더 · 레이 캐스팅
판정에 쓰는 깊이 정보와 기하 요소
깊이 버퍼 · 깊이 테스트 · 프래그먼트 · 바운딩 볼륨 · 바운딩 박스 · 뷰 절두체 · 밉맵
판정 대상을 미리 갈라 두는 공간 분할 구조
공간 분할 · BSP 트리 · 옥트리 · BVH · 포털 · 씬 그래프
늦은 판정이 부르는 문제와 기대는 성질
팝핑 · 파이프라인 플러시 · 시간 일관성 · 지연 시간
오클루전 컬링이 줄이려는 낭비와 그 지표
오버드로 · 드로 콜 · 프레임 시간 · 필레이트 · GPU 바운드 · CPU 바운드
같은 목적으로 그릴 양을 줄이는 기법
레벨 오브 디테일 · 얼리 Z · Z 프리패스 · 인스턴싱 · 임포스터 · 디퍼드 셰이딩
오클루전 컬링 판정을 호출하거나 설정하는 API와 엔진
OpenGL · Vulkan · Direct3D · Unreal Engine · Unity
오클루전 컬링이 쓰이는 상위 분야
그래픽스 · 실시간 렌더링 · 게임 개발 · 게임 엔진 · 렌더 파이프라인 · 그래픽스 파이프라인
다른 이름: occlusion culling