[Learn OpenGL] Part1. Getting started 메모

haeryong·2023년 8월 14일
0

https://learnopengl.com/

1. Introduction

  • OpenGL은 platform이 아닌 Graphics API으로, 이것을 동작시킬 language가 필요하다. 교재에서는 C++을 선택.

2. OpenGL

  • OpenGL은 주로 graphics/images를 조작하는 여러 함수들을 제공하는 API로 생각된다. 하지만 OpenGL 자체로는 API가 아니라 단지 규격/사양일 뿐이다.

  • 따라서 OpenGL specification은 구체적인 구현을 제공하지 않으며, 버전에 따라 다르게 구현되어 있을 수 있다. 하지만 동일한 사양을 만족하므로 사용자 입장에서는 차이가 없다.

2.3. State machine

  • OpenGL은 그 자체로 거대한 'state machine', 즉 OpenGL이 현재 어떻게 동작해야하는지를 정의하는 변수들의 집합이다. 여러 변수들로 결정된 OpenGL의 현재 state를 context라 한다.
  • OpenGL를 사용할 때, 몇몇 옵션을 세팅해서 state를 변경하고, 버퍼들을 조작하고 현재 context를 이용해 render를 수행한다.

2.4. Objects

  • glGenObject: object 생성 및 reference를 id의 형태로 저장. 실제 object의 data는 감춰져 있다.

  • glBindObject : object를 context의 타겟(GL_WINDOW_TARGET) 위치에 bind.

  • glSetObjectOption : window option 세팅.

  • glBindObject(GL_WINDOW_TARGET, 0) : GL_WINDOW_TARGET에 bind되어있는 object를 unbind.

// create object
unsigned int objectId = 0;
glGenObject(1, &objectId);
// bind/assign object to context
glBindObject(GL_WINDOW_TARGET, objectId);
// set options of object currently bound to GL_WINDOW_TARGET
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// set context target back to default
glBindObject(GL_WINDOW_TARGET, 0);
  • 수많은 model을 그리는 경우에, 여러 object에 미리 각각의 모델을 구성해두고, 그때그때 알맞은 object를 bind해서 draw할 수 있다.

3. Creating a window

3.1. GLFW

  • GLFW : C로 작성된 library. OpenGL context를 생성하고, window parameter를 정의하고, user input를 처리한다.

3.5. GLAD

  • GLAD : 다양한 OpenGL 버전이 존재하므로, 제공하는 함수들의 위치가 컴파일 타임에는 알 수 없고, 런타임에 쿼리되어야 한다. 보통은 이것을 개발자들이 아래와 같이 함수의 위치를 얻어서 저장해야한다. 이것은 또한 OS에 따라 다르다. GLAD 라이브러리가 이 과정을 대신해준다.
// define the function’s prototype
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// find the function and assign it to a function pointer
GL_GENBUFFERS glGenBuffers =
(GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// function can now be called as normal
unsigned int buffer;
glGenBuffers(1, &buffer);

4. Hello Window

#include <glad/glad.h> // GLFW보다 먼저 와야함.
#include <GLFW/glfw3.h>
  • glViewport(x, y, w, h) : OpenGL window의 사이즈 정의. (x, y)는 window의 lower left corner 기준 위치. (w, h)는 window의 width, height.

  • OpenGL 내부에서glViewport에서 정의한 data를 이용해서 normalized 2D coordinate에서 스크린 상의 coordinate 변환을 수행한다. 예를 들어 (w=800,h=600)인 경우, (-0.5, 0.5)는 (200, 450)으로 매핑된다. 즉 (-1~1,-1~1)의 좌표가 (0~w,0~h)로 선형 매핑된다.

  • glfwSwapBuffers(window) : OpenGL에서는 double buffer를 사용해서 flickering 문제를 해결한다. front buffer에는 screen에 출력할 이미지를 담고 있고, back buffer에 rendering 함수를 이용해 그림을 그린 뒤 glfwSwapBuffers를 이용해 두 버퍼를 swap한다.

  • glfwTerminate() : 프로그램 종료 시 호출해 GLFW 관련 자원 반환.

  • glfwPollEvents() : 키보드, 마우스 이벤트 등이 발생했는 지 체크해서 state를 변경. 내가 정의한 callback method를 호출할 수 있다.

  • if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS : ESC 키를 누르고 있는 지 체크. input을 체크해서 원하는대로 state를 변경할 수 있다.

  • glClear(GL_*_BUFFER_BIT) : 이전 루프에서 그렸던 이미지를 초기화하는 코드. 인자로 어떤 buffer를 clear할 것인지 결정할 수 있다. (COLOR, DEPTH, STENCIL)

  • glClearColor(r, g, b, 1.0f) : glClear(GL_COLOR_BUFFER_BIT)을 이용해 색상을 초기화할 때 glClearColor를 이용해서 미리 세팅해둔 rgb 값으로 초기화됨.

5. Hello Triangle

  • OpenGL이 하는 일은 3D 좌표계에서 screen 상의 2D pixel로 변환하는 것이다. 이것은 OpenGL의 graphics pipeline에 의해 이뤄진다.

  • graphics pipeline : 크게 3D -> 2D 변환과 2D -> pixel 변환 두 가지로 나눌 수 있다. graphics pipeline은 여러 step으로 나눌 수 있고, 각 스텝들은 병렬적으로 처리될 수 있다.

  • shaders : graphics pipeline의 과정이 parallel하게 처리되므로 GPU 내부의 processing core가 파이프라인의 각 스텝을 작은 프로그램으로 나누어 실행하는데, 이 small program을 shaders라 한다.

  • shaders는 GLSL 언어로 작성된다.

  • pipeline의 input은 vertex data로, 3D 좌표의 배열이다. vertex data는 vertex attributes를 이용해 vertex data의 구성을 정의한다.(3D position + color + ...등등)

  • primitives : vertex data를 이용해서 점, 삼각형, 선분들 중 어떤 것을 그리고 싶은 지 OpenGL에게 힌트를 줘야한다. 이 힌트를 primitives라 하고, GL_POINTS, GL_TRIANGLES, GL_LINE_STRIP 등이 있다.

pipeline

  1. vertex shader : pipeline의 첫번째 부분으로 1개의 vertex를 input으로 받는다. 3D 좌표를 다른 3D 좌표로(나중에 자세히 다룸) 변환함.

  2. primitive assembly : vertex shader로부터 모든 vertices를 받아 모든 점들을 주어진 primitives에 따라 assemble한다.

  3. geometry shader : 새로운 vertices를 추가해 새로운 primitives를 구성, 새로운 shape를 만들어낼 수 있음.

  4. rasterization state : 결과 primitives를 screen 상의 pixel로 매핑. clipping을 구행해 window의 범위를 벗어나는 fragment들을 모두 버림.

  5. fragment shader : 픽셀의 최종 색상을 계산. 주로 fragment shader에 3D scene에 대한 데이터(ligt, shadow, color of light...)를 포함하고 있으며 이것들을 이용해서 최종 색상을 계산한다.

6-1. alpha test / blending : fragment의 depth/stencil value를 체크해 결 fragment가 object의 앞/뒤에 있는 지 체크하고 버릴 지를 판단. alpha value(object의 opacity 투명도)를 검사해 blending 수행.

6개의 pipeline 중 우리가 작성해야하는 것은 vertex shader / fragment shader이다.
geometry shader는 필수적이지 않고, 주로 default shader를 그대로 사용.
이외에도 tessellation / transform feedback loop 가 있으나 나중에 다룸.

5.1. Vertex input

  • NDC in OpenGL : pixel 좌표계와 달리, (0, 0)이 top-left가 아닌 중심이며, y축의 방향이 아래쪽이 아닌 위쪽이다. z축의 경우 멀어지는 방향이 +이고, x, y, z축은 모두 (-1.0, 1.0) 사이의 값을 가져야 한다. OpenGL이 3D 좌표를 2D로 변환할 때는 이 NDC 내부의 점들만 변환하게 된다.

  • VBO(vertex buffer objects) : GPU 메모리 상에 vertices를 저장할 수 있는 오브젝트.
  • glGenBuffers() : Buffer 또한 오브젝트이므로 glGenBuffer를 이용해 생성하며, ID의 레퍼런스를 받는다.
  • glBindBuffer() : 여러 타입의 버퍼가 존재하는데, vertex buffer object를 사용하려면 GL_ARRAY_BUFFER 타입과 생성한 버퍼를 bind.
  • glBufferData() : bind한 타입의 버퍼에 실제 데이터를 넣어줌. 4번째 인자의 경우 GL_STREAM_DRAW / GL_STATIC_DRAW / GL_DYNAMIC_DRAW를 용도에 맞게 사용. STREAM :

GL_STREAM_DRAW : data set은 최초 한 번. GPU에 의한 사용 빈도 적음.
GL_STATIC_DRAW : data set은 최초 한 번. GPU에 의한 사용 빈도 많음.
GL_DYNAMIC_DRAW : data가 자주 변함. GPU에 의한 사용 빈도 많음.

unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

5.2. Vertex shader

  • layout (location = #) in type name : input data.
  • gl_Position : vertex shader의 output이 저장되는 변수.
#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
  • 위에서 작성한 shader 코드를 const char* vertexShaderSource에 저장.
  • vertex shader 오브젝트 생성
  • shader 소스코드와 shader 오브젝트를 붙여준 뒤 컴파일.
const char *vertexShaderSource = "~~~";

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

5.4. Fragment shader

  • out vec4 FragColor : fragment shader의 output
  • vertex shader에서 out 구문을 이용해 output을 내보내고, 동일한 변수명으로 in 구문을 사용하면 fragment shader에서 해당 변수값을 가져와서 사용할 수 있다.
#version 330 core
out vec4 FragColor;

void main()
{
	FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
  • vertex shader와 동일하게 컴파일.
const char *fragmentShaderSource = "~~~";

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
  • Shader Program object : 여러 셰이더가 합쳐진 오브젝트. 컴파일된 셰이더를 사용하기 위해서는 이것들을 shader program object에 "link"하고, object를 렌더링하는 순간에 활성화해야한다.

  • 셰이더를 linking하게되면 앞선 shader의 output이 뒤 shader의 input과 연결된다. 변수명이 동일해야하므로 주의.

  • glCreateProgram() : program object 생성.

  • glAttatchShader() : shader를 program에 추가.

  • glLinkProgram() : shader들을 linking.

  • glUseProgram() : 프로그램 활성화. 해당 프로그램을 사용하려고 할 때 사용.

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);

glLinkProgram(shaderProgram);


...

# when using Program
glUseProgram(shaderProgram);


glDeleteShader() : program object의 link가 완료되면 shader object는 더이상 필요하지 않기 때문에 제거해줌.

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

5.5. Linking Vertex Attributes

vertex data는 배열 타입으로, 사용자가 정의하는 대로 data가 나열되어 있다. 따라서 vertex data에 저장된 데이터의 어떤 부분이 vertex shader의 layout (location = #) 변수에 해당하는 지를 정해줘야한다.

현재 내가 정의한 vertex data는 위와 같은데,

  • position data는 4byte float 값이다.

  • 각 position은 3개의 float으로 구성된다.

  • 각 float 값 사이에 빈 공간이나 다른 value는 존재하지 않는다.(tightly packed)

  • data의 처음 값은 buffer의 처음 부분에 위치한다.

  • glVertexAttribPointer() : 바로 위에있는 정보들을 인자로 넣어준다. OpenGL이 vertex data를 해석할 수 있도록 정보 제공. 이 함수를 호출하기 전에 현재 내가 사용할 VBO를 bind 해주어야 한다.
    들어갈 parameter는 아래와 같다.

    • location
    • vertex attribute의 크기 (vec3인 경우 3)
    • data type(GL_FLOAT)
    • data를 normalize함(GL_TRUE)
    • stride
    • offset
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  • glEnableVertexAttribArray() : 위에서 작성한 glVertexAttribPointer는 disabled 되어있으므로 해당 함수를 사용해서 enable 해야한다.

지금가지의 과정을 순서대로 코드로 작성하면 아래와 같다.

// VBO bind
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// bind한 버퍼에 data를 dumping
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// vertex attribute pointer 세팅
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// vertex attribute enable
glEnableVertexAttribArray(0);
// program 활성화
glUseProgram(shaderProgram);

// TODO : Draw Triangle 

위 과정을 object를 그릴 때마다 반복해야 하는데, 꽤나 번거롭다. 이것들을 VAO에 저장해서 그때그때 불러와서 사용할 수 있다.

  • VAO(Vertex Array Object) : vertex buffer object와 vertex attribute들을 bind할 수 있음. 그러면 vertex attribute pointer를 한번만 생성하고, object를 그릴 때마다 사용할 수 있다.

  • VAO가 저장하는 것들

    • glEnableVertexAttribArray or glDisable..
    • glVertexAttribPointer로 작성된 vertex attribute
    • vertex attribute와 연관된 VBO

  • glGenVertexArrays() : VAO 생성
  • glBindVertexArray() : VAO bind. VBO bind 이전에 사용해야함.
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

...

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
//TODO : Draw triangle

그려야 할 object가 여러개라면 우선 VAO를 모두 생성해둔 뒤, 이후에 VAO bind를 해가며 그릴 수 있다.

  • glDrawArrays() : 현재 활성화된 program을 이용해서 primitives를 그릴 수 있도록 OpenGL에서 제공하는 함수. 입력하는 인자는 아래와 같다.
    • OpenGL의 primitive type(GL_TRIANGLES, ...)
    • 우리가 그리고싶은 vertex array가 시작하는 index
    • 몇 개의 vertex를 사용해서 그릴 지.
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 0);

5.6. Element Buffer Objects

만약 사각형을 그리고 싶다면 삼각형 2개를 이용하면 된다.
그렇다면 하나의 사각형을 위해 6개의 vertex를 사용해야 하는데, 겹치는 vertex가 존재하므로 최소 4개의 vertex만 있어도 그릴 수 있어야 한다.

  • EBO(Element Buffer Objects) : EBO를 사용하면 vertices는 4개의 position만 저장하고, indices를 새롭게 정의해 4개 중 몇 번째 vertex들을 이용해 각 삼각형을 그릴 지 선택할 수 있다.
unsigned int EBO;
glGenBuffers(1, &EBO);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
  • 그림을 그릴 때 EBO를 bind하고, glDrawarrays대신 glDrawElements를 사용하면 된다.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

EBO 또한 VAO에 함께 저장할 수 있다.

  • 주의 : VAO를 unbind하기 전에 EBO를 unbind해버리면 VAO에 저장되지 않는다.
// initialization
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

...

// render loop
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0); //unbind

6. Shaders

0개의 댓글