처음부터 수학 함수들을 직접 만들면서 OpenGL을 공부하는 것도 하나의 방법이지만
많은 사람들이 그래픽 프로그래밍을 제대로 맛보기도 전에 포기하는 경우가 더 많다고 한다.
외국 포럼에서도 직접 수함 함수를 구현하는 것보다도 그냥 빠르게 결과물을 보고
동작원리를 이해하는 것이 OpenGL에는 더 도움이 된다는 의견이 압도적으로 많다.
물론 OpenGL만 공부한다고 했을 때 경우를 가정했을 때라는 전제 조건이 붙는다.
그렇지 않다고 하더라도 나쁘지 않다는 의견도 많다.
결국 동작원리만 안다면 웬만한 코드들은 구현에 시간만 조금 들뿐이니까.
그러니 우리도 GLM을 사용해서 그래픽 프로그래밍을 좀 더 재밌고 편리하게 해 보자.
GLM 이란?
GLM(OpenGL Mathematics)는 이름 그대로 OpenGL에서 사용할 수 있는 수학 라이브러리다.
CPP로 작성되어 있고 GLSL(OpenGL Shading Language)에서도 사용할 수 있다.
세상 똑똑한 천재(박사)들이 만들어서 성능까지 좋기 때문에 OpenGL을 공부할 때 빼놓을 수 없는 라이브러리다.
GLM 내려받기
GLM은 굉장히 작은 라이브러리다.
문서를 포함한 잡다구리(?)들을 제외하고 실제 사용하는 라이브러리 크기는 3MB도 되지 않는다.
이 정도면 와이파이가 잠깐 연결되는 열악한 환경에서도 내려받을 용기가 생기는 크기다.
GLM 공식 릴리즈 저장소에서 내려받을 수 있다.
▼ zip 파일을 내려받으면 된다.
GLM 설치하기
GLM을 내려받아서 압축을 풀면 여러 가지 폴더가 보이는데 glm 빼고 모두 필요 없는 것들이다.
그러니 glm 빼고 싹 다 지운다.
▼ 이전
▼ 이후
이제 0. OpenGL 개발환경 설정하기 때와 비슷한 흐름으로 진행된다.
glm-0.9.9.8을 GLM으로 변경해서 우리가 GLFW, GLEW 라이브러리를 둔 라이브러리 폴더로 복사한다.
▼ GLFW, GLEW, GLM 삼총사가 모두 모였다.
이제 비주얼 스튜디오에서 추가 포함 위치를 설정해줘야 한다.
이것도 0. OpenGL 개발환경 설정하기 와 다르지 않아서 어렵지 않다.
▼ 추가 포함 위치 설정
GLFW와 GLEW 경우와 달리 링커를 따로 설정해주지 않아도 된다.
솔직히 어떤 경우 링커를 설정해주고 안 해주는지 잘 모르겠다.
정상적으로 GLM이 포함되었는지 확인해보자.
바로 직전의 4. 삼각형 움직이기 코드에서 헤더 부분에 GLM 라이브러리의 헤더를 하나 추가해서 확인한다.
▼ 빨간 줄이 안 뜨면 정상적으로 포함된 것이다.
#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// GLM 헤더 추가
#include <glm/glm.hpp>
//---------------------------------------------
const GLint WIDTH = 720, HEIGHT = 480;
4. 삼각형 움직이기 를 GLM 버전으로 변환하기
이제 이번 수정에 필요한 GLM 헤더들을 포함시켜 보자.
#include <GL/glew.h>
#include <GLFW/glfw3.h>
GLFW와 GLEW랑 삼총사이니 이 둘 바로 아래에 포함시키는 게 나중에 관리하기에도 편할 것이다.
▼ 필요한 헤더 파일 추가
#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// 필요 GLM 헤더 추가
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//---------------------------------------------
const GLint WIDTH = 720, HEIGHT = 480;
기본적으로 GLM은 행렬(Matrix) 연산으로 작동한다.
각각의 헤더가 어떤 기능을 추가해주는지 간단하게 알아보자.
glm/glm.hpp는 GLM의 가장 기본이 되는 헤더로
GLM에서 사용할 수 있는 행렬을 비롯한 여러 자료형들을 가지고 있다.
glm/gtc/matrix_transform.hpp는 행렬 변환에 사용되는 수학 함수들을 가지고 있다.
우리는 삼각형을 좌우로 움직이는 것뿐이지만 이것도 행렬 연산으로 작동하기 때문에 이 헤더 파일을 포함한다.
glm/gtc/type_ptr.hpp는 GLM 자료형들의 메모리와 관련된 기능을 가지고 있다.
이제 정말 몇 줄의 코드를 변경해서 GLM을 사용하여 똑같은 결과를 내보자.
먼저 우리가 사용하던 uniformXMove를 uniformModel로 바꾼다.
▼ uniformXMove를 uniformModel로 바꿔준다.
//---------------------------------------------
// uniformXMove -> uniformModel
GLuint VAO, VBO, shader, uniformModel;
//---------------------------------------------
변수형이 GLuint로 똑같지만 굳이 바꾸는 이유는 좀 더 직관적으로 이해하기 위해서다.
GLM에서는 모델 좌표계에 해당하는 변수에 Model이라고 붙이는 걸 권장한다.
모델 좌표계는 하나의 메쉬(면, 모델)가 각각의 좌표를 가진다고 가정하는 좌표계다.
우리가 그린 삼각형도 하나의 모델이기 때문에 모델 좌표계를 사용한다.
▼ 모델 좌표에 관해서는 아래의 사이트에서 자세히 설명하고 있다.
learnopengl.com/Getting-started/Coordinate-Systems
아무튼 uniformModel로 변수명을 바꿈으로써 모델 좌표계 변수라는 것을 알 수 있게 되었다.
이제 정점 쉐이더를 수정한다.
앞서서 작성했던 uniform float xMove; 은 단순한 float 형이기 때문에 GLM과 연산할 수 없다.
이걸 mat4(Matrix4x4)로 바로 위에서 선언한 uniformModel과 연동할 변수로 바꿔준다.
▼ GLM에서 제공하는 mat4로 변경한다.
// 정점 쉐이더
static const char* vShader = R"(
#version 330
layout (location = 0) in vec3 pos;
//---------------------------------------------
// model로 수정
uniform mat4 model;
//---------------------------------------------
void main()
{
//---------------------------------------------
// model을 곱해준다.
gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0);
//---------------------------------------------
})";
추가적으로 gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0); 로 변경되어 있음을 알 수 있다.
객체의 위치 값에 직접적으로 더하는 게 아니라 이동 행렬을 곱해서 이동하게 하는 것이다.
쉐이더가 수정되었기 때문에 CompileShader() 함수도 수정해줘야 한다.
uniformXMove = glGetUniformLocation(shader, "xMove"); 에서 두 변수명이 모두 변경되었기 때문에
우리가 수정한 uniformModel과 model로 변경해주기만 하면 된다.
▼ CompileShader에서 uniformModel과 쉐이더의 model을 연결한다.
void CompileShader()
{
shader = glCreateProgram();
if (shader == NULL)
{
printf("Error Creating Shader Program!\n");
return;
}
AddShader(shader, vShader, GL_VERTEX_SHADER);
AddShader(shader, fShader, GL_FRAGMENT_SHADER);
GLint result = 0;
GLchar eLog[1024] = { 0 };
// 쉐이더 프로그램 연결
glLinkProgram(shader);
glGetProgramiv(shader, GL_LINK_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
printf("Error Linking Program: '%s'\n", eLog);
return;
}
// 쉐이더 프로그램 검증
glValidateProgram(shader);
glGetProgramiv(shader, GL_VALIDATE_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
printf("Error Validating Program: '%s'\n", eLog);
return;
}
//---------------------------------------------
// unifomModel과 쉐이더의 model을 연결한다.
uniformModel = glGetUniformLocation(shader, "model");
//---------------------------------------------
}
마지막으로 GLFW 순환문에서 값을 넣어주는 과정만 해주면 된다.
이제 float형으로 바로 더하는 방식이 아니기 때문에glUniform1f(uniformXMove, triOffset); 은 의미가 없다.
행렬 연산을 통해서 삼각형이 움직이기 때문에 별도의 행렬을 저장할 model을 선언해서 저장한다.
▼ triOffset이 y에도 추가로 들어있다.
// 연두색 화면 그리기
glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
// OpenGL 배경색상 초기화
glClear(GL_COLOR_BUFFER_BIT);
// Shader 적용
glUseProgram(shader);
//---------------------------------------------
// mat4 model 초기화
glm::mat4 model = glm::mat4(1.0f);
// 우리가 원하는 값만큼 행렬 연산
model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f));
// Mat4를 uniformModel로 변환한다.
glUniformMatrix4fv(uniformModel,1, GL_FALSE,glm::value_ptr(model));
//---------------------------------------------
// VBO에 있는 데이터 바인딩
glBindVertexArray(VBO);
// 데이터를 바탕으로 그리기
glDrawArrays(GL_TRIANGLES, 0, 3);
// 데이터 바인딩 해제
glBindVertexArray(0);
// Shader 해제
glUseProgram(0);
// GLFW 더블 버퍼링
glfwSwapBuffers(mainWindow);
간단히 요약하면 우리가 움직인 값의 행렬을 만들어서 기존의 삼각형 위치 행렬과 연산한다.
정점 쉐이더에서 gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0); 를 생각해보자.
▼ 실행화면
삼각형이 오른쪽 위에서 왼쪽 아래 대각선으로 움직이고 있다.
위 코드의 model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f)); 에서
y축도 같이 움직이도록 수정을 했기 때문이다.
이게 행렬을 사용하는 장점 중 하나다.
이전의 방법이었다면 우리는 y축을 움직이기 위해서 별도의 uniform을 할당해줘야 했을 것이다.
하지만 이제 변동 사항은 이 glm::translate()를 통해서 수정하고 model만 던져 주면 된다.
▼ 전체 코드
#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// 필요 GLM 헤더 추가
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//---------------------------------------------
const GLint WIDTH = 720, HEIGHT = 480;
//---------------------------------------------
// uniformXMove -> uniformModel
GLuint VAO, VBO, shader, uniformModel;
//---------------------------------------------
// 방향값(왼쪽, 오른쪽)
bool direction = true;
// 삼각형의 차이값
float triOffset = 0.0f;
// 삼각형의 최대 차이값
float triMaxOffset = 1.0f;
// 삼각형의 변화값
float triIncrement = 0.01f;
// 정점 쉐이더
static const char* vShader = R"(
#version 330
layout (location = 0) in vec3 pos;
//---------------------------------------------
// model로 수정
uniform mat4 model;
//---------------------------------------------
void main()
{
//---------------------------------------------
// model을 곱해준다.
gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0);
//---------------------------------------------
})";
// 조각 쉐이더
static const char* fShader = R"(
#version 330
out vec4 colour;
void main()
{
colour = vec4(1.0, 1.0, 1.0, 1.0);
})";
void CreateTriangle()
{
GLfloat vertices[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f
};
// OpenGL 정점 배열 생성기를 사용해서 VAO를 생성
glGenVertexArrays(1, &VAO);
// 우리가 생성한 VAO를 현재 수정 가능하도록 연결한다.
glBindVertexArray(VAO);
// OpenGL 정점 배열 생성기를 사용해서 VBO를 생성
glGenBuffers(1, &VBO);
// 우리가 생성한 VBO를 현재 수정 가능하도록 연결한다.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 우리가 만든 삼각형 정점 좌표를 VBO에 저장한다.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// VAO에 이 VAO를 어떻게 해석해야 할 지 알려줍니다.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
// VAO 사용 허용
glEnableVertexAttribArray(0);
// VBO 수정 종료 및 연결 초기화
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 수정이 완료 되면 연결을 끊기 위해 초기값으로 연결한다.
glBindVertexArray(0);
}
void AddShader(GLuint theProgram, const char* shaderCode, GLenum shaderType)
{
// 쉐이더 생성
GLuint theShader = glCreateShader(shaderType);
// 쉐이더 코드를 저장할 배열 생성
const GLchar* theCode[1];
theCode[0] = shaderCode;
// 쉐이더 코드 길이를 저장할 배열 생성
GLint codeLength[1];
codeLength[0] = strlen(shaderCode);
// 쉐이더에 우리가 작성한 쉐이더 코드를 저장한다.
glShaderSource(theShader, 1, theCode, codeLength);
// 쉐이더 컴파일
glCompileShader(theShader);
// 에러 검출을 위한 변수 선언
GLint result = 0;
GLchar eLog[1024] = { 0 };
// 쉐이더 컴파일 정상완료 여부 저장
glGetShaderiv(theShader, GL_COMPILE_STATUS, &result);
if (!result)
{
// 쉐이더 오류 로그를 저장하고 출력합니다.
glGetShaderInfoLog(theShader, sizeof(eLog), NULL, eLog);
printf("Error Compiling the %d shader: '%s'\n", shaderType, eLog);
return;
}
// 쉐이더 프로그램에 쉐이더를 등록합니다.
glAttachShader(theProgram, theShader);
}
void CompileShader()
{
shader = glCreateProgram();
if (shader == NULL)
{
printf("Error Creating Shader Program!\n");
return;
}
AddShader(shader, vShader, GL_VERTEX_SHADER);
AddShader(shader, fShader, GL_FRAGMENT_SHADER);
GLint result = 0;
GLchar eLog[1024] = { 0 };
// 쉐이더 프로그램 연결
glLinkProgram(shader);
glGetProgramiv(shader, GL_LINK_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
printf("Error Linking Program: '%s'\n", eLog);
return;
}
// 쉐이더 프로그램 검증
glValidateProgram(shader);
glGetProgramiv(shader, GL_VALIDATE_STATUS, &result);
if (!result)
{
glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
printf("Error Validating Program: '%s'\n", eLog);
return;
}
//---------------------------------------------
// unifomModel과 쉐이더의 model을 연결한다.
uniformModel = glGetUniformLocation(shader, "model");
//---------------------------------------------
}
int main()
{
// 로케일 국가 한국 지정
_wsetlocale(LC_ALL, L"Korean");
// GLFW 초기화
if (glfwInit() == GLFW_FALSE)
{
wprintf(L"GLFW 초기화 실패\n");
glfwTerminate();
return (1);
}
// OpenGL 버전 지정
// OpenGL MAJOR.MINOR 방식으로 표현
// 이번엔 3.3을 사용한다.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// OpenGL 코어 프로필 설정
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// OpenGL 상위호환 활성화
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
// GLFW 윈도우 생성
GLFWwindow* mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL TRIANGLE", NULL, NULL);
if (mainWindow == NULL)
{
wprintf(L"GLFW 윈도우 생성이 실패했습니다.\n");
glfwTerminate();
return (1);
}
// 버퍼 가로, 버퍼 세로 선언
int bufferWidth, bufferHeight;
// mainWindow로부터 버퍼 가로 크기와 버퍼 세로 크기를 받아온다.
glfwGetFramebufferSize(mainWindow, &bufferWidth, &bufferHeight);
glfwMakeContextCurrent(mainWindow);
// GLEW의 모든 기능 활성화
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
wprintf(L"GLEW 초기화가 실패했습니다.\n");
// mainWindow 삭제
glfwDestroyWindow(mainWindow);
glfwTerminate();
return (1);
}
CreateTriangle();
CompileShader();
// OpenGL Viewport 생성
glViewport(0, 0, bufferWidth, bufferHeight);
// GLFW가 종료되지 않는 한 계속 도는 순환문
while (glfwWindowShouldClose(mainWindow) == GLFW_FALSE)
{
// GLFW 이벤트 입력
glfwPollEvents();
// 방향이 오른쪽인지 왼쪽인지
if (direction == true)
{
triOffset += triIncrement;
}
else
{
triOffset -= triIncrement;
}
// 최대 차이값을 넘기게 되면 방향 전환
if (abs(triOffset) >= triMaxOffset)
{
direction = !direction;
}
// 연두색 화면 그리기
glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
// OpenGL 배경색상 초기화
glClear(GL_COLOR_BUFFER_BIT);
// Shader 적용
glUseProgram(shader);
//---------------------------------------------
// mat4 model 초기화
glm::mat4 model = glm::mat4(1.0f);
// 우리가 원하는 값만큼 행렬 연산
model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f));
// Mat4를 uniformModel로 변환한다.
glUniformMatrix4fv(uniformModel,1, GL_FALSE,glm::value_ptr(model));
//---------------------------------------------
// VBO에 있는 데이터 바인딩
glBindVertexArray(VBO);
// 데이터를 바탕으로 그리기
glDrawArrays(GL_TRIANGLES, 0, 3);
// 데이터 바인딩 해제
glBindVertexArray(0);
// Shader 해제
glUseProgram(0);
// GLFW 더블 버퍼링
glfwSwapBuffers(mainWindow);
}
return (0);
}
'<LIBRARY> > OPENGL' 카테고리의 다른 글
6. 삼각형 회전 시키기 (0) | 2021.01.25 |
---|---|
4. 삼각형 움직이기 (0) | 2021.01.17 |
3. 삼각형 그리기(2) (0) | 2021.01.17 |
2. 삼각형 그리기 (1) (0) | 2021.01.14 |
1. 기본 코드 작성하기 (0) | 2021.01.11 |
댓글