그래픽 렌더링 파이프라인은 GPU에서 일어나는 실제로 화면에 그래픽이 어떻게 표시될지 계산하고 출력하는 과정이다.
렌더링 파이프라인에서 거치는 과정은 다음과 같다.
- Vertex Shader
- Rasterizer
- Fragment Shader
- Output Merger
이 중, shader가 붙어있는 두 가지는 프로그램으로 사용자가 직접 코드를 작성하여 파이프라인에 제공해야 하는 것이고, rasterizer와 output merger의 경우에는 하드웨어로 고정된 단계로 GPU에서 알아서 계산이 되어 결과를 반환하게 된다.
오늘은 각각이 어떤 역할을 하는지에 대해서 간략하게 정리를 해볼 것이다.
Vertex Shader
이름에서도 짐작할 수 있듯이 vertex, 점과 관련된 계산을 하는 단계이다.
그래픽 공간에 있는 모든 물체들은 점으로 구성되어 있으며, 우리는 이 점들에 대한 정보(위치 벡터)와 점이 이루는 면에서 어떤 순서의 점들로 구성되어 있는지 인덱스 정보들을 가진 상태로 시작하게 된다.
vertex shader에서는 이 점들을 알맞은 공간으로 변환을 시켜주는 작업들을 하게 되는데 이를 영어로 transform이라 한다.
World Transform
첫 번째로 진행하게 되는 변환은 world transform이다.
물체(object)들은 최초에 자신들만의 공간에 정의되어 있다. 이를 object space에 정의되어 있다고 하는데, object space에 정의된 물체들을 scaling, rotation, transform과 같은 변환들을 시켜 다른 object들과 같은 공간에 있도록 만드는 것을 world transform이라 한다.
이 world transform을 거치게 되면 오브젝트들은 world space에 위치하게 된다.
View Transform
물체들을 화면에 나타내기 위해서는 물체를 찍을 것이 필요한데, openGL에서는 이를 가상의 camera로 찍는다고 표현한다.
따라서 다음과 같이 그에 맞는 요소들이 필요하게 된다.
- EYE : 카메라의 위치
- AT : 카메라가 보고 있는 위치
- UP : 카메라의 위로 향하는 수직 벡터
이 세 가지 요소를 이용하여 world space에 있는 물체들은 카메라를 기준으로 하는 camera space로 옮겨주는 과정을 거친다.
중심 좌표를 EYE로 하고, 다음과 같은 방식을 통해 world space의 x, y, z축과 같이 새로운 축을 정의내린다.
이 때 조심해야되는 것이 그래픽 엔진마다 다른 좌표계를 사용한다는 것이다.
현재 글을 작성하고 있는 OpenGL의 경우에는 오른손 좌표계를 기본으로 보지만, DirectX, Unity와 같은 엔진들의 경우에는 왼손 좌표계를 기본으로 하기 때문이다.
따라서 같은 위치에 있는 물체를 camera space에 옮겨서 화면에 띄운다고 해도 다음 그림과 같이 방향이 다르게 되어 나올 수 있다.
이는 단순히 z 좌표를 반대로 해줌으로써 해결이 가능하다. 이를 z-negation이라 한다.
Camer Space에서는 외부적으로 정한 EYE, AT, UP과 같은 요소들말고, 내부적으로 정해진 요소들을 통해 또 하나의 영역이 정의가 되는데 이는 view frustum이라 하는 피라미드 모양의 영역이다.
view frustum은 space 내부의 모든 물체를 렌더링을 하게 되는 것이 비효율적이기 때문에 실제로 화면에 보일 영역을 정의하는 것으로 fovy(field of view y-axis)와 aspect(종횡비), n(near), f(far)를 통해서 정의가 된다.
위 사진과 같이 피라미드 영역 밖에 있는 부분들을 잘라내고 안에 있는 물체들만 실제로 렌더링을 진행하게 되는 것이다.
Projection Transform
하지만, 공간이 피라미드 형태로 되어있으면 어떤 면이 잘리고 어떤 선분이 잘리는지 계산하기 어렵기 때문에 이를 정육면체 형태의 공간으로 바꿔주는 작업을 거치게 되는데, 이를 projection transform이라고 한다.
projection transform을 거치게 되면 물체들은 clip space에서 정의되는데, 이는 공간 밖에 있는 물체들을 잘라내는 것을 clipping이라고 하기 때문에 이 이름을 붙인 것이다.
view frustum은 projection line이라 불리는 선들로 하나의 점에 모이게 되는데, 이 line들에 맞게 실제 크기가 다른 선분이어도 보이는 것은 같은 길이로 보이는 원근감을 projection transform을 통해 만들어낼 수 있다.
이 projection transform은 vertex에 다음과 같은 행렬을 곱해서 행해지게 된다.
이 때 주의해야할 점은, vertex shader에서 어떤 좌표계를 사용하냐에 따라 z 쪽 값의 부호가 결정되는데, 현재 설명중인 OpenGL에서는 오른손 좌표계를 사용하는데, rasterizer에서는 항상 왼손좌표계를 사용하므로 좌표계를 통일시켜야 한다.
따라서 아까 말했던 z-negation을 이 행렬에 대해 수행해주고, OpenGL의 vertex shaer에서는 z값에 -를 곱한 다음 행렬이 vertex들에 곱해진다.
여기까지가 vertex들에 대한 계산 작업을 하는 vertex shader에서의 변환 과정이고, 파이프라인에서는 최종적으로 나온 clip space에 정의된 vertex들에 대한 정보들을 rasterizer로 넘겨주게 된다.
Rasterizer
Rasterizer에서는 clip space의 vertex들을 조립하여 삼각형 형태로 만들게 되고, 이 삼각형들을 쪼개 fragment라 불리는 일종의 점들(이해가 쉽게 말하면 pixel)로 만드는 작업을 하게 된다.
그 과정에서 clipping이나 back-face culling과 같은 최적화 작업들을 거치게 된다.
Clipping
clip space에서 수행되는 것으로, 다음 그림과 같이(그림에서는 이해가 쉽도록 view frustum의 형태로 나와있다) clip space 바깥의 오브젝트들을 잘라내고 렌더링하지 않는 작업을 수행한다.
이 때, 겹치게 되는 삼각형들을 새로 제작하는 작업도 하게 된다.
Perspective Division
camera space에 있는 vertex들을 clip space로 변환해주는 projection transform을 수행하게 되면, vertex의 벡터의 마지막 요소가 1인 homogenous space가 된 상태로 계산이 완료되지 않는다.
따라서 결과 값인 -z으로 나눠주는 작업을 거치게 되는데 이를 perspective division이라 한다.
이를 수행하게 되면 z축 방향으로 멀리 있게 될수록 더 큰 수로 나누어지게 되므로 원근감이 구현된다.
perspective division을 거치고 나서야 비로소 NDC(normalized device coordinates)라 불리는 공간 안에 clip space가 들어있게 된다.
※ NDC : x, y, z의 범위가 모두 -1 ~ 1이 되는 정육면체 공간
Back-face Culling
카메라를 향해 바라보고 있지 않은, 반대로 된 polygon들을 back face라 하는데, 이 back face들을 실제로 렌더링하지 않는 것을 back-face culling이라 한다.
반대를 바라보고 있는 것은 polygon의 normal vector(수직 벡터)와 polygon에서 카메라를 향하는 벡터 간의 내적 값을 통해 판별을 하게 된다.
위 그림과 같이 내적값이 0보다 크면 카메라를 바라보고 있다는 뜻이므로 그대로 놔두고, 0보다 작으면 보이지 않으므로 정리(culling)하게 된다.
Viewport Transform
실제 screen 내에서 그래픽을 표현할 영역을 window space 혹은 screen space라 한다.(왼손좌표계 사용)
viewport는 실제 window(화면) 안에서 그래픽을 표현할 영역을 직사각형 형태로 정의하게 된다.
viewport transform 은 NDC에 있는 물체들을 이 screen space로 변환시켜주는 작업을 하게 되는데, 위 사진에서와 같이 가로, 세로 길이와 깊이(최소 z, 최대 z)에 맞게 변환이 된다.
viewport는 직육면체이므로 NDC → viewport로의 변환은 scaling과 translation을 통해 이루어지며 최종적으로는 다음과 같은 행렬이 곱해짐으로써 수행된다.
현재까지의 space transform
Scan Conversion
scan conversion은 현재 최종적으로 screen space에 옮겨진 오브젝트의 vertex들을 이용하여 fragment로 변환하는 작업을 하는 것이다.
이 때, fragment들은 vertex간의 선형 보간을 통해서 만들어지며, 각각의 fragment들의 normal(수직 벡터)과 같은 vertex가 가지고 있는 특성들도 보간을 통해서 계산된다.(좌표, 수직 벡터 등)
Fragment Shader
rasterizer까지 작업을 해서 나온 fragment에는 normal과 texture 좌표 정보가 들어있다.
fragment shader는 이 데이터들을 활용하여 각 fragment의 색상을 Lighting과 Texturing을 통해 결정한다.
Texturing
텍스처링은 오브젝트의 표면에 색상, 질감, 패턴 등을 입혀 더 사실적인 이미지를 생성하는 작업이다.
그 중 가장 쉬운 방법은 image texturing으로, 이 글에서는 이미지 텍스처링만 다루도록 하겠다.
먼저 texture는 2차원 texel(texture element) 배열로 나타내진다. 여기서 texel이란, 쉽게 말해 texture에 있는 pixel이라고 보면 된다.(pixel은 화면에 표시되는 것이기에 다른 단어 사용)
texturing에 되려면 위 그림과 같이 모델링 단계에서 polygon mesh의 각 정점에 texture 좌표가 설정이 되어있어야 한다.
Texture의 좌표를 폴리곤 메쉬의 각 정점에 대해서 설정하는 과정을 surface parameterization 또는 parameterization 이라고 하는데, 이를 진행하려면, 3D로 되어 있는 면을 2D 평면으로 펴주는 작업이 필요하다.
펴주는 작업을 마친 뒤에야 좌표를 매칭시키는 parameterization이 가능해진다고 보면 된다.
원통형 mesh처럼 2D 평면으로 잘 펴지는 것이 있는 반면, 위 그림처럼 2D로 펴기 복잡한 polygon들이 존재하게 된다.
이러한 복잡한 폴리곤들은 patch라는 단위로 나누어 각각을 따로 피는 작업을 거치게 된다.
이러한 patch를 위한 image texture를 chart라 부르고, 여러 chart들을 한 곳에 모아둔 더 큰 texture를 atlas라 한다.
2D로 펴진 폴리곤 메쉬의 정점들은 atlas 내부의 좌표와 연결된다.
텍스처링 과정을 요약하면 다음과 같다.
1) 3D 폴리곤 메쉬를 2D 평면으로 편다.(unfold)
2) 2D 평면으로 펴진 메쉬의 정점들과 image texture의 좌표를 연결한다.(parameterization)
3) image texture에 맞는 그림을 그린다.(chart / atlas 생성)
4) 삼각형 메쉬에서 정점들로 선형 보간을 통해 fragment들을 만든다.(rasterization의 scan conversion)
5) 각 fragment들에 대해 texturing 수행
Lighting
lighting 또는 illumination은 빛과 물체의 상호작용을 다루는 것이다.
라이팅 모델은 두 가지 종류로 나뉘게 되는데 local illumination과 gloabl illumination이다.
이때, local illumination에서는 phong lighting model 이라는 것을 사용하게 되고, global에서는 ray tracing이 사용된다.
이 Phong model은 4가지 항으로 나누어지게 되는데, 바로 diffuse(난반사), specular(정반사), ambient(배경 빛), emissive(자체 발광)이다.
- Diffuse : 해와 같은 directional light source에 대한 빛을 처리하는 항이다.
멀리 떨어진 빛에 대해서 처리를 하는 것이기 때문에 장면을 구성하는 모든 물체의 점에 입사하는 빛은 항상 같은 방향을 가진다고 가정한다.
따라서, 이 항에서는 방향 벡터를 통해 빛의 강도를 처리하고 빛의 RGB값과 물체의 RGB값을 곱한 값을 더해준다.
여기서 동그라미 안에 X가 들어간 연산자는 각각의 RGB값 끼리 곱해주는 연산이다. - Specular : 특정한 방향 혹은 해당 방향을 중심으로만 반사가 되는 것을 정반사라고 한다.
specular 항은 카메라가 어느 위치에 있는지가 중요한 요소가 된다.
specular 항에서는 빛의 요소를 계산하기 위해서 빛이 들어오는 방향의 반대인 light vector(l)을 이용하게 되고, l을 이용하여 반사된 벡터(reflection vector(r))와 view vector(v)라 불리는 반사된 점에서 카메라로의 벡터를 구하게 된다.
이 때, 카메라로 들어오게 되는 빛의 강도는 r · v 로 r과 v가 모두 길이가 1인 vector이므로 cosρ 과 같게 된다.
하지만 좀 더 상식적으로 빛의 세기를 조절하기 위해서 cosρ를 sh라 하는 빛의 세기를 조절하는 숫자로 제곱시켜주어 강도를 조절하게 된다.
최종적으로 나오게 되는 specular term은 다음과 같다.
여기서 m_s는 diffuse term의 m_d와 다르게 gray scale로 계산된다. 이는 물체의 색상이 아니라 광원에 영향을 받는, 물체의 표면이 얼마나 빛나는지를 정하는 term이기 때문이다. - Ambient(배경광 반사) : 직접적인 광원에서 들어오는 빛이 아닌, 물체를 통해서 반사된 빛을 계산하는 항이다.
이 빛은 특정 방향이 아닌, 모든 방향에서 들어온다고 가정하기 때문에 light vector, normal vector가 필요없다.
따라서 이 항은 다음과 같은 식으로 정리된다.
- Emissive : 물체가 스스로 발광한다 했을 때 물체가 내는 빛을 정의하는 항으로, m_e 라는 항으로 정의가 된다.
이 때, emissive만 light로 표현하고 나머지는 reflection으로 표현하게 된다.
phong lighting model은 위 4가지 항을 더해 오브젝트의 lighting을 표현하고 fragment의 최종 색상을 보여준다.
Output Merger
Output Merger에서는 fragment shader에서 texturing과 lighting을 통해 얻은 각 fragment들의 색상 값을 통해 viewport(실제 screen에 보여질 영역)에 어떻게 보일지 결정하게 된다.
OpenGL에서는 스크린에 보여질 영역을 잠시 보관하고 있는 Buffer가 3가지가 있다.
- Color buffer : 스크린에 보여질 픽셀들을 잠시 보관하는 buffer
- Depth buffer(또는 z-buffer) : color buffer에 저장된 픽셀들의 z값을 갖고 있는 buffer
이 때, color buffer와 depth buffer의 해상도는 갖게 된다.(같은 viewport 안의 픽셀들에 대한 정보를 갖고 있으므로) - Stencil buffer : 스크린에 보여질 픽셀들의 렌더링 여부에 대한 정보를 갖고 있는 buffer
위 세 가지를 합쳐 frame buffer라 하는데, 각 frame 별 계산된 값들이 해당 buffer들에 담기게 된다.
Z-buffering
rasterizer에서 scan conversion을 진행할 때, z 좌표에 대해서도 보간을 하여 z 좌표 값도 나오게 된다.
각 픽셀들의 z 값들을 비교하여 더 작은 값의 픽셀의 색상이 화면에 최종적으로 보여지도록 결정하는 작업이 z-buffering 이다.
단순하게 파란색 삼각형이 빨간색 삼각형보다 앞선 부분에서는 빨간색이 아닌, 파란색을 보여주는 과정을 여기서 계산하게 된다.
Alpha Blending
fragment shader에서는 단순히 픽셀의 RGB값만 주는 것이 아닌, RGBAZ를 주게 된다.
여기서 A는 RGB 값의 불투명도 α 값을 의미하며, Z는 픽셀의 z 좌표 값(depth)을 의미한다.
output merger에서는 이 α 값을 이용하여 다음과 같은 수식으로 불투명도를 반영하여 최종적으로 화면에 표시될 색상을 계산하게 된다.
최종적으로 output merger에서는 fragment들의 색상 정보들과 불투명도, z좌표를 이용하여 화면에 표시될 색상을 계산하여 사용자에게 보여주게 된다.