* always thanks to https://learnopengl.com/
5. Hello Triangle
- OpenGL에서 모든 것은 3D공간에 존재하지만 우리의 모니터는 2D기 때문에 OpenGL의 많은 작업이 3D좌표를 2D픽셀로 transform하는데 소요된다.
- 위의 역할을 하는 것이 OpenGL의 graphics pipeline이다. graphics pipeline은 크게 두 부분으로 나뉘어진다. 첫번째는 3D좌표를 2D좌표로 변환하는 것이고 두번째는 그렇게 변환된 2D좌표에 색을 입히는 것이다.
- graphics pipeline는 여러 단계를 거치게 되어있다. 이 스텝들은 각각 전 단계의 output을 필요로하며 쉽게 병렬적 수행이 가능하게 되어있다. 이러한 별령처리를 위해 그래픽카드는 수천개의 작은 processing core를 가지고 있다. 각각의 processing core들은 작은 프로그램들을 수행하게 되는데 이 small programs를 shaders라고 한다.
- 일부 shaders는 개발자가 직접 configure할 수 있다. 이때 OpenGL Shading Language(GLSL)을 사용하며 위에 설명했던 것 처럼 GPU에서 돌아가게 된다.
- 아래 그림이 pipeline의 단계들에 대한 추상적 표현이다. blue section이 개발자가 직접 configure한 shader를 넣을 수 있는 영역이다.
- 맨 처음 전달하는 3D coordinate list를 Vertex Data라고 한다. 이 좌표들이 삼각형의 vertices이다. 일단 각각의 vertex는 3D coordinates와 color value를 가지고 있다고 생각하자.
- 그다음으로 이 점들을 가지고 어떤 형태를 rendering할 것인지 OpenGL에게 hint를 주어야한다. 단순히 점을 그릴 것인지? 삼각형을 그릴 것인지? 아니면 직선을 그릴 것인지? 와 같이 미리 알려주는 내용을 primitives라고 한다.
- pipeline의 첫번째 부분은 vetex shader이다 vertex shader는 single vertex를 input으로 받는다. vertex shader는 3D coordinate을 다른 종류의 3D coordinate으로 바꿔준다. vertex attribute의 기본적인 processing을 수행한다.
- primitive assembly 단계는 vertext shader로부터 모든 vertices좌표를 input으로 받고 primitive대로 vertices를 합쳐준다.
- geometry shader 단계는 이전단계에서의 output인 primitive를 구성하는 collection of vertices를 input으로 받는다. 이 단계에서 새로운 vertex를 추가해서 새로운 primitive를 만들 수 있게 해준다.
- rasterization stage는 이전단계의 output을 input으로 받는다. 여기서는 resulting primitives를 최종 screen에 pixel로 대응하는 작업이 수행된다. 이어서 fragment shader가 사용할 수 있게 fragments를 만들어낸다. fragments는 OpenGL이 single pixel을 render하기 위해 필요한 모든 데이터이다. fragment shader단계로 진입하기 전에 clipping이라는 작업이 진행되는데 이는 화면 밖으로 나간 fragments들을 버리는 작업이다. 성능 향상을 위해 수행되는 동작이다. (어차피 화면에서 안보이니까)
- fragment shader는 픽셀의 최종 색을 계산한다. 이때 3D scene의 모든 정보(그림자, 빛, 빛의 색)를 고려해서 pixel color를 칠하게 된다.
- 이렇게 모든 픽셀 color가 결정되면 alpha test and blending stage로 넘어가게 된다. 이 단계에서는 fragment의 depth value와 alpha value(투명도)를 계산하게 된다. 즉 입체감을 주는 단계이다. 따라서 fragment shader에서 계산된 pixel color가 반드시 최종 결과와 일치하는 것은 아니다.
- 위의 간략히 살펴본 단계 외에도 tessellation, transform feedback loop 등과 같은 단계들이 더 존재한다.
- moden OpenGL에서는 최소한으로 vertex와 fragment shader만큼은 직접 정의하도록 하고 있다.
5.1 Vertex input
- OpenGL은 3D graphics library이기 때문에 모든 좌표는 삼차원 좌표로 제공되어야 한다.
- 각 좌표는 x, y, z축에 대해서 -1.0 과 1.0 사이의 값이다. 이 범위의 좌표를 normalized device coordinates라고 한다. 이 범위를 visible region of OpenGL이라고 한다.
- 삼각형을 그릴 것이기 때문에 세 vertices를 정의하면 다음과 같다.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
- z 좌표를 0으로 줌으로써 triangle의 depth를 없애 2D triangle을 만들것이다.
* Normalized Device Coordinates (NDC)
- vertex shader를 거치면 모든 좌표는 normalized device coordinates가 된다.
- normalized device coordinates는 아래와 같다.
- 중심이 (0,0)이고 위쪽이 positive y-axis 오른쪽이 positive x-axis이다.
- 따라서 -1.0 부터 1.0 사이의 범위를 벗어나는 좌표들은 모두 clipped되어 screen에 보이지 않는다.
- NDC coordinates는 viewport transform(glViewport)을 통해 screen-space coordinates으로 바뀐다. 이렇게 바뀐 coordinates가 fragment shader를 거쳐 fragments로 바뀌게 된다.
- vertex data를 graphics pipeline의 첫번째 step vertex shader로 보내야 한다. GPU에 vertices를 위한 메모리를 할당해야하고 OpenGL이 이 메모리를 어떻게 해석해야하는지 configure해주고 graphics card로 data를 어떻게 보낼 것인지 specify하면 최종적으로 vertex shader가 메모리의 vertices를 process한다.
- vertex buffer objects(VBO)로 이 메모리를 관리한다. 이것은 많은 수의 vertices를 gpu 메모리안에 저장할 수 있게한다. 이 버퍼의 사용으로 큰 data batch를 한번에 graphics card로 보낼 수 있고 메모리에 유지되도록 한다. 좌표를 하나씩 보낼 필요가 없는 것이다. cpu에서 data를 graphics card에게 보내는 것은 매우 느리기 때문에 가능한 한번에 많은 data를 보내야 한다.
- 그렇게 data가 graphics card 메모리에 도착하면 vertex shader가 거의 즉시 vertices에게 극도로 빠른 속도로 접근할 수 있게 된다.
- 이 버퍼는 첫 챕터에서 말했던 그 object이다. unique ID가 존재하며 glGenBuffers function으로 만들 수 있다.
unsigned int VBO;
glGenBuffers(1, &VBO);
- OpenGL은 많은 종류의 buffer objects가 있는데 그중 vertex buffer object의 type은 GL_ARRAY_BUFFER이다.
- glBindBuffer는 여러 buffer를 bind하도록 해준다.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
- currently bound buffer, VBO버퍼를 GL_ARRAY_BUFFER target과 bind시킨다.
- glBufferData function 으로 이전에 정의했던 vertex를 buffer memory로 복사한다.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
- glBufferData는 사용자가 정의한 data를 currently bound buffer로 복사하는 역할을 한다. 첫번째 인자는 data를 복사해서 가져다 두고 싶은 buffer의 타입이다. 두번째 인자는 전달하려는 data의 크기(bytes)이다. 세번째 인자는 보내려고 하는 실제 데이터의 주소이고 네번째 인자는 graphics card가 이 데이터를 어떻게 사용하는지에 대한 configuration이다.
- GL_STREAM_DRAW : data가 딱 한번 set되고 GPU에 의해 가끔 사용됨
- GL_STATIC_DRAW : data가 딱 한번 set되고 GPU에 의해 자주 사용됨
- GL_DYNAMIC_DRAW : data가 빈번하게 set되고 GPU에 의해 자주 사용됨
- 삼각형의 위치에 대한 data가 변하지 않고 자주 사용될 것이기 때문에 GL_STATIC_DRAW가 가장 알맞다.
- GL_DYNAMIC_DRAW를 사용하면 graphics card의 write 작업이 매우 빠르게 되도록 할 수 있다.
- VBO 라는 이름의 vertex buffer object로 vertex data를 graphics card memory에 저장했다면 이제 vertex와 fragment shader를 생성해야 한다.
5.2 Vertex shader
- 사용자가 직접 program할 수 있는 shader이다. rendering을 하고싶다면 최소한 vertex 와 fragment shader는 정의해야 한다.
- GLSL로 기초적인 vertex shader를 작성하고 컴파일해서 삼각형을 그리는데 사용해보자.
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
- c와 유사하다.
- 모든 shader는 시작할때 GLSL version을 명시한다. GLSL 330이 OpenGL 3.3에 매치된다.
- "in" 키워드로 vertex shader에 input vertex의 attributes를 선언한다.
- 현재로서는 position data만을 신경쓰면 되기 때문에 하나의 vertex attribute를 필요로 한다.
- GLSL은 vector 데이터 타입(크기1 ~ 크기4)을 가지고 있다
- 우리가 input으로 제공할 좌표는 3D coordinate 이므로 aPos라는 이름의 vec3 타입의 변수를 만든다.
- layout (location = 0) 는 layout을 통한 input variable의 위치인데 뒤에서 자세히 알아보자.
- graphics programming에서 위치와 방향을 잘 표현해주는 vector를 자주 사용하게 될 것이다.
- GLSL에서 vector의 최대 크기는 4이다. 각각의 value는 x, y, z, w 를 나타내고 w만 제외하고 모두 공간에서의 좌표를 나타낸다.
- w는 perspective division인데 추후 뒤에서 다루어본자. 또한 벡터에 대해서는 뒤에서 더 깊게 다루어 보자.
- vertex shader의 output을 set하기 위해 vec4인 이미 정의된 gl_Position이라는 변수에 position을 할당한다. 즉 gl_Position으로 set한 값들이 output이 된다.
- 우리가 input으로 전달한 vector의 크기가 3이므로 이를 크기가 4인 vector로 cast해줘야 한다.
- vec4 생성자에 vec3 변수를 넣어줌으로써 cast가 가능하다. 여기서 w는 1.0f로 set되는데 자세한건 뒤에서 알아보자.
- 이렇게 정의한 vertex shader는 input data에 대해 어떠한 processing도 하지 않는 가장 간단한 형태이다. 그저 데이터를 받아서 shader의 output으로 넘겨주기만 한다. 하지만 실제 응용프로그램 개발에 있어서는 input data가 normalized device coordinates 아닐 경우가 많기 때문에 가장 먼저 input data를 OpenGL의 region에 보이도록 transform해야한다.
5.3 Compiling a shader
- vertex shader에 대한 코드를 const C string에 저장하고 소스코드파일 맨 위에 둔다.
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
- OpenGL이 이 shader를 사용하기 위해 런타임에서 동적으로 컴파일 되어야 한다.
- shader object를 만들고 우리가 만든 vertex shader를 unsigned int 타입으로 선언한 변수에 저장한다. 이때 사용하는 함수가 glCreateShader이다.
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 우리가 만들고자 하는 shader의 type을 glCreateShader의 매개변수로 전달한다. 지금 vertex shader를 만들고 있기 때문에 GL_VERTEX_SHADER를 전달한다.
- shader object에 shader에 대한 source code를 붙인다. 그 다음 shader를 컴파일 한다.
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
- glShaderSource의 첫번째 인자로 컴파일 할 shader object를 받는다. 두번째 인자로는 source code로 몇개의 string을 전달할지를 알려준다. 세번째 인자는 실제 컴파일할 소스코드이다. 네번째 인자는 NULL을 전달하자.
* 컴파일이 잘 되었는지 확인하기 위해 아래 코드를 추가하자
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
- success 여부를 판단할 integer와 error message를 담을 공간 정의
- glGetShaderiv 는 컴파일이 실패했을 때 에러메세지를 glGetShaderInfoLog를 통해 알 수 있도록 한다.
if(!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
- 현재까지의 전체 코드
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// vertex shader
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow *window) {
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, 800, 600);
// vertices
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// vertex buffer object
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// vertex shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check vertex shader compilation done successfully
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// render loop
while(!glfwWindowShouldClose(window)) {
// input
processInput(window);
// rendering commands here
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// check and call events and swap the buffers
glfwPollEvents();
glfwSwapBuffers(window);
}
glfwTerminate();
return 0;
}
'ComputerScience > Computer Graphics' 카테고리의 다른 글
OpenGL - 5 Hello Triangle (3) (0) | 2021.06.30 |
---|---|
OpenGL - 5 Hello Triangle (2) (0) | 2021.06.29 |
OpenGL - 4 Hello Window (0) | 2021.06.25 |
OpenGL - 3 Creating a window (0) | 2021.06.22 |
OpenGL - 2 OpenGL (0) | 2021.06.22 |