지난 강좌
1. OpenGL 윈도우 만들기
2. 삼각형 그리기
3. 알록달록한 삼각형 그리기
4. 텍스쳐 입히기
5. 변환 - 이동, 회전, 스케일 행렬
전체 소스 코드 : https://github.com/sunduk/opengl-3d-examples
(5. 변환 강좌에서 이어지는 내용 입니다)
이동, 회전, 스케일 행렬을 하나로 만들기
앞서 x,y,z축의 회전 행렬들을 하나로 합쳤던 것 처럼 이동, 회전, 스케일 행렬도 하나의 행렬로 만들어 계산하면 연산량을 더욱 줄일 수 있습니다. 행렬의 곱셈을 이용하여 세 행렬을 하나의 행렬로 만드는 코드를 작성 해 보겠습니다.
//---------------------------------------------
// 이동 행렬
//---------------------------------------------
// 이동할 거리.
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},
};
//---------------------------------------------
// 스케일 행렬
//---------------------------------------------
// 스케일 값.
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},
};
//---------------------------------------------
// 회전 행렬
//---------------------------------------------
// 회전 값.
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);
//---------------------------------------------
// 이동, 회전, 스케일을 하나의 행렬로 만든다.
//---------------------------------------------
float trsMatrix[4][4]{};
Matrix4_Multiply3(translationMatrix, rotationMatrix, scaleMatrix, trsMatrix);
// 버텍스 좌표에 행렬 적용.
for (auto& v : mVertices)
{
float position[4] = { v.mPosition.x, v.mPosition.y, v.mPosition.z, 1.0f };
float result[4]{};
Matrix4_Vec4(trsMatrix, position, result);
v.mPosition = glm::vec3(result[0], result[1], result[2]);
}
x축으로 1.0만큼 이동, xy축으로 0.5비율만큼 축소, z축으로 45도 회전한 행렬들을 생성한 뒤 Matrix4_Multiply3함수를 통해 세 행렬을 곱하여 하나의 행렬로 만들었습니다. 이 때 행렬이 놓인 위치가 매우 중요합니다. 행렬의 곱셈은 교환법칙이 성립하지 않아 곱하는 자리가 뒤바뀌면 예상했던 결과와 다르게 나오기 때문입니다. 일반적으로 3D 게임 프로그래밍에서는 스케일(Scale) -> 회전(Rotation) -> 이동(Translation) 순서로 행렬을 적용하는 것이 직관적입니다.
왜냐하면 어떤 물체를 회전 시킬 때 보통 자기 자신을 중심으로 회전하며, 스케일을 할 때도 자신을 중심으로 확대/축소가 이루어져야 예측하기 쉽고 직관적인 결과가 나오기 때문 입니다. 그렇지 않고 이동을 먼저 해 버리면 이미 버텍스가 원점에서 벗어난 상태이므로 그 이후에 적용하는 스케일과 회전이 원하는 대로 이루어지지 않게 됩니다.
이동, 회전, 스케일을 하나의 행렬로 만들어 적용한 모습은 아래와 같습니다.

6. 셰이더 프로그래밍 기초
3D 게임 프로그래밍을 처음 배우는데 가장 난해한 부분은 셰이더라고 생각합니다. 3D 게임이나 그래픽스 분야를 제외하면 거의 사용하지 않는 기술이라서 다소 생소할 수 있기 때문이죠. 3D 환경에서는 물체의 위치, 방향, 크기를 자유롭게 변경 할 수 있고 카메라 시점도 실시간으로 바꿀 수 있습니다. 따라서 수 많은 버텍스의 위치를 시시각각 변환 해 줘야 하는 작업이 필요하고 앞서 배웠던 이동, 회전, 스케일 행렬을 통해 이것을 간략하게 구현 해 봤습니다. 그런데 우리가 만든 예제들은 모두 CPU수준에서 작동 되는 것들 이었습니다. C++언어로 프로그래밍 하여 행렬을 벡터에 곱하는 함수를 만들어 호출했던 것을 기억하실 겁니다. 하지만 행렬의 곱셈과 같은 연산은 CPU보다는 그래픽 카드에 존재 하는 GPU가 훨씬 더 잘 하는 분야 입니다. 따라서 GPU가 해석할 수 있는 형태로 프로그램을 작성한다면 더 성능 좋은 결과물을 만들어 낼 수 있을 것입니다.
아쉽게도 C++언어로 작성한 프로그램을 GPU에서 해석할 수는 없기 때문에 GPU가 해석 가능한 언어로 프로그램을 작성하는 것이 필요하고 이것을 셰이더 프로그래밍 이라고 합니다. 과거에는 프로그래머가 직접 셰이더 코드를 작성하지 않아도 미리 만들어진 함수들을 호출하여 3D 렌더링을 수행하는 것이 가능했었습니다. 이런 방식을 고정 파이프라인 이라고 부르는데 사용하기에는 쉽지만 커스터마이징은 불가능 했기 때문에 지금은 사용하지 않는 기법 입니다. 이제는 삼각형 하나만 렌더링 하려고 해도 셰이더 코드를 작성해야 하는 시대라서 셰이더 프로그래밍을 하지 않고 3D 게임을 만들 수 있는 방법은 없다고 봐야 합니다. 다행인 것은 셰이더에서 사용하는 언어가 C언어와 비슷한 문법을 갖고 있기 때문에 배우기에 그리 어렵지는 않다는 것입니다. 여기서는 OpenGL에서 사용하는 GLSL(OpenGL Shading Language)이라는 셰이더 언어를 통해 셰이더 프로그래밍을 배워보도록 하겠습니다.
셰이더 프로그램의 구조
셰이더 파일 로드
셰이더 라는 것을 이해하기 쉽게 정의 한다면 버텍스의 위치와 픽셀의 색상을 결정 하는 작업 이라고 할 수 있습니다. 셰이더에는 여러 종류가 있지만 여기서 다룰 것은 버텍스 셰이더와 프래그먼트(픽셀) 셰이더 입니다. 이 두 셰이더를 제일 많이 사용하기도 하고 3D 렌더링 파이프라인을 이해 하는데 꼭 필요한 것들이기 때문입니다. 사실 우리는 셰이더 라는 것을 이미 사용해 오고 있었습니다. 다시 처음으로 돌아가 삼각형을 그리는 코드에서 사용한 셰이더 프로그램을 확인해 보겠습니다.
void CreateDefaultShader()
{
// 버텍스 셰이더 코드.
std::string vertexCode = "#version 330 core\n"
"layout(location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(position, 1.0f);\n"
"}\n";
// 프래그먼트 셰이더 코드.
std::string fragmentCode = "#version 330 core\n"
"out vec4 Color;\n"
"void main()\n"
"{\n"
" Color = vec4(1, 0, 0, 1); \n"
"}\n";
...
}
화면에 빨간색 삼각형을 그릴 때 사용했던 셰이더 코드 입니다. 이것을 그대로 사용하면 셰이더 코드를 수정 할 때 마다 매번 프로그램을 다시 컴파일 해야 하므로 별도의 텍스트 파일로 빼서 작업할 수 있는 환경을 만들어 보겠습니다.
bool LoadShaderFromFile(std::string vertexPath, std::string fragmentPath)
{
auto LoadTextFile = [](const std::string& filePath)
{
std::ifstream shaderFile(filePath);
std::stringstream stream{};
stream << shaderFile.rdbuf();
std::string text = stream.str();
shaderFile.close();
return text;
};
std::string vertexCode{};
std::string fragmentCode{};
try
{
vertexCode = LoadTextFile(vertexPath);
fragmentCode = LoadTextFile(fragmentPath);
}
catch (std::ifstream::failure& e)
{
std::cout << "[error] shader file read error : " << e.what() << std::endl;
return false;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
// compile shaders
unsigned int vertex{};
unsigned int fragment{};
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
CheckShaderCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
CheckShaderCompileErrors(fragment, "FRAGMENT");
// shader Program
mDefaultShaderID = glCreateProgram();
glAttachShader(mDefaultShaderID, vertex);
glAttachShader(mDefaultShaderID, fragment);
glLinkProgram(mDefaultShaderID);
CheckShaderCompileErrors(mDefaultShaderID, "PROGRAM");
glDeleteShader(vertex);
glDeleteShader(fragment);
return true;
}
기존에 하드코딩 되었던 셰이더 코드를 제거 하고 외부 파일에서 읽어오도록 수정 하였습니다. 셰이더 코드는 별도의 텍스트 파일로 생성 하면 됩니다. 여기서는 각각 .vs, .fs 라는 확장자를 붙이도록 하겠습니다. 물론 .txt나 .shader등 다른 확장자를 사용하여도 무방합니다. 버텍스 셰이더와 프래그먼트 셰이더는 각각 아래와 같이 작성 해 보겠습니다.
// 버텍스 셰이더 : Color.vs
#version 330 core
layout (location = 0) in vec3 inPosition;
void main()
{
gl_Position = vec4(inPosition, 1.0f);
}
// 프래그먼트 셰이더 : Color.fs
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1, 0, 0, 1);
}
셰이더 파일을 로딩 하고 실행 해 본 모습입니다.
void CreateDefaultShader()
{
LoadShaderFromFile("../resources/shaders/Color.vs", "../resources/shaders/Color.fs");
}

전체 소스 코드 : Example07.h / cpp (https://github.com/sunduk/opengl-3d-examples)
이번 시간에는 셰이더란 무엇인지 간략하게 알아봤고 외부 텍스트 파일에서 셰이더 코드를 읽어오는 작업을 해 봤습니다. 다음번에는 버텍스 셰이더, 프래그먼트 셰이더에 대해서 좀 더 자세히 알아보고 C++코드에서 했던 행렬 계산을 셰이더 코드로 옮기는 방법에 대해 배워보도록 하겠습니다.
>>> 다음 강좌 : 7. 셰이더 프로그래밍 기초 - 버텍스, 프래그먼트 셰이더, 행렬계산
감사합니다.