지난 강좌
1. OpenGL 윈도우 만들기
2. 삼각형 그리기
3. 알록달록한 삼각형 그리기
4. 텍스쳐 입히기
5. 변환 - 이동, 회전, 스케일 행렬
6. 셰이더 프로그래밍 기초 - 기반코드 작성
전체 소스 코드 : https://github.com/sunduk/opengl-3d-examples
버텍스 셰이더
3D 환경에서 각 버텍스의 위치를 계산 하는 역할을 하는 것이 버텍스 셰이더 입니다. 우리가 배우는 GLSL은 C언어와 비슷한 문법을 사용하고 있기 때문에 작성하는데 큰 걸림돌은 없으실 겁니다. 셰이더의 첫 발을 딛는 시점이므로 입력된 값을 그대로 리턴하는 아주 간단한 버텍스 셰이더 프로그램을 작성 해 보겠습니다.
#version 330 core
layout (location = 0) in vec3 inPosition;
void main()
{
gl_Position = vec4(inPosition, 1.0f);
}
C언어에도 main함수가 프로그램의 시작 지점인 것처럼 버텍스 셰이더 역시 main함수부터 시작 됩니다. 입력받은 position변수를 vec4로 변환하여 gl_Position에 대입해 주는 코드 입니다. 프로그램에서 버텍스 좌표를 설정할 때는 x,y,z축을 갖고 있는 3차원 벡터로 표현 하지만 셰이더에서는 여러가지 연산을 위해서 4차원 벡터로 변환하여 계산 합니다. 앞서 이동, 회전, 스케일 행렬을 계산할 때도 버텍스의 마지막 요소에 1을 추가해서 4개의 요소를 갖는 벡터로 사용했던 것을 기억하실 겁니다. gl_Position은 OpenGL에 내장된 변수명이므로 이름을 바꾸면 제대로 작동하지 않게 되므로 코딩에 주의해야 합니다. 이 과정이 모든 버텍스에 대해서 수행되면 버텍스 셰이더의 처리가 완료되고 다음 렌더링 파이프라인 과정을 지나 프래그먼트 셰이더의 실행으로 넘어갑니다. 이 과정을 간략하게 그려보면 다음과 같습니다.

프래그먼트 셰이더
버텍스 셰이더를 거치고 나면 화면에 출력될 버텍스들의 위치가 결정 됩니다. 우리가 작성한 버텍스 셰이더 코드에서는 아무런 변환 없이 그대로 gl_Position변수로 넘겼으므로 최종 좌표 역시 우리가 입력한 상태를 유지하게 됩니다. 이 버텍스들은 세 개씩 모여 삼각형을 이루게 되고 이 삼각형 내부에 어떤 색상을 칠할지 결정하는 것이 프래그먼트 셰이더의 역할입니다.
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1, 0, 0, 1);
}
FragColor에 빨간색을 입력한 코드 입니다. 색상값은 4개의 요소를 갖고 있는 벡터 형태이며 앞에서 부터 Red, Green, Blue, Alpha값을 의미 합니다. 각각 0 ~ 1 까지의 범위를 갖고 있고 완전 불투명한 빨간색으로 칠하기 위해 1, 0, 0, 1 로 입력해 준 것입니다. 이 과정이 삼각형 내의 모든 픽셀들에 대해서 수행되고 여기서 결정된 색상값으로 화면에 렌더링 됩니다. 아래 그림은 C++코드에서 버텍스 셰이더, 프래그먼트 셰이더 까지 거치는 과정을 간략하게 나타낸 모습입니다.

행렬 계산
앞서 이동, 회전, 스케일 행렬 연산 코드를 구현할 때 모든 과정을 CPU에서 수행하도록 했습니다. 행렬 연산은 CPU보다 GPU가 더 잘 한다고 했으니 이제 이것을 버텍스 셰이더로 옮겨 구현 해 보도록 하겠습니다. 앞에서 사용 했던 예제 코드를 다시 가져왔습니다. 이전에는 2차원 배열을 이용해서 행렬을 직접 구성하였는데 이제부터는 glm이라는 수학 라이브러리의 도움을 받아 좀 더 편하게 만들어 사용할 것입니다.
#include "glm/glm.hpp"
...
// 이동할 거리.
float dx = 1.0f;
float dy = 0.0f;
float dz = 0.0f;
// 기존 코드.
/*float translationMatrix[4][4] =
{
{1, 0, 0, dx},
{0, 1, 0, dy},
{0, 0, 1, dz},
{0, 0, 0, 1},
};*/
// 새로운 코드.
glm::vec3 position(dx, dy, dz);
glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), position);
행렬 함수를 사용하기 위해 glm.hpp 헤더파일을 추가 해 줍니다. 이동 행렬은 glm::translate함수로 생성 합니다. 첫 번째 인자로 단위 행렬을 넣고 두 번째 인자에 이동하려는 값을 갖고 있는 벡터를 넣어 줍니다. 단위 행렬은 숫자 1과 같이 어떤 행렬을 곱해도 자기 자신이 나오는 행렬을 말합니다. 행렬을 초기화 할 때 주로 사용하는 행렬 입니다. 이렇게 생성된 이동 행렬은 glm::mat4 라는 자료형으로 받을 수 있습니다.
// 스케일 값.
float scaleX = 0.5f;
float scaleY = 0.5f;
float scaleZ = 1.0f;
// 기존 코드.
//float scaleMatrix[4][4] =
//{
// {scaleX, 0, 0, 0},
// {0, scaleY, 0, 0},
// {0, 0, scaleZ, 0},
// {0, 0, 0, 1},
//};
// 새로운 코드.
glm::vec3 scale(scaleX, scaleY, scaleZ);
glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale);
스케일 행렬을 생성 하는 코드는 glm::scale함수를 사용 합니다. 마찬가지로 첫 번째 인자에는 단위 행렬을, 두 번째 인자에 스케일 값을 나타내는 벡터를 입력 합니다.
// 회전 값.
float rotationX = glm::radians(0.0f);
float rotationY = glm::radians(0.0f);
float rotationZ = glm::radians(45.0f);
// 기존 코드.
/*
// 각 축 별로 cos, sin값을 구한다.
float cosX = glm::cos(rotationX);
float sinX = glm::sin(rotationX);
float cosY = glm::cos(rotationY);
float sinY = glm::sin(rotationY);
float cosZ = glm::cos(rotationZ);
float sinZ = glm::sin(rotationZ);
// 회전 행렬.
float rotationMatrixX[4][4] =
{
{1, 0, 0, 0},
{0, cosX, -sinX, 0},
{0, sinX, cosX, 0},
{0, 0, 0, 1},
};
float rotationMatrixY[4][4] =
{
{cosY, 0, sinY, 0},
{ 0, 1, 0, 0},
{-sinY, 0, cosY, 0},
{ 0, 0, 0, 1},
};
float rotationMatrixZ[4][4] =
{
{cosZ, -sinZ, 0, 0},
{sinZ, cosZ, 0, 0},
{ 0, 0, 1, 0},
{ 0, 0, 0, 1},
};
float rotationMatrix[4][4]{};
Matrix4_Multiply3(rotationMatrixY, rotationMatrixX, rotationMatrixZ, rotationMatrix);
*/
// 새로운 코드.
glm::mat4 rotationMatrixX = glm::rotate(glm::mat4(1.0f), rotationX, glm::vec3(1.0f, 0.0f, 0.0f));
glm::mat4 rotationMatrixY = glm::rotate(glm::mat4(1.0f), rotationY, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 rotationMatrixZ = glm::rotate(glm::mat4(1.0f), rotationZ, glm::vec3(0.0f, 0.0f, 1.0f));
glm::mat4 rotationMatrix = rotationMatrixY * rotationMatrixX * rotationMatrixZ;
회전 행렬은 glm::rotate함수로 생성 합니다. 기존 코드에 비해 굉장히 많이 간결해 진 것을 볼 수 있습니다. 다른 함수와 마찬가지로 첫 번째 인자에 단위 행렬을, 두 번째 인자에 라디안으로 표현된 회전 값을 입력 하고, 마지막 인자에 회전 축을 나타내는 단위 벡터를 지정 합니다.
//---------------------------------------------
// 이동, 회전, 스케일을 하나의 행렬로 만든다.
//---------------------------------------------
// 기존 코드.
/*float trsMatrix[4][4]{};
Matrix4_Multiply3(translationMatrix, rotationMatrix, scaleMatrix, trsMatrix);*/
// 새로운 코드.
mTRSMatrix = translationMatrix * rotationMatrix * scaleMatrix;
마지막으로 세 행렬을 모두 곱해 하나로 만들어 줍니다. 단순히 곱셈 연산자로 행렬을 곱할 수 있기 때문에 별도의 함수로 구현할 필요도 없어졌습니다. 이제 이렇게 만든 행렬을 버텍스 셰이더로 넘겨 줘야 합니다. glUniformMatrix4fv함수를 사용하면 C++프로그램에서 버텍스 셰이더로 행렬을 전달 해 줄 수 있습니다.
void Render()
{
glUseProgram(mDefaultShaderID);
glUniformMatrix4fv(glGetUniformLocation(mDefaultShaderID, "TRSMatrix"), 1, GL_FALSE, &mTRSMatrix[0][0]);
...
}
버텍스 셰이더에서 이 행렬값을 받아 행렬과 버텍스 포지션을 곱하는 연산을 구현 해 보겠습니다.
#version 330 core
layout (location = 0) in vec3 inPosition;
// 추가.
uniform mat4 TRSMatrix;
void main()
{
// 기존 코드.
//gl_Position = vec4(inPosition, 1.0f);
// 새로운 코드.
gl_Position = TRSMatrix * vec4(inPosition, 1.0f);
}
TRSMatrix라는 변수명으로 4x4행렬을 선언 합니다. 이 값은 앞서 프로그램 코드에서 전달 해 준 값입니다. 이 행렬과 버텍스 포지션을 곱해서 gl_Position에 대입 해 줍니다. 이 과정은 GPU에서 병렬로 처리되기 때문에 CPU상에서 모든 버텍스들을 루프 도는 것 보다 훨씬 더 빠른 속도로 처리 할 수 있습니다.

지금은 프래그먼트 셰이더에서 무조건 빨간색으로만 칠하도록 했기 때문에 텍스쳐 대신 빨간색으로 나올겁니다. 앞서 만들었던 예제 처럼 텍스쳐가 나오도록 바꿔보겠습니다. 이 작업은 버텍스 셰이더와 프래그먼트 셰이더가 서로 협력해서 이루어 집니다. 버텍스 셰이더에서 각 버텍스에 해당 하는 텍스쳐의 UV좌표를 프래그먼트 셰이더로 넘겨 주면 프래그먼트 셰이더에서는 UV좌표에 따라 텍스쳐의 색상을 읽어 렌더링 해주는 형태로 구성 됩니다. 버텍스 셰이더와 프래그먼트 셰이더의 최종 코드는 다음과 같습니다.
// 버텍스 셰이더 코드.
#version 330 core
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inVertexColor;
layout(location = 2) in vec2 inTextureUV;
uniform mat4 TRSMatrix;
out vec2 TextureUV;
void main()
{
gl_Position = TRSMatrix * vec4(inPosition, 1.0f);
TextureUV = inTextureUV;
}
(inTextureUV 변수를 설정하는 부분이 잘 기억이 안나신다면 텍스쳐 입히기 강좌를 참고 해 주시기 바랍니다)
// 프래그먼트 셰이더 코드.
#version 330 core
in vec2 TextureUV;
out vec4 FragColor;
uniform sampler2D sample0;
void main()
{
// 기존 코드.
//FragColor = vec4(1, 0, 0, 1);
// 새로운 코드.
FragColor = texture(sample0, TextureUV);
}
프래그먼트 셰이더에서는 버텍스 셰이더에서 넘겨준 TextureUV변수를 받아 텍스쳐에서 색상을 읽어오는 작업을 합니다. 새로운 코드에 texture라는 함수가 추가 되었는데 이 함수가 sample0이 가리키는 텍스쳐에서 TextureUV에 해당하는 색상값을 가져다 주는 함수 입니다. 이렇게 하고 실행 해 보면 아래와 같이 텍스쳐가 입혀진 모습에 행렬 연산이 적용되어 렌더링 되는 것을 볼 수 있습니다.

전체 소스 코드 : Example07.h / cpp (https://github.com/sunduk/opengl-3d-examples)
지금 까지 버텍스 셰이더와 프래그먼트 셰이더의 작동 방식에 대해 아주 간략하게 배워 봤습니다. 사실 셰이더 파트만 따로 빼서 집중적으로 학습하는 것이 더 좋을 수 있습니다. 하지만 일단 화면에 뭐라도 렌더링 하려면 최소한 이정도는 알아둬야 해서 미리 맛보기만 해본 것입니다. 다음번에는 3D환경에서 공간 변환을 통해 가상의 3D물체가 어떻게 2D윈도우 화면에 렌더링 될 수 있는지 알아보겠습니다.
>>> 다음 강좌 : 8. 공간의 개념
감사합니다.