개발/OpenGL / / 2022. 3. 25. 19:56

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

반응형

The Base Vertex

void glDrawElementsBaseVertex(GLenum mode,
    GLsizei count,
    GLenum type,
    GLvoid * indices,
    GLint basevertex);

위 함수는 glDrawElement() 함수의 고급 버전이라고 한다. 이는 동일한 버퍼에 다른 지오메트리 조각을 저장하고 basevertex를 사용해서 해당 위치의 offset을 지정할 수 있다. 

 

위 그림은 인덱스된 드로잉 커맨드에서 basevertex가 버텍스에 어떻게 더해지는지에 대해 나타낸다. 버텍스 인덱스는 추가적인 연산을 통해 가져오는데, 이는 내부 버텍스를 가져오기 전 opengl이 basevertex를 인덱스에 더하는 연산이다. 

그렇기에 glDrawElementBasevertex()는 basevertex가 0이면 glDrawElement()와 동일하다.

 

Combining Geometry Using Primitive Restart

지오메트리를 strip화 한다는 개념이 뭘까?

연결되지 않은 삼각형들이 큰 집합을 취해서 하나의 삼각형 스트립으로 합쳐 성능을 향상시킨다는 것이다.

삼각형 스트립은 위 그림처럼 연결된 삼각형으로 정점을 공유해서 버텍스 개수를 줄인 형태를 말한다.

반대로 삼각형 soup는 연결되지 않은 삼각형들이다.

 

지오메트리에서 삼각형을 soup에서 strip으로 만들어주는 툴들이 발전해왔다.하지만 스트립과정에서도 성능 저하가 일어나는 현상이 있는데, 이 때문에 primitive restart를 해준다.지오메트리에서 스트립이 시작하고 끝나는 위치를 배열에 미리 정한 값으로 저장하고, 다시 버텍스 인덱스를 가져올 때 미리 저장했던 값을 통해서 현재 스트립을 끝내고 다음 버텍스로 새로운 스트립에 시작한다. 

glEnable(GL_PRIMITIVE_RESTART);

glDisable(GL_PRIMITIVE_RESTART);

위 함수들로 활성화 / 비활성화 시킬 수 있다.

프리미티브 재시작 모드를 활성화하면 opengl은 배열 버퍼에서 값을 가져올 때 특정 인덱스 값을 지켜보다가, 값이 나오면 현재 스트립을 멈추고 새 스트립을 시작한다.

glPrimitiveRestartIndex(index);

이 함수를 opengl이 지켜볼 특정 인덱스를 지정할 때 사용한다.

 

위의 경우 8번 인덱스가 특정 재시작 마커로 인식되어, 삼각형 스크립이 7에서 종료된 후 8은 무시되고, 9에서부터 새로운 스트립이 시작된다.

Instancing

동일한 객체를 반복해서 그리는 상황은 많이 발생한다. 예를들어 바닥에 깔린 잔디 같은 거 말이다. 그러면 모든 잔디 잎을 반복문으로 각각 glDrawArrays()를 통해 렌더링할 수는 있다.

glBindVertexArray(grass_vao);
for (int n = 0; n < number_of_blades_of_grass; n++)
{
    SetupGrassBladeParameters();
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);
}

이렇게 말이다. 위 코드에서는 각 잔디가 네 삼각형 스트립으로 이루어져있다.

 

위 코드에서 잔디의 수 number of blades of grass의 수가 엄청 크면, 오히려 렌더링보다 opengl에 커맨드를 전송하는 시간이 많아질 것이다. 즉 한 번의 호출로 여러 복사본을 그리도록 하면 좋다. 그것이 바로 인스턴스 렌더링이다.

 

void glDrawArraysInstanced(GLenum mode,
    GLint first,
    GLsizei count,
    GLsizei instancecount);
    
void glDrawElementsInstanced(GLenum mode,
    GLsizei count,
    GLenum type,
    const void * indices,
    GLsizei instancecount);

instance함수들이지만, glDrawArrays, glDrawElements와 비슷하게 동작한다. 다른 점은 opengl에 지오메트리 복사본을 instancecount만큼 렌더링하라는 명령이 추가된 것이다. 

 

위에서 base vertex함수도 봤었는데, basevertex를 0으로 하면 기존 함수와 같은 기능을 한다고 하였다. 

void glDrawElementsInstancedBaseVertex(GLenum mode,
    GLsizei count,
    GLenum type,
    GLvoid * indices,
    GLsizei instancecount,
    GLint basevertex);

위처럼 instancing과 base vertex가 모두 합쳐있는 함수도 있다. (glDrawElements만 해당한다는 것을 기억하자)

또 baseinstance라는 인자를 가진 glDrawArraysInstancedBaseInstance(), glDrawElementsInstancedBaseInstance()와 같은 버전도 있는데, 

glDrawArraysInstancedBaseInstance(), glDrawElementsInstancedBaseVertexBaseInstance() 처럼 모두 인자가 있는 버전이 있다.

 

인스턴스 렌더링의 강점은 glsl의 내장 변수 gl_InstanceID이다. 이는 버텍스에 들어있고, 정적 정수 버텍스 속성인 것 같다. (하지만 아니다) 첫 번째 복사본부터 ID가 0으로 시작하여 1씩 증가해서 instancecount - 1까지 할당된다.

 

// Loop over all of the instances (i.e. instancecount)
for (int n = 0; n < instancecount; n++)
{
    // Set the gl_InstanceID attribute - here gl_InstanceID is a C variable
    // holding the location of the "virtual" gl_InstanceID input.
    glVertexAttrib1i(gl_InstanceID, n);
    
    // Now, when we call glDrawArrays, the gl_InstanceID variable in the
    // shader will contain the index of the instance that's being rendered.
    glDrawArrays(mode, first, count);
}

위 코드는 glDrawArraysInstanced() 함수의 동작과 동일하다. 

for (int n = 0; n < instancecount; n++)
{
    // Set the value of gl_InstanceID
    glVertexAttrib1i(gl_InstanceID, n);
    // Make a normal call to glDrawElements
    glDrawElements(mode, count, type, indices);
}

마찬가지로 위 코드는 glDrawElementsInstanced()의 동작과 동일하다.

 

gl_InstanceID로 뭘 할 수 있을까?

gl_InstanceID의 특정 비트를 이용해서 어떤 비트 구간은 x, 또 어떤 비트 구간은 z 좌표에 넣음으로써 일정 간격으로 오브젝트들을 배치할 수 있다.

위 그림이 gl_InstanceID의 특정 비트구간을 특정 좌표로 사용하여 만든 일정한 격자 패턴으로 나열한 모습이다.

단조로우므로 난수를 이용해서 어느 정도 움직이자. 

위 그림은 난수 발생기에 gl_InstanceID를 이용해서 두 연속적인 난수를 발생시켜 각각 x, z 축으로 이동시킨 결과이다. 

 

위 그림은 이제 텍스터처를 사용해서 위 삼각형 잔디의 길이를 조정한 것이다. 잔디의 지오메트리의 각 버텍스의 y좌표에 길이 인자를 텍스처에 넣고 곱함으로써 길이를 변경한 잔디를 얻을 수 있다.

 

그리고 텍스처의 다른 인자를 이용해서 잔디 잎을 해당 축을 중심으로 회전시키는 동작도 할 수 있다. 이는 텍스처의 특정 채널에 y축으로 회전하는 각도를 저장하여 적용할 수 있다.

 

위 그림의 경우 잔디가 바람에 휘날리는 것 마냥 누워있다. 이번에는 텍스처의 특정 채널에 인자를 저장해서 그 값으로 x축 주위로 회전시키는 방법을 이용하는 것이다. (위에서 y축으로 회전시키는 작업은 이 작업 이후에 수행하도록 한다)

 색상 또한 텍스처의 채널을 이용해서 인자값을 전달하면 그 값으로 잔디의 팔레트에 저장하는 방식으로 변경가능하다. 

 

이렇게 많은 잔디를 생성하는데도 불구하고, opengl로 전달하는 지오메트리는 총 6개 버텍스(삼각형 4개의 스트립이기 때문)뿐이고, 잔디를 그리는 것은 glDrawArraysInstanced()하나 뿐으로 가능한 작업이다.

Getting Your Data Automatically

인스턴스 드로잉 커맨트들을 호출할 때 쉐이더에서 gl_InstanceID를 통해 어떤 인스턴스를 작업중인지 알 수 있다. 

렌더링할 인스턴스 개수와 동일한 길이의 배열이 있을 때 인덱스로 gl_InstanceID를 사용할 수 있다. 예를들어 텍스처의 텍셀을 참조하거나 유니폼 배열에 대한 인덱스 사용이 가능하다. 마치 배열을 '인스턴스 속성'으로 간주하는 것과 같다. 

void glVertexAttribDivisor(GLuint index,
	GLuint divisor);

위 함수는 opengl이 인스턴스당 한 번씩 배열로부터 속성을 읽도록 한다. 

 

#version 450 core

in vec4 position;
in vec4 color;

out Fragment
{
	vec4 color;
} fragment;

uniform mat4 mvp;

void main(void)
{
    gl_Position = mvp * position;
    fragment.color = color;
}

위 코드는 버텍스별로 다른 색상으로 그리는 코드이다. 버텍스마다 color 속성이 한 번  읽히므로 각 버텍스가 다른 color값을 가진다.

애플리케이션의 경우 모델의 버텍스 개수많은 많은 색상 배열을 제공해야하지만, 쉐이더에서 인스턴싱을 알지 못한다.

glVertexAttribDivisor(index_of_color, 1);

따라서 위와 같이 함수를 사용함으로써 color를 인스턴스 배열로 저장한다.

dividor 인자에 값에 따라서 매 n 번째 인스턴스마다 color의 값을 배열의 새로운 값을 읽어오므로 color에 새로운 값을 저장하게 된다 .

#version 450 core

in vec4 position;
in vec4 instance_color;
in vec4 instance_position;

out Fragment
{
	vec4 color;
} fragment;

uniform mat4 mvp;

void main(void)
{
    gl_Position = mvp * (position + instance_position);
    fragment.color = instance_color;
}

위 코드에서는 버텍스별 position에 instance position이 들어가면서 인스턴스별로도 다르게 위치가 지정된다.

glVertexAttribDivisor(index_of_instance_position, 1);

이번에 들어가있는 index_of_instance_position은 instance_position속성이 바인딩 된 위치의 인덱스이다.

 위에서는 이동만 사용하지만, 복잡한 애플리케이션의 경우 행렬 버텍스 속성을 사용하거나, 변환 행렬을 유니폼에 넣어놓고 인스턴스 배열에 행렬 가중치를 전달하는 방법을 사용하기도 한다.

 

static const GLfloat square_vertices[] =
{
    -1.0f, -1.0f, 0.0f, 1.0f,
    1.0f, -1.0f, 0.0f, 1.0f,
    1.0f, 1.0f, 0.0f, 1.0f,
    -1.0f, 1.0f, 0.0f, 1.0f
};

static const GLfloat instance_colors[] =
{
    1.0f, 0.0f, 0.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 0.0f, 1.0f
};

static const GLfloat instance_positions[] =
{
    -2.0f, -2.0f, 0.0f, 0.0f,
    2.0f, -2.0f, 0.0f, 0.0f,
    2.0f, 2.0f, 0.0f, 0.0f,
    -2.0f, 2.0f, 0.0f, 0.0f
};

GLuint offset = 0;

glGenVertexArrays(1, &square_vao);
glGenBuffers(1, &square_vbo);
glBindVertexArray(square_vao);
glBindBuffer(GL_ARRAY_BUFFER, square_vbo);
glBufferData(GL_ARRAY_BUFFER,
    sizeof(square_vertices) +
    sizeof(instance_colors) +
    sizeof(instance_positions), NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, offset,
    sizeof(square_vertices),
    square_vertices);
offset += sizeof(square_vertices);
glBufferSubData(GL_ARRAY_BUFFER, offset,
    sizeof(instance_colors), instance_colors);
offset += sizeof(instance_colors);
glBufferSubData(GL_ARRAY_BUFFER, offset,
    sizeof(instance_positions), instance_positions);
offset += sizeof(instance_positions);

glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0,
    (GLvoid *)sizeof(square_vertices));
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0,
    (GLvoid *)(sizeof(square_vertices) +
    sizeof(instance_colors)));

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);

위 코드는 위에서 다루었던 쉐이더를 실제 프로그램에 연결한다. 

코드를 보면 데이터를 선언하고, 버퍼에 로딩한다. 그리고 버퍼를 버텍스 배열 객체에 attach시킨다. 일부 데이터는 버텍스별 위치로 사용되고, 나머지는 인스턴스별 색상 및 위치로 사용된다.

 

glVertexAttribDivisor(1, 1);
glVertexAttribDivisor(2, 1);

위와 같이 버텍스 속성 divisor를 설정하고, 

static const GLfloat black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
glClearBufferfv(GL_COLOR, 0, black);

glUseProgram(instancingProg);
glBindVertexArray(square_vao);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 4);

위와 같은 렌더링 루프로 네 개의 인스턴스들을 그린다. 

 

#include <sb6.h>
#include <vmath.h>

static const char * square_vs_source[] =
{
    "#version 410 core                                                               \n"
    "                                                                                \n"
    "layout (location = 0) in vec4 position;                                         \n"
    "layout (location = 1) in vec4 instance_color;                                   \n"
    "layout (location = 2) in vec4 instance_position;                                \n"
    "                                                                                \n"
    "out Fragment                                                                    \n"
    "{                                                                               \n"
    "    vec4 color;                                                                 \n"
    "} fragment;                                                                     \n"
    "                                                                                \n"
    "void main(void)                                                                 \n"
    "{                                                                               \n"
    "    gl_Position = (position + instance_position) * vec4(0.25, 0.25, 1.0, 1.0);    \n"
    "    fragment.color = instance_color;                                            \n"
    "}                                                                               \n"
};

static const char * square_fs_source[] =
{
    "#version 410 core                                                                \n"
    "precision highp float;                                                           \n"
    "                                                                                 \n"
    "in Fragment                                                                      \n"
    "{                                                                                \n"
    "    vec4 color;                                                                  \n"
    "} fragment;                                                                      \n"
    "                                                                                 \n"
    "out vec4 color;                                                                  \n"
    "                                                                                 \n"
    "void main(void)                                                                  \n"
    "{                                                                                \n"
    "    color = fragment.color;                                                      \n"
    "}                                                                                \n"
};

class instancing_app : public sb6::application
{
public:
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Instanced Attributes";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    void startup(void)
    {
        static const GLfloat square_vertices[] =
        {
            -1.0f, -1.0f, 0.0f, 1.0f,
             1.0f, -1.0f, 0.0f, 1.0f,
             1.0f,  1.0f, 0.0f, 1.0f,
            -1.0f,  1.0f, 0.0f, 1.0f
        };

        static const GLfloat instance_colors[] =
        {
            1.0f, 0.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            1.0f, 1.0f, 0.0f, 1.0f
        };

        static const GLfloat instance_positions[] =
        {
            -2.0f, -2.0f, 0.0f, 0.0f,
             2.0f, -2.0f, 0.0f, 0.0f,
             2.0f,  2.0f, 0.0f, 0.0f,
            -2.0f,  2.0f, 0.0f, 0.0f
        };

        GLuint offset = 0;

        glGenVertexArrays(1, &square_vao);
        glGenBuffers(1, &square_buffer);
        glBindVertexArray(square_vao);
        glBindBuffer(GL_ARRAY_BUFFER, square_buffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(square_vertices) + sizeof(instance_colors) + sizeof(instance_positions), NULL, GL_STATIC_DRAW);
        glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(square_vertices), square_vertices);
        offset += sizeof(square_vertices);
        glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(instance_colors), instance_colors);
        offset += sizeof(instance_colors);
        glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(instance_positions), instance_positions);
        offset += sizeof(instance_positions);

        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid *)sizeof(square_vertices));
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid *)(sizeof(square_vertices) + sizeof(instance_colors)));

        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glEnableVertexAttribArray(2);

        glVertexAttribDivisor(1, 1);
        glVertexAttribDivisor(2, 1);

        square_program = glCreateProgram();

        GLuint square_vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(square_vs, 1, square_vs_source, NULL);
        glCompileShader(square_vs);
        glAttachShader(square_program, square_vs);
        GLuint square_fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(square_fs, 1, square_fs_source, NULL);
        glCompileShader(square_fs);
        glAttachShader(square_program, square_fs);

        glLinkProgram(square_program);
        glDeleteShader(square_vs);
        glDeleteShader(square_fs);
    }

    void shutdown(void)
    {
        glDeleteProgram(square_program);
        glDeleteBuffers(1, &square_buffer);
        glDeleteVertexArrays(1, &square_vao);
    }

    void render(double t)
    {
        static const GLfloat black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
        glClearBufferfv(GL_COLOR, 0, black);

        glUseProgram(square_program);
        glBindVertexArray(square_vao);
        glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 4);
    }

private:
    GLuint      square_buffer;
    GLuint      square_vao;

    GLuint      square_program;
};

DECLARE_MAIN(instancing_app);

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

 

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