momodudu.zip

#4 vertex array, buffer objects 본문

Graphics/OpenGL

#4 vertex array, buffer objects

ally10 2019. 9. 25. 14:58

<Vertex Buffer Object (VBO)>

우리가 사용하고자 하는 vertex data는 DRAM, 즉 client단의 memory에 저장되어 있는것 뿐이다. 이를 graphic memory에 copy하기 위해 사용하는 명령어가 바로 glDrawArrays, glDrawElements같은 call이다.

하지만 매 draw call마다 CPU<->GPU간의 데이터 교환이 자주 일어나게 된다면 memory bandwidth로 인해 당연히 성능상에 문제가 발생하므로, CPU에서 그릴 vertex를 생성해놓고, "캐시"처럼 사용하고자 하는게 바로 VBO이다.

 

OpenGL ES3.0에서는 vertex buffer인 GL_ARRAY_BUFFER와 index버퍼인 GL_ELEMENT_ARRAY_BUFFER , 두가지 buffer object 타입을 제공한다. 그 외에도 uniform buffer, transform feedback buffer, pxiel unpack/pack buffer, copy buffer등 많은 cache type의 buffer를 제공한다. 현재 포스팅에선 vertex array, element buffer만 다룬다.

 

 

1. Buffer생성

buffer 생성 과정은 아래와 같다.

Buffer생성(glGenBuffer) -> Buffer 바인딩(glBindBuffer) -> Buffer에 데이터 넣기(glBufferData)

GLuint Renderer::CreateVBO(const GLfloat* data)
{
	GLuint vertexBuffer;

	/// 버퍼 생성
	/// @ param : 버퍼 개수, 버퍼 name array
	glGenBuffers(1, &vertexBuffer);

	/// 버퍼 바인딩
	glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);

	/// 버퍼에 데이터 저장
	glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);

	return vertexBuffer;
}

1) glGenBuffer(GLsizei n, GLuint *buffers)

리턴될 buffer object의 name 개수 및 그 vbo name이 담긴 n개의 array가 리턴된다.

 

2) glBindBuffer(GLenum target, GLuint buffer)

타겟은 위에서 설명했던 buffer object type들을 지정할 수 있다. 여기서는 vertex만 사용할것이므로 GL_ARRAY_BUFFER를 사용한다. 그리고 binding할 버퍼 타겟을 파라미터로 넘긴다.

여기서 사용하는 target enum인 buffer usage에 대해서 짤막하게 설명하자면, 위 예제에서 넘긴 GL_STATIC_DRAW는 수정이 딱 한번만 되고, 특정 primitive를 많이, 자주그릴때 사용한다. STATIC/DRAW/STREAM으로 나뉘며 이는 한번 수정되는가? 혹은 그 이상 수정되는가? 에 달려있다. STREAM의 경우에는 몇번 사용하지 않을 때 쓴다. operation은 draw, read, copy가 있다. draw는 말그대로 그릴 때 사용되고 read는 openGLES로부터 read back이 필요할 때 쓴다. copy는 바로 primitive를 그릴 때 사용된다고 한다.

 

3) glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage)

buffer object type, size, 실제 사용될 vertex data, buffer usage를 넘긴다.

 

위 과정까지는 실제로 CPU에서 DRAM을 alloc하는것과 똑같은 루틴으로, vram allocation이 발생하므로 한번만 해주고 vbo name으로 관리를 해주면 된다.

 

2. 생성한 Buffer를 이용해서 그리기

이제 이 생성한 VBO를 가지고 어떻게 그리는가? 에 대한 설명이다.

void Renderer::DrawTriangle() 
{
	glUseProgram(m_shaderProgram);

	/// shader에 지정된 레이아웃 location index를 enable
	glEnableVertexAttribArray(IN_VERTEX_LOCATION);

	/// 생성한 vbo를 바인딩.
	glBindBuffer(GL_ARRAY_BUFFER, m_vboName);

	/// shader에 어떻게 attribute를 보낼것인지 알려준다.
	/// IN_VERTEX_LOCATION에 3개의 FLOAT TYPE의 데이터를 넣을것이고,
	/// 정규화는 하지 않고 스트라이드는 없다.
	/// 마지막 param은 vbo에 데이터가 저장되어있으므로 null처리
	glVertexAttribPointer(IN_VERTEX_LOCATION, 3, GL_FLOAT, GL_FALSE, 0, 0);

	/// draw call. triangles로 그린다.
	glDrawArrays(GL_TRIANGLES, 0, 3);

	/// attribute location disable
	glDisableVertexAttribArray(0);
}

생성했으면 그리는 과정은 간단하다.

먼저 shader에 어떤 attribute index로 데이터가 들어갈것인지 알려주고, vbo를 바인딩 한 이후에 다시 shader에게 attribute들이 이렇게 들어갈것이라고 알려준다. 해당 location에 몇개의, 어떤타입에 데이터가 들어갈것인지. 그리고나서 primitive type을 지정해준 후 그리면 된다.

 

1) glEnableVertexAttribArray(GLuint index)

vertex shader에 어떤 attribute index로 데이터가 들어갈지 알려주는 call이다. 여러가지 지정 방법이 있는데, 나는 쉐이더에 layout 한정자를 이용해서 쓰는걸 좋아해서, 위 코드에서 쓴것처럼 VERTEX/TEXCOORD 이런식으로 define을 정의해서 index를 전달한다. 

 

2) glBindBuffer(GLenum target, GLuint buffer)

buffer binding. 추가로, vbo를 쓰지 않고 매 프레임 CPU->GPU로 데이터를 전달하고싶다면 bind 마지막 인자를 NULL로 주고  아래 3)의 마지막 파라미터로 데이터를 전달해주면 된다.

 

3) glVertexAttribPointer(GLuint index, GLuint size, GLuint type, GLboolean normalized, GLsizei stride, const void*)

vertex shader에다가 1)에서 지정한 index에 어떤 "포맷"으로 데이터가 들어갈것인지 지정해주는것이다.

위 예제에서는 3개의 float type data가 정규화 되지 않고 스트라이드가 없는 상태로 들어가는것을 의미한다.

 

4) glDrawArrays(GLenum mode, GLint start, GLsizei count)

primitive가 그려질 모드( triangles, triangle pan, triangle strip 등등... )와 시작 인덱스, vertex개수를 전달하고 draw한다.

 

 

<Vertex Array Object(VAO)>

VAO는 OepnGL ES3.0에 새로 도입된 feature인듯하다. 위에서 설명했듯이, 하나의 VBO를 만들고 나서 drawCall까지 꽤 많은 명령어를 수행해야 된다. glBindBuffer() -> glVertexAttribPointer() -> glEnableVertexAttriArray() 이 일련의 call이 매 프레임마다 발생한다. 이러한 vertex configuration과정을 줄여 도입한것이 VAO라고 한다. 기본적으로 OpenGL ES3.0에는 default VAO가 있어 따로 생성하지 않아도 default VAO로 그릴 수 있다.

 

어렵게 설명이 되어있지만, VBO로 draw할때 쓰는 일련의 콜까지 묶어서, 벡터 state로 encapsulation해놓은것이 VAO라고 보면 된다. 즉, VBO생성 후 draw call에서 일어나는 call을 VAO로 묶고, draw call에선 VAO를 바인딩해서 drawElements call만 날려주면 된다.