0.1 GPU 파이프라인

이전 장에서는 애플리케이션이 드라이버를 통해 GPU와 어떻게 연결되는지 배웠습니다. 하지만 드라이버는 단순히 애플리케이션과 GPU 사이에서 데이터만 전달할 뿐, 실제 그래픽 처리 작업은 하지 않습니다. 그리기 명령이 GPU로 전달되면, 이제 GPU에서 실행되어 최종적으로 화면에 픽셀이 나타나야 합니다. 이번 절에서는 GPU가 그리기 명령을 어떻게 처리해 픽셀을 생성하는지, 즉 GPU 파이프라인 과정을 살펴보겠습니다.
GPU 파이프라인은 픽셀을 생산하는 공장과도 같습니다. 설정 값과 입력 데이터를 원료로 받아서, 픽셀이라는 최종 결과물을 생산합니다. 이 픽셀 생산 과정에서 여러 단계를 거치며, 이 중 일부는 프로그래밍이 가능하고, 일부는 설정만 할 수 있으며, 나머지는 고정된 기능만을 제공합니다. 각 파이프라인 단계들을 차근차근 살펴보겠습니다.

그래픽스 프로그래밍의 본질은 바로 GPU 파이프라인을 설정하고, 입력 데이터를 전달하며, 그 결과를 받는 일입니다. 따라서 GPU 파이프라인을 정확하게 이해하는 것이 무엇보다 중요합니다. GPU 프로그래밍의 핵심 활동은 파이프라인 정의와 셰이더 프로그램 제공이죠. 특정 그래픽스 API를 아는 것보다, GPU 파이프라인의 원리를 아는 것이 훨씬 중요합니다. 여러 API를 다뤄본 분들은 모두가 GPU 파이프라인을 중심으로 설계되었다는 사실을 알게 될 것입니다. 파이프라인을 잘 이해하면 새로운 API도 빠르게 적응할 수 있습니다.

우선, GPU로 어떤 데이터와 명령이 전달되는지 정리해봅시다. 그동안 우리는 이걸 그리기 명령(Draw Command) 또는 그리기 요청이라고만 해왔는데, 실제로는 무엇이 포함될까요? GPU 프로그래밍을 하려면, 다음 세 가지 종류의 데이터를 꼭 제공해야 합니다.

첫 번째는 파이프라인 설정 데이터입니다. 비유하자면, GPU가 팔방미인 예술가라면, 다빈치풍, 루벤스풍 등 스타일을 정해주는 게 바로 셰이더 프로그램입니다. 설정에는 다양한 값이 포함되지만, 그중 핵심은 셰이더 프로그램입니다. 셰이더란 GPU에서 실행되어 도형을 변환하고, 픽셀 색상을 정하는 프로그램입니다. 하지만 셰이더만으로는 설정이 완성되지 않고, 여러 추가 단계가 필요합니다. 이 부분은 책의 다음 챕터에서 더 자세히 배웁니다.

두 번째로, 파이프라인 설정이 끝나면 GPU가 픽셀을 만들 준비가 완료됩니다. 이제 삼각형, 텍스처 등 수백만 개의 입력 데이터를 원료로 공급해야 합니다. 이 데이터들은 파이프라인을 거쳐 픽셀로 변환됩니다.

마지막으로, 위 과정에서 필요한 추가적인 메타데이터(주로 유니폼 버퍼 형태)가 있을 수 있습니다. 셰이더 프로그램이 실제 GPU에서 실행되는 코드라면, 유니폼은 마치 프로그램에 넘겨주는 명령줄 파라미터와 같습니다.

이제 GPU 파이프라인에 대해 이론적으로 알아보겠습니다. 파이프라인은 GPU 하드웨어의 추상화 모델입니다. 실제 하드웨어 구현과 완전히 일치하지는 않지만, 소프트웨어 엔지니어링 관점에서 실시간 3D 프로그래밍의 가장 핵심적인 개념입니다. 우리가 하는 모든 그래픽스 프로그래밍 작업이 결국 이 파이프라인을 잘 사용하는 데 집중되어 있기 때문이죠.

GPU 파이프라인을 이해하는 좋은 방법은, GPU를 하나의 공장에 비유하는 것입니다. 삼각형 등으로 표현된 도형이 원재료라면, 픽셀은 완성품입니다. 이 원재료가 완성품으로 변환되는 과정이 바로 GPU 파이프라인입니다.

3D 세계에서는 삼각형이 모든 도형의 기본 단위입니다. 2D 그래픽에서 픽셀이 기본이듯이요. 왜 삼각형을 쓸까요? 삼각형은 어떤 형태의 곡면도 빈틈없이(watertight) 가장 효율적으로 표현할 수 있기 때문입니다. Watertight는 컴퓨터 그래픽스에서 매우 중요한 개념입니다. 메시(mesh)에 구멍이 없고, 안팎이 명확히 구분되는 상태를 watertight하다고 합니다. watertight의 엄밀한 정의가 궁금하다면 이 블로그이 영상을 참고하세요.

물론, 삼각형만이 3D 형태를 표현하는 유일한 방법은 아닙니다. 예를 들어, 점 기반 렌더링(point-based rendering)은 작은 원판으로 3D 표면을 표현합니다. 하지만 이런 원판들은 표면에 아주 촘촘하게 배치하지 않으면 빈틈이 생기거나 이상한 효과(artifact)가 발생할 수 있습니다. 아무리 촘촘하게 배치해도 충분히 확대하면 빈틈이 보이기도 하죠. 반면, 삼각형은 얼마나 확대하더라도 표면 전체를 빈틈없이 덮을 수 있습니다. 예를 들어, 구(sphere) 모델을 삼각형 메시로 표현하면, 각 삼각형이 부드럽게 연결되어 이음새 없는 표면이 만들어집니다. 그래서 3D 도형을 확대해도 항상 정확하게 렌더링할 수 있습니다.

GPU 공장 안에서 삼각형들은 여러 단계를 거쳐 픽셀로 바뀝니다. 이 일련의 단계가 바로 GPU 파이프라인입니다. 과거에는 이 파이프라인이 고정되어 있었고, 정해진 방식으로만 픽셀을 만들 수 있었습니다. 하지만 오늘날에는 프로그래밍이 가능해져서, 우리가 셰이더라는 GPU 프로그램을 직접 작성해, 픽셀 생산 방식을 정의할 수 있게 되었습니다.

고전적인 GPU 파이프라인은 네 가지 주요 단계로 구성됩니다: 버텍스(정점) 단계, 래스터라이즈(래스터화), 프래그먼트 단계, 블렌딩 단계입니다. 최근에는 더 다양한 단계가 추가되었지만, 이 네 가지를 이해하면 대부분의 그래픽 작업을 할 수 있습니다. 이 중 프로그래밍 가능한 단계는 버텍스와 프래그먼트 단계입니다. 각 단계에서 데이터가 조금씩 가공되어, 삼각형이 픽셀로 변신합니다. 이제 각 단계별로 살펴봅시다.

투영 행렬 적용은 마치 물체를 화면에 맞게 끼워 넣는 것과 같습니다. \Label{Proj}
투영 행렬 적용은 마치 물체를 화면에 맞게 끼워 넣는 것과 같습니다. \Label{Proj}

첫 번째는 버텍스(정점) 단계입니다. 이 단계의 핵심 목적은 3차원 도형을 2차원 화면으로 펼치는 것입니다. 입력 데이터는 월드 공간(world space) 좌표의 삼각형들이고, 출력은 화면 좌표계(clip space)에 맞춰 ‘납작해진’ 삼각형입니다. 이 좌표 변환 방식은 버텍스 셰이더 프로그램이 결정합니다. 대표적으로 원근 투영(perspective projection), 직교 투영(orthogonal projection) 등이 쓰입니다. 어떤 변환이 쓰이든, 목표는 3D 객체를 2D로 투영해 픽셀로 샘플링할 수 있도록 하는 것입니다.

또한, 이 단계에서는 이후 단계를 최적화하기 위한 기술도 적용됩니다. 대표적으로 컬링(culling, 뷰에 안 보이는 삼각형 제거), 클리핑(clipping, 화면 바깥 삼각형 제거) 등이 있습니다.

백페이스 컬링은 최종 이미지에 기여하지 않는 삼각형을 제거합니다.
백페이스 컬링은 최종 이미지에 기여하지 않는 삼각형을 제거합니다.

두 번째는 래스터라이즈(래스터화) 단계입니다. 래스터화란 2D 도형을 해당 영역을 덮는 픽셀들로 변환하는 과정입니다. 이 단계의 출력물은 프래그먼트(fragments)입니다. 주의할 점은, 프래그먼트는 아직 픽셀이 아니라는 것입니다. 둘 다 2차원 평면의 점이긴 하지만, 픽셀은 색상까지 정해져 화면에 표시할 수 있고, 프래그먼트는 다양한 데이터만 가질 뿐 색상은 아직 정해지지 않았습니다. 색상 결정은 다음 단계에서 이뤄집니다.

클리핑은 화면 영역 밖의 삼각형을 제거합니다.
클리핑은 화면 영역 밖의 삼각형을 제거합니다.

삼각형 데이터를 파이프라인에 넣을 때, 각 정점에 메타데이터(예: 텍스처 좌표, 색상 등)를 추가할 수 있습니다. 삼각형은 정점이 3개뿐이지만, 프래그먼트는 삼각형을 덮는 수많은 점들로 생성됩니다. 그렇다면 정점의 메타데이터를 각 프래그먼트에 어떻게 할당할까요? 바로 래스터화 단계에서 보간(interpolation)을 이용합니다. 보간이란, 프래그먼트의 위치와 정점과의 거리를 반영해 가중평균으로 값을 산출하는 방식입니다.

래스터화는 각 삼각형을 프래그먼트로 덮습니다.
래스터화는 각 삼각형을 프래그먼트로 덮습니다.

래스터화 과정은 삼각형 벽돌을 쌓는 것과 비슷합니다. 프래그먼트가 생성되면, 이제 프래그먼트 단계로 넘어갑니다. 이 단계에서는 각 프래그먼트에 색상을 지정하여, 비로소 픽셀이 됩니다. 색상 결정 논리는 우리가 작성한 프래그먼트 셰이더 프로그램에 의해 정해집니다.

새 픽셀은 블렌딩을 통해 기존 픽셀과 합쳐집니다.
새 픽셀은 블렌딩을 통해 기존 픽셀과 합쳐집니다.

마지막은 블렌딩 단계입니다. 이 단계에서는 새로 만들어진 픽셀을 프레임 버퍼(frame buffer)에 저장된 기존 이미지에 적용합니다. 프레임 버퍼는 현재까지 그려진 모든 결과를 담고 있는 임시 저장소입니다. 블렌딩 단계에서는 기존 픽셀과 새 픽셀을 어떻게 섞을지 우리가 직접 정의할 수 있습니다. 블렌딩은 투명도 구현 등에 유용하게 쓰입니다. 블렌딩이 끝나면, 프레임 버퍼가 사용자에게 표시되고 한 프레임의 렌더링이 완료됩니다.

WebGPU를 비롯한 모든 3D 그래픽스 API는 우리가 그래픽 파이프라인을 정의하고, 리소스를 GPU로 보내 처리할 수 있도록 다양한 기능을 제공합니다. 즉, 파이프라인 설정이야말로 그래픽스 프로그래밍의 핵심입니다.

마지막으로, 왜 GPU가 필요한지 생각해봅시다. 그냥 강력한 CPU만으로 그래픽 작업을 처리하면 안 될까요? 비유하자면, 여러분이 공장 사장이라면 직원 전원을 박사 학위자(Ph.D.)로만 뽑으시겠습니까? 만약 특허 연구소라면 당연히 박사 채용이 좋겠지만, 바나나 껍질 벗기기만 하는 바나나 공장이라면 100명의 원숭이를 뽑는 게 더 나을 수 있습니다. 똑같은 자원으로 원숭이팀을 훨씬 크게 꾸릴 수 있죠. 박사는 똑똑하고 협업도 잘하지만, 바나나 껍질 벗기기에는 그렇게 똑똑하거나 협동이 필요하지 않습니다. CPU는 박사처럼 유능하고 동기화에 최적화되어 있습니다. 반면, GPU는 단순한 연산을 빠르게 처리하는 작은 유닛이 대량으로 모여 있어, 서로 독립적인 연산을 동시에 처리하기에 유리합니다. 삼각형을 펼치고 픽셀 색칠하기처럼 간단하고 독립적인 작업에는 GPU가 최고의 하드웨어입니다. 예전엔 그래픽 전용으로만 쓰였던 GPU도, 프로그래밍이 가능해지자 인공지능, 물리 시뮬레이션, 영상/이미지 처리 등 다양한 분야에서 활약하게 되었습니다.

정리하면, 이 장에서는 GPU 드라이버와 그래픽 파이프라인 개념을 알아보고, 파이프라인이 삼각형을 픽셀로 바꾸는 원리를 설명했습니다. 다음 장에서는 실제로 WebGPU 파이프라인을 설정해 간단한 장면을 렌더링하는 방법을 배워봅니다.

[1]:

이 책 전체 그림에 사용된 카메라 모델, "1930's Movie Camera" by Daz는 크리에이티브 커먼즈 저작자 표시 라이선스로 배포됩니다.

GitHub에서 의견 남기기