TECHARTNOMAD | MAZELINE.TECH

TECH.ART.FLOW.IO

[번역] Modern rendering culling techniques

jplee 2026. 4. 24. 19:23

원문: Modern rendering culling techniques | krupitskas 🌦️

Modern rendering culling techniques | krupitskas 🌦️

🌲 The best work is the work that never gets executed

krupitskas.com

원문으로 읽으실 분은 위 링크로 바로~


역자의 말: 프라그마타 그래픽스 분석글을 정리하고 있는데 내용이 너허어어어무 길어서 잠시 포기 하고 가볍게 읽을 토픽을 하나 찾아서 공유 해 봅니다. 개인적으로는 GPU Frustum Culling 과 Draw-Indirect 결합을 해서 고객사에 제공하고 있는데 꽤나 성능향상폭이 큽니다. 중국 모 게임사에 2019년도에 근무 할 무렵에도 오픈월드 프로젝트에 일부 적용했던 것이기도 하고요. 요즘 개방형 오픈월드 프로젝트가 한국에서도 조금씩 태동하려는 것을 보면 HLOD 를 떠나 GPU 드리븐 컬링 기법도 매우 중요한 부분으로 봐야 하죠. 너무 당연한 것이죠.  배경 아티스트 특히 레벨아티스트 담당자라면 모듈 디자인을 할 때 프리미티브 쿼리에 대한 최소화 전략까지도 알아야 합니다. 단순히 예전의 OBB 타입 컬링은 이제 사용하지 않거든요. 아무튼 지하철에서 출근길과 퇴근길을 달래면서 ~ Krupitskas 의 Modern rendering culling techniques 토픽을 한번 보는것도 좋겠네요.


저자: krupitskas

소개

AI 코딩, "AI 게임 생성", DLSS 5, 언리얼 엔진 5, 그리고 놀라운 가우시안 스플랫 데모가 넘쳐나는 요즘, 사람들은 그래픽스와 게임이 이미 해결된 문제라고 생각하는 경향이 있다. "AI로 며칠 만에 게임을 만들어봐"라고들 하지. 당연히 헛소리다. 어려운 엔지니어링 작업, 전문 지식, 트레이드오프, 아트 디렉션은 사라지지 않는다. 게임이 2D든 3D든, 사실적이든 카툰풍이든, 폐쇄된 화성 기지가 배경이든 좀비 가득한 오픈 월드 뉴욕이든, 여전히 최적화가 필요하다. 모든 게임이 사용해왔고 앞으로도 계속 사용할 가장 중요한 최적화 중 하나가 바로 컬링이다.
좋은 소식: 내가 커리어 동안 봐온 최적화의 거의 80%는 "필요 없을 때 쓸데없는 작업을 하지 않는 것"으로 귀결된다.
나쁜 소식: 씬 구조, 게임 디자인, 아트 디렉션, 하드웨어 한계, 성능 예산의 균형을 맞추면서 컬링을 구현해야 한다는 건 여전히 사실이다.
그래서 이 글에서는 모던 실시간 렌더러에서 사용되는 주요 컬링 기법들을 살펴본다. 서로의 관계를 파악하기 쉽도록 카테고리별로 묶었다. 이 기법들은 거의 모두 별도의 글이 필요할 정도인데, 언제나 그렇듯 악마는 디테일에 있기 때문이다.


1. 기본기: 거리, 백페이스, 절두체

이것들은 가장 저렴하고 보편적으로 적용되는 기법들이다. 더 비싼 것들이 실행되기 전에 명백한 케이스들을 걸러낸다.

거리 컬링

가장 단순한 형태: 오브젝트가 카메라로부터 최대 거리보다 멀면, 건너뛴다. 그게 전부다.
이는 연산 비용이 거의 없으며, 사라질 때 시각적 영향이 미미한 작은 소품에는 잘 작동한다. 대부분의 엔진에서는 메시나 머티리얼 단위로 컬링 거리를 설정할 수 있다.
까다로운 부분은 눈에 띄는 팝인(pop-in)을 피하는 것이다. 일반적인 완화 방법으로는 디더드(dithered) 페이드아웃, 컬링 포인트 전 적극적인 LOD 전환, 또는 먼 거리에서 실제 메시를 대체하는 임포스터(billboard)가 있다.
이에 대해서는 아래 스크린 사이즈 컬링 섹션에서 더 자세히 다루지만, 여기서 짚어둘 가치가 있다: 오브젝트가 몇 개의 픽셀에만 투영된다면, 굳이 그릴 비용을 쓸 이유가 없는 경우가 많다. 거리만으로는 이를 깔끔하게 잡아낼 수 없고, 화면 공간 크기 체크도 함께 필요하다.

집과 작은 돌들을 이용한 과장된 예시. 작은 돌들이 거리에서 사라지는 건 거의 눈에 띄지 않지만, 큰 오브젝트가 갑자기 나타났다 사라지는 건 바로 알아챌 수 있다.

백페이스 컬링

그래픽스 API를 처음 다룰 때 가장 먼저 접하게 되는 컬링 기법으로, PSO(파이프라인 스테이트 오브젝트)의 일부로 설정되며 활성화하기 가장 쉬운 최적화 중 하나다.

모든 삼각형에는 앞면과 뒷면이 있다. 닫힌 메시의 경우, 뒷면은 오브젝트 내부에 있기 때문에 절대 보이지 않는다. GPU는 와인딩 오더(winding order)를 기반으로 이를 자동으로 건너뛸 수 있으며, 일반적인 지오메트리에서 래스터화와 프래그먼트 작업을 약 절반 가량 절약할 수 있다.

 
Triangle winding order

회전하는 정이십면체 - 카메라를 향하는 삼각형은 래스터화되고 나머지는 건너뜀
알아둘 점: 전통적인 버텍스 + 프래그먼트 파이프라인에서 백페이스 컬링은 버텍스 셰이더가 이미 버텍스를 처리한 이후에 발생한다. 따라서 버텍스 작업은 절약되지 않고, 래스터화와 프래그먼트 작업만 절약된다. 더 GPU 드리븐(GPU-driven) 파이프라인에서는 이 결정을 더 앞으로 옮길 수 있는데, 예를 들어 래스터화에 도달하기 전에 메시렛을 컬링하는 컴퓨트나 태스크/증폭(amplification) 작업에서 처리할 수 있다.
이는 거의 공짜에 가깝지만, 투명도, 양면 머티리얼, 그리고 이를 명시적으로 활용하는 일부 컬링 알고리즘과 어떻게 상호작용하는지 이해하는 것이 중요하다.

절두체 컬링

탑다운 뷰: 절두체 밖의 오브젝트들은 컬링되어 렌더링 파이프라인에 제출되지 않는다

원근 카메라에서 뷰 절두체는 카메라가 볼 수 있는 것을 나타내는 잘린 피라미드 모양의 볼륨이다. 그 밖에 있는 것들은 렌더링할 필요가 없다. 절두체 컬링은 보통 구(sphere)나 AABB 같은 바운딩 볼륨을 통해 오브젝트를 절두체의 여섯 평면에 대해 테스트하고, 교차하지 않는 것들을 건너뛴다.

이는 거의 항상 컬링 파이프라인의 첫 번째 패스, 또는 거리 컬링 다음 두 번째 패스다. 빠르고 저렴하며, 특히 맵의 넓은 영역이 카메라 뒤나 옆에 있는 오픈 월드에서 씬의 상당 부분을 한 번에 제거할 수 있다.

Horizon Zero Dawn의 절두체 컬링이 작동하는 방식

위 gif를 보면 산 같은 큰 오브젝트들은 절두체를 거의 벗어났을 때도 여전히 렌더링된다는 것을 알 수 있다. 이것이 오브젝트 레벨 컬링의 핵심 트레이드오프다: 작은 오브젝트가 많으면 세밀한 컬링 기회가 생기지만 각각이 드로우 콜과 CPU 측 가시성 테스트가 된다. 큰 오브젝트 몇 개는 드로우 콜 비용이 저렴하지만, 삼각형의 90%가 화면 밖에 있어도 전체를 렌더링해야 하고, 그 삼각형 전부에 버텍스 셰이더 비용이 발생한다 - 래스터라이저는 버텍스 셰이딩 이후에 클리핑하기 때문이다. 이렇게 화면 밖 지오메트리에 낭비되는 버텍스 작업이 바로 4섹션의 메시렛 컬링이 해결하는 문제다.

2. 오클루전 컬링

오클루전 컬링은 다른 것들 뒤에 있는 것을 알려준다. 더 어렵지만, 도시나 실내처럼 밀도 높은 씬에서 종종 가장 큰 성과를 가져다준다.

이것은 오클루전 컬링만 적용한 것이다. 오른쪽에서 집 뒤의 상자들이 사라지는 것을 주목하라. 모퉁이를 돌아보면 일부가 다시 나타난다.

하드웨어 오클루전 쿼리

모든 주요 그래픽스 API는 오클루전 쿼리 방식의 기능을 제공한다. Direct3D 12에는 쿼리 힙, Vulkan에는 오클루전 쿼리, Metal에는 가시성 결과 버퍼가 있다. 개념은 동일하다: 보통 오브젝트의 바운드인 프록시 지오메트리를 렌더링하고, 어떤 샘플이 깊이 테스트를 통과했는지 카운트한다. 가시 샘플이 0이면 프록시가 그 뷰에서 완전히 가려진 것이므로, 실제 오브젝트를 보통 건너뛸 수 있다.

DX12에서는 정확한 샘플 수 대신 0이나 1만 반환하는 D3D12_QUERY_TYPE_BINARY_OCCLUSION을 사용할 수 있다 - 더 저렴하고 컬링에는 충분하다:

// setup (once)
D3D12_QUERY_HEAP_DESC desc = { D3D12_QUERY_HEAP_TYPE_OCCLUSION, objectCount };
device->CreateQueryHeap(&desc, IID_PPV_ARGS(&queryHeap));

// per frame - render proxy, wrap with query
cmdList->BeginQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex);
cmdList->DrawIndexedInstanced(...); // draw bounding box
cmdList->EndQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex);

// resolve to a readback buffer (still on GPU timeline)
cmdList->ResolveQueryData(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION,
                          0, objectCount, readbackBuffer, 0);

문제는 레이턴시와 동기화다. GPU가 완료된 후에야 CPU에서 결과를 볼 수 있으므로, 실제로는 N+1 프레임을 렌더링하면서 N 프레임의 결과를 읽는 경우가 많다. 이 한 프레임의 지연은 보통 허용 가능하지만, 방금 가려진 것을 잠깐 계속 렌더링하거나, 방금 보이게 된 것을 건너뛰는 일이 생길 수 있다.

소프트웨어 오클루전 컬링 (CPU)

GPU에 묻는 대신, CPU에서 저해상도 깊이 버퍼를 래스터화하고 오브젝트를 그것과 테스트한다. Intel의 Masked Software Occlusion Culling(MSOC)이 아마 가장 잘 알려진 구현일 것이다. SIMD를 사용해 8x4 픽셀 타일로 삼각형을 래스터화하고 초당 수백만 개의 삼각형을 처리할 수 있다.
장점은 GPU에 아무것도 제출하기 전에 CPU에서 모든 것이 처리되므로 리드백 레이턴시가 0이라는 것이다. 단점은 CPU 비용과, 전체 씬 지오메트리를 래스터화할 여력이 없으므로 별도의 단순화된 오클루더 메시를 유지해야 한다는 점이다.

Battlefield 3 - 최종 렌더링 씬
Battlefield 3 - final rendered scene

Battlefield 3 - CPU 소프트웨어 오클루더가 래스터화한 동일한 씬. 결과는 의도적으로 거칠다 - 실제로 보이는 것을 과도하게 컬링하는 것이 건너뛰어야 할 것을 덜 컬링하는 것보다 더 나쁘다.

Hi-Z (계층적 Z 버퍼)

Hi-Z는 깊이 버퍼의 밉 체인으로, 깊이 피라미드(depth pyramid)라고도 불리며, 각 레벨은 화면의 더 넓은 영역에 대한 보수적인 깊이 값을 저장한다.

매우 단순한 3D 씬의 시각화

오브젝트가 가려졌는지 테스트하려면, 오브젝트의 바운드를 화면 공간에 투영하고, 그 발자국과 대략 맞는 밉 레벨을 선택한 다음, 오브젝트의 가장 가까운 깊이를 피라미드와 비교한다. 일반적인 LESS 깊이 테스트에서 이 피라미드는 보통 각 영역의 최대 깊이를 저장하고, Reversed-Z에서는 보통 최소를 저장한다. 중요한 것은 표현이 보수적으로 유지된다는 점이다. 테스트가 "가려짐"이라고 하면 안전하게 오브젝트를 건너뛸 수 있다. 그렇지 않으면 유지한다. 좋은 구현은 거짓 음성(false negative)보다 거짓 양성(false positive)을 선호한다.

Hi-Z 피라미드
이것이 오늘날 대부분의 GPU 드리븐 오클루전 컬링의 기반이다. 빠르게 생성하고 쿼리할 수 있으며, 완전히 GPU 위에서만 동작한다.

투 패스 오클루전 컬링

GPU 드리븐 렌더러의 일반적인 패턴: 이전 프레임의 Hi-Z를 사용해 현재 프레임을 렌더링하기 전에 오브젝트를 컬링한다.
단순한 버전은 원 패스다: 이전 프레임의 Hi-Z에 대해 모든 것을 컬링하고, 살아남은 것을 렌더링한다. 저렴하지만, 방금 보이게 된 오브젝트는 잘못 컬링되어 한 프레임 동안 보이지 않는다.
투 패스 버전이 이것을 해결한다. 패스 1은 지난 프레임에 보였던 오브젝트들을 테스트하고, 살아남은 것들을 렌더링한 다음, 그로부터 새로운 Hi-Z를 생성한다. 패스 2는 패스 1에서 컬링된 모든 것을 가져와 새 Hi-Z에 대해 다시 테스트한다. 방금 보이게 된 것들은 두 번째 기회를 얻고 이번 프레임에 렌더링된다. 패스 1에서 사용된 Hi-Z는 여전히 한 프레임 오래된 것이므로, 추가 패스로는 해결할 수 없는 작은 잔류 부정확도가 있다. "일반적인 게임플레이"에서는 눈치채지 못할 것이다. 문제가 생기는 경우는 갑작스러운 카메라 컷, 예를 들어 갑작스러운 90도 회전 같은 것이다: 패스 1의 가시 세트가 기본적으로 틀리고, 재생성된 Hi-Z는 신뢰할 수 없으며, 한 프레임이 망가진다. 엔진들은 보통 이를 감지하고 해당 프레임에는 전체 뎁스 프리패스로 폴백한다.
GPU 측 비용이 항상 전체 뎁스 프리패스를 하는 것보다 훨씬 낮기 때문에, 대부분의 모던 게임 엔진이 이 방식을 사용한다.


3. 그 외의 컬링 기법들!

스크린 사이즈 컬링

고정된 월드 공간 거리 대신, 투영된 화면 넓이를 기반으로 컬링한다. 10미터 떨어진 오브젝트는 렌더링할 가치가 있을 수 있지만, 2000미터 떨어진 동일한 오브젝트는 3픽셀에 투영될 수 있어 드로우 콜 오버헤드가 아깝다. 스크린 사이즈 컬링은 단순한 거리 임계값보다 이를 더 우아하게 처리한다.
예를 들어 언리얼은 스태틱 메시 LOD 전환의 기본 지표로 스크린 사이즈를 사용하며, 최소/최대 드로우 거리는 별도의 거리 기반 컨트롤이다.

PVS (잠재적 가시 집합)

PVS는 세계의 각 영역에서 다른 어떤 영역이 보일 수 있는지를 사전 계산한다. 런타임에는 현재 영역의 PVS를 조회하고, 그 안에 없는 것들을 건너뛰기만 하면 된다. 런타임에는 극히 빠르지만 계산 비용이 많이 들고 동적 오브젝트를 잘 처리하지 못한다.

이것은 사전 계산되고 효과적이지만, 절차적으로 생성된 게임에서는 비실용적이거나 불가능할 수 있다.
Quake가 PVS를 유명하게 만들었다. 씬 지오메트리가 정적이고 베이크 시간이 허용 가능한 일부 실내 게임에서는 여전히 유용하다.

포탈 컬링

방과 문이 잘 정의된 실내 씬에서 포탈 컬링은 매우 효과적이다. 모든 출입구가 포탈이 된다. 카메라의 시야를 포탈을 통해 추적하고, 보이는 포탈을 통해 도달할 수 있는 방들만 렌더링한다. 이렇게 하면 방 전체의 지오메트리를 매우 저렴하게 제거할 수 있다.
포탈 컬링은 건물 배경의 1인칭 게임에 많이 사용된다. 여러 문을 통해 볼 때 유효 뷰 콘이 자연스럽게 좁아지므로 절두체 컬링을 잘 보완한다.


4. GPU 드리븐 렌더링과 클러스터 컬링

이제 흥미로워지는 부분이다!
CPU가 무엇을 그릴지 결정하고 오브젝트당 하나의 드로우 콜을 발행하는 대신, 컬링 로직을 GPU로 밀어넣고 인다이렉트 드로우 콜을 사용해 GPU가 결정하게 한다.

인다이렉트 드로잉

DirectX, Vulkan, Metal 모두 인다이렉트 드로잉을 지원하지만 정확한 API는 다르다. 인덱스 카운트와 베이스 버텍스 같은 드로우 인자들이 CPU 코드가 아닌 GPU 버퍼에서 온다. 컴퓨트 셰이더가 컬링을 실행하고 살아남은 오브젝트들만 해당 버퍼에 기록한다. 그러면 하나 또는 소수의 인다이렉트 드로우가 그 압축된 리스트를 소비하므로, CPU는 더 이상 보이는 모든 오브젝트를 순회할 필요가 없다.

// compute shader - one thread per object
[numthreads(64, 1, 1)]
void CullCS(uint id : SV_DispatchThreadID)
{
    if (id >= g_objectCount) return;

    ObjectData obj = g_objects[id];
    if (!SphereInFrustum(obj.center, obj.radius)) return;

    uint slot;
    InterlockedAdd(g_drawCount[0], 1, slot); // atomic compact slot
    g_drawArgs[slot] = MakeDrawArgs(obj);    // write draw arguments
}
// CPU side - dispatch cull, barrier, draw
cmdList->Dispatch((objectCount + 63) / 64, 1, 1);
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::UAV(drawArgsBuffer));
cmdList->ExecuteIndirect(cmdSig, objectCount, drawArgsBuffer, 0, countBuffer, 0);

InterlockedAdd는 살아남은 드로우를 압축된 리스트에 패킹한다. ExecuteIndirect는 GPU가 기록한 카운트를 받아 실제로 컬링 패스에서 살아남은 것만 처리한다.

메시렛 / 클러스터 컬링

모던 GPU 드리븐 렌더러는 서브 메시 레벨에서 컬링할 수 있는데, 컴퓨트 또는 메시/태스크/증폭 셰이더 스테이지에서 처리된다. 메시는 메시렛(meshlet), 즉 작은 삼각형 클러스터로 분할되는데, 보통 각각 64-128개의 삼각형으로 이루어진다. 각 메시렛에는 자체 바운딩 스피어와 삼각형 법선의 범위를 나타내는 콘(cone)이 있다.

여기서 각 색상 패치가 개별 메시렛이다
GPU에서 각 메시렛에 독립적으로 절두체 컬링, 오클루전 컬링, 백페이스 컬링을 실행할 수 있는데, 이는 오브젝트 레벨 컬링보다 훨씬 정밀하다. 큰 캐릭터 메시는 수백 개의 메시렛을 가질 수 있고, 어떤 특정 각도에서는 그 중 일부분만 보인다.
법선 콘은 특히 영리하다. 메시렛의 모든 법선이 대략 같은 방향을 향한다면, 매우 저렴한 콘 대 뷰 테스트로 전체 메시렛을 거부할 수 있다. 기본적으로 클러스터 레벨의 백페이스 컬링이다.


5. 가상화된 지오메트리: Nanite

Nanite(언리얼 엔진 5)는 위의 여러 아이디어들을 하나의 통합 시스템으로 결합하기 때문에 별도로 언급할 가치가 있다. 목표는 수동으로 LOD 체인을 만들지 않아도 된다는 것이다. 대신 모든 메시는 더 세밀한 것을 단순화해서 만든 거칠한 레벨들로 이루어진 클러스터 계층 구조로 저장된다.

메시렛이 더 많아요!!!
런타임에는 이 계층 구조가 GPU에서 탐색된다. 각 클러스터는 가시성과 화면 공간 오차에 대해 테스트된다. 클러스터가 화면에서 충분히 작으면 해당 디테일 레벨로 렌더링된다. 너무 크면 시스템이 한 레벨 더 깊이 들어간다. 중요한 것은 클러스터 선택과 컬링이 GPU에서 일어나, 오브젝트당 CPU 제출 작업이 대폭 줄어든다는 점이다.
또 다른 핵심 요소는 Nanite가 매우 작은 삼각형에 사용하는 소프트웨어 래스터화 경로다. 삼각형이 작아지면 고정 기능 하드웨어 래스터라이저는 삼각형당 많은 오버헤드를 부담하기 시작한다. Nanite는 그런 경우를 커스텀 소프트웨어 경로로 처리하고, 더 큰 삼각형은 여전히 하드웨어 래스터화를 사용한다.
결과적으로 보이는, 적절한 크기의 삼각형만 실제로 래스터화되는 수십억 개의 삼각형을 가진 씬을 만들 수 있다.


6. 라이트 및 섀도우 컬링

위의 모든 것은 지오메트리에 초점을 맞췄지만, 라이트와 섀도우에는 자체적인 컬링 스토리가 있으며 밀도 높은 씬에서는 종종 더 큰 병목이 된다. 여기서는 깊이 들어가지 않고 주요 아이디어를 빠르게 훑겠다.
타일드 라이트 컬링 (Forward+) - 화면을 2D 타일로 나누고 각 타일은 자신과 겹치는 라이트만 기록하므로, 셰이딩은 씬의 모든 라이트 대신 작은 로컬 리스트를 읽는다.
클러스터드 라이트 컬링 - 깊이 축을 따라 뷰 절두체를 클러스터로 분할해 타일링을 3D로 확장하며, 특히 깊이 복잡도가 다양한 씬에서 훨씬 촘촘한 라이트 리스트를 제공한다.
라이트별 화면 및 깊이 바운드 - 라이트를 셰이딩하거나 섀도잉하기 전에, 보통 라이트가 영향을 줄 수 없는 픽셀에 시간을 낭비하지 않도록 화면 공간 직사각형, 깊이 범위, 또는 라이트 볼륨으로 영향을 클리핑한다.
섀도우 캐스케이드 및 섀도우 캐스터 컬링 - CSM의 각 캐스케이드는 서로 다른 깊이 범위를 커버하고, 각 라이트는 자체 섀도우 절두체나 볼륨을 가진다. 그 영역 밖의 지오메트리는 해당 섀도우 패스에서 완전히 건너뛸 수 있다.

마무리

컬링은 1만 피트 상공에서는 단순해 보이지만 실제 렌더러를 만드는 순간 트레이드오프의 더미로 변하는 주제 중 하나다. 정답은 거의 단일 기법이 아니다. 실제로는 여러 기법을 쌓는다: 먼저 거리와 절두체 컬링, 다음으로 어떤 형태의 오클루전, 그리고 콘텐츠가 추가 복잡도를 정당화할 때 메시렛, 라이트, 섀도우 컬링 같은 더 세밀한 시스템을 쓴다.
경험칙은 단순하다: 정확성에는 보수적으로, 낭비되는 작업에는 공격적으로. 거짓 음성은 그냥 필요 이상으로 조금 더 렌더링했다는 의미다. 거짓 양성은 플레이어 눈앞에서 뭔가가 사라졌다는 뜻이고, 그건 사람들이 즉시 알아채는 종류의 버그다.
처음부터 렌더러를 만든다면, 먼저 지루한 승리부터 시작하라: 좋은 바운드, 절두체 컬링, 합리적인 LOD, 스크린 사이즈 임계값. 그런 다음 프로파일링하라. 밀도 높은 실내나 도시가 프레임을 지배한다면, 오클루전을 추가하라. CPU 제출이나 버텍스 비용이 문제라면, GPU 드리븐으로 가라. 콘텐츠가 터무니없이 밀도 높고 삼각형이 많다면, 클러스터 기반 시스템과 Nanite 같은 아이디어가 의미를 갖기 시작한다.
은탄환은 없다. 그저 제거하는 작업보다 저렴한 가시성 테스트의 레이어들이 있을 뿐이다. 그것이 렌더링 엔지니어링의 핵심이다.
행운을 빈다, 즐겨라!

친구의 추가 섹션 요청 - "baz!" 😜

메시 셰이더에서의 삼각형 컬링

메시 셰이더는 래스터화가 시작되기도 전에 개별 삼각형을 컬링할 수 있게 해준다.
아이디어는 단순하다: 메시 셰이더 내에서, 방출하려는 각 삼각형에 대해 몇 가지 저렴한 테스트를 실행한다. 삼각형이 테스트 중 하나라도 실패하면, 컬링된 것으로 표시한다. 메시 셰이더는 프리미티브당 가시성 값을 내보낼 수 있게 해주며(HLSL에서 SV_CullPrimitive를 통해), 래스터라이저는 표시된 삼각형을 완전히 건너뛴다. 픽셀 셰이더 호출도 없고, 깊이 쓰기도 없고, 아무것도 없다. 삼각형은 사라진다.
이 레벨에서 컬링할 가치가 있는 것은?

백페이스 컬링

하드웨어가 이 작업을 하지만, 우리는 더 일찍 처리해 다운스트림 비용을 절약할 수 있다. 트릭은 Olano와 Greer의 "Triangle Scan Conversion using 2D Homogeneous Coordinates"의 2D 동차 행렬식이다 - 삼각형의 클립 공간 xyw 좌표로 3×3 행렬을 만들고 부호를 확인한다. 원근 분할이 필요 없어서 w가 0에 가까울 때의 엣지 케이스들을 피할 수 있다.

if (determinant(float3x3(v[0].xyw, v[1].xyw, v[2].xyw)) >= 0)
    return CULLED;

니어(Near) 플레인 클리핑

w < 0인 버텍스의 수를 센다. 세 개 모두 카메라 뒤에 있으면, 삼각형은 완전히 밖에 있다 - 컬링한다. 일부만 뒤에 있다면, 내버려두고 하드웨어 클리퍼가 처리하게 한다(부분 클리핑을 직접 처리하려는 건 골치 아픈 일이다).

절두체 컬링

세 버텍스에서 화면 공간 AABB를 만들고 테스트한다. 박스가 완전히 화면 밖에 있으면, 컬링한다.

소삼각형 / 오버랩 컬링

이것이 흥미로운 것이다. 절두체 안에 있고 앞면인 삼각형도, 픽셀보다 작거나 픽셀 중심 사이에 걸리면 여전히 0픽셀로 래스터화될 수 있다. 이것을 감지하려면 하드웨어가 하는 것과 정확히 일치해야 한다 - 23.8 고정 소수점 스냅핑(대부분의 GPU에서 서브픽셀 비트 8개가 표준). 버텍스를 서브픽셀 그리드에 스냅하고, 바운딩 박스를 만들고, 어떤 픽셀 중심이 그 안에 있는지 확인한다. 없으면, 삼각형은 아무것도 래스터화하지 않으며, 컬링한다.

const uint SUBPIXEL_BITS = 8;
const uint SUBPIXEL_SAMPLES = 1U << SUBPIXEL_BITS;
// ... snap vertices, build fixed-point AABB ...
// if no pixel center is covered, cull

이 마지막 것은 밀도 높은 지오메트리에서 놀라울 정도로 많은 불필요한 것들을 잡아낸다 - 멀리 있는 풀, 머리카락, 또는 많은 삼각형이 서브픽셀이 되는 과하게 세분화된 메시를 생각해보라.
왜 특별히 메시 셰이더인가?
이론적으로 드로우 전에 컴퓨트 셰이더에서 이 작업을 할 수 있지만, 살아남은 삼각형을 새 인덱스 버퍼로 압축해야 하는데, 이건 어색하다. 메시 셰이더는 각 스레드가 하나의 삼각형을 처리하고, 테스트를 실행하고, 압축 없이 출력에 플래그만 세우면 된다 - 래스터라이저가 프리미티브 내보내기 마스크를 직접 소비한다.
증폭 셰이더 스테이지의 메시렛당 절두체 및 Hi-Z 컬링과 결합하면, 계층 구조가 완성된다: 오브젝트 → 메시렛 → 삼각형, 각 레벨이 다음 레벨이 고려할 필요가 없는 것들을 제거한다.

추가 읽을거리