개발 · 컴퓨터공학 / / 2022. 3. 23. 18:33

OpenGL 공부일지 - OpenGL Super Bible 버텍스 프로세싱 및 드로잉 커맨드 - 1

728x90
반응형

Vertex Processing and Drawing Commands

opengl 파이프라인의 초반부 스테이지를 깊게 공부해보자. 어떤 것들이냐면, 버텍스 어셈블리 및 버텍스 쉐이딩에 대해서 공부할 것이다. 드로잉 커맨드의 구성과 opengl 파이프라인에 작업을 전달하는 방법, primitive를 통한 rasterization수행에 대해서 알아보자.

 

Vertex Processing

opengl 파이프라인에서 프로그래밍 가능한 첫 스테이지가 vertex shader이다. 쉐이더 수행 전, opengl이 vertex fetch 스테이지에서 입력을 vertex shader로 가져간다. 버텍스 쉐이더는 단지 버텍스의 위치를 파이프라인의 다음 스테이지로 보내는 역할을 한다. 또한 opengl 버텍스에 추가 정보인 다른 사용자의 정의 및 내장 출력도 가능하다.

Vertex Shader Inputs

opengl 파이프라인의 첫 단계는 vertex fetch stage이다. vertex 에는 다양한 속성들이 있다.

void glVertexAttribFormat(GLuint attribindex, GLint size,
        GLenum type, GLboolean normalized,
        GLuint relativeoffset);
void glVertexAttribBinding(GLuint attribindex,
	GLuint bindingindex);
void glBindVertexBuffer(GLuint bindingindex,
        GLuint buffer,
        GLintptr offset,
        GLintptr stride);

glVertexAttribPointer()함수를 통해 버텍스 속성을 거의 설정할 수 있지만, 위 함수들과 같이 저수준 함수들로 결정할 수 있다.

#version 450 core

// Declare a number of vertex attributes
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 tex_coord;

// Note that we intentionally skip location 3 here
layout (location = 4) in vec4 color;
layout (location = 5) in int material_id;

위 쉐이더 코드는 레이아웃 지시어를 사용해서 입력 위치를 설정하였다. 이를 C언어 구조체로 표현하면 다음과 같다.

typedef struct VERTEX_t
{
    vmath::vec4 position;
    vmath::vec3 normal;
    vmath::vec2 tex_coord;
    GLubyte color[3];
    int material_id;
} VERTEX;

각 요소가 되는 변수들에 대해서 알아보자. 위 구조체의 변수들은 glVertexAttribFormat()을 통해 값을 지정할 수 있다.

position은 버텍스의 위치로 vec4로 이루어져있다. 

normal은 버텍스의 geometry normal이고, tex_coord는 2차원 텍스처 좌표에 대한 정보이다. 

color값은 vec4로 선언하지만 위 VERTEX_t의 구조체에서는 3byte 배열이다. opengl에서 데이터를 버텍스 쉐이더로 읽을 때 변환해주니 걱정할 필요없다. 값의 범위를 옵션을 통해 정규화한 0~1.0 범위 혹은 0~255 범위 둘 다 설정할 수 있다.

 

버텍스 속성에 대한 타입들은 위 표와 같은 종류가 있는데 그냥 있다는 것만 알고 자세히 사용할 필요성을 느끼기 전에는 그냥 넘어가자.

 

void glVertexAttribIFormat(GLuint attribindex,
    GLint size,
    GLenum type,
    GLuint relativeoffset);

위 함수는 material_id 필드를 사용해서 정수값을 버텍스 쉐이더에 전달하는 함수이다. glVertexAttribFormat()과는 조금 다르다.

 

// position
glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, offsetof(VERTEX, position));

// normal
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, offsetof(VERTEX, normal));

// tex_coord
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, offsetof(VERTEX, texcoord));

// color[3]
glVertexAttribFormat(4, 3, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(VERTEX, color));

// material_id
glVertexAttribIFormat(5, 1, GL_INT, offsetof(VERTEX, material_id));

위와 같은 코드로 각각의 구조체의 요소들 즉 버텍스 속성들에 대해서 설정한다.

이 이후에는 opengl에 버퍼를 사용해서 데이터를 읽을 것을 전달해주어야 한다.

 

각각의 버텍스 쉐이더는 입력 속성을 가질 수 있고, opengl이 그 내용을 버퍼로부터 읽어 데이터를 제공한다. 버텍스 속성 중 일부는 버퍼 내 공간을 공유 가능해서 위 구조체에서 다른 속성 요소가 다른 버퍼에 있는 경우도 있다. 이 경우 각각의 입력에 대해서 어떤 버퍼 객체를 사용할지 지정해야하고, 지정한 것들을 그룹지어 버텍스 바인딩 포인트에 연결시킬수 있다. 만약 바인딩 포인트에 연결시킨 버퍼를 변경하면 변경된 버퍼의 매핑된 속성 데이터로 변경된다.

 

glVertexAttribBinding(0, 0); // position
glVertexAttribBinding(1, 0); // normal
glVertexAttribBinding(2, 0); // tex_coord
glVertexAttribBinding(4, 0); // color
glVertexAttribBinding(5, 0); // material_id

위 glVertexAttribBinding()은 버텍스 쉐이더 입려고가 버퍼 바인딩 포인트 간 매핑을 만들기 위한 함수이다. 

인자는 각각 버텍스 속성의 인덱스와 버퍼 바인딩 포인트 인덱스이다.

위 코드에서는 같은 버퍼 바인딩 포인트 인덱스를 사용하여 같은 버퍼에 모든 버텍스 속성을 저장하였다.

glVertexAttribBinding(0, 0); // position
glVertexAttribBinding(1, 0); // normal
glVertexAttribBinding(2, 0); // tex_coord
glVertexAttribBinding(4, 1); // color
glVertexAttribBinding(5, 2); // material_id

하지만 위와 같이 서로 다른 바인딩 포인트를 설정하여 버퍼를 구분할 수도 있다. 이 코드에서는 color는 두 번째 버퍼에, material_id는 세 번째 버퍼에 저장되었다.

 

이렇게 바인딩 포인트를 설정했다면 버퍼 객체를 매핑에 사용될 바인딩 포인트에 바인딩하기 위해 glBindVertexBuffer()를 호출한다.

Vertex Shader Outputs

버텍스 쉐이더의 버텍스 데이터를 결정했다면, 데이터를 출력으로 보내야한다. 

out gl_PerVertex
{
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
};

위와 같은 gl_PerVertex 인터페이스 블록이 필요하다. 내부의 gl_Position은 내장 출력 변수이고, gl_ClipDistance는 클리핑을 위해 사용되는데 후에 자세히 알아보자.

Variable Point Sizes

glPointSize()를 통해 opengl이 그리는 점의 크기를 변경할 수 있다. glGetIntegerv()를 통해 점의 최댓값도 알 수 있다.

점의 크기는 버텍스 쉐이더에서 gl_PointSize를 통해 설정할 수 있다. 

glEnable(GL_PROGRAM_POINT_SIZE);

설정한 후 위 함수를 통해 점 크기를 적용한다. 사실 이는 뷰어로부터의 거리에 기반한 크기이고, glPointSize() 함수를 사용하면 위치에 상관없이 동일한 크기를 갖도록 할 수 있다.

위 공식은 거리 기반의 점 크기 감쇄를 구현할 때 사용된다. d는 눈으로부터의 점의 거리, a,b,c는 2차 방정식에 대한 설정 변수이다. 이 변수들을 유니폼에 저장하여 애플리케이션에서 갱신할 수 있다. 원하는 값이 있다면 버텍스 쉐이더에서 상수 설정도 가능하다. 

 

Drawing Commands

opengl의 드로잉 커맨드는 glDrawArrays()이외에도 다양하다.

Indexed Drawing Commands

대표적으로 glDrawArrays()는 인덱스되지 않은 드로잉 커맨드이다. 무슨 말이냐면 버텍스가 순서대로 처리되고, 버퍼에 저장된 버텍스 속성과 연관된 버텍스 데이터들은 모두 버퍼에 나타나는 순서대로 버텍스 쉐이더에 제공된다.

 그렇다면 인덱스 드로우의 경우 어떨까?

버퍼의 각 데이터를 배열로 간주해서, 배열에 대한 인덱스를 사용하지만, 다른 인덱스의 배열을 사용해서 읽는다.

즉 인덱스를 읽은 다음에 opengl이 이 인덱스를 사용해서 배열을 참조한다.

void glDrawElements(GLenum mode,
    GLsizei count,
    GLenum type,
    const GLvoid * indices);

위 함수는 인덱스 드로잉 커맨드를 호출하는 함수이다.

인덱스 드로우에 사용되는 인덱스들은 opengl에서 위 그림처럼 사용된다. 그림처럼 배열에 저장된 인덱스들을 따라 인덱스에 해당하는 vertex의 번지를 찾아가는 것이다.

그 외에 opengl의 드로잉 커맨드는 위 함수들과 같은 것들이 있다.

 

인덱스된 드로우를 사용하면 필요한 버텍스만을 사용하여 버텍스 데이터 개수를 줄이는 것이 용이하다. 

static const GLfloat vertex_positions[] =
{
    -0.25f, -0.25f, -0.25f,
    -0.25f, 0.25f, -0.25f,
    0.25f, -0.25f, -0.25f,
    0.25f, 0.25f, -0.25f,
    0.25f, -0.25f, 0.25f,
    0.25f, 0.25f, 0.25f,
    -0.25f, -0.25f, 0.25f,
    -0.25f, 0.25f, 0.25f,
};

static const GLushort vertex_indices[] =
{
    0, 1, 2,
    2, 1, 3,
    2, 3, 4,
    4, 3, 5,
    4, 5, 6,
    6, 5, 7,
    6, 7, 0,
    0, 7, 1,
    6, 0, 2,
    2, 4, 6,
    7, 5, 3,
    7, 3, 1
};

glGenBuffers(1, &position_buffer);
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);
glBufferData(GL_ARRAY_BUFFER,
    sizeof(vertex_positions),
    vertex_positions,
    GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);

glGenBuffers(1, &index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
    sizeof(vertex_indices),
    vertex_indices,
    GL_STATIC_DRAW);​

 

이전에 (36개의 삼각형 * vec3 = 432byte)였던 것이, (8개 꼭짓점 * vec3 = 72byte)와 36개의 16bit정수 (72byte)로 총 144byte가 되어 상당히 감소시킬 수 있다.

 

// Clear the framebuffer with dark green
static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, green);

// Activate our program
glUseProgram(program);

// Set the model-view and projection matrices
glUniformMatrix4fv(mv_location, 1, GL_FALSE, mv_matrix);
glUniformMatrix4fv(proj_location, 1, GL_FALSE, proj_matrix);

// Draw 6 faces of 2 triangles of 3 vertices each = 36 vertices
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0);​

위 코드는 앞에서 인덱스시킨 정육면체를 geometry로 그리는 코드이다. glUniforMatrix을 통해서 회전행렬을 실행한다. 

36개의 버텍스를 그리고 있지만, 인덱스를 사용하여 오직 8개의 버텍스로 이루어진 배열을 참조한다. 

이렇게 렌더링하면 회전하는 큐브예제와 똑같은 결과를 볼 수 있다.

(github 소스코드에서 해당 예제는 indexedcube project이다)

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유