게임공장
[Learn OpenGL 번역] 2-4. 시작하기 - Hello Triangle 본문
Hello Triangle
시작하기/Hello Triangle
OpenGL에서 모든 것은 3D 공간 안에 있습니다. 하지만 화면과 윈도우 창은 2차원 픽셀 배열입니다. 그렇기에 OpenGL 작업의 큰 부분을 차지하는 것은 모든 3D 좌표를 화면에 맞게 2D 픽셀로 변환하는 작업입니다. 3D 좌표를 2D 좌표로 변환하는 작업은 OpenGL의
그래픽 파이프라인은 입력으로 3D 좌표를 받고 이 것을 여러분의 화면에 색이 입혀진 2D 픽셀로 변환합니다. 그래픽 파이프라인은 여러 단계로 나누어 집니다. 각 단계는 전 단계의 결과를 입력으로 받게 됩니다. 모든 단계는 매우 특성화(그들은 하나의 특정한 기능을 가지고 있습니다)되어 있습니다. 그리고 쉽게 병렬로 실행될 수 있습니다. 병렬성으로 인해 최근에 나온 대부분의 그래픽 카드가 그래픽 파이프라인의 각 단계에서 GPU 위에 작은 프로그램들을 실행시킴으로써 데이터를 빠르게 처리하기 위해 수천개의 작은 프로세싱 코어를 가지고 있습니다. 이 작은 프로그램들이라는 것은
이 쉐이더 중 일부는 이미 존재하는 기본 쉐이더를 대체하기 위해 우리 자신만의 쉐이더를 작성함으로써 개발자에 의해 설정가능한 쉐이더도 있습니다. 이는 파이프라인의 특정한 부분을 좀 더 세밀한 조작을 가능하게 합니다. 그리고 쉐이더는 GPU 위에서 실행되기 때문에 귀중한 CPU 시간을 절약할 수 있습니다. 쉐이더는
아래 그림에서 그래픽 파이프라인의 모든 단계가 추상화된 모형을 볼 수 있습니다. 파란색을 칠해진 부분이 우리가 작성한 쉐이더를 주입할 수 있는 부분입니다.
보시다시피 그래픽 파이프라인은 많은 부분들을 포함하고 있습니다. 이 부분들은 정점 데이터를 완전히 렌더링된 픽셀로 변환하는 데에 각자 하나의 특정 부분을 관리합니다. 여러분에게 파이프라인이 어떻게 동작하는지에 대해 이해하기 쉽도록 파이프라인의 각 부분에 대해서 간단히 설명드리도록 하겠습니다.
그래픽 파이프라인의 입력으로서 삼각형을 구성할 수 있는 정점 데이터라 불리는 3개의 3D 좌표 리스트를 전달할 수 있습니다. 이 정점 데이터는 정점들의 집합입니다.
파이프라인의 첫번째 부분은 하나의 정점을 입력으로 받는
primitive assembly 단계의 결과값은
geometry shader의 출력값은
모든 해당 컬럼 값들이 결정된 후에 최종 결과물은 하나의 단계를 더 거치게 됩니다. 이 단계는
보시다시피 그래픽 파이프라인은 꽤 복잡하고 많은 설정가능한 부분들을 포함하고 있습니다. 하지만 대부분의 예제에서 우리는 vertex shader와 fragment shader만 사용할 것입니다. Geometry shader는 선택적으로 사용하고 일반적으로 기본값으로 남겨놓습니다.
현대 OpenGL에서 최소한 vertex shader와 fragment shader는 우리 스스로 작성한 것을 사용하기를 요구합니다(GPU에 기본 vertex/fragment shader가 존재하지 않습니다). 이러한 이유로 첫 번째 삼각형을 렌더링 하기 전에 많은 지식을 요구하기 때문에 현대 OpenGL 공부를 시작하기 꽤 어렵습니다. 이 장의 마지막에 최종적으로 여러분의 삼각형을 한번 렌더링 해본다면 결국 그래픽 프로그래밍에 대해서 많은 것을 알게 될 것입니다.
Vertex 입력
무언가를 그리는 것을 시작하기 위해 우리는 먼저 OpenGL에게 입력 vertext 데이터를 주어야합니다. OpenGL은 3D 그래픽 라이브러리이기 때문에 우리가 명시한 모든 좌표는 3D 공간의 좌표입니다(x
, y
, z
좌표). OpenGL은 모든 3D 좌표를 화면상의 2D 픽셀로 간단히 변환하는게 아닙니다. OpenGL은 3D 좌표가 모든 3개의 축(x
, y
, z
)에서 값이 모두 -1.0
와 1.0
사이에 있어야만 처리하게 됩니다. 소위
우리는 하나의 삼각형을 렌더링하기 원하기 때문에 우리는 3D 위치를 가지고 있는 3개의 정점들을 명시해야 합니다. 우리는 그것들을 앞서 말한 normailzed device doordinates(OpenGL의 보일 수 있는 영역) 범위 안에 float
배열로 정의해야 합니다.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
OpenGL은 3D 공간에서 동작하기 때문에 우리는 z
좌표가 0.0
인 정점들을 이용해 2D 삼각형을 그립니다. 이 방법은 2D처럼 보이도록 삼각형의 깊이(depth)를 모두 같게 만듭니다.
vertex shader에서 여러분의 정점 좌표들이 처리가 되면 정점들이 x
, y
, z
값이 모두 -1.0
와 1.0
사이에 있는 작은 공간입니다. 이 영역 밖에있는 모든 좌표들은 폐기되고 여러분의 화면에 보이지 않게 됩니다. 아래에서 normalized device coordinates에 명시된 삼각형을 볼 수 있습니다(z
축은 무시)
일반적인 화면 좌표와 달리 y축이 양수인 곳은 화면의 위쪽을 향합니다. 그리고 (0, 0)
좌표는 좌측 상단이 아니라 화면의 중앙을 나타냅니다. 결국 모든(변환된) 좌표들이 이 영역 내부에 있어야 합니다. 그렇지 않으면 보이지 않습니다.
그런 다음 NDC 좌표는
정점 데이터가 정의되면 우리는 그 데이터를 그래픽 파이프라인의 첫 번째 단계인 vertex shader에게 전달합니다. GPU에 정점 데이터를 저장할 공간의 메모리를 할당하고 OpenGL이 어떻게 메모리를 해석할 것인지 구성하고 데이터를 어떻게 그래픽 카드에 전달할 것인지에 대해 명시함으로써 이 작업이 완료됩니다. 그런 다음 vertex shader는 우리가 말한 만큼의 정점들을 메모리에서 처리합니다.
우리는
vertex buffer object는 우리가 사용할 첫 번째 OpenGL 객체입니다. OpenGL의 모든 객체처럼 이 버퍼도 버퍼에 맞는 고유한 ID를 가지고 있습니다. 그래서 우리는
unsigned int VBO;
glGenBuffers (1, &VBO);
OpenGL은 많은 유형의 버퍼 객체를 가지고 있으며 vertex buffer object의 버퍼 유형은 GL_ARRAY_BUFFER입니다. OpenGL은 버퍼 유형이 다른 여러가지 버퍼를 바인딩할 수 있습니다. 우리는 새롭게 생성된 버퍼를
glBindBuffer (GL_ARRAY_BUFFER, VBO);
그 시점부터 우리가 호출하는 모든 버퍼(GL_ARRAY_BUFFER를 타겟으로 하는)는 현재 바인딩 된 버퍼(VBO)를 사용하게 됩니다. 그런 다음 우리는
glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
sizeof
키워드를 써도 충분합니다. 세 번째 파라미터는 우리가 보낼 실제 데이터를 받습니다.
네 번째 파라미터는 그래픽 카드가 주어진 데이터를 관리하는 방법을 받습니다. 이 것은 3가지의 종류가 있습니다.
- GL_STATIC_DRAW: 데이터가 거의 변하지 않습니다.
- GL_DYNAMIC_DRAW: 데이터가 자주 변경됩니다.
- GL_STREAM_DRAW: 데이터가 그려질때마다 변경됩니다.
삼각형의 위치 데이터는 모든 렌더링 호출 때마다 변하지 않고 항상 같으므로 GL_STATIC_DRAW가 가장 알맞습니다. 예를 들어 자주 바뀔 수 있는 데이터가 들어있는 버퍼일 경우 GL_DYNAMIC_DRAW, GL_STREAM_DRAW로 설정하면 그래픽 카드가 빠르게 쓸 수 있는 메모리에 데이터를 저장합니다.
현재로서는 정점 데이터를 그래픽 카드의 메모리에 저장했습니다. 이 메모리는 VBO라고도 불리는 vertex buffer object가 관리하게 됩니다. 다음에 우리는 실제적으로 이 데이터를 처리하는 vertex shader와 fragment shader를 만들어 볼 것입니다.
Vertex shader
Vertex shader는 우리가 프로그래밍할 수 있는 shader 중 하나입니다. 현대 OpenGL에서는 무엇인가를 렌더링 하기 위해서 최소한 vertex shader와 fragment shader는 세팅 하기를 요구합니다. 그래서 우리의 첫 번째 삼각형을 그리기 위한 shader에 대해 간단히 살명하고 2개의 아주 간단한 shader를 구성해볼 예정입니다. 다음 장에서는 shader에 대해서 좀 더 상세히 다루도록 하겠습니다.
먼저 우리가 해야할 일은 shader 언어인 GLSL(OpenGL Shading Language)를 통해 vertex 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);
}
보시다시피 GLSL은 C와 비슷하게 보입니다. 각 shader는 버전을 선언하는 것부터 시작합니다. OpenGL 3.3 이상 버전부터 GLSL 버전은 OpenGL 버전과 맞아야 합니다(예를 들어 GLSL 420버전은 OpenGL 4.2버전과 맞습니다). 또한 우리가 core profile 기능을 사용할 것이라고 명시해주어야 합니다.
그런 다음 vertex shader에 in
키워드를 사용하여 모든 입력 정점 속성을 선언해야 합니다. 현재는 위치 데이터만 사용하므로 오직 하나의 정점 속성만 필요합니다. GLSL은 vector 데이터타입을 가지고 있습니다. vector는 1~4개의 실수로 구성되어집니다. 각 정점은 3D 좌표를 가지고 있기 때문에 우리는 aPos라는 이름을 가진 vec3
타입 입력 변수를 생성합니다. 또한 layout (location = 0)
코드를 통해 입력 변수의 location을 설정합니다. 왜 location이 필요한지는 나중에 설명드리도록 하겠습니다.
그래픽 프로그래밍에서 수학적인 개념을 상당히 자주 사용합니다. 어떠한 공간에서의 위치/방향을 깔끔하게 표현하고 유용한 수학적 특성들을 가지고 있기 때문입니다. GLSL의 벡터는 최대 크기가 4이고 각 값은
vec.y
, vec.z
를 통해 접근할 수 있습니다. 여기서 각 값들은 공간에서의 좌표를 나타냅니다. vec.w
는 공간에서 위치를 나타내지 않고(우리는 4D가 아니라 3D를 사용합니다)
Vertex shader의 출력값을 설정하기 위해 미리 선언된 gl_Position 변수에 위치 데이터를 할당해야 합니다. 이 변수는 vec4
타입의 변수입니다. vec3
값을 vec4
의 생성자 안에 추가하고 w
값을 1.0f
으로 설정하여 형변환을 수행할 수 있습니다(그 이유는 나중에 설명하겠습니다).
현재 vertex shader는 우리가 생각할 수 있는 가장 간단한 vertex shader입니다. 입력 데이터에 대해 아무런 처리도 하지않고 간단히 shader의 출력값으로 전달했기 때문입니다. 실제 응용 프로그램에서 입력 데이터는 일반적으로 normalized device coordinates 영역에 포함되지 않는 값들입니다. 그래서 우리는 먼저 입력 데이터를 OpenGL의 표시할 수 있는 영역 내에 있는 좌표로 변환해야 합니다.
Shader 컴파일
Vertex shader의 소스 코드를 작성해 보았습니다. 하지만 OpenGL이 shader를 사용하기 위해서는 런타임시에 shader 소스 코드를 동적으로 컴파일해야 합니다.
먼저 해야할 일은 ID를 참조하여 shader 객체를 생성하는 것입니다.그래서 우리는 vertex shader를 unsigned int
타입으로 저장해야 하고
unsigned int vertexShader;
vertexShader = glCreateShader (GL_VERTEX_SHADER);
그런 다음 shader의 소스 코드를 shader 객체에 첨부한 후 shader를 컴파일합니다.
glShaderSource (vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader (vertexShader);
NULL
로 남겨두겠습니다.
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;
}
컴파일 도중 에러가 발생하지 않았다면 vertex shader가 컴파일 완료된 것입니다.
Fragment shader
Fragment shader 삼각형을 렌더링하기 위해 생성해야할 두 번째이자 마지막 쉐이더입니다. Fragment shader는 픽셀의 출력 컬러 값을 계산하는 것에 관한 쉐이더입니다. 간단하게 하기 위해서 frament shader는 항상 주황색을 출력하게 할 것입니다.
0.0
와 1.0
사이의 값으로 설정합니다. 예를 들어 빨간색을 1.0f
, 녹색을 1.0f
로 설정하면 두 가지의 색을 혼합하여 노란색을 출력할 수 있습니다. 주어진 3가지의 색으로 1,600만개 이상의 다른 색을 만들 수 있습니다.
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
Fragment shader는 오직 하나의 출력 변수를 필요로합니다. 그 변수는 크기가 4인 vector 이고 우리가 스스로 계산해야하는 최종 컬러를 정의합니다. out
키워드를 사용하여 출력 값을 선언할 수 있습니다. 여기서 FragColor 변수가 바로 그것입니다. 그런 다음 출력 컬러에 vec4
타입으로 컬러(주황색)를 간단히 할당합니다. 알파값은 1.0
으로 설정합니다(1.0
은 완전한 불투명입니다).
Fragment shader 컴파일은 vertex shader와 비슷합니다. GL_FRAGMENT_SHADER 상수를 shader 타입으로 설정해주기만 하면 됩니다.
unsigned int fragmentShader;
fragmentShader = glCreateShader (GL_FRAGMENT_SHADER);
glShaderSource (fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader (fragmentShader);
두 개의 shader는 지금 컴파일되었고 마지막 할일은
Shader program
Shader program 객체는 여러 shader를 결합한 마지막 연결된 버전입니다. 컴파일된 shader들을 사용하기위해 shader들을 shader program 객체로
Shader들을 program에 연결할 때 각 shader들의 출력 값을 다음 shader의 입력값으로 연결합니다. 출력과 입력이 일치하지 않으면 연결 오류가 발생합니다.
Program 객체를 생성하는 것은 쉽습니다.
unsigned int shaderProgram;
shaderProgram = glCreateProgram ();
glAttachShader (shaderProgram, vertexShader);
glAttachShader (shaderProgram, fragmentShader);
glLinkProgram (shaderProgram);
코드는 따로 설명이 필요가 없습니다. shader들을 program에 붙이고
glGetProgramiv (shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
결과는
glUseProgram (shaderProgram);
Oh yeah, 그리고 shader들을 program 객체로 연결하고 나면 shader 객체들을 제거하는 것을 잊지마세요. 우리는 더 이상 필요하지 않습니다.
glDeleteShader (vertexShader);
glDeleteShader (fragmentShader);
우리는 방금 입력 정점 데이터를 GPU로 보냈고 GPU에게 정점 데이터를 어떻게 처리해야 하는지 vertex shader와 fragment shader를 통해 지시하였습니다. 거의 다 왔습니다. 하지만 아직 아닙니다. OpenGL은 메모리 상의 정점 데이터를 어떻게 해석해야하는지 아직 모릅니다. 또한 정점 데이터를 vertex shader의 속성들과 어떻게 연결해야 하는지도 모릅니다. OpenGL에게 이러한 것들을 알려주는 게 좋을 것입니다.
정점 속성 연결(Linking Vertex Attributes)
Vertex shader는 우리가 원하는 모든 입력들을 정점 속성의 형식으로 지정할 수 있도록 해줍니다. 또한 유연성이 좋은 반면에 입력 데이터의 어느 부분이 vertex shader의 어떠한 정점 속성과 맞는지 직접 지정해야 합니다. 이는 렌더링 하기 전에 OpenGL이 정점 데이터를 어떻게 해석해야하는지를 우리가 지정해주어야 한다는 뜻입니다.
우리의 vertex buffer 데이터는 다음과 같은 형식을 취하고 있습니다.
- 위치 데이터는 32-비트(4 바이트)의 실수형 점입니다.
- 각 위치는 3가지의 값으로 구성되어있습니다.
- 각 3개 값의 집합들 사이에 공백은 없습니다. 값들은 배열에
빽빽히 채워져(tightly packed) 있습니다. - 데이터의 첫 번째 값은 버퍼의 시작 지점에 있습니다.
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnable VertexAttribArray (0);
- 첫 번째 파라미터는 설정할 vertex 속성을 지정합니다. Vertex shader에서
layout (location = 0)
코드를 사용하여 position vertex 속성의 위치를 지정했었던 것을 기억하세요. 이는 vertex 속성의 위치(location)을 0으로 설정하고 우리는 데이터를 이 vertex 속성에 전달하고자 하기 때문에0
에 전달합니다. - 다음 파라미터는 vertex 속성의 크기를 지정합니다. 이 vertex 속성은
vec3
타입이므로3
개의 값으로 이루어져 있습니다. - 세 번째 파라미터는 데이터의 타입을 지정합니다. 여기서는 GL_FLOAT으로 지정합니다(GLSL에서
vec*
은 실수형 점으로 이루어집니다). - 다음 파라미터는 데이터를 정규화할 것인지 지정합니다. 이 파라미터를 GL_TRUE로 설정하면
0
(혹은 부호를 가진 데이터라면-1
)와1
사이에 있지 않는 값들의 데이터들이 그 사이의 값들로 매핑됩니다. 우리는 GL_FALSE로 놔두겠습니다. - 다섯 번째 파라미터는
stride 라고도 불리며 연이은 vertex 속성 세트들 사이의 공백을 알려줍니다. 다음 포지션 데이터의 세트는 정확히float
타입 3개의 크기 뒤에 떨어져 있습니다. 우리는 이 값을 stride로 지정합니다. 이 배열이 빽빽히 채워져있다(다음 vertex 속성 값 사이에 공백이 없음)는 것을 알고 있다면 stride를0
으로 지정하여 OpenGL이 stride를 지정하게 할 수 있습니다(값들이 빽빽히 채워져 있을 때만 동작). 더 많은 vertex 속성들이 있다면 우리는 각 vertex 속성들 사이의 공간을 조심스럽게 정의해야 합니다. 우리는 나중에 이러한 상황의 예제를 살펴볼 것입니다. - 마지막 파라미터는
void*
타입이므로 형변환이 필요합니다. 이는 버퍼에서 데이터가 시작하는 위치의offset 입니다. 위치 데이터가 데이터 배열의 시작 부분에 있기 때문이 이 파라미터는0
으로 지정합니다. 우리는 이 파라미터에 대해 나중에 좀 더 자세히 살펴볼 것입니다.
0
이 해당 vertex 정점과 연결됩니다.
이제 OpenGL이 vertex 데이터를 어떻게 해석해야하는지 지정했으므로
// 0. 정점 배열을 OpenGL에서 사용하기 위해 버퍼에 복사
glBindBuffer (GL_ARRAY_BUFFER, VBO);
glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 그런 다음 vertex 속성 포인터를 설정
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnable VertexAttribArray (0);
// 2. 오브젝트를 그리고 싶을 때 우리가 생성한 shader program을 사용
glUseProgram (shaderProgram);
// 3. 이제 오브젝트를 그립니다.
someOpenGLFunctionThatDrawsOurTriangle();
오브젝트를 그려야할 때마다 우리는 이 과정을 반복해야 합니다. 그렇게 많아 보이지 않을 수 있지만 5개 이상의 vertex 속성과 100개의 다른 오브젝트들(흔한 경우입니다)이 있다고 생각해보세요. 오브젝트들에 대해 신속히 적절한 buffer 객체를 바인딩하는 것과 모든 vertex 속성들을 구성하는 것은 번거로운 과정이됩니다. 이 모든 상태 설정을 객체에 저장하고 간단히 이 객체를 바인딩하여 상태를 복원할 수 있는 방법이 있다면 어떨까요?
Vertex Array Object
Vertex array object는 다음 항목들을 저장합니다.
함수나glEnable VertexAttribArrayglDisableVertexAttribArray 함수의 호출 함수를 통한 Vertex 속성의 구성glVertexAttribPointer 함수를 통해 vertex 속성과 연결된 Vertex buffer objects(VBOs)glVertexAttribPointer
VAO를 생성하는 과정은 VBO와 비슷합니다.
unsigned int VAO;
glGenVertexArrays (1, &VAO);
VAO를 사용하기 위해 해야할 일은
// ..:: 초기화 코드 (한번만 실행됩니다(오브젝트가 자주 변경되지 않는 한)) :: ..
// 1. Vertex Array Object 바인딩
glBindVertexArray (VAO);
// 2. OpenGL이 사용하기 위해 vertex 리스트를 복사
glBindBuffer (GL_ARRAY_BUFFER, VBO);
glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 그런 다음 vertex 속성 포인터를 세팅
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnable VertexAttribArray (0);
[...]
// ..:: 드로잉 코드 (렌더링 루프 내부) :: ..
// 4. 오브젝트를 그립니다.
glUseProgram (shaderProgram);
glBindVertexArray (VAO);
someOpenGLFunctionThatDrawsOurTriangle();
이게 다입니다! 지금까지 우리가 한 모든 일이 이 것을 위한 것이었습니다. VAO는 vertex 속성 구성을 저장합니다. 일반적으로 여러가지의 오브젝트들을 그리고 싶을 때 먼저 모든 VAO(또한 필요한 VBO와 속성 포인터들)를 생성하고 구성합니다. 그리고 그 것들을 나중에 사용하기 위해 저장합니다. 오브젝트들 중에 하나를 그리고 싶다면 해당 VAO를 가져와 바인딩하고 오브젝트를 그린 후 VAO를 다시 언바인딩합니다.
기다리고 기다리던 우리의 삼각형
우리의 오브젝트를 그리기 위해서 OpenGL은
glUseProgram (shaderProgram);
glBindVertexArray (VAO);
glDrawArrays (GL_TRIANGLES, 0, 3);
0
으로 지정합니다. 마지막 파라미터는 몇개의 vertex를 그리기 원하는지를 지정합니다. 우리의 경우에는 3
(1개의 삼각형을 그리기 때문에 3개의 정점)입니다.
이제 코드를 컴파일해봅시다. 컴파일이 완료 되면 다음과 같은 결과를 확인할 수 있을 것입니다.
완전한 프로그램의 전체 소스 코드는 여기에서 볼 수 있습니다 .
이 화면과 같이 나오지 않는다면 아마 잘못된 방법을 사용했을 것이므로 소스 코드를 세세히 확인해보세요.
Element Buffer Objects
정점들을 렌더링할 때 생각해야할 게 하나 더 있습니다. 그것은
float vertices[] = {
// 첫 번째 삼각형
0.5f, 0.5f, 0.0f, // 우측 상단
0.5f, -0.5f, 0.0f, // 우측 하단
-0.5f, 0.5f, 0.0f, // 좌측 상단
// second triangle
0.5f, -0.5f, 0.0f, // 우측 하단
-0.5f, -0.5f, 0.0f, // 좌측 하단
-0.5f, 0.5f, 0.0f // 좌측 상단
};
보시다시피 정점을 명시하는데에 중복되는 것이 있습니다. 우측 하단
와 <좌측 상단>이 2개씩 존재합니다. 동일한 사각형이 6개의 정점 대신 4개의 정점으로 생성될 수 있기 때문에 이는 50% 오버헤드가 발생한다고 생각할 수 있습니다. 1000개 이상의 삼각형들로 이루어진 복잡한 모델에서는 이는 더 심각해집니다. 오직 고유한 정점들만 저장하고 그들의 순서를 지정하는 좋은 방법은 없을까요? 이 경우에 사각형을 그리기 위해 4개의 정점만을 저장하면 됩니다. 그런 다음 그들을 그리는 순서를 지정하면 됩니다. OpenGL이 이러한 기능을 제공한다면 얼마나 좋을까요?
고맙게도 element buffer objects는 정확이 이렇게 동작합니다. EBO는 VBO와 같은 버퍼입니다. EBO는 OpenGL이 어떠한 정점들을 그려야할 지 결정할 수 있는 인덱스들을 저장합니다. 이는
float vertices[] = {
0.5f, 0.5f, 0.0f, // 우측 상단
0.5f, -0.5f, 0.0f, // 우측 하단
-0.5f, -0.5f, 0.0f, // 좌측 하단
-0.5f, 0.5f, 0.0f // 좌측 상단
};
unsigned int indices[] = { // 0부터 시작한다는 것을 명심하세요!
0, 1, 3, // 첫 번째 삼각형
1, 2, 3 // 두 번째 삼각형
};
보시다시피 인덱스를 사용하면 6개가 아닌 오직 4개의 정점만이 필요합니다. 그런 다음 element buffer object를 생성해야 합니다.
unsigned int EBO;
glGenBuffers (1, &EBO);
VBO와 비슷하게
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData (GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
이번에는 GL_ELEMENT_ARRAY_BUFFER를 버퍼 타겟으로 지정했다는 것을 생각하세요. 남은 마지막 해야할 일은
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
첫 번째 파라미터는
초기화와 드로잉 코드는 이제 다음과 같아질 것입니다.
// ..:: 초기화 코드 :: ..
// 1. Vertex Array Object 바인딩
glBindVertexArray (VAO);
// 2. OpenGL이 사용하기 위해 vertex 리스트를 vertex 버퍼에 복사
glBindBuffer (GL_ARRAY_BUFFER, VBO);
glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. OpenGL이 사용하기 위해 인덱스 리스트를 element 버퍼에 복사
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData (GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 그런 다음 vertex 속성 포인터를 세팅
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnable VertexAttribArray (0);
[...]
// ..:: 드로잉 코드 (렌더링 루프 내부) :: ..
glUseProgram (shaderProgram);
glBindVertexArray (VAO);
glDrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray (0);
실행 중인 프로그램은 아래와 같은 화면을 보여주어야 합니다. 왼쪽 이미지는 익숙한 화면이고 오른쪽 이미지는
Wireframe mode로 삼각형을 그리기 위해서
lgPolygonMode (GL_FRONT_AND_BACK, GL_LINE)
함수를 통해 OpenGL이 primitive를 어떻게 그릴 것인지 설정할 수 있습니다. 첫 번째 파라미터는 모든 삼각형의 앞과 뒤에 이것을 적용하기를 원한다는 것을 말하고 두 번째 파라미터는 그것을 선으로 그리라고 지정합니다. 그 후의 모든 드로잉 명령은 glPolygonMode (GL_FRONT_AND_BACK, GL_FILL)
함수를 통해 기본값으로 설정할 때까지 wireframe mode로 삼각형을 그려지게 됩니다.
오류가 발생한다면 무엇을 놓쳤는지를 잘 살펴보세요. 또한 완성된 소스 코드는 여기에서 볼 수 있습니다.
우리가 해왔던 것처럼 삼각형이나 사각형을 그렸다면 축하합니다. 여러분은 현대 OpenGL의 가장 어려운 부분 중 하나인 삼각형 그리기를 마쳤습니다. 첫 번째 삼각형을 그리기 전에 알아야할 지식들이 너무나도 많기 때문이 이부분은 어렵습니다. 고맙게도 우리는 이제 막 장벽을 넘었으며 앞으로의 강좌는 이해하기 훨씬 쉬울 것입니다.
추가 자료
- antongerdelan.net/hellotriangle: Anton Gerdelan의 첫 번째 삼각형 렌더링
- open.gl/drawing: Alexander Overvoorde의 첫 번째 삼각형 렌더링
- antongerdelan.net/vertexbuffers: vertex buffer objects에 대한 추가 자세한 사항
- learnopengl.com/#!In-Practice/Debugging: 이 강좌와 관련된 많은 단계들이 있습니다. 만약 어려움이 생기면 디버그 섹션이 디버그 하는데에 도움이 많이 될 것입니다.
연습
학습한 개념들을 잘 이해하기 위해 몇가지 연습 문제를 만들어보았습니다. 다음 강좌로 넘어가기 전에 수행하여 여러분이 무엇이 어떻게 돌아가는지 알 수 있도록 상기시키세요.
함수를 사용하여 서로의 옆에 있는 2개의 삼각형을 그려보세요. 추가적인 정점을 추가하세요: 해답glDrawArrays - 이제 2개의 다른 VAOs와 VBOs를 사용하여 그들의 데이터에 대한 똑같은 2개의 삼각형을 생성하세요: 해답
- 2개의 shader program을 생성하세요. 두 번째 shader proram은 다른 fragment shader를 사용합니다. 그 fragment shader는 노란색 컬러를 출력값으로 내보냅니다. 두 개의 삼각형을 모두 그려보세요: 해답
'OpenGL' 카테고리의 다른 글
[Learn OpenGL 번역] 2-6. 시작하기 - Textures (4) | 2018.07.15 |
---|---|
[Learn OpenGL 번역] 2-5. 시작하기 - Shaders (6) | 2018.07.15 |
[Learn OpenGL 번역] 2-3. 시작하기 - Hello Window (5) | 2018.07.15 |
[Learn OpenGL 번역] 2-2. 시작하기 - Window 생성 (0) | 2018.07.15 |
[Learn OpenGL 번역] 1. 소개 (1) | 2018.07.15 |