개인 블로그에서 21.05.18에 작성된 글입니다. 참고 바랍니다.
https://kyoungwhankim.github.io/ko/blog/opengl_triangle3/
.cpp 파일들을 작성하여 프로그램을 만들고자 한다면 이 소스 파일들을 컴파일, 링크 등의 절차를 거쳐 하나의 실행 파일로 만들어야합니다. 셰이더도 마찬가지입니다. 작성한 shader.vert와 shader.frag를 컴파일하여 하나의 실행 가능한 Shader Program으로 만들어봅시다.
우선, 셰이더 파일들을 읽어올 함수가 필요합니다. 파일을 읽어와 스트링으로 저장하기 위해 string, fstream을 include 해주시고 에러 메세지 출력을 위해서 iostream 혹은 stdio.h도 include 해주세요.
그리고 셰이더 프로그램을 구성하는 데에 필요한 함수들이 필요합니다. 이 함수들은 셰이더 파일들을 컴파일하고 링크할 것입니다.
GLuint shader;
std::string ReadFile(const char* filePath)
{
std::string content;
std::ifstream fileStream(filePath, std::ios::in);
while (!fileStream.is_open())
{
printf("Failed to read %s file! The file doesn't exist.\n", filePath);
return "";
}
std::string line = "";
while (!fileStream.eof())
{
std::getline(fileStream, line);
content.append(line + "\n");
}
fileStream.close();
return content;
}
GLuint AddShader(const char* shaderCode, GLenum shaderType)
{
GLuint new_shader = glCreateShader(shaderType);
const GLchar* code[1];
code[0] = shaderCode;
glShaderSource(new_shader, 1, code, NULL);
GLint result = 0;
GLchar err_log[1024] = { 0 };
glCompileShader(new_shader);
glGetShaderiv(new_shader, GL_COMPILE_STATUS, &result);
if (!result)
{
glGetShaderInfoLog(new_shader, sizeof(err_log), NULL, err_log);
printf("Error compiling the %d shader: '%s'\n", shaderType, err_log);
return 0;
}
return new_shader;
}
void CompileShader(const char* vsCode, const char* fsCode)
{
GLuint vs, fs;
shader = glCreateProgram();
if (!shader)
{
printf("Error: Cannot create shader program.");
return;
}
vs = AddShader(vsCode, GL_VERTEX_SHADER);
fs = AddShader(fsCode, GL_FRAGMENT_SHADER);
glAttachShader(shader, vs); // Attach shaders to the program for linking process.
glAttachShader(shader, fs);
GLint result = 0;
GLchar err_log[1024] = { 0 };
glLinkProgram(shader); // Create executables from shader codes to run on corresponding processors.
glGetProgramiv(shader, GL_LINK_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(err_log), NULL, err_log);
printf("Error linking program: '%s'\n", err_log);
return;
}
}
void CreateShaderProgramFromFiles(const char* vsPath, const char* fsPath)
{
std::string vsFile = ReadFile(vsPath);
std::string fsFile = ReadFile(fsPath);
const char* vsCode = vsFile.c_str();
const char* fsCode = fsFile.c_str();
CompileShader(vsCode, fsCode);
}
GLuint shader;
셰이더 프로그램은 ID 번호를 갖게 됩니다. ID 번호를 저장할 GLuint 변수를 전역 변수로 선언합니다.
ReadFile 함수
셰이더 파일을 읽어오는 함수입니다. 인자로 읽어올 파일의 경로를 받습니다. 파일 관련 함수들은 여기서 개별적으로 설명하지 않겠습니다. 참고 글을 읽어보시면 도움이 될 것입니다.
CreateShaderProgramFromFiles 함수
ReadFile로 파일을 읽어와 컴파일 단계로 넘겨주는 함수입니다. 인자로 Vertax Shader의 파일 경로와 Fragment Shader의 파일 경로를 순서대로 받습니다.
CompileShader 함수
셰이더 파일들을 컴파일하고 하나의 실행 프로그램으로 만드는 함수입니다. 인자로 Vertax Shader 코드와 Fragment Shader 코드를 저장해둔 문자열을 순서대로 받습니다.
glCreateProgram(): 셰이더 프로그램을 생성하고 그 ID를 반환하는 OpenGL 함수입니다.
AddShader(): 셰이더 프로그램에 연결할 셰이더 파일의 ID를 생성하고 셰이더 파일을 컴파일할 함수입니다.
glAttachShader(): 링크 과정을 위해 작성한 셰이더를 프로그램에 연결해주는 OpenGL 함수입니다.
glLinkProgram(): 셰이더 파일들을 링크하고 GPU에서 실행할 실행 파일을 만드는 OpenGL 함수입니다.
glGetProgramiv(): 셰이더 프로그램의 상태를 마지막 인자인 포인터 변수를 통해 반환하는 함수입니다. 여기선 링크가 잘 됐는지 확인하기 위해서 GL_LINK_STATUS 정보를 반환하도록 하였으며 링크가 잘 되었으면 GL_TRUE를, 실패 시 GL_FALSE를 반환합니다.
AddShader 함수
셰이더 파일을 컴파일하고 ID를 반환하는 함수입니다. 인자로 셰이더 코드와 GLenum 자료형의 셰이더 타입을 받습니다. 셰이더 타입은 GL_VERTAX_SHADER, GL_FRAGMENT_SHADER 등이 있습니다.
glCreateShader(): 셰이더 프로그램과 연결할 셰이더를 생성하며 그 ID를 반환합니다.
glShaderSource(): 직접 작성한 셰이더 코드를 glCreateShader를 통해 만든 셰이더에 연결합니다.
glCompileShader(): 셰이더 코드를 컴파일합니다.
glGetShaderiv(): 셰이더 코드의 상태를 마지막 인자인 포인터 변수를 통해 반환하는 함수입니다. 여기선 컴파일이 잘 됐는지 확인하기 위해서 GL_COMPILE_STATUS 정보를 반환하도록 하였으며 컴파일이 잘 되었으면 GL_TRUE, 실패 시 GL_FALSE를 반환합니다.
자, 이제 main 함수에서 CreateShaderProgramFromFiles 함수를 호출하고, while문 안의 glUseProgram 함수에 만든 셰이더 프로그램의 ID를 인자로 전달하면 됩니다. 아래 전체 코드로 확인해보시길 바랍니다.
(참고로 저의 셰이더 코드 파일들은 cpp 파일과 동일한 경로에 위치해있으며, 파일 경로는 cpp 파일이 위치한 곳을 루트로 상대 경로를 작성해주시면 됩니다.)
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stdlib.h>
#include <stdio.h>
#include <fstream>
#include <string>
GLuint VBO, VAO, shader;
std::string ReadFile(const char* filePath)
{
std::string content;
std::ifstream fileStream(filePath, std::ios::in);
while (!fileStream.is_open())
{
printf("Failed to read %s file! The file doesn't exist.\n", filePath);
return "";
}
std::string line = "";
while (!fileStream.eof())
{
std::getline(fileStream, line);
content.append(line + "\n");
}
fileStream.close();
return content;
}
GLuint AddShader(const char* shaderCode, GLenum shaderType)
{
GLuint new_shader = glCreateShader(shaderType);
const GLchar* code[1];
code[0] = shaderCode;
glShaderSource(new_shader, 1, code, NULL);
GLint result = 0;
GLchar err_log[1024] = { 0 };
glCompileShader(new_shader);
glGetShaderiv(new_shader, GL_COMPILE_STATUS, &result);
if (!result)
{
glGetShaderInfoLog(new_shader, sizeof(err_log), NULL, err_log);
printf("Error compiling the %d shader: '%s'\n", shaderType, err_log);
return 0;
}
return new_shader;
}
void CompileShader(const char* vsCode, const char* fsCode)
{
GLuint vs, fs;
shader = glCreateProgram();
if (!shader)
{
printf("Error: Cannot create shader program.");
return;
}
vs = AddShader(vsCode, GL_VERTEX_SHADER);
fs = AddShader(fsCode, GL_FRAGMENT_SHADER);
glAttachShader(shader, vs); // Attach shaders to the program for linking process.
glAttachShader(shader, fs);
GLint result = 0;
GLchar err_log[1024] = { 0 };
glLinkProgram(shader); // Create executables from shader codes to run on corresponding processors.
glGetProgramiv(shader, GL_LINK_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(err_log), NULL, err_log);
printf("Error linking program: '%s'\n", err_log);
return;
}
}
void CreateShaderProgramFromFiles(const char* vsPath, const char* fsPath)
{
std::string vsFile = ReadFile(vsPath);
std::string fsFile = ReadFile(fsPath);
const char* vsCode = vsFile.c_str();
const char* fsCode = fsFile.c_str();
CompileShader(vsCode, fsCode);
}
void CreateTriangle()
{
GLfloat vertices[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f
};
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
int main(void)
{
if (!glfwInit())
exit(EXIT_FAILURE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(1080, 720, "title", NULL, NULL);
if (!window)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
int framebuf_width, framebuf_height;
glfwGetFramebufferSize(window, &framebuf_width, &framebuf_height);
glViewport(0, 0, framebuf_width, framebuf_height);
if (glewInit() != GLEW_OK)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
CreateTriangle();
CreateShaderProgramFromFiles("shader.vert","shader.frag");
while (!glfwWindowShouldClose(window))
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shader);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
드디어 삼각형을 창에 출력할 수 있게 되었습니다.
이로서 OpenGL의 기본 구조를 전부 살펴보았습니다. 하지만 이제 시작입니다. 앞으로 2D 삼각형이 아닌 3D 오브젝트를 그리는 법, 이미 모델링된 매쉬를 임포트 하는 법, 텍스쳐를 적용하는 법, 카메라 이동, 빛과 그림자 표현 등등 OpenGL로 할 수 있는 것들이 너무 많습니다.
다음 글에선 3D 오브젝트를 그려내기 위해 알아야할 Indexed Drawing에 대해 알아보도록 하겠습니다.