GPU
GPU는 CPU와 사뭇 다른 아키텍처 처리 방식을 가지고 있습니다. GLSL, HLSL 등등은 C-like 언어로 불리지만, 작동방식의 차이점을 어느 정도 이해하고 그래픽카드의 핵심적인 부분인 그래픽스 파이프라인을 들여다볼 때 좀 더 수월하게 이해하실 수 있을 겁니다.
GPU 제어를 위한 언어의 특징
GPU제어를 위한 언어는 목적부터 어떻게 다른지와 용어에 대해서 잠깐 설명 해보겠습니다. GPU는 근본적으로 그래픽적인 연산을 수행하기 위한 목적을 가지고 있습니다. 다시 말해 그래픽을 다루기 위해 나름(?) 최적화된 연산시스템이 기본이적으로 제공됩니다. 비교해서 설명하자면 CPU는 기본적으로(디폴트로) int 연산에 최적화되어있지요. 반면 GPU는 float연산을 훨씬 더 수월하게 할 수 있도록 제공합니다. float연산이 일단 기본 세팅이라고 보시면 됩니다. 또한 행렬연산에 많은 지원을 해줍니다. "왜 뜬금없이 행렬이냐?" 라고 물으신다면 수학적으로 설명할 게 너무 많습니다... 줄여서 말씀드리자면 그래픽적 이동, 크기, 회전 등의 연산은 행렬연산으로 이루어지기에 행렬연산을 지원하게 됩니다. 또 행렬연산과 관련해서 나름 CPU에 비해 특이한 개념인 샘플러라는 단어가 나오는데, 텍스쳐, 노말맵등 각종 이미지를 받아와 연산하기 위해 샘플러라는 개념을 도입했다고 생각하시면 됩니다. 샘플러는 여러 기능이 있지만, 주로 텍스쳐(텍셀) 필터링 및 텍스쳐 좌표(UV)를변환하여 3D 객체에 올바르게 매핑할 수 있도록 합니다.
GPU 내부구조
이전에 포스팅한 그래픽스 파이프라인개념과 좀 더 심화인 개념을 같이 설명해보려고 합니다.
Untitled Diagram.drawio - draw.io (diagrams.net)
## 개요
위 그림을 보면 조금 놀라실 수 있지만.. 하나하나 차근차근 따라가다보면 그래도 어느 정도 이해할 만합니다. 중요한 점은 프로세스를 전체적으로 이해하는 것이기도 하지만, OpenGL을 다룸에 있어서 Vertex와 Fragment shader에 대해서 정확히, 세세하게 아는 것이 매우 중요하다고 할 수 있습니다. "알면 상식으로 좋지 않냐?"라고 하실 수도 있지만, 나머지 Primitive Assembly나 Rasterization을 깊게 알아봤자, Fixed Hardware일 뿐입니다. 그 말은 즉슨, 하드웨어 딴에서 제어되고, 사용자가 제어할 수 있는 부분이 없기 때문에, 깊게 알아봤자 제어할 수 있는 부분이 없기다는 것이죠, 즉 Shader, Vertex를 OpenGL 등으로 제어하게 될텐데, 이 부분은 제어를 할 수 없으니 득이 거의 없다고 볼 수 있겠습니다. 자 이제 각각에서 어떤 일이 일어나는지 하나하나 짚어볼까요?
일단 Register의 이름에 대해서 간단하게 정리하고 들어가봅시다, attribute와 varying, unifom이 있는데 각각 이름에 맞는 ㄴ역할을 한다고 보시면 됩니다. attribute는 Primitive의 attribute를 나타내고, Varying은 바뀐다는 단어의 의미처럼 데이터를 변환하여 전달하는 데 사용됩니다. Uniform은 일종의 전역변수라고 보시면 되는데, 값이 변하지 않고 일정하다는 특징이 있지요.
## Vertex Shader
사용자가 주는 버텍스 자료를 그래픽적으로 표현하기 위한 좌표, 즉 새로운 꼭짓점 좌표로 변환하는 것을 의미합니다. 변환하기 위해선 당연히 사용자가 주는 '인풋 버텍스'가 필요할 겁니다. 이러한 인풋 버텍스를 저장하는 레지스터를 특별히 attribute Register라고 부릅니다.
그러고 나서, attribute를 이용해 계산한 아웃풋이 나오게 될 텐데 이렇게 나오게 된 아웃풋 버텍스 레지스터랑 Varying Register라고 부르게 됩니다. 이때, 아웃풋 중에는 자주 쓰이는 데이터들이 있는데, 이때 gl_Position, gl_PositionSize처럼 특적 아웃풋 레지스터에 저장이 됩니다. 이런 레지스터를 Pre-defined Output Registers라고 합니다.
## Primitive Assembly & Rasterization
버텍스 좌표 변환 과정 이후에, Varying에 데이터가 저장된다고 했죠? 그다음 Primitive Assembly는 이를 결합해 삼각형으로 만들게 됩니다. Rasterization 단계에서 이 삼각형이 겹친 부분을 픽셀로 만들게 됩니다. 이 부분은 앞서 말씀드린 것처럼 Fixed Hardware 부분이라 사용자가 직접 제어할 수 없는 부분이기도 합니다. 이렇게 픽셀로 만들게 되면, 대부분의 경우에 아주 많은 Fragment가 생기게 되지요.
-참고) 이중선형 보간 (second linear interpolation)
Fixed Hardware 부분에선 재밌는(?) 일이 하나 생기는데요, 바로 이중선형 보간으로 여러 가지 값(깊이갚, 컬러 등등)의 보간이 이루어진다는 사실입니다. 컬러로 예를 들자면 삼각형 각 꼭짓점에 Red, Green, Blue가 있을 때, vertex 사이에 있는 Fragment 값들은 edge(변, 선분(?))에 있는 fragment의 값을 결정하게 됩니다. 그러고 나서 한번 더 선형보간을 진행하는데요 (second linear interpolation) 여기서는 가로(horizontal)로 비례식을 진행해 똑같이 각 fragment에 값을 대입하게 됩니다.
## Fragment Shader
자 이제 이렇게 픽셀로 변환이 되었고, 무수히 많은 값이 생겼다고 했습니다. 픽셀 하나하나는 고유한 값을 가지고 있고, 이때 처리를 하기 위해 Fragment Shader를 동작시키게 됩니다. 처음에 변환되어 들어올 때, 인풋을 Varying Register라고 부르게 됩니다. 주의할 점은, Vertex Shader의 Output인 Varying 레지스터랑은 다르다는 점에 유의해야 합니다. Vertex Shader의 Varying Register의 데이터는 꼭짓점정보밖에 없는 반면에 Fragment Shader가 인풋으로 받는 Varying은 정보가 어마무시하게 많지요.
버텍스에서 자주 쓰는 값을 저장하듯, Fragment Shader에서도 Pre_defined 레지스터에 저장되는 경우가 있습니다. gl_FragCoord와, gl_PointCoord가 있겠습니다.
이렇게 과정을 거쳐 마지막엔 프레임버퍼에 들어가고, 모니터에 송출되게 되지요.
이번 포스팅에서는 전 포스팅에 이어, 전반적인 Simplified Graphic Rendering Pipeline에 대해서 각 단계를 굵직하게 설명해보려고 했습니다. 이젠 저희가 집중해서 알아봐야 할 Fragment Shader와, Vertex Shader를 알아봐야 할 차례군요!