Skip to content
This repository has been archived by the owner on Feb 24, 2024. It is now read-only.

Ch06 Vertex Attributes, Vertex Arrays, and Buffer Objects

Hyunwoo Jo edited this page Sep 18, 2023 · 1 revision

6장. Vertex Attributes, Vertex Arrays,

and Buffer Objects

이 챕터에서는 OpenGL ES 3.0 에서 vertex attributes 와 data 를 어떻게 다루는지에 대해 배웁니다.

구체적으로는,

  • vertex attributes 가 무엇인지 배웁니다.
  • vertex attributes 를 어떻게 지정하는지, 지원하는 Data 포멧은 무엇인지 배웁니다.
  • vertex attributs 를 어떻게 vertex shader 에 binding 해 사용하는지에 대해 배웁니다.

이 챕터를 모두 읽고나면 vertex attributes 에 대한 이해와 이를 이용해 어떻게 OpenGL 이 primitives 를 그리는지에 대해서 말할 수 있습니다.

VertexData


vertex data (a.k.a vertex attributes) 는 정점별 정보(per-vertex)를 지정합니다.

이 정점별 정보는 각각의 정점에 개별적으로 지정 할 수 있고, 혹은 const value 를 이용해 모든 정점에 대해서 사용할 수도 있습니다.

예시로, 단일 색상을 가진 삼각형을 그리고자 할때, const value 를 선언해 삼각형의 세 정점 모두에서 사용할 수 있습니다.

하지만, 각 정점의 위치는 모두 다를것이기 때문에 우리는 vertex array 에 세 정점의 위치 정보를 저장할 필요가 있습니다.

Specifying Vertex Attribute Data


Vertex attribute data 는 vertex array를 이용함으로서 각 vertex 에 대해 지정할 수 있고, 혹은 constant value 를 사용해 모든 primitive 의 모든 정점들에게 일괄적으로 적용 할 수도 있습니다.

모든 OpenGl Es 3.0 의 구현채들은 적어도 16개의 Vertex attributes 를 지원합니다.

또한, query 를 통해 각 실행환경에서 몇개의 vertex attributes 를 지원하는지 확인해 볼 수도 있습니다.

GLint maxVertexAttribs; // 반드시 16보다는 커야합니다.
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs); 
  • 실행결과

    스크린샷 2023-08-24 오전 12.29.57.png

    16개의 vertex attribs 를 지원하는걸 확인 할 수 있습니다.

Constant Vertex Attribute

constant vertex attribute 는 동일 primitive 에 대한 모든 정점에 대해 동일하기 때문에, 단 한번만 지정해주면 됩니다.

아래의 함수를 통해 지정할 수 있습니다.

void glVertexAttrib1f(GLuint index, GLfloat x);
void glVertexAttrib2f(GLuint index, GLfloat x, GLfloat y);
void glVertexAttrib3f(
	GLuint index, 
	GLfloat x, 
	GLfloat y, 
	GLfloat z
);
void glVertexAttrib4f(
	GLuint index, 
	GLfloat x, 
	GLfloat y, 
	GLfloat z, 
	GLfloat w
);

void glVertexAttrib1fv(GLuint index, const GLfloat *values);
void glVertexAttrib2fv(GLuint index, const GLfloat *values);
void glVertexAttrib3fv(GLuint index, const GLfloat *values);
void glVertexAttrib4fv(GLuint index, const GLfloat *values);
// glVertexAttrib*fv 의 'v' suffix는 vector 를 의미한다고 합니다.

glVertexAttrib* 함수들은 일반적인 vertex attribute 를 index 에 의해 지정합니다.

예를 들어,

glVertexAttrib1f, glVertexAttrib1fv 는 (x, 0.0, 0.0, 1.0) 을 일반적인 vertex attribute 로 지정합니다.

glVertexAttrib2f, glVertexAttrib2fv 는 (x, y, 0.0, 1.0) 을,

glVertexAttrib3f, glVertexAttrib3fv 은 (x, y, z, 1.0)을,

glVertexAttrib4f, glVertexAttrib4fv 은 모든 값인 (x, y, z, w) 를 일반적인 vertex attribute 로 지정합니다.

Vertex Arrays


Vertex array 는 각 정점들에 해당하는 정보를 지정하고, 이 버퍼는 application 의 주소 공간에 저장됩니다. (이를 OpenGL ES 에서는 Client Space 라 부릅니다.)

Vertex array 는 vertex buffer object 의 기저(basis) 를 효율적이고 유연한 방법으vertex attribute data 를 지정함으로서 제공합니다.

Vertex array는 glVertexAttribPointerglVertexAttribIPointer 함수를 통해 지정 할 수 있습니다.

void glVertexAttribPointer(
	GLuint index, GLint size, GLenum type,
	GLboolean normalized, SLsizei stride, const void *ptr)

void glVertexAttribIPointer(
	GLuint index, GLint size, GLenum Type,
	GLsizei stride, const void *ptr)

--------------------------
index: vertex attribute  index를 지정합니다.
유효한 범위는 (0 < length - 1) 입니다.

size: vertex attribute  구성 요소 개수를 지정해줍니다.
유효한 범위는 (1 < size <= 4) 입니다.

type: vertex Attribute data  data type  의미합니다.
	GL_FLOAT:  값은 float 형식의 데이터를 의미합니다. 
		예를 들어, 텍스처 좌표나 법선 벡터와 같은 부동 소수점 형태의 데이터를 나타낼  사용됩니다.
	GL_INT:  값은 정수(int) 형식의 데이터를 의미합니다. 
		예를 들어, 정수로  인덱스나 어떤 속성을 표현할  사용됩니다.
	GL_UNSIGNED_BYTE:  값은 부호 없는 8비트 바이트(byte) 형식의 데이터를 의미합니다. 
	예를 들어, 색상 값을 나타내는 경우에 사용될  있습니다.
	GL_SHORT:  값은 짧은(short) 정수 형식의 데이터를 의미합니다. 
		예를 들어, 어떤 정점 속성 값을 표현할  사용될  있습니다.
... 등등 여러 data type  지원합니다.

normalized: glVertexAttribPointer 에서만 요구합니다.
	non-floating data format type  floating-point data type 으로
	변환  , normalized 되어야 하는지를 표시합니다.
	glVertexAttribIPointer 에서는 값들은 정수로 취급됩니다.

stride: 버퍼 내에서 연속된 데이터간의 간격을 나타냅니다.
	, index i  정점 V(i)  V(i + 1)  차이를 의미합니다.
	만약 stride  0 이라면, 모든 정점에 대한 attribute data  사용되고 있습니다.

ptr: vertex attribute data  가리키고 있는 pointer 입니다.
	client-side vertex array  사용할경우, 배열의 포인터를,
	VBO  사용할 경우 오프셋 값을 넘겨주어야 합니다.

예제를 보며 glVertexAttribPointer 의 작동방식을 알아봅시다.

일반적으로 vertex attribute data 를 할당하는데에는 두 방법이 있는데요,

  • vertext attribute data 를 하나의 버퍼 안에 저장하는 방법 ( an array of structures 라고 불립니다.)과
  • 각각 의 vertex attribute 를 분리된 버퍼 안에 저장하는 방법 ( structure of arrays 라고 불립니다.)이 있습니다.

각각의 정점이 네개의 vertex attributes 를 가진다고 생각해 봅시다. 각 속성은 위치, 법선 벡터(normal), 그리고 두개의 텍스처 좌표를 의미합니다.

이 속성은 모든 정점에 대해 할당되어 한 버퍼에 같이 저장될 때, 정점의 위치 속성은 3개의 float 백터로 표현됩니다. (x, y, z), 법선 벡터 또한 세개의 float 벡터로 표시되고, 각 텍스쳐 좌표는 두개의 float 벡터로 표현 될 수 있습니다.

IMG_9AB0D37DBD10-1.jpeg

정점의 위치 (x, y, z), 법선백터 (x, y, z), 텍스처 좌표 0(x, y), 텍스처 좌표 1(x, y)

위의 그림은 버퍼의 메모리 구조를 표현합니다.

이 경우에는, 버퍼의 간격은 모든 속성의 크기의 합과 같습니다. (12 byte (위치) + 12 byte (법선벡터) + 8 byte (텍스처 0번) 8 byte (텍스처 1번) ⇒ 10 float, 혹은 40 byte )

아레의 예제코드는 4개의 정점 속성을 어떻게 glVertexAttribPointer 를 통해 지정하는지 설명합니다.

client-side vertex array 를 정점 별 데이터를 설정하는 법을 설명하기 위해 사용하는 점을 기억하세요.

퍼포먼스를 위해서는 client-side vertex array 가 아닌, VBO 를 사용해야 합니다.

이는 GLES 2.0 을 위한 하위호환을 지원하기 위해 남은 기능입니다. OpenGL ES 3.0 에서는 VBO 사용이 권장됩니다.

  • ex) an array of structures

    #define VERTEX_POS_SIZE 3
    #define VERTEX_NORMAL_SIZE 3
    #define VERTEX_TEXCOORD0_SIZE 2
    #define VERTEX_TEXCOORD1_SIZE 2
    
    #define VERTEX_POS_INDX 0
    #define VERTEX_NORMAL_INDEX 1
    #define VERTEX_TEXCOORD0_INDX 2
    #define VERTEX_TEXCOORD1_INDX 3
    
    #define VERTEX_POS_OFFSET 0
    #define VERTEX_NORMAL_OFFSET 3
    #define VERTEX_TEXCCORD0_OFFSET 6
    #define VERTEX_TEXCCORD1_OFFSET 8
    
    #define VERTEX_ATTRIB_SIZE (VERTEX_POS_SIZE + \
    	VERTEX_NORMAL_SIZE + \
    	VERTEX_TEXCOORD0_SIZE + \
    	VERTEX_TEXCOORD1_SIZE)
    
    float *p = (float*) malloc(
    	numVertices * VERTEX_ATTRIB_SIZE * sizeof(float)
    );
    
    // 위치정보는 속성 0 번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_POS_INDX, 
    	VERTEX_POS_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	p
    );
    
    // 법선벡터는 속성 1번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_NORMAL_INDEX, 
    	VERTEX_NORMAL_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	(p + VERTEX_NORMAL_OFFSET)
    );	
    
    // 텍스쳐 좌표 0 번은 속성 2번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_TEXCOORD0_INDX,
    	VERTEX_TEXCOORD0_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	(p + VERTEX_TEXCCORD0_OFFSET)
    );
    
    // 텍스쳐 좌표 1 번은 속성 3번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_TEXCOORD1_INDX,
    	VERTEX_TEXCOORD1_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	(p + VERTEX_TEXCCORD1_OFFSET)
    );

다음의 경우에선 position, normal , texture coordinates 0, 1 이 각각 다른 버퍼에 저장되는 예시입니다.

  • ex) structure of arrays

    float *position = (float*) malloc(
    	numVertices * VERTEX_POS_SIZE * seizof(float)
    );
    float *normal = (float*) malloc(
    	numVertices * VERTEX_NORMAL_SIZE * seizof(float)
    );
    float *texccord0 = (float*) malloc(
    	numVertices * VERTEX_TEXCOORD0_SIZE * seizof(float)
    );
    float *texccord1 = (float*) malloc(
    	numVertices * VERTEX_TEXCOORD1_SIZE * seizof(float)
    );
    
    // 위치정보는 속성 0 번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_POS_INDX, 
    	VERTEX_POS_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	position
    );
    
    // 법선벡터는 속성 1번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_NORMAL_INDEX, 
    	VERTEX_NORMAL_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	normal
    );	
    
    // 텍스쳐 좌표 0 번은 속성 2번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_TEXCOORD0_INDX,
    	VERTEX_TEXCOORD0_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	texcoord0
    );
    
    // 텍스쳐 좌표 1 번은 속성 3번에 저장됩니다.
    glVerTexAttribPointer(
    	VERTEX_TEXCOORD1_INDX,
    	VERTEX_TEXCOORD1_SIZE,
    	GL_FLOAT,
    	GL_FALSE,
    	VERTEX_ATTRIB_SIZE * sizeOf(float),
    	texcoord1
    );

위에서 우리는 정점 속성을 저장하는 가장 대표적인 두 방법의 예시를 봤습니다.

an array of structures, structure of arrays.

따라서 다음과 같은 질문이 생겼을 겁니다.

어떤 방법이 가장 효율적이지?

대부분의 경우, 정답은 array of structures 입니다. 각각의 정점에 대해 연속적인 방법으로 속성 정보를 읽을 수 있기 때문입니다.

이러한 방법은 대부분의 경우에서 메모리에 효율적으로 접근하는 방식입니다.

array of structures 방식을 사용할 때, 프로그램이 특정 속성을 변경하려하면 비효율이 발생하게 됩니다.

만약 정점 속서의 부분집합이 변경될 필요가 있다면, (텍스쳐 좌표를 변경한다던지) 이 변경은 vertex buffer 의 불연속적인 변경을 발생시킵니다.

vertex bufferbuffer object 로 공급되는 경우, 전체 vertex attribute 버퍼는 다시 로딩될 필요가 있습니다.

동적인 특성이 있는 별도의 버퍼들에 vertex attributes를 저장하는것으로 이 비효율을 피할 수 있습니다.

정점 속성의 Data Format

glvertexAttribPointertype 인자에 의해 지정된 vertex attribute data 포멧은 정점 속성 정보에 대한 그래픽 메모리 저장소의 요구사항 뿐만 아니라, 프레임을 렌더링하는데 필요한 메모리 대역폭의 전체 성능에도 영향을 줄 수 있습니다.

data footprint 작을수록 필요한 메모리 대역폭 또한 줄어듭니다.

OpenGL ES 3.0 은 GL_HALF_FLOAT 이라는 이름으로 16bit floating point vertex 포멧을 지원합니다. 가능하면 어플리케이션에 GL_HALF_FLOAT 를 사용할 것을 권장합니다.

텍스처 좌표, 법선벡터, 종법선벡터 (binormal), 탄젠트 벡터 등은 GL_HALF_FLOAT 을 사용하기 좋은 예시입니다. 색정보는 4개의 컴포넌트가 있는 GL_UNSIGNED_BYTE 로 저장할 수 있습니다. 정점 위치정보는 GL_HALF_FLOAT 을 권장하지만, 필요할 시에 GL_FLOAT 을 사용할 수 있습니다.

glVertexAttribPointer 의 정규화 플래그 동작방식

정점 속성은 vertex shader 에서 사용되기 이전에 내부적으로 single-precision floating-point number 로 저장됩니다.

만약 데이터 타입이 정점 속성이 float 가 아니라는걸 나타내면, 정점 속성은 사용 이전에 float 으로 변환됩니다.

normalized 플래그는 float 가 아닌 정점속성 정보를 single floating-point로 어떻게 변환할지에 대한 여부를 제어합니다.

플래그가 false 면, vertex data 가 직접 float 으로 변환합니다. 이는 float type 이 아닌 정보를 float 으로 형변환 하는것과 유사합니다.

GLfloat f;
GLbyte b;
f = (GLfloat)b; // f 는 [-128.0, 127.0] 이내의 값을 표현합니다. 

정규화 플래그가 true 값을 가진다면, vertex data 는 데이터 유형이 GL_BYTE, GL_SHORT, GL_FIXED 일 경우 [-1.0, 1.0] 범위로 매핑하고, GL_UNSINGNED_BYTE, GL_UNSIGNED_SHORT 인 경우 [0.0, 1.0] 에 매핑합니다.

Vertex Data Format Conversion to Floating Point
GL_BYTE max( c / (2^7 - 1 ), -1.0)
GL_UNSIGNED_BYTE c / (2^8 - 1)
GL_SHORT max( c / (2^16 - 1), -1.0)
GL_UNSIGNED_SHORT c / (2^16 - 1)
GL_FIXED c / 2^16
GL_FLOAT c
GL_HALF_FLOAT_OES c

c 는 각 열에 지정된 타입의 값을 의미합니다.

vertex shader 에서 정수 정점 속성 데이터를 float 으로 변환하지 않고 정수로서 사용할 수도 있습니다.

이 경우 glVertexAttribIPointer 함수를 사용해 vertex shader 에서 속성을 정수 타입으로 선언해야합니다.

Const 정점 속성과 Vertex Array 중에서 선택하기

어플리케이션은 OpenGL ES 가 const 데이터와 vertex array 의 데이터 중 하나를 사용하도록 설정 할 수 있습니다.

IMG_67A9D2BF9DA6-1.jpeg

위의 그림은 OpenGL ES 3.0 에서 이 기능이 어떻게 동작하는지 설명합니다.

glEnableVertexAttribArrayglDisableVertexAttribArray 는 일반적인 정점 속성 배열을 활성화하거나 비활성화 하는데 사용됩니다.

일반적인 정점 속성 배열을 비활성화 하면, 상수 정점속성 정보가 사용됩니다.

void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(GLuint index);
------------------------
index: 일반 정점속성 데이터의 인덱스를 지정합니다.
유효한 범위는 (0 < length - 1) 입니다.

Vertex Shader 에서 Vertex attribute 선언하기

우리는 이제까지 vertex attribute가 무엇인지, 그것들을 어떻게 지정하는지 살펴보았습니다. 이제는 어떻게 셰이더 안에서 선언하는지 보겠습니다.

Vertex Shader 안에선 변수는 in 키워드로 vertex attribute로 선언합니다. 이때, layout 키워드를 추가적으로(optionally) 지정할 수 있습니다. 다음은 예시코드입니다.

layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texcoord;
layout(location = 2) in vec3 a_normal;

in 키워드는 float, vec2, vec3, vec4, int, ivec2, ivec3, ivec4, uint, uvec2, uvec3, uvec4, mat2, mat2x2, mat2x3, mat2x4, mat3, mat3x3, mat3x4, mat4, mat4x2, mat4x3 의 타입에 사용할 수 있습니다.

배열과 구조체타입은 attribute변수가 될 수 없습니다.

다음은 오류 코드 예시입니다.

in foo_t a_A; // foo_t 는 structure이다.
in vec4 a_B[10];

OpenGL ES 3.0 구현체는 GL_MAX_VERTEX_ATTRIBS 개의 4개의 원소가 있는 vector타입만큼 vertex attributes의 수를 지원합니다. 스칼라 타입으로 선언되거나, 2원소 벡터, 3원소 벡터의 의 유형으로 선언된 vertex attributes 들은 모두 4원소 벡터로 취급됩니다. 2차원, 3차원, 4차원 matrix들은 각각 2개, 3개, 4개의 4원소 vector로 새어질 것입니다. uniform과 vertex shader output/fragment shader input 변수들과 달리 컴파일러에 의해 압축(packed) 되지 않습니다. 정점 속성의 최대개수는 한정되있기 때문에, 4성분 벡터보다 작은 크기의 정점 속성을 선언은 가능한 피하는것이 좋습니다.

예컨대 4개의 scalar값을 선언할때 각각 선언하는게 아니라 1개의 vector로 선언하는것이 자원낭비를 줄일 수 있습니다.

vertex shader에서 vertex attribute로 선언된 변수들은 read-only라는 것을 명심하십시오. 다음 코드는 오류 코드 예시입니다.

in vec4 a_pos;
uniform vec4 u_v;
void main()
{
 a_pos = u_v; <--- a_pos 는 읽기 전용이므로 값을 할당 할 수 없습니다.
}

만약 shader안에서 vertex attribute 변수가 선언만 되어있고, 사용되지 않는다면 active상태가 아니라고 컴파일러가 판단하고 최대 개수 제한에 영향을 주지 않습니다.

만약, GL_MAX_VERTEX_ATTRIBS갯수를 넘어선다면 링킹에 실패할 것입니다.

일단 program이 성공적으로 link되었다면, 현재 활성화한 vertex attribute 갯수를 조회해야 할 수도 있습니다.

이 단계는 layout 키워드를 attributes에 적용하지 않을 경우에만 필요합니다.

OpenGL ES 3.0에서는, layout qualifier를 사용하기를 권장합니다. 그러므로 항상 이 정보를 쿼리할 필요는 없습니다.

하지만 완결성을 위해 active vertex attribute 갯수를 조회하는 함수를 소개해드리겠습니다:

glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &numActiveAttribs);

glGetProgramiv 에 대한 자새한 내용은 chapter4에 있습니다.

glGetActiveAttrib 명령을 사용하여 program에서 사용되는 active vertex attribute 목록과 데이터 타입을 조회할 수 있습니다.

void glGetActiveAttrib(
	GLuint program, GLuint index, 
	GLsizei bufsize, GLsizei *length, 
	GLenum *type, GLint *size, 
	GLchar *name)

program: 이전에 성공적으로 연결한 program object의 이름
index: 쿼리할 정점 속성을 지정 (0 < index <= GL_ACTIVE_ATTRIBUTES - 1)
bufsize: null 문자를 포함해, 이름에 기록할 수 있는 최대 문자 수를 지정합니다.
length: 길이가 null 이 아닌 경우, null 문자를 제외한 이름의 길이를 반환합니다.
type: 속성의 유형을 반환합니다.
				GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3,
				GL_FLOAT_VEC4, GL_FLOAT_MAT2, GL_FLOAT_MAT3,
				GL_FLOAT_MAT4, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4, 
				GL_FLOAT_MAT3x2, GL_FLOAT_MAT3x4, GL_FLOAT_MAT4x2, 
				GL_FLOAT_MAT_4x3, GL_INT, GL_INT_VEC2, GL_INT_VEC3, 
				GL_INT_VEC4, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, 
				GL_UNSIGNED_INT_VEC3, GL_UNSIGNED_INT_VEC4
size: 속성의 크기를 반환합니다.
			변수가 배열이 아닌경우 size 는 항상 1 이 됩니다.
			변수가 배열인 경우 size 는 배열의 크기를 반환합니다.
name: vertex shader 에 선언된 속성 변수의 이름.

glGetActiveAttrib 함수 호출은 *index*에 해당하는 attribute의 정보들을 제공합니다. Index는 0에서 GL_ACTIVE_ATTRIBUTES-1 사이의 값이어야 합니다.

0은 첫번째, GL_ACTIVE_ATTRIBUTES-1은 마지막 vertex attribute를 선택합니다.

Vertex Shader 에 Vertex attribute 를 attribute 변수에 바인딩 하기

앞서 vertex shader에서 vertex attribute variables는 in 한정자를 통해 지정되고, 활성 attributes 갯수는 glGetProgramiv 를 통해 , 활성화 상태 attributes 목록들을 getGetActiveAttrib 를 통해 알 수 있음을 알게되었습니다.

또한 0부터 (GL_MAX_VERTEX_ATTRIBS-1) 사이의 generic attribute index가 generic vertex attribute를 활성화 시키고 constant나 array값을 명시하는데 활용하는지 glVertexAttrib* , glVertexAttribPointer 함수를 통해 설명하였습니다. 이제부터 우리는 이 generic attribute index를 어떻게 vertex shader안의 attribute variable에 매핑시킬지 알아볼 것입니다. 앞으로 알아볼 이것들은 적절한 vertex data를 vertex shader의 vertex attribute variable에 매핑되게 할것입니다.

다음 그림은 어떻게 generic vertex attributes가 명시되는지, 어떻게 vertex shader의 attribute name에 연결되는지 나타냅니다.

Untitled

OpenGL ES 3.0에서 3가지 방식으로 generic vertex attribute index를 attribute variable name에 매핑하는 방식이 있다.

  • Vertex shader source 코드 안에서 layout(location = N) 을 명시함으로써(추천되는 방식)
  • OpenGL ES 3.0가 부여하는 방식
  • Application이 vertex attribute index를 attribute variable name에 바인딩 하는 방식

가장 쉬운 방식은 첫번째 방식으로, layout(location = N) 를 명시하는 것이다. 가장 적은양의 코드로 작성가능하다. 하지만, 특정 경우에 나머지 두가지 옵션들이 적절할 때가 있다. glBindAttribLocation 명령으로 vertex attribute index를 attribute variable name에 바인딩할 수 있다. 이 바인딩 방식은 프로그램이 다음번에 링킹 될 시 효과가 나타난다. 즉, 지금 즉시 링크된 program 안의 바인딩을 바꾸지는 않는다.

void glBindAttribLocation(GLuint program, GLuint index, 
													const GLchar *name)

program: name of a program object
index: generic vertex attribute index
name: name of the attribute variable

만약 name이 전에 바인딩 되어있었다면, 기존의 할당된 바인딩은 새로운 index로 교체된다. 심지어 program object에 shader들이 부착(attached)되기 전에도 호출될 수 있다. 결과적으로 이 함수는 어떠한 attribute name을 바인딩 하더라도 전부 사용될 수 있다. 만약 존재하지 않는 이름이라면 함수 호출이 무시될 것이다.

다른 옵션은 OpenGL ES 3.0이 바인딩 해주도록 하는 방식이다. 이 바인딩 방식은 프로그램이 링크될 시 수행된다. 링킹 단계에서 OpenGL ES 3.0 구현체는 각 attribute variable마다 다음 과정들을 행한다.

  1. 각 attribute variable마다 glBindAttribLocation함수에 의해 이미 바인딩 되어있는지 체크하기.
  2. 만약 이미 바인딩 되어 있다면 해당 index를 사용한다.
  3. 아니라면, 구현체가 generic vertex attribute index를 부여한다.

3번과정은 구현체의 구현에 의존적이다. 즉, (링킹하는) 구현체에 따라 서로 다른 index를 가질 수 있다. Application은 index를 조회하기 위해 glGetAttribLocation 명령을 사용할 수 있다.

GLint glGetAttribLocation(GLuint program, const GLchar *name)

program: program object
name: name of attribute variable

glGetAttribLocation 은 마지막으로 링킹된 program object안의 attribute variable name에 매핑된 generic attribute index를 반환합니다. 만약 name이 활성화 상태의 attribute variable이 아니라면, 혹은 program이 유효하지 않거나 링킹에 실패하였을때, 유효하지 않은 attribute index를 나타내는 -1이 return됩니다.

Vertex Buffer Objects

vertex array로 명시된 vertex data는 client memory에 저장이 됩니다. 이 데이터는 glDrawArrays 함수나 glDrawElements 함수가 호출될때 client memory에서 graphics(GPU) 메모리로 반드시 카피됩니다. 위의 2가지 명령은 chapter 7의 “Primitive Assembly and Rasterization” 장에서 설명될 것입니다. 하지만 우리가 만약 매 draw (glDrawArrays, glDrawElements) 함수 호출마다 카피가 안일어나게 하고 graphics memory에 캐싱을 해놓는다면 훨씬 좋을것입니다. 이 방식은 작은 device에 필요한 rendering performance, memory bandwidth와 power consumption requirements들을 개선시킬것입니다. Vertex Buffer Object(이하 VBO) 가 이러한 도움을 줄 수 있습니다. VBO는 OpenGL ES 3.0 application이 vertex data를 고성능의 graphics memory에 할당 및 캐싱하고, 거기서 render되게 함으로써 primitive가 draw될때 데이터 재전송을 방지합니다. Vertex data뿐만 아니라 primitive의 vertex indices를 나타내어 glDrawElements 에 인자로 들어가는 element indices역시 (VBO가) 캐싱할 수 있습니다.

OpenGL ES 3.0은 두가지 종류의 buffer object를 지원합니다. 1. array buffer objects, 2. element array buffer objects. GL_ARRAY_BUFFER 토근에 의해 정의된 array buffer objects는 vertex data를 담슴니다. GL_ELEMENT_ARRAY_BUFFER토큰에 의해 정의된 element array buffer object는 primitive의 index 정보를 담슴니다. OpenGL ES 3.0에서 지원하는 다른 buffer object 타입들은 책의 다른곳에서 설명합니다. Uniform buffers(Chapter 4), transform feedback buffers(Chapter 8), pixel unpac buffers(Chapter 9), pixel pack buffers(Chapter 11), copy buffers(이 챕터의 copying Buffer Objects 부분). 지금은 vertex attrubutes와 element arrays를 명시하는 buffer object들에 집중하겠습니다.

노트: 성능을 위해 OpenGL ES 3.0 application들은 vertex data, element indices들에 대하여 VBO를 사용할 것을 권함.

Buffer object를 활용하여 render하기 전에, buffer object들을 활당하고 vertex data와 element indices를 적절한 buffer object들에 업로드 해야합니다. 다음은 그 예시 코드입니다:

void initVertexBufferObjects(vertex_t *vertexBuffer,
														GLushort *indices,
														GLuint numVertices,
														GLuint numlndices,
														GLuint *vboIds)
{
		 glGenBuffers(2, vboIds);
		 glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
		 glBufferData(GL_ARRAY_BUFFER, numVertices *sizeof(vertex_t), vertexBuffer,	GL_STATIC_DRAW);
		 // bind buffer object for element indices
		 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds[1]);
		 glBufferData(GL_ELEMENT_ARRAY_BUFFER, numIndices * sizeof(GLushort),	indices, GL_STATIC_DRAW);
}

위 코드 예시는 2개의 buffer object를 생성합니다: vertex attribute data를 저장하기위해, primitive를 구성하는 element indices를 저장하기 위한 버퍼를 각각 생성합니다. 이 예시에서, glGenBuffers 명령은 2개의 사용되지 않는 buffer object 이름(name)들을 얻기 위해 호출됩니다(vboIds에 저장). 사용되지 않는 buffer object 이름들은 vboIds를 통해 리턴되며 각각은 array buffer object와 element array buffer object를 생성하기 위해 사용됩니다. Array buffer object는 한개 또는 그 이상의 primitive의 정점과 관련된 vertex attribute data를 저장하기 위해 사용됩니다. Element array buffer object는 1개 또는 그 이상의 primitive의 index들을 저장하는데 사용됩니다. 실제 array, element 데이터는 glBufferData 를 통해 명시됩니다. GL_STATIC_DRAW가 glBufferData 의 인자로 들어가는것에 주목하세요. 이 값은 어떤방식으로 buffer가 application에의해 접근되는지 나타내는 인자입니다. 좀 더 자새한 내용은 조금 뒤에 설명하겠습니다.

void glGenBuffers(GLsizei n, GLuint *buffers)

n: number of buffer object names to return
buffers: pointer to an array of n entries, where allocated buffer objects are returned

glGenBuffers는 n개의 buffer object 이름(name)들을 할당하고 buffers에 담아서 return합니다. glGenBuffers 에 의해서 return되는 buffer object name들은 0이 아닌 부호없는 정수값(unsigned integer numbers)입니다. 0값은 OpenGL ES에 의해 예약된 값이고 buffer object를 가르키지 않습니다. 이 0 buffer object에 관해서 state를 변경하려는 시도는 error를 유발합니다.

glBindBuffer 명령은 특정 buffer object를 현재 바인딩 되게 만듭니다. Buffer object name(부호없는 정수값)이 첫번째로 glBindBuffer 명령에 의해 바인딩 된다면, 해당 buffer object는 기본 상태값을 할당받습니다. 그리고 만약 이 할당 과정이 실패없이 이루어 진다면, 이 buffer object를 현재의 buffer object로 바인딩합니다.

void glBindBuffer(GLenum target, GLuint buffer)

target: can be set to any of the following targets:
				GL_ARRAY_BUFFER
				GL_ELEMENT_ARRAY_BUFFER
				GL_COPY_READ_BUFFER
				GL_COPY_WRITE_BUFFER
				GL_PIXEL_PACK_BUFFER
				GL_PIXEL_UNPACK_BUFFER
				GL_TRANSFORM_FEEDBACK_BUFFER
				GL_UNIFORM_BUFFER
buffer: buffer object to be assigned as the current object to target

glBindBuffer를 사용하기 이전에 반드시 glGenBuffers를 통해 buffer object name을 할당할 필요는 없다는 것을 명심하십시오. 사용되지 않는 buffer object name을 application이 glBindBuffer에 인자로 넣을수도 있습니다. 그러나, 우리(책의 저자들)는 OpenGL ES application들은 glGenBuffers를 호출하는것을 권하며 해당 함수에서 return된 buffer object name들을 사용할 것을 권장합니다.

Buffer object와 연관된 상태(state)는 다음의 분류로 카테고리화 할 수 있습니다:

  • GL_BUFFER_SIZE: 이것은 glBufferData 에 의해 명시된 buffer object data의 size를 의미합니다. glBindBuffer에 의하여 셋팅되는 초기값은 0.
  • GL_BUFFER_USAGE: buffer object에 저장되 있는 data를 application이 어떻게 사용할지 힌트를 제공하는것(구현체에). 초기값은 GL_STATIC_DRAW이다. 아래의 Buffer Usage에서 각 상태값을 자세히 설명.

Buffer Usage:

Buffer Usage Enum 설명
GL_STATIC_DRAW buffer object data가 한번 수정되고 draw시 여러번 사용된다.
GL_STATIC_READ buffer object data가 한번 수정되고 OpenGL ES가 data를 여러번 읽는데 활용된다.? —> gpu상의 메모리를 cpu가 읽는 경우가 많을때를 의미한다.
GL_STATIC_COPY ?
GL_DYNAMIC_DRAW buffer object data가 여러번 수정되고 draw시 여러번 사용된다.
GL_DYNAMIC_READ ?
GL_DYNAMIC_COPY ?
GL_STREAM_DRAW buffer object data가 한번 수정되고 draw에 적은 횟수로 사용이 된다.
GL_STREAM_READ ?
GL_STREAM_COPY ?

이전에도 언급했지만, GL_BUFFER_USAGE는 오직 힌트입니다. 따라서 (구현체에 따라) application은 buffer object data를 GL_STATIC_DRAW 값으로 설정해놓고 자주 수정을 할 수도 있습니다.

Vertex array data 또는 element array data 저장공간의 생성 및 초기화는 glBufferData 명령을 통해 이루어 집니다.

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

target: can be set to any of the following targets:
				GL_ARRAY_BUFFER
				GL_ELEMENT_ARRAY_BUFFER
				GL_COPY_READ_BUFFER
				GL_COPY_WRITE_BUFFER
				GL_PIXEL_PACK_BUFFER
				GL_PIXEL_UNPACK_BUFFER
				GL_TRANSFORM_FEEDBACK_BUFFER
				GL_UNIFORM_BUFFER
size: size of buffer data store in bytes
data: pointer to the buffer data supplied by the application
usage: a hint on how the application will use the data stored in 
			the buffer object (refer to Table 6-2 for details)

glBufferData는 먼저 size에 해당하는 데이터를 저장할 공간을 만들것입니다. Data인자가 NULL이라면, uninitialized된 채로 놔둘것입니다. 만약 data인자가 유효한 포인터라면, data의 내용물이 copy될 것입니다. Buffer object data공간의 초기화 또는 수정은 glBufferSubData 명령을 통해 할 수 있습니다.

void glBufferSubData(GLenum target, GLintptr offset, 
										GLsizeiptr size, const void *data)

target: can be set to any of the following targets:
				GL_ARRAY_BUFFER
				GL_ELEMENT_ARRAY_BUFFER
				GL_COPY_READ_BUFFER
				GL_COPY_WRITE_BUFFER
				GL_PIXEL_PACK_BUFFER
				GL_PIXEL_UNPACK_BUFFER
				GL_TRANSFORM_FEEDBACK_BUFFER
				GL_UNIFORM_BUFFER
offset: offset into the buffer data store and number of bytes of the
size: data store that is being modified
data: pointer to the client data that need to be copied into the 
			buffer object data storage

glBufferData나 glBufferSubData함수를 통해 buffer object data가 초기화/수정 되었으면 클라이언트쪽의 data는 필요없고, 해제될 수 있습니다. 정적인 geometry를 그린다면 application은 클리이언트 data는 해제시킨 후 전체적인 메모리 사용량을 줄일 수 있습니다. 동적인 geometry를 그릴때는 불가능합니다.

💡 glBufferData는 새로운 메모리를 항상 새롭게 할당한 뒤, data를 채워넣는 반면, glBufferSubData는 기존의 메모리를 활용하며 따라서 기존의 연산이 다 끝날때 까지 기다린 후 데이터를 변경하는 동기화가 발생할 수 있다. 따라서 성능상으로 glBufferSubData는 느릴 수 있으며 주의깊게 사용해야 한다.

이제 하나는 buffer object 를 활용한, 하나는 buffer object를 활용하지 않은 primitives를 그리는 예제를 볼것입니다. Vertex attributes를 설정하는 코드들은 서로 비슷하다는 것에 주목하십시오. 이 예제에서는 vertex의 모든 attributes들은 하나의 동일한 buffer object를 사용할것입니다. GL_ARRAY_BUFFER buffer object가 사용될 시, glVertexAttribPointerpointer 인자는 actual data(client의 vertex array)에서 glBufferData로 할당한 vertex buffer store(VBO)의 offset으로 변경됩니다. 비슷하게, GL_ELEMENT_ARRAY_BUFFER object가 사용된다면, glDrawElements 안의 indices 인자는 actual element indices를 가르키는 pointer에서 glBufferData 로 할당한 VBO의 offset 주소로 바뀝니다.

#define VERTEX_POS_SIZE 3	// x, y, and z
#define VERTEX_COLOR_SIZE 4	// r, g, b, and a
#define VERTEX_POS_INDX 0
#define VERTEX_COLOR_INDX 1
//
// vertices - pointer to a buffer that contains vertex
// attribute data
// vtxStride - stride of attribute data / vertex in bytes
// numIndices - number of indices that make up primitives
// drawn as triangles
// indices - pointer to element index buffer
//
void DrawPrimitiveWithoutVBOs(GLfloat *vertices,
	GLint vtxStride,
	GLint numIndices,
	GLushort *indices)
{
	GLfloat *vtxBuf = vertices;
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glEnableVertexAttribArray(VERTEX_POS_INDX);
	glEnableVertexAttribArray(VERTEX_COLOR_INDX);
	glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZㄹE,
		GL_FLOAT, GL_FALSE, vtxStride,
		vtxBuf);
	vtxBuf += VERTEX_POS_SIZE;
	glVertexAttribPointer(VERTEX_COLOR_INDX,
		VERTEX_COLOR_SIZE, GL_FLOAT,
		GL_FALSE, vtxStride, vtxBuf);
	glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
		indices);
	glDisableVertexAttribArray(VERTEX_POS_INDX);
	glDisableVertexAttribArray(VERTEX_COLOR_INDX);
}

void DrawPrimitiveWithVBOs(ESContext *esContext,
	GLint numVertices, GLfloat *vtxBuf,
	GLint vtxStride, GLint numIndices,
	GLushort *indices)
{
	UserData *userData = (UserData*) esContext->userData;
	GLuint offset = 0;
	// vboIds[0] - used to store vertex attribute data
	// vboIds[l] - used to store element indices
	if (userData->vboIds[0] == 0 && userData->vboIds[1] == 0)
	{
		// Only allocate on the first draw
		glGenBuffers(2, userData->vboIds);
		glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
		glBufferData(GL_ARRAY_BUFFER, vtxStride *numVertices,
			vtxBuf, GL_STATIC_DRAW);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
			userData->vboIds[1]);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER,
			sizeof(GLushort) *numIndices,
			indices, GL_STATIC_DRAW);
	}

	glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1]);
	glEnableVertexAttribArray(VERTEX_POS_INDX);
	glEnableVertexAttribArray(VERTEX_COLOR_INDX);
	glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
		GL_FLOAT, GL_FALSE, vtxStride,
		(const void *) offset);
	offset += VERTEX_POS_SIZE* sizeof(GLfloat);
	glVertexAttribPointer(VERTEX_COLOR_INDX,
		VERTEX_COLOR_SIZE,
		GL_FLOAT, GL_FALSE, vtxStride,
		(const void *) offset);
	glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
		0);
	glDisableVertexAttribArray(VERTEX_POS_INDX);
	glDisableVertexAttribArray(VERTEX_COLOR_INDX);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

void Draw(ESContext *esContext)
{
	UserData *userData = (UserData*) esContext->userData;
	// 3 vertices, with (x, y, z),(r, g, b, a) per-vertex
	GLfloat vertices[3 *(VERTEX_POS_SIZE + VERTEX_COLOR_SIZE)] = {-0.5 f, 0.5 f, 0.0 f,	// v0
		1.0 f, 0.0 f, 0.0 f, 1.0 f,	// c0
		-1.0 f, -0.5 f, 0.0 f,	// v1
		0.0 f, 1.0 f, 0.0 f, 1.0 f,	// c1
		0.0 f, -0.5 f, 0.0 f,	// v2
		0.0 f, 0.0 f, 1.0 f, 1.0 f,	// c2
	};

	// index buffer data
	GLushort indices[3] = { 0, 1, 2 };

	glViewport(0, 0, esContext->width, esContext->height);
	glClear(GL_COLOR_BUFFER_BIT);
	glUseProgram(userData->programObject);
	glUniform1f(userData->offsetLoc, 0.0 f);
	DrawPrimitiveWithoutVBOs(vertices,
		sizeof(GLfloat) *(VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
		3, indices);
	// offset the vertex positions so both can be seen
	glUniform1f(userData->offsetLoc, 1.0 f);
	DrawPrimitiveWithVBOs(esContext, 3, vertices,
		sizeof(GLfloat) *(VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
		3, indices);
}

위 예시에서, 우리는 1개의 buffer object를 사용해 모든 vertex data를 담았습니다. 이것은 이전에 살펴보았던 array of structures 방식입니다. 이와 반대로 각 vertex attribute마다 각 buffer object를 사용할 수도 있습니다-이것은 structure of arrays 방식이 될것입니다. 다음에 나오는 예시의 drawPrimitiveWithVBOs 는 각각 분리된 버퍼(structure of arrays)방식을 적용한 예시입니다.

#define VERTEX_POS_SIZE 3	// x, y, and z
#define VERTEX_COLOR_SIZE 4	// r, g, b, and a
#define VERTEX_POS_INDX 0
#define VERTEX_COLOR_INDX 1

void DrawPrimitiveWithVBOs(ESContext *esContext,
	GLint numVertices, GLfloat **vtxBuf,
	GLint *vtxStrides, GLint numIndices,
	GLushort *indices)
{
	UserData *userData = (UserData*) esContext->userData;
	// vboIds[0] - used to store vertex position
	// vboIds[1] - used to store vertex color
	// vboIds[2] - used to store element indices
	if (userData->vboIds[0] == 0 && userData->vboIds[1] == 0 &&
		userData->vboIds[2] == 0)
	{
		// allocate only on the first draw
		glGenBuffers(3, userData->vboIds);
		glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
		glBufferData(GL_ARRAY_BUFFER, vtxStrides[0] *numVertices,
			vtxBuf[0], GL_STATIC_DRAW);
		glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[1]);
		glBufferData(GL_ARRAY_BUFFER, vtxStrides[1] *numVertices,
			vtxBuf[1], GL_STATIC_DRAW);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
			userData->vboIds[2]);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER,
			sizeof(GLushort) *numIndices,
			indices, GL_STATIC_DRAW);
	}

	glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
	glEnableVertexAttribArray(VERTEX_POS_INDX);
	glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
		GL_FLOAT, GL_FALSE, vtxStrides[0], 0);
	glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[1]);
	glEnableVertexAttribArray(VERTEX_COLOR_INDX);
	glVertexAttribPointer(VERTEX_COLOR_INDX,
		VERTEX_COLOR_SIZE,
		GL_FLOAT, GL_FALSE, vtxStrides[1], 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[2]);
	glDrawElements(GL_TRIANGLES, numIndices,
		GL_UNSIGNED_SHORT, 0);
	glDisableVertexAttribArray(VERTEX_POS_INDX);
	glDisableVertexAttribArray(VERTEX_COLOR_INDX);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

application이 buffer object를 다 사용한 후, glDeleteBuffers 명령을 통해 지울 수 있습니다.

void glDeleteBuffers(GLsizei n, const GLuint *buffers)

n: number of buffer objects to be deleted
buffers: array of n entries that contain the buffer objects to be deleted

glDeleteBuffers 는 명시된 buffers를 지웁니다. 지워지고 나면, 그것들은 다른 primitive를 나타내는 vertex attributes나 element indices를 저장하는 새로운 buffer로 사용될 수 있습니다.

예시코드들을 살펴 보면서, vertex buffer object를 사용하는것은 vertex arrays만 사용하는것에 비해 조금의 코드가 추가될 뿐입니다. 이 조금의 추가된 코드는 퍼포먼스를 고려하면 충분히 (추가할) 가치가 있습니다. 다음 챕터에서는 어떻게 glDrawArraysglDrawElements 를 통해 어떻게 primitives를 그리는지 살펴볼것이며 어떻게 primitive assembly와 rasterization 파이프라인이 OpenGL ES 3.0에서 작동하는지 살펴 볼 것입니다.

Vertex Array Object

  • 기존의 vertex array나 VBO를 사용하기 위해 glBindBuffer, glVertexAttribPointer, glEnableVertexAttribArray 를 모두 호출해야 하는 불편함 존재.
  • VAO를 사용하면 이러한 vertex array나 vertex buffer object 설정들을 기억(caching)할 수 있다.

Mapping Buffer Object

  • GPU상의 메모리를 process상에 공유하여 사용할 수 있도록 mapping한다.
  • 데이터를 설정하면, 별도의 카피가 필요 없으므로 성능상으로 VBO보다 좋다.
  • glMapBufferRange 함수:

Untitled01

Untitled02