OpenGL_03, Hello Triangle

김경주·2024년 3월 21일
0

OpenGL

목록 보기
4/10

OpenGL에서 모든 것은 3차원 공간에 있다. 그러나 스크린 또는 창은 2차원 픽셀 배열이다. 그래서 OpenGL 작업 중 큰 부분은 모든 3차원 좌표를 스크린에 맞춘 2차원 픽셀로 변환하는 것에 관한 것이다.

3차원 좌표를 2차원 픽셀로 변환하는 작업은 OpenGL의 그래픽 파이프라인에 의해서 관리된다. 그래픽 파이프라인은 크게 두 파트로 나눌 수 있다. 첫번째는 3차원을 2차원으로 변환하는 것, 두번째는 그 2차원을 실제 색상을 입힌 픽셀로 변환하는 것.

그래픽 파이프라인은 3차원 좌표들로 이루어진 집합을 입력값으로 받고 이를 색상을 입힌 2차원 픽셀로 변환하는 것. 그래픽 파이프라인은 여러 스텝으로 나누어질 수 있는데 각각의 단계에서 입력값으로 이전의 출력값을 요구한다. 이러한 모든 단계들은 각 단계에 맞게 분화되어있고 쉽게 병렬로 처리될 수 있다. 이러한 성질로 오늘날 그래픽 카드는 그래픽 파이프라인 내부에서 빠르게 데이터를 처리하기 위해서 수천개의 작은 프로세싱 코어들을 가진다. 이러한 코어들을 처리하는 것은 파이프라인 각각의 단계에 맞게 GPU에 있는 작은 프로그램을 실행한다. 이런 작은 프로그램들을 shader라 부른다.

현재 사용하는 디폴트 shader를 대체하기 위해서 몇몇 shader들은 개발자들 개인에게 맞는 shader를 작성할 수 있게 환경 설정이 가능하다. 이는 우리에게 파이프라인 특정부분에서 좀더 세세하게 제어할 수 있게 해주며 shader들이 GPU에서 작동하므로 CPU 처리(가장 중요한) 시간을 줄여준다. OpenGL Shading Language(GLSL)로 shader들은 작성되어있다.

그래픽 파이프라인의 전과정을 추상화한 것을 볼 수 있다.

각각의 단계를 간략하게 살펴보자

VERTEX DATA라 불리는 배열 하나의 삼각형 형식으로 되어야하는 세개의 3차원 좌표의 목록을 그래픽 파이프라인의 입력값으로 전달. vertex는 각 3차원 좌표에 해당하는 데이터 모음이다. vertex 데이터는 원하는 어떤 데이터든 담을 수 있는 vertex attribute(속성)을 사용하여 나타낸다. 간단하게 3차원 위치와 색상 값으로 구성되어 있다고 가정하자.

OpenGL이 좌표들의 모음과 색상 값으로 무엇을 만들어낼건지 알기 위해서 OpenGL은 데이터에 맞는 형식으로 어떤 종류의 렌더 타입인지 알려주길 요구한다. 데이터가 점이나 삼각형 또는 긴 선분으로 그려지길 원하는가? 이런 힌트들은 primitives 불리며 그리는 명령어들이 호출되는 중에 OpenGL에 전달된다. 그것들 중에 대표적으로 GL_POINTS, GL_TRIANGLES 그리고 GL_LINE_STRIP이 있다.

파이프라인의 첫 단계인 단일 vertex를 입력값으로 받는 vertex shader이다. 이 vertex shader의 주된 목적은 3차원 좌표를 또다른 3차원로 변환하고 vertex attribute에 몇 가지 기본적인 처리작업을 하게 해준다.

primitive assembly 단계는 주어진 primitive 모양에서 primitive 구성하고 모든 점을 모아 조합하는 vertex shader로부터 모든 vertices를 입력값으로 받거나 GL_POINTS이 선택되어있으면 vertex를 받는다. 여기서는 삼각형 모형.

primitive assembly 단계에서의 출력값은 geometry shader로 전달된다. primitive 형식인 vertices의 모음을 입력값으로 받고 새로운 거나 또 다른 primitive를 구성하기 위해서 새로운 vertices를 냄으로서 다른 모양을 생산할 능력을 가진다. 여기서는 주어진 모양에서 두번째 삼각형을 생성.

geometry shader의 출력값은 rasterization 단계에 전달. rasterization단계에서 결과물이 스크린에 상응하는 픽셀과 맵핑되며 fragment shader를 사용하기 위해 fragment로 된다. fragment shader가 실행되기 전에 clipping이 수행된다. clipping은 성능향상을 위해 view 바깥쪽의 fragment를 폐기한다.

OpenGL에서 하나의 fragment는 OpenGL이 하나의 픽셀을 렌더하기 위해 요구되는 모든 데이터들이다.

fragment shader의 주된 목적은 픽셀의 마지막 색상을 계산하는 것이며 이는 일반적으로 고급 OpenGL 효과들이 일어나는 단계이다. fragment sahder는 일반적으로 (광원, 그림자, 광원색 등과 같은) 마지막으로 색상을 계산하기 위해서 사용하는 3차원 장면에 관한 데이터들을 담고 있다.

상응하는 모든 색상 값들이 정해진 후 마지막 object(물체)가 alpha test와 blending로 불리는 단계를 통해서 전달된다. 이 단계에서 fragment의 depth와 stencil 값이 상응하는지 확인하고 다른 물체보다 앞에 있는지 뒤에 있는지 확인하기 위해서 그 값들을 사용하고 그것들에 맞춰 잘라낸다. 이 단계에서 또 alpha 값을 확인하고 그에 맞게 물체를 blend(조합)한다. fragment shader에서 픽셀 출력값이 계산된다고 하더라도, 마지막 픽셀 색상은 여러 삼각형들이 그려질 때 완전히 다른 어떤 색이 될 수 있다.

이렇듯 그래픽스 파이프라인은 전체적으로 복잡하고 많은 환경설정해야하는 부분들을 포함한다. 그렇지만, 거의 대부분의 경우 vertex, fragment shader 이 두 가지만 작업한다. geometry shader는 옵션이고 보통 디폴트로 놔둔다. 또 tessellation 단계와 변환 피드백 루프가 있지만 나중에 알아본다.

현대 OpenGL은 최소한 vertex shader와 fragment shader를 정의하길 요구된다. 이 이유로 배우기 어렵고 힘들다.

Vertex Input

무엇을 그리기전에 OpenGL에 입력 vertex 데이터를 주어야한다. OpenGL은 3차원 그래픽 라이브러리이며 OpenGL에서 명시한 좌표들은 전부 3차원이다. OpenGL은 간단하게 3차원을 2차원으로 변환하지 않는다. OpenGL은 좌표들이 세 축(x,y,z)에서 -1.0 ~ 1.0 범위안에 있을때만 3차원 좌표들을 처리. 모든 좌표가 normalized device coordinates 범위 안에 있으면 스크린에 나타난다.

단일 삼각형을 그리기 때문에 각 vertex가 3차원 위치를 갖는 세개의 vertices를 다 명시해야한다. float형 배열에 그 vertices를 normalized device coordinates로 정의한다.

float vertices[] = {
			-0.5f, -0.5f, 0.0f,
			 0.5f, -0.5f, 0.0f,
			 0.0f,  0.5f, 0.0f
};

OpenGL은 3차원 공간에서 작업하므로 각각의 vertex의 z축은 0.0을 가진다.(2차원 삼각형이기때문에)

Normalized Device Coordinates(NDC)

vertex 좌표들이 vertex shader에서 처리되었다면, 그 좌표들은 NDC.

NDC는 glViewport 함수에 건네는 데이터를 사용해서 viewport transform을 통해서 screen-space coordinates로 변환된다. 그 후 screen-space coordinates는 fragment shader의 입력값으로 fragments로 변환된다.

일반적인 스크린 좌표계와는 달리 (0, 0)은 좌상단이 아닌 정중앙.

이 NDC는 glViewport 데이터를 사용하여 viewport transform을 거쳐서 screen-space coordinates로 변형되고 그 결과물은 fragment shader의 입력값으로 fragment로 변형된다.

그래픽 파이프라인의 첫 번째 과정에 입력값으로서 보내려고 정의된 vertex data는 vertex shader이다. 이는 GPU에서 메모리 생성으로부터 끝나는데 이 메모리는 vertex data를 저장하고 어떻게 OpenGL이 메모리를 해석하는지 환경 설정하고 그래픽 카드에 데이터를 어떻게 보낼지 명시하는 곳이다. 그러고 vertex shader는 그 메모리로부터 가능한 많은 vertices을 보낼 수 있게 양을 처리한다.

GPU 메모리에 아주 많은 vertices를 저장할 수 있는 vertex buffer object(VBO)라 불리는 곳을 통하여 메모리 관리. 이러한 버퍼 오브젝트를 사용하는 이점은 데이터를 한꺼번에 크게 묶어서 그래픽카드에 보낼 수 있게 해주며 한번에 하나의 데이터를 보내는 것 없이 메모리가 충분히 남았다면 메모리에서 데이터를 유지할 수 있다. CPU에서 그래픽 카드로 데이터를 보내는 것은 상대적으로 느리다. 그래서 한번에 모아서 보내려고 한다. 그래픽 카드에 데이터가 있자마자 vertex shader는 거의 즉각 vertices에 접근해서 빠르게 처리한다.

다른 object들과 같이 VBO는 버퍼에 상응하는 고유 ID를 가진다. glGenBuffers 함수를 이용해서 buffer ID와 buffer 생성

unsigned int VBO;
glGenBuffers(1, &VBO);

OpenGL은 다양한 타입의 버퍼 오브젝트를 가지고 있으며 VBO의 버퍼 타입은 GL_ARRAY_BUFFER이다. OpenGL은 버퍼들이 서로 다른 타입이라면 한번에 여러 버퍼들을 바인드 할 수 있게 해준다. 새로 생성된 버퍼를 glBindBuffer 함수를 사용해 GL_ARRAY_BUFFER 타겟으로 묶을 수 있다.

glBindBuffer(GL_ARRAY_BUFFER, VBO);
									target

GL_ARRAY_BUFFER 타겟에서 버퍼 호출들은 현재 바인드된 버퍼, 즉 VBO를 환경 설정하기 위해 사용될 것. 이후 버퍼 메모리에 이전에 정의된 버퍼들을 복사하는 glBufferData 함수를 호출할 수 있다.

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

glBufferData 사용자 정의된(user-defined) 데이터를 현재 바인드된(bound) 버퍼에 복사하기 위한 함수. 첫번째 인자는 데이터를 복사할 버퍼의 타입(여기서 GL_ARRAY_BUFFER 타겟에 바인드된 VBO). 두번째는 버퍼로 보낼 데이터의 사이즈, 세번째 인자는 실제 데이터, 네번째 인자는 주어진 데이터를 그래픽 카드가 어떻게 처리할지에 대한 정보, 여기에 3가지 형식이 있다.

GL_STREAM_DRAW: 데이터는 한번만 설정되고 GPU에 의해 많아봐야 몇번 사용되어진다.
GL_STATIC_DRAW: 데이터는 변하지 않고 많이 사용된다.
GL_DYNAMIC_DRAW: 데이터는 정말 많이 변하고 많이 사용된다.

삼각형의 위치 데이터는 변하지 않으며 많이 사용되고 매번 렌더 호출 있을 때마다 같은 곳에 있기 때문에 이에 맞는 타입은 GL_STATIC_DRAW이다. 만약 예를들어 자주바뀌는 데이터를 가진 버퍼를 가진다면 GL_DYNAMIC_DRAW 타입의 사용이 그래픽 카드가 더 빠른 작업을 하게 메모리에 데이터를 놓아두게 해준다.

Vertex shader

Vertex shader는 사용자가 프로그래밍 가능한 shader 중 하나. 현대 OpenGL은 최소한 vertex, fragment shader 설정하는 것을 요구.

GLSL shader 언어에서 vertex shader 작성이 첫번째 해야할 일이고 그리고 난 뒤 이 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들은 처음에 그 version을 선언. GLSL 버전과 OpenGL 버전과 매치해야한다(420이면 OpenGL 4.2). 또한 core profile 기능을 사용할거라고 명시.

다음은 vertex shader에 vertex 속성을 ‘in’이라는 키워드와 함께 입력값을 선언한다. 현재 위치 데이터만 신경쓰므로 단일 vertex attribute만 필요. GLSL는 postfix digit을 기반으로 1 ~ 4개의 float형을 담고 있는 벡터 데이터 타입을 가진다. 각각의 vertex는 3차원 좌표값을 가지고 있기에 aPos 명으로 vec3 변수를 생성. layout (location = 0)을 통해 입력 변수의 위치를 특정해서 설정한다. 이는 왜 필요한지 나중에 알아갈 것

컴퓨터 그래픽스에서의 Vector - 수학적 개념으로 많이 사용. 이는 점/위치 개념으로 사용하고 유용한 수학적 성질을 가진다. GLSL에서 벡터의 최대 사이즈는 4이며 각각의 값에 검색/접근은 vec.x, vec.y, vec.z 그리고 vec.w이다. vec.w의 성분은 공간의 위치 개념이 아닌 원근법을 나타낸다.

vertex shader의 출력값을 설정하기위해 미리 정의된 gl_Position(vec4) 변수에 위치 데이터를 할당해야한다. 메인 함수 끝에 gl_Position에 설정하는 어떤 값이든 vertex shader의 출력값으로 사용되어질 것. 여기서 사용되는 벡터의 크기는 vec3라서 vec4로 형변환을 해야한다. w 성분값에 1.0f를 넣는다.

위 예시는 가장 간단한 케이스다. 실제 응용 프로그램의 데이터들은 Normalized Device Coordinates가 안된 상태이기에 OpenGL 영역에 맞는 좌표로 변환해줘야한다.

Compiling a 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를 사용할 수 있게 소스코드로부터 실행시간에 동적으로 컴파일해야 한다. 첫번째는 ID에 의해서 참조되게 shader object 생성. 그래서 vertexShaderunsinged int로 선언하고 glCreateShader로 shader 생성.

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

glCreateShader의 인자로 원하는 shader type을 전달한다. vertex shader를 필요하니 GL_VERTEX_SHADER를 전달한다.

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

glShaderSource 함수는 컴파일 할 shader object를 첫번째 인자로 받고 두번째는 전달하는 소스코드가 얼마나 많은 문자열(string)이 있는지 명시. 세번째는 vertex shader의 실제 코드를 인자값으로 받는다. 여기서 네번째 인자는 NULL이다.

glCompileShader 함수가 성공적으로 확인하기 원하면 컴파일 때 에러 체크를 하기위해서 아래와 같이 작성

int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

glGetShaderiv 함수를 이용해서 컴파일이 성공적인지 확인할 수 있다. 실패하면 success 변수에 0이 할당되고 아래와 같이 분기문을 만들어 그 분기문 안으로 들어가게 하여 glGetShaderInfoLog 함수를 호출하여 infoLog를 매개 변수로 전달하여 에러 메시지를 담는다.

if(!success)
{
	glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
	std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" <<
	infoLog << std::endl;
}

Fragment shader

Fragment shader는 픽셀의 섹상 출력값을 계산한다. 여기서는 오렌지 계열 색상 이용

컴퓨터 그래픽스에서 색은 4개의 값으로된 배열을 표현. RGBA(빨강, 초록, 파랑, 투명도 / Red, Green, Blue, Alpha)로 나타낸다. OpenGL 또는 GLSL에서 색을 정의할 때, 성분의 값을 0.0에서 1.0 사이의 값으로 설정. 주어진 색상 3가지 성분으로 1600만개의 다른 색을 표현 수 있다.

#version 330 core
out vec4 FragColor;

void main()
{
	FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

여기에서 fragment shader는 vec4로 최종 색상값이 정의된 하나의 출력 변수만 요구. ‘out’ 키워드와 함께 출력 변수 FragColor 선언. 그리고 그 변수에 vec4의 색상값 할당.

fragment shader를 컴파일하는 과정은 shader type으로 GL_FRAGMENT_SHADER인 거 말고는 vertex shader와 비슷하다.

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

Shader program

Shader program object는 결합된 여러 shader들의 최종 링크된 버전. 현재 컴파일된 shader들을 사용하기 위해서 shader program object에 링크해야하고 object들을 렌더링할 때 shader program을 활성화해야한다. 활성화된 shader program의 shader들은 렌더 호출이 일어날 때 사용될 것.

shader들을 프로그램으로 연결시킬 때 각각의 shader의 출력값들을 다음 shader에 연결한다. 여기서 또한 입력물과 출력물의 매칭이 잘못되면 에러.

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram 함수는 program을 생성하고 새로 생성된 program object의 ID 참조값을 리턴한다. glLinkProgram 함수를 사용하여 이전에 컴파일된 shader들을 program object에 붙이고 그것들을 연결한다.

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

새로 생성된 program object를 인자로 받아서 glUseProgram 함수를 호출함으로써 program object를 활성화 할 수 있다.

glUseProgram(shaderProgram);

glUseProgram 이후 모든 쉐이더와 렌더링 호출은 이 프로그램 객체를 사용할 예정.

shader들을 program object에 링크한 이후 shader object들을 삭제해야한다.

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

현재 입력 vertex 데이터를 GPU로 보냈고 vertex, fragment shader 안에 있는 vertex 데이터들을 어떻게 처리해야하는지 GPU에 알려줬다.

아직 OpenGL은 메모리에 있는 vertex 데이터를 어떻게 해석하는지 모르고 어떻게 vertex 데이터를 shader attribute와 연결하는지 모른다.

Linking vertex attributes

vertex shader는 어느 입력값이든 vertex attribute의 형식에 맞게 명시할 수 있게 해준다. 이는 더 나은 유연성을 주지만 수동으로 vertex shader에서 어느 vertex attribute로 입력 데이터의 어떤 부분이 가는지 명시해야한다는 것. 이는 OpenGL에게 렌더링 전에 어떻게 입력 데이터를 해석해야하는지 명시하는 것.

vertex buffer data는 위와 같이 구성되어있다.

  • 위치 데이터는 32-bit float 값으로 저장
  • 각각의 위치는 세개의 값으로 구성
  • 세가지 값 사이 공간이나 다른 값은 없다. 배열이 값들로 꽉 채워져있다.
  • 첫번째 값은 버퍼의 시작점이다.

glVertexAttribPointer 함수로 OpenGL에게 vertex 데이터를 어떻게 해석해야하는지 알려준다.

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

glVertexAttribPointer 함수는 조금 많은 인자를 가진다.

  • 첫번째는 어떤 vertex attribute를 환경 설정 원하는지 명시. vertex shader에서 layout (location = 0)와 position vertex attribute의 location을 명시한 걸 기억. 이는 vertex attribute의 position을 0으로 설정했고 이 vertex attribute에 데이터를 보내길 원하기 때문에 0을 전달한다.
  • 두번째는 vertex attribute의 크기를 명시. 여기서 vertex attribute는 vec3이기에 세개의 값으로 구성
  • 세번째는 데이터 형식을 명시. GLSL에서 vec*은 float형으로 되어있다.
  • 네번째는 정규화가 되어있는지 아닌지 명시. 만약에 integer 형의 데이터 타입을 넣는다면, GL_TRUE를 설정. 정수형 데이터는 float형으로 변환될 때 0(또는 부호가 있을 때 -1)과 1로 normalized 되어있다. 여기서는 GL_FALSE.
  • 다섯번째 인자는 stride로 연속적인 vertex attribute들의 간의 크기를 말한다. 다음 위치 데이터의 설정은 정확히 stride로 값을 명시한 것처럼 float형의 3배 크기만큼 떨어져 있다. 배열이 꽉꽉 채워있기때문에(vertex attribute 간의 빈 공간이 없으므로) stride를 0으로 주어 OpenGL이 stride를 알아낼 수 있게 할 수 있다(vertex attribute 간 빈 공간이 없을 경우). vertex attribute가 더 많아질 수록 신경써서 그 간격의 공간을 정의
  • 마지막 인자는 void*(보이드 포인터)형으로 버퍼에서 위치 데이터가 시작하는 곳의 offset. 여기서 배열의 데이터가 시작하는 점이 위치 데이터이므로 값 0이 전달. 각각의 vertex attribute는 VBO에 의해 관리되는 메모리로부터 자기의 데이터를 가져오고 VBO가 그 데이터를 어디서 가져오는지는 glVertexAttribPointer 함수 호출시 현재 GL_ARRAY_BUFFER와 묶인(bound) VBO에 의해 결정. 이전에 정의된 VBO는 계속 glVertexAttribPointer 호출전에 묶여(bound)있기에 vertex attribute 0은 그 vertex 데이터와 관련있다.

이제 OpenGL에게 vertex data를 어떻게 해석해야하는지 명시했고 vertex attribute location을 인자로 받는 glEnableVertexAttribArray 함수 활성화?해야한다. (OpenGL이 vertex attribute array에 접근 가능하게 하는 건지 vertex attribute array가 뭘 할 수 있게 하는지 잘 모르겠음). vertex attribute는 디폴트로 disable이다. 이점으로부터 모든게 준비되었다. → vertex buffer object 사용할 버퍼에 vertex data를 초기화, vertex와 fragment 쉐이더 설정과 OpenGL에 vertex shader의 vertex attribute에 vertex data를 어떻게 연결하는지 알려줬다. OpenGL에서 하나의 object를 그리는 것은 아래와 같다.

// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0);
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object
someOpenGLFunctionThatDrawsOurTriangle();

object를 그릴 때마다 위와 같은 프로세스가 반복해야한다. 만약 수백개의 object를 위와 같이 해야한다면? 적절한 buffer object를 바인딩하고 각각의 object에 관한 vertex attribute를 설정하는 것은 굉장히 벅차고 힘든 일이된다. 이러한 모든 설정들을 하나의 object(객체)에 저장하고 그 상태를 되찾기 위해 그 object를 간단히 바인딩하는 방법이 있다면?

Vertex array object

vertex array object(VAO)는 VBO와 같이 묶여(bind)질 수 있고 차후 vertex attribute는 VAO안에 저장될 지점으로부터 호출. 이는 vertex attribute pointer들을 설정할 때 그러한 호출을 한번에 만들어주게 하고 object를 그리기 원할 때마다 그에 상응하는 VAO로 bind할 수 있다. 다른 VAO를 바인딩하는 거 만큼 쉽게 다른 vertex data와 attribute 설정 사이를 전환하게 만든다. 설정한 모든 상태(state)는 VAO안에 저장된다.

Core OpenGL은 VAO를 사용하여 vertex 입력값과 어떻게 할 것을 알려주는 것을 요구. VAO bind가 실패했을 때 OpenGL은 그리는 것을 거절할 것.

VAO는 다음을 저장

  • glEnableVertexAttribArray 또는 glDisableVertexAttribArray 호출
  • glVertexAttribPointer을 통한 vertex attribute 환경 설정
  • glVertexAttribPointer 호출에 의한 vertex attribute들과 연관된 Vertex buffer objects

    VAO를 생성하는 것은 VBO를 생성하는 것과 비슷하다.
unsigned int VAO;
glGenVertexArrays(1, &VAO);

VAO를 사용하기 위해서 해야할 것은 glBindVertexArray 사용하여 VAO를 binding하는 것. 이지점으로부터 상응하는 VBO와 attribute pointer를 bind와 configure해야하고 그런 뒤에 나중에 사용을 위해서 VAO를 unbind한다. object를 그릴때 그전에 정해놓은 설정과 VAO를 간단하게 bind한다.

// ..:: Initialization code (done once (unless your object frequently changes)) :: ..

// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: Drawing code (in render loop) :: ..
// 4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

다음 내용들 모두 이지점으로부터 이끌어진다. VAO는 vertex attribute 설정과 어떤 VBO을 사용할지 저장되어있다. 보통 여러 object들을 그릴 때 먼저 모든 VAO를 생성, 설정하고 저장. 여러 object들 중 하나를 그리는 순간 그에 상응하는 VAO를 바인드하고 그리고 언바인드한다.

선택한 오브젝트를 그리기위해 OpenGL은 glDrawArrays 함수를 제공한다. 이 함수는 현재 활성화된 shader, 이전에 정의된 vertex attributes 설정과 VAO를 통해 간접적으로 바인딩된 VBO의 vertex를 사용하여 primitives를 그린다.

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays 함수는 첫번째 인자로 primitive type을 두번짼는 vertex array의 시작 index 마지막 세번째는 얼마나 많은 vertex(vertices)를 그릴 것인지 전달

Element Buffer Objects

EBO(element buffer objects) 이 어떻게 동작하는지는 예를 통해서 보는게 좋다.

직사각형을 그린다고 하자 → 삼각형 두 개를 사용하여 그릴 수 있다. (OpenGL은 주로 삼각형을 대상으로 한다.) 그래서 두 vertex(vertices)를 만들어보면

float vertices[] = {
	// first triangle
	0.5f, 0.5f, 0.0f, // top right
	0.5f, -0.5f, 0.0f, // bottom right
	-0.5f, 0.5f, 0.0f, // top left
	// second triangle
	0.5f, -0.5f, 0.0f, // bottom right
	-0.5f, -0.5f, 0.0f, // bottom left
	-0.5f, 0.5f, 0.0f // top left
};

여기서 bottom right와 top left는 겹친다. 4개의 정점만 있으면 만들어지는 직사각형이기때문에 이는 50퍼센트의 오버헤드다. 이는 더 복잡하고 큰 오브젝트를 그릴때 더 큰 오버헤드, 즉 겹치는 부분들이 무수히 많을 것이다. 그럼 어떻게 해야할까?

OpenGL은 이에 대한 해결책으로 EBO 제공, 이는 vertex buffer object와 같은 버퍼이며 OpenGL이 어떤 vertices를 그릴지 결정하기 위해 사용하는 indices를 저장하는 버퍼. 이는 indexed drawing이라 불리며 위에 언급된 문제를 정확하게 해결

float vertices[] = {
	0.5f, 0.5f, 0.0f, // top right
	0.5f, -0.5f, 0.0f, // bottom right
	-0.5f, -0.5f, 0.0f, // bottom left
	-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = { // note that we start from 0!
	0, 1, 3, // first triangle
	1, 2, 3 // second triangle
};

vertices가 6개에서 4개로 줄었고 indices가 추가. 이 후 EBO 생성

unsigned int EBO;
glGenBuffers(1, &EBO);

VBO와 비슷하게 EBO도 바인드하고 glBufferData로 버퍼에 indices를 복사. GL_ELEMENT_ARRAY_BUFFER을 버퍼 타입으로 명시

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

그리고 glDrawArrays 대신 glDrawElements 을 호출. 이는 현재 바인딩된 EBO에서 제공된 indices를 사용하여 그린다.

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawArrays 와 같이 첫번째 인자는 그리는 형식, 두번째는 elements의 수, 세번째는 indices의 타입, 마지막은 EBO에서 offset을 명시. 이 함수는 현재 GL_ELEMENT_ARRAY_BUFFER 에 바인딩된 EBO로부터 indices를 가진다. 이는 복잡하고 큰 indices를 가진 오브젝트를 렌더링할때마다 상응하는 EBO를 바인딩해야하는 것. VAO가 EBO 바인딩들을 계속 찾아가게 만든다.

VAO는 타겟이 GL_ELEMENT_ARRAY_BUFFER 이라면 glBindBuffer 호출을 저장. 이는 또한 unbind 함수도 저장하기에 VAO를 unbind하기전에 element array buffer를 unbind해서는 안되는 것. 그렇지 않으면 설정된 EBO를 가지지 못한다.

// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

아래와 같은 결과물들을 얻을 수 있다.


두번째 결과물은 wireframe mode에서 나온 것. 아래의 코드를 render loop안에 추가하면 확인 가능.

// render loop
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

함수 첫번째 인자는 삼각형의 전면과 후면에 적용하는 플래그를 입력, 두번째는 선으로 나타내는 플래그 입력

profile
Hello everyone

0개의 댓글