OpenGL Vertex Buffer Object (VBO)

A extensão GL_ARB_vertex_buffer_object tem por objetivo aumentar a performance do OpenGL ao prover os benefícios do vertex array e do display list, ao mesmo tempo que evita os aspectos negativos de suas implementações. O Vertex buffer object (VBO) permite que arrays de vértices sejam armazenados na memória gráfica de alta performance do lado do servidor e promove transferência eficiência de dados. Se o objeto do buffer é usado para armazenar dados de pixels, é chamado Pixel Buffer Object (PBO).

O uso de arrays de vértices pode reduzir o número de chamadas de função e usos redundantes dos vértices compartilhados. Porém, a desvantagem disso é que as funções estão no lado do cliente e os arrays precisam ser re-enviados ao servidor a cada vez que são referenciados.

Por outro lado, o display list é uma função do lado do servidor, de forma que sofre do problema da sobrecarga de transferência de dados. Mas, uma vez que um display list é compilador, os dados que são exibidos não podem ser modificados.

Um Vertex buffer object (VBO) cria “objetos de buffer” para atributos de vértices na memória de alta performance do lado do servidor e prove acesso a funções que referenciam arrays, que são usadas em arrays de vértices, como glVertexPointer(), glNormalPointer(), glTexCoordPointer(), etc.

O gerenciamento de memória do vertex buffer object irá por os objetos na melhor posição de memória baseando-se nas dicas do usuário: modos “target” e “usage”. Além disso, o gerenciamento de memória pode otimizar os buffers para balanceá-los entre 3 tipos de memória: system, AGP e video memory.

Ao contrário de uma display lists, os dados de um vertex buffer object podem ser lidos e atualizados pelo mapeamento do buffer no espaço de memória do cliente.

Uma outra vantagem importante do VBO é o compartilhamento dos objetos de buffer co vários clientes, como display lists e texturas. Como o VBO está no lado do servidor, múltiplos clientes terão acesso ao mesmo buffer com o identificador correspondente.

Criando um VBO

A criação de um VBO requer 3 passos;

  1. Geração de um novo objeto de buffer com glGenBuffersARB().
  2. Ligar o objeto de buffer com glBindBufferARB().
  3. Copiar os dados do vértice no objeto de buffer com glBufferDataARB().

glGenBuffersARB()

glGenBuffersARB() cria objetos de buffer e retorna o identificador do objeto. Essa função precisa de dois parâmetros: o primeiro é o número de objetos a serem criados, e o segundo é o endereço  de uma variável GLuint ou array para armazenamento de um ID único ou múltiplos IDs.


void glGenBuffersARB(GLsizei n, GLuint* ids)

glBindBufferARB()

Uma vez que o objeto de buffer tenha sido criado, precisamos ligar esse objeto com o ID correspondete antes de usa-lo. Essa função precisa de dois parâmetros: target e ID.


void glBindBufferARB(GLenum target, GLuint id)

Target é uma dica que diz ao VBO se esse objeto irá armazenar um array de vértices ou um array de indíces: GL_ARRAY_BUFFER_ARB, or GL_ELEMENT_ARRAY_BUFFER_ARB. Qualquer atributos de um vértice, como as coordenadas, textura, normais ou componentes da cor devem usar GL_ARRAY_BUFFER_ARB. Um array de índice, usado por glDraw[Range]Elements(), deve ser ligado com GL_ELEMENT_ARRAY_BUFFER_ARB. Note que o parâmetro target auxilia o VBO a decidir o local mais eficiente para objetos de buffer, por exemplo, alguns sistemas preferem por os índices na memória do sistema ou AGP, e os vértices na memória de vídeo.

Uma vez que glBindBufferARB() seja chamado uma vez, O VBO inicializa o buffer com um buffer de memória de tamanho zero e ajusta os valores iniciais do VBO, como uso e propriedades de acesso.

glBufferDataARB()

Você pode copiar dados no objeto de buffer com glBufferDataARB() quando o buffer é inicializado.


void glBufferDataARB(GLenum target, GLsizei size, const void* data, GLenum usage)

Novamente, o primeiro parâmetro deve ser  GL_ARRAY_BUFFER_ARB ou GL_ELEMENT_ARRAY_BUFFER_ARB. Size é o número de bytes de dados a serem transferidos. O terceiro parâmetro é o ponteiro para o array de dados. Se os dados forem um ponteiro NULL, o VBO apenas reserva o espaço de memória dado por size. O último parâmetro é usado para fins de performance do VBO para indicar como o objeto irá ser usado: de forma estárica, dinâmica, stream, e leitura, cópia ou desenho.

O VBO especifica 9 valores para esse parâmetro;


GL_STATIC_DRAW_ARB
GL_STATIC_READ_ARB
GL_STATIC_COPY_ARB
GL_DYNAMIC_DRAW_ARB
GL_DYNAMIC_READ_ARB
GL_DYNAMIC_COPY_ARB
GL_STREAM_DRAW_ARB
GL_STREAM_READ_ARB
GL_STREAM_COPY_ARB

“Static” significa que os dados do VBO não serão alterados (especificado uma vez e usado várias), “dynamic” significa que os dados serão alterados frequentemente (especificaos e usado repetidamente), e “stream” significa que os dados serão alterados a cada frame (especificados uma vez e usados uma vez). “Draw” significado que os dados serão enviados para a GPU para serem desenhados (aplicação para GL), “read” significa que os dados serão lidos pelo aplicativo cliente (GL para a aplicação) e “copy” significa que os dados serão usados tanto para desenho quanto para leitura (GL para GL).

Note que apenas draw é útil para o VBO, enquanto copy e read serão úteis apenas para objetos de pixel ou de quadro (PBO ou FBO).

O gerenciamento de memória de VBO irá escolher o melhor local para o objeto de buffer baseando-se no uso desse parâmetro, por exemplo, GL_STATIC_DRAW_ARB e GL_STREAM_DRAW_ARB podem usar a memória de vídeo, e GL_DYNAMIC_DRAW_ARB pode usar a memória AGP. Qualquer buffer relacionado a _READ_ devem estar otimizados na memória do sistema ou AGP pois devem ser de fácil acesso.

glBufferSubDataARB()


void glBufferSubDataARB(GLenum target, GLint offset, GLsizei size, void* data)

Como o glBufferDataARB(), glBufferSubDataARB() é usado para copiar dados em um VBO, mas apenas substitui uma faixa de dados em um buffer existente, começando com o deslocamento fornecido (O tamanho total do buffer precisa ser ajustado por glBufferDataARB() antes do uso de glBufferSubDataARB().)

glDeleteBuffersARB()


void glDeleteBuffersARB(GLsizei n, const GLuint* ids)

Você pode apagar um ou vários VBOs com a função glDeleteBuffersARB() se eles não forem mais ser usados. Depois que um objeto de buffer é apagado, seu conteúdo será perdido.

O seguinte código é um exemplo de criação de um único VBO para coordenadas de vértices. Observe que você pode apagar a alocação de memória para o array de vértices em sua aplicação depois de copiar os dados no VBO.


GLuint vboId;                              // ID of VBO
GLfloat* vertices = new GLfloat[vCount*3]; // create vertex array
...

// generate a new VBO and get the associated ID
glGenBuffersARB(1, &vboId);

// bind VBO in order to use
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);

// upload data to VBO
glBufferDataARB(GL_ARRAY_BUFFER_ARB, dataSize, vertices, GL_STATIC_DRAW_ARB);

// it is safe to delete after copying data to VBO
delete [] vertices;
...

// delete VBO when program terminated
glDeleteBuffersARB(1, &vboId);

Desenhando um VBO

Por VBOs serem baseados sobre a implementação existente de arrays de vértices, a renderização de um VBO é quase a mesma do uso de vertex array. A única diferença é que o ponteiro para o array de vértices é agora um deslocamento para um objeto de buffer. Apesar disso, nenhuma API adicional é necessária para desenhar um VBO exceto glBindBufferARB().


// bind VBOs for vertex array and index array
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId1);         // for vertex coordinates
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, vboId2); // for indices

// do same as vertex array except pointer
glEnableClientState(GL_VERTEX_ARRAY);             // activate vertex coords array
glVertexPointer(3, GL_FLOAT, 0, 0);               // last param is offset, not ptr

// draw 6 quads using offset of index array
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, 0);

glDisableClientState(GL_VERTEX_ARRAY);            // deactivate vertex array

// bind with 0, so, switch back to normal pointer operation
glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);

Associar um objeto de buffer com 0 desliga a operação do VBO. É uma boa ideia fazer isso após o uso, de forma que operações normais com arrays de vértices com ponteiros absolutos sejam reativadas.

Atualizando um VBO

A vantagem do VBO sobre display list é que o cliente pode ler e modificar os dados do objeto de buffer, o que uma display list não pode fazer. O método mais simples de atualizar um VBO é copiar novos dados no VBO em uso com glBufferDataARB() ou glBufferSubDataARB(). Nesse caso, sua aplicação deve possuir um array de vértices válido todo o tempo. Isso significa que você precisa ter sempre 2 cópias de dados de vértices: uma em sua aplicação e outra no VBO.

A outra maneira de modificar um objeto de buffer é mapear o objeto na memória do cliente, de forma que o cliente possa atualizar os dados com o ponteiro para o buffer mapeado. O exemplo a seguir descreve como mapear o VBO na memória do cliente e acessar os dados mapeados.

glMapBufferARB()

O VBO fornece a função glMapBufferARB() para mapear o objeto de buffer na memória do cliente.


void* glMapBufferARB(GLenum target, GLenum access)

Se o OpenGL for capaz de mapear o objeto de buffer no espaço de endereçamento do cliente, glMapBufferARB() retorna o ponteiro para o buffer. Caso contrário, retorna NULL.

O primeiro parâmetro, target, foi mencionado anteriormente durante a explicação de glBindBufferARB(), e o segundo parâmetro, access, especifica o que será feito como os dados mapeados: read, write ou both.


GL_READ_ONLY_ARB
GL_WRITE_ONLY_ARB
GL_READ_WRITE_ARB

Note que glMapBufferARB() causa um problema de sincronização. Se a GPU ainda estiver trabalhando com o objeto de buffer, glMapBufferARB() não retornará um valor até que a GPU termine seu trabalho com o objeto correspondente.

Para evitar essa espera (idle), você pode chamar primeiro glBufferDataARB() como um ponteiro NULL, e depois chamar glMapBufferARB(). Nesse caso, os dados antigos serão descartados e glMapBufferARB() retornará um novo ponteiro alocado imediatamente mesmo se a GPU ainda estiver trabalhando nos dados anteriores.

Porém, esse método é válido apenas se você quiser atualizar todo o conjunto de dados, por estar descartando os dados antigos. Se quiser altear apenas uma parte dos dados ou quiser ler os dados, é melhor não liberar os dados anteriores.

glUnmapBufferARB()


GLboolean glUnmapBufferARB(GLenum target)

Depois de modificar os dados do VBO, é preciso desfazer o mapeamento dele da memória do cliente. A função glUnmapBufferARB() retorna GL_TRUE em caso de sucesso. Quando ela retorna GL_FALSE, o conteúdo do VBO fica corrompido enquanto o buffer estiver mapeado. Essa corrupção resulta de mudanças na resolução da tela ou eventos especificos do sistema de janela. Nesse caso, os dados precisam ser re-enviados.

Abaixo segue um exemplo simples de código para modificar o VBO com o método de mapeamento.


// bind then map the VBO
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);
float* ptr = (float*)glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);

// if the pointer is valid(mapped), update VBO
if(ptr)
{
    updateMyVBO(ptr, ...);                 // modify buffer data
    glUnmapBufferARB(GL_ARRAY_BUFFER_ARB); // unmap it after use
}

// you can draw the updated VBO
...

Exemplo

Example of VBO
Essa aplicação faz o VBO oscilar pelas normais. Ela mapeia um VBO e atualiza seus vértices a cada quadro com o ponteiro para o buffer mapeado. Você pode comparar a performance com uma implementação tradicional que usa arrays de vértices.

Ela usa 2 buffers de vértices; um para as coordenadas de vértices e outra para os índices.

Baixe o código fonte aqui: vbo.zipvboSimple.zip.

vboSimple é um exemplo simples para desenhar um cubo usando VBO e Vertex Array. Você pode facilmente visualizar os pontos comuns e diferenças entre VBO e VA.

Também foi incluido um makefile (Makefile.linux) para sistemas Linux na pasta src/, de forma que você possa gerar um executável em sua máquina Linux, por exemplo:

> make -f Makefile.linux

Traduzido de songho.ca