개발/OpenGL / / 2022. 3. 27. 13:18

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

반응형

Indirect Draws

앞에서 다룬 것들은 직접 드로잉 커맨드였기 때문에 버텍스 개수나 인스턴스 개수와 같은 인자를 드로우 명령에 직접 전달했다.

 하지만 각 드로우의 인자를 버퍼 객체에 저장할 수 있도록 하는 드로잉 커맨드도 있다. 따라서 애플리케이션이 드로잉 커맨드를 호출할 시점에 이 인자를 알지 못하고, 인자가 저장된 버퍼의 위치만을 안다. 이러한 경우 다음과 같은 것들이 가능하다. 

  • 애플리케이션이 드로잉 커맨드의 인자를 미리 생성할 수 있다. 오프라인으로도 가능하고, 버퍼에 로드하여 그릴 준비가 되면 opengl로 보낼 수 있다.
  • opengl을 사용해서 인자를 실시간에 생성하여 쉐이더에서 버퍼 객체에 저장할 수도 있다. 나중에 장면의 일부를 렌더링할 때 사용가능하다.
void glDrawArraysIndirect(GLenum mode,
    const void * indirect);
void glDrawElementsIndirect(GLenum mode,
    GLenum type,
    const void * indirect);

opengl에는 4개의 간접 드로잉 커맨드가 있고, 그 중 위 두 커맨드는 직접 드로잉과 동일하다.

 

typedef struct {
    GLuint vertexCount;
    GLuint instanceCount;
    GLuint firstVertex;
    GLuint baseInstance;
} DrawArraysIndirectCommand;

glDrawArraysIndirect()함수는 위와 같은 버퍼의 데이터 포맷을 가지고있다. 

위 구조체를 통해 indirect함수에 전달하는 것은 glDrawArraysInstancedBaseInstance() 호출과 유사하게 동작한다.

typedef struct {
    GLuint vertexCount;
    GLuint instanceCount;
    GLuint firstIndex;
    GLint baseVertex;
    GLuint baseInstance;
} DrawElementsIndirectCommand;

glDrawElementsIndirect()의 경우 위와 같은 버퍼의 데이터 포맷이다. 각 인자들은 구조체의 필드값으로 설정된다.

위 구조체를 통해 indirect함수에 전달하는 것은 glDrawElementsInstancedBaseVertexBaseInstance() 호출과 유사하게 동작한다.

 

void glMultiDrawArraysIndirect(GLenum mode,
    const void * indirect,
    GLsizei drawcount,
    GLsizei stride);

void glMultiDrawElementsIndirect(GLenum mode,
    GLenum type,
    const void * indirect,
    GLsizei drawcount,
    GLsizei stride);

firstIndex인자의 경우 indices단위이기 때문에 인덱스 타입의 크기만큼 곱하는데 위 함수들은 이 작업을 수월하게 해준다.

 

여러 드로잉 커맨드를 일괄적으로 처리하는 것은 좋지만 한계는 메모리 공간의 제한이다. 명령어가 16이나 20byte정도이므로 시간이 꽤 걸린다. 하지만 수천 수만개 정도의 드로잉 커맨드는 하나의 버퍼로 합칠 수 있다. 드로잉 커맨드를 위한 이자를 버퍼 객체에 미리 로딩하거나 GPU에서 생성하는 등 방법은 있다.

 

소행성 배경을 렌더링하는 코드를 보며 알아보자.

// 7-10
typedef struct {
    GLuint vertexCount;
    GLuint instanceCount;
    GLuint firstVertex;
    GLuint baseInstance;
} DrawArraysIndirectCommand;

DrawArraysIndirectCommand draws[] =
{
    {
        42, // Vertex count
        1, // Instance count
        0, // First vertex
        0 // Base instance
    },
    {
        192,
        1,
        327,
        0,
    },
    {
        99,
        1,
        901,
        0
    }
};

// Put 'draws[]' into a buffer object
GLuint buffer;

glGenBuffers(1, &buffer);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffer);
glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(draws),
    draws, GL_STATIC_DRAW);

// This will produce 3 draws (the number of elements in draws[]), each
// drawing disjoint pieces of the bound vertex arrays
glMultiDrawArraysIndirect(GL_TRIANGLES,
    NULL,
    sizeof(draws) / sizeof(draws[0]),
    0);

위 코드가 간접 커맨드를 사용하는 예시이다. 

// 7-11
object.load("media/objects/asteroids.sbm");

glGenBuffers(1, &indirect_draw_buffer);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirect_draw_buffer);
glBufferData(GL_DRAW_INDIRECT_BUFFER,
    NUM_DRAWS * sizeof(DrawArraysIndirectCommand),
    NULL,
    GL_STATIC_DRAW);

DrawArraysIndirectCommand * cmd = (DrawArraysIndirectCommand *)
    glMapBufferRange(GL_DRAW_INDIRECT_BUFFER,
        0,
        NUM_DRAWS * sizeof(DrawArraysIndirectCommand),
        GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
    
for (i = 0; i < NUM_DRAWS; i++)
{
    object.get_sub_object_info(i % object.get_sub_object_count(),
    cmd[i].first,
    cmd[i].count);
    cmd[i].primCount = 1;
    cmd[i].baseInstance = i;
}

glUnmapBuffer(GL_DRAW_INDIRECT_BUFFER);

위 코드는 간접 드로우 버퍼를 설정하는 코드이다. 

object 클래스의 여러 메시를 한 파일로 저장하는 기능을 사용한다. 이 팔일은 모든 버텍스 데이터를 하나의 버퍼 객체로 로딩하고, 하나의 버텍스 배열 객체에 연결시킨다. 하위 객체들은 시작 버텍스와 버텍스 수를 갖는다. 

object.get_sub_object_info()를 통해 버텍스 값들을 얻을 수 있고, object.get_sub_object_count()를 통해 sbm파일의 하위 객체의 전체 개수를 얻을 수 있다.

// 7-12
#version 450 core

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;

layout (location = 10) in uint draw_id;

위 코드는 어떤 소행성을 그릴지 버텍스 쉐이더에 알리기 위해 버텍스 쉐이더에 입력을 설정하는 작업이다.

간접 드로잉 커맨드로부터 쉐이더로 직접 제공하지는 못하지만, 인스턴스 드로잉 커맨드를 통해 버텍스 속성을 설정하고, 버텍스 쉐이더로 전달할 데이터의 속성 배열 내 인덱스를 간접 드로잉 명령 구조체의 baseInstance 필드에 지정함으로써 전달이 가능하다.

 

// 7-13
glBindVertexArray(object.get_vao());

glGenBuffers(1, &draw_index_buffer);
glBindBuffer(GL_ARRAY_BUFFER, draw_index_buffer);

glBufferData(GL_ARRAY_BUFFER,
    NUM_DRAWS * sizeof(GLuint),
    NULL,
    GL_STATIC_DRAW);

GLuint * draw_index =
    (GLuint *)glMapBufferRange(GL_ARRAY_BUFFER,
        0,
        NUM_DRAWS * sizeof(GLuint),
        GL_MAP_WRITE_BIT |
        GL_MAP_INVALIDATE_BUFFER_BIT);

for (i = 0; i < NUM_DRAWS; i++)
{
    draw_index[i] = i;
}

glUnmapBuffer(GL_ARRAY_BUFFER);

glVertexAttribIPointer(10, 1, GL_UNSIGNED_INT, 0, NULL);
glVertexAttribDivisor(10, 1);
glEnableVertexAttribArray(10);

이전 코드에서 location 10 draw_id라는 속성을 사용해서 드로우 인덱스를 저장했었다.

이 속성은 인스턴스화 될 것이고, 버퍼에 연결되어 고유의 매핑을 위해 사용된다. object의 로더함수로 추가 버텍스 속성을 삽입하기 위해 버텍스 배열/객체에 접근/수정하기 위해 위 코드를 사용한다.

 

// 7-14
#version 450 core

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;

layout (location = 10) in uint draw_id;

out VS_OUT
{
    vec3 normal;
    vec4 color;
} vs_out;

uniform float time = 0.0;

uniform mat4 view_matrix;
uniform mat4 proj_matrix;
uniform mat4 viewproj_matrix;

const vec4 color0 = vec4(0.29, 0.21, 0.18, 1.0);
const vec4 color1 = vec4(0.58, 0.55, 0.51, 1.0);

void main(void)
{
    mat4 m1;
    mat4 m2;
    mat4 m;
    float t = time * 0.1;
    float f = float(draw_id) / 30.0;
    
    float st = sin(t * 0.5 + f * 5.0);
    float ct = cos(t * 0.5 + f * 5.0);
    
    float j = fract(f);
    float d = cos(j * 3.14159);
    
    // Rotate around Y
    m[0] = vec4(ct, 0.0, st, 0.0);
    m[1] = vec4(0.0, 1.0, 0.0, 0.0);
    m[2] = vec4(-st, 0.0, ct, 0.0);
    m[3] = vec4(0.0, 0.0, 0.0, 1.0);
    
    // Translate in the XZ plane
    m1[0] = vec4(1.0, 0.0, 0.0, 0.0);
    m1[1] = vec4(0.0, 1.0, 0.0, 0.0);
    m1[2] = vec4(0.0, 0.0, 1.0, 0.0);
    m1[3] = vec4(260.0 + 30.0 * d, 5.0 * sin(f * 123.123), 0.0, 1.0);
    
    m = m * m1;
    
    // Rotate around X
    st = sin(t * 2.1 * (600.0 + f) * 0.01);
    ct = cos(t * 2.1 * (600.0 + f) * 0.01);
    
    m1[0] = vec4(ct, st, 0.0, 0.0);
    m1[1] = vec4(-st, ct, 0.0, 0.0);
    m1[2] = vec4(0.0, 0.0, 1.0, 0.0);
    m1[3] = vec4(0.0, 0.0, 0.0, 1.0);
    
    m = m * m1;
    
    // Rotate around Z
    st = sin(t * 1.7 * (700.0 + f) * 0.01);
    ct = cos(t * 1.7 * (700.0 + f) * 0.01);
    
    m1[0] = vec4(1.0, 0.0, 0.0, 0.0);
    m1[1] = vec4(0.0, ct, st, 0.0);
    m1[2] = vec4(0.0, -st, ct, 0.0);
    m1[3] = vec4(0.0, 0.0, 0.0, 1.0);
    
    m = m * m1;
    
    // Non-uniform scale
    float f1 = 0.65 + cos(f * 1.1) * 0.2;
    float f2 = 0.65 + cos(f * 1.1) * 0.2;
    float f3 = 0.65 + cos(f * 1.3) * 0.2;
    
    m1[0] = vec4(f1, 0.0, 0.0, 0.0);
    m1[1] = vec4(0.0, f2, 0.0, 0.0);
    m1[2] = vec4(0.0, 0.0, f3, 0.0);
    m1[3] = vec4(0.0, 0.0, 0.0, 1.0);
    
    m = m * m1;
    
    gl_Position = viewproj_matrix * m * position;
    vs_out.normal = mat3(view_matrix * m) * normal;
    vs_out.color = mix(color0, color1, fract(j * 313.431));
}

버텍스 쉐이더 입력 이후, 해당 입력을 사용해서 각 메시를 고유하게 만들수 있다. 이 코드에서는 소행성을 만드는데 이 입력이 없으면 공전하지 않고 원점에 위치해있을 것이다. draw_id를 이용해서 방향과 위치 행렬을 버텍스 쉐이더에서 생성한다.

위 코드에서는 버텍스 쉐이더에서 소행성의 회전, 위치, 색상을 draw_id로부터 직접 계산하였다. 

draw_id를 부동소수점으로 변환 후 스케일, time 유니폼 값을 사용해서 이동, 스케일, 회전 행렬을 계산하여 모델 행렬 m을 만든다. 

위치는 모델 행렬로 변환 후 뷰-프로젝션 행렬로 변환된다. 버텍스의 노말은 모델과 뷰 행렬로 변환된다.

출력 색상은 두 색상(갈색, 회색)사이를 보간해서 버텍스별로 게산하여 결정한다. 

프래그먼트 쉐이더에서 간단히 라이팅을 적용하였다.

// 7-15
glBindVertexArray(object.get_vao());

if (mode == MODE_MULTIDRAW)
{
    glMultiDrawArraysIndirect(GL_TRIANGLES, NULL, NUM_DRAWS, 0);
}
else if (mode == MODE_SEPARATE_DRAWS)
{
    for (j = 0; j < NUM_DRAWS; j++)
    {
        GLuint first, count;
        object.get_sub_object_info(j % object.get_sub_object_count(),
            first, count);
        glDrawArraysInstancedBaseInstance(GL_TRIANGLES,
            first,
            count,
            1, j);
    }
}

위 코드는 애플리케이션 렌더링 루프이다. 뷰 및 프로젝션 행렬을 설정하고, glMultiDrawArraysIndirect()함수로 모든 모델을 렌더링한다. 

위 코드를 보면 object.get_vao()로 객체의 버텍스 배열 객체를 바인딩 후 결과를 glBindVertexArray() 전달한다. mode인자에 따라 다양한 방법으로 그릴 수 있다. (자세한 설명은 생략한다)

결과는 위와 같이 나오지만 sbm파일을 얻을 수 없는 관계로 그림으로만 볼 수 있다.

 

보통 그래픽 카드에서는 30,000개 모델에 대해 60프레임을 얻지만, 이는 180만 번의 드로잉 커맨드를 초마다 수행하는 것이다. 

드로잉 커맨드를 전송하는 부분에서 병목현상이 일어나지 않는다.

 

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