Jump to content

Bem vindo à Unidev
Registre para ter acesso a todos os recursos do site. Uma vez registrado e logado, você poderá criar tópicos, postar em tópicos já existentes, gerenciar seu perfil e muito mais. Se você já tem uma conta, faça login aqui - ou então crie aqui uma conta agora mesmo!
- - - - -
Photo

Instanciação em OpenGL Desmistificada


por Martin Thomas

Quando tentei implementar instanciação algum tempo atrás, não achei quase nenhum tutorial/artigo sobre isso. Assim como com qualquer coisa um pouco além do OpenGL 2.1, isto parece ser um tabu entre os programadores de OpenGL. Todo mundo sabe sobre ele, mas não são muitos que realmente estão o usando, apesar de ser fácil. Não mais.

INSTANCIAÇÃO PARA TODOS
HISTÓRIA

Instanciação se tornou uma característica fundamental do OpenGL começando com a versão 3.1 por volta de 2009, chamada ARB_draw_instanced. Nesse momento você poderia usar somente Texture Buffer Objects (TBOs) ou Uniform Buffer Objects (UBOs) para realmente enviar alguns dados para seus shaders. Um ano depois, o OpenGL 3.3 chegou com a nova extensão ARB_instanced_arrays agora sendo uma característica fundamental. Com esta adição você realmente poderia usar Vertex Buffer Objects (VBOs) para enviar seus dados. Yay!
Entretanto há uma restrição a isto, onde você somente pode passar 16 atributos de vértices para seus shader de vértice (por especificação, ou GL_MAX_VERTEX_ATTRIB_BINDINGS), que resulta em 16 * vec4 = 64 valores float.

Em 2010, ARB_draw_indirect (OpenGL 4.0) também chegou como característica fundamental. Ele permite que você passe parâmetros para funções glDrawArrays* indiretamente, que é de um pedaço de memória.

Em 2011, ARB_base_instance (OpenGL 4.2) se tornou característica fundamental também. Ele permite você especificar uma taxa meio aberta [x...y) de quais dados de instancia você gostaria de desenhar.

ARB_transform_feedback também foi adicionada, que permite você usar dados de feedback de transformações como dados de instancia para desenhar.

O GRANDE CONCEITO

Então, quando é apropriado usar instanciação? Bem, quando você quer desenhar a mesma coisa milhares de vezes. A razão para isto é que uma única chamada de draw (glDraw*) custa muita CPU, enquanto o driver precisa fazer algumas checagens e preparação (mágica!) antes da chamada da função retornar. Normalmente em um PC comum, 2000 chamadas de draw é o máximo que você pode fazer sem baixar demais seu frame rate (lembre-se: você tem no máximo 33 ms por frame!). Então desenhar alguma coisa 1000 vezes consumiria metade de suas chamadas de draw, e isso é ruim. Instanciação resolve este problema, permitindo você dizer ao driver "Hey, eu gostaria de desenhar este pedaço de geometria 1000 vezes". Mas você acabaria com 1000 objetos no mesmo lugar, certo? Para resolver isto, você pode passar dados que serão únicos para cada um dos 1000 objetos desenhados. Isto é o que eu chamo de "Dados de Instancia". Isto é normalmente uma matriz (visão de modelo), mas, para simplificar, eu somente armazenarei um vec4 (posição).

VISÃO DO ALGORITMO

Renderização normal:

Para cada frame:
Para cada objeto:

-upload dos dados específicos do objeto para os uniforms (UBOs)
-renderizar objeto

Instanciação:

Para cada frame:
Para cada objeto:

-prepare dados de instancia, armazene-os em um buffer (não precisa fazer isso a cada frame se o buffer é estático)

-upload do buffer para a GPU
-renderizar objetos usando instanciação, usando os Dados de Instancia providenciados

Você pode ver claramente que o número de chamadas de draw é reduzido de n para 1 (além de não ter passagem pelo uniform!).

A IMPLEMENTAÇÃO

Vou usar um pequeno framework (~600 linhas) que escrevi para prototificar técnicas. Isto me permite esconder código irrelevante. Iremos desenhar cubos. O primeiro passo para desenhar cubos é criar um VBO que contém dados de vértice.

GLuint box = frm.create_box(); //Vertex Array Object (VAO) of the box

Então iremos criar um VBO para dados de instancia: as posições dos cubos. Para fazer isto, precisamos de um buffer (memoria) e um VBO. Primeiro, vamos vincular o VAO novo.

glBindVertexArray( box );

Então criar o buffer.

vector<vec4> positions;
positions.resize( size * size ); //make some space

E então criar o VBO para esses dados.

GLuint position_vbo;
glGenBuffers( 1, &position_vbo ); //gen vbo
glBindBuffer( GL_ARRAY_BUFFER, position_vbo ); //bind vbo

Aqui vem a parte interessante: você precisa dizer ao driver que você vai usar este VBO para instanciação. Para fazer isto você precisa dizer estas coisas:

-qual localização de atributo do vértice você vai usar? (2)
-quantos componentes cada pedaço de dados tem? (vec4, então 4)
-que tipo de dado você está passando? (floats)
-este dado esta normalizado? (eles são posições, então provavelmente não)
-quantos bytes tem cada pedaço de dados? (vec4, então 4 * sizeof(float))
-se estes dados consistem de mais de quatro componentes (assim como mat4), então onde estão localizados estes dados específicos (relativos ao pedaço inteiro de dados, em bytes)?
-estes dados estão instanciados?

Tudo isso neste código:

GLuint location = 2;
GLint components = 4;
GLenum type = GL_FLOAT;
GLboolean normalized = GL_FALSE;
GLsizei datasize = sizeof( vec4 );
char* pointer = 0; //no other components
GLuint divisor = 1; //instanced


glEnableVertexAttribArray( location ); //tell the location
glVertexAttribPointer( location, components, type, normalized, datasize, pointer 


); //tell other data
glVertexAttribDivisor( location, divisor ); //is it instanced?

Se o dado que você gostaria de passar é um mat4 por exemplo, então você acabaria usando 4 localizações de atributos de vértices para passar este dado. Isto iria requerer que você configurasse o VBO um pouco diferente, dizendo onde cada coluna (vec4) da matriz está em cada pedaço de dados em bytes. Isto é necessário porque você está passando-o em GLvoid*, que significa que o tamanho dos dados em bytes é desconhecido (sem aritméticas de ponteiros). Então você precisa trabalhar em bytes e converter para GLvoid*.

Em código:

GLuint location = 2;
GLint components = 4;
GLenum type = GL_FLOAT;
GLboolean normalized = GL_FALSE;
GLsizei datasize = sizeof( mat4 );
char* pointer = 0;
GLuint divisor = 1;


/**
Matrix:
float mat[16] =
{
 1, 0, 0, 0, //first column:  location at 0 + 0 * sizeof( vec4 ) bytes into the 


matrix
 0, 1, 0, 0, //second column: location at 0 + 1 * sizeof( vec4 ) bytes into the 


matrix
 0, 0, 1, 0, //third column:  location at 0 + 2 * sizeof( vec4 ) bytes into the 


matrix
 0, 0, 0, 1  //fourth column  location at 0 + 3 * sizeof( vec4 ) bytes into the 


matrix
};
/**/


//you need to do everything for each vertex attribute location
for( int c = 0; c < 4; ++c )
{
  glEnableVertexAttribArray( location + c ); //location of each column
  glVertexAttribPointer( location + c, components, type, normalized, datasize, 


pointer + c * sizeof( vec4 ) ); //tell other data
  glVertexAttribDivisor( location + c, divisor ); //is it instanced?
}

O divisor diz ao driver se o dado está instanciado. Se o divisor for 0 (por padrão) significa que o dado não está instanciado. Se for 1, então estará instanciado. Para qualquer outro valor >1 o id da instancia (gl_InstanceID) no shader do vértice será dividido por este valor.

A seguir você precisar carregar os shaders. Estou usando um shader deferido super simples para maximizar a eficiência do shader, e faze-lo-os simples.

Shader de vértice:

#version 330 core


uniform mat4 mvp; //modelviewprojection matrix
uniform mat3 normal_mat;


layout(location=0) in vec4 in_vertex; //cube vertex position
layout(location=1) in vec3 in_normal; //cube face normal
layout(location=2) in vec4 pos; //instance data, unique to each object (instance)


out vec3 normal;


void main()
{
  normal = normal_mat * in_normal;
  gl_Position = mvp * vec4(in_vertex.xyz + pos.xyz, 1); //write to the depth 


buffer
}

Shader de pixel:

#version 330 core


in vec3 normal;


layout(location=0) out vec4 color; //normals go here


void main()
{
  color = vec4(normal * 0.5 + 0.5, 1);
}


Carregando os shaders:


GLuint gbuffer_instanced_shader = 0;
frm.load_shader( gbuffer_instanced_shader, GL_VERTEX_SHADER, 


"../shaders/instancing2/gbuffer_instanced.vs" );
frm.load_shader( gbuffer_instanced_shader, GL_FRAGMENT_SHADER, 


"../shaders/instancing2/gbuffer.ps" );


GLint gbuffer_instanced_mvp_mat_loc = glGetUniformLocation( 


gbuffer_instanced_shader, "mvp" );
GLint gbuffer_instanced_normal_mat_loc = glGetUniformLocation( 


gbuffer_instanced_shader, "normal_mat" );
Finalmente, tudo o que você precisa fazer é renderizar os cubos. Normalmente isto se pareceria com isso:

//regular rendering
glBindVertexArray( box );


for( int c = 0; c < size; ++c )
{
  for( int d = 0; d < size; ++d )
  {
    glUniform4f( gbuffer_pos_loc, c * 3 - size, -2 + 0.5 * sin( radians( ( c + d 


+ 1 )* timer.getElapsedTime().asSeconds() ) ), -d * 3, 0 ); //this gives it some ocean-like movement
    glDrawElements( GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0 ); //two triangles per face, that is 6 * 6 = 36 vertices
  }
}

Entretanto, para instanciação, você precisa atualizar o buffer de instancia, que se parece com isso:

//instanced rendering
glBindVertexArray( box );


//store positions in the buffer
for( int c = 0; c < size; ++c )
{
  for( int d = 0; d < size; ++d )
  {
    positions[c * size + d] = vec4( c * 3 - size, -2 + 0.5 * sin( radians( ( c + 


d + 1 )* timer.getElapsedTime().asSeconds() ) ), -d * 3, 0 );
  }
}


//upload the instance data
glBindBuffer( GL_ARRAY_BUFFER, position_vbo ); //bind vbo 
//you need to upload sizeof( vec4 ) * number_of_cubes bytes, DYNAMIC_DRAW because 


it is updated per frame
glBufferData( GL_ARRAY_BUFFER, sizeof( vec4 ) * positions.size(), 


&positions[0][0], GL_DYNAMIC_DRAW );


glDrawElementsInstanced( GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0, positions.size() 


);

É isso. O resto do código é configurar o shader deferido, e alguns controles que devem ser bem diretos.

PONTOS INTERESSANTES

Curiosamente, fazendo o simples sin() na CPU para atualizar as posições se tornou o gargalo depois de ~1.000.000 cubos. Se eu usasse uma matriz, então a multiplicação da matriz era um problema depois de ~160.000 cubos. Isto significa que mesmo fazendo instanciação você ainda precisa ser esperto quanto o lado da CPU (fazendo as multiplicações de matriz usando instruções SIMD, ou nos shaders). Depois de tudo, atualizar posições para muitos dados é uma tarefa de dados paralelos que a GPU normalmente gosta.

CONCLUSÃO

Instanciação é muito importante para ter certeza de que as chamadas de draw não sejam o gargalo. Espero que cada vez mais pessoas acabem usando isto no futuro.

Recursos adicionais:

-fonte do projeto:
controles:WASD, espaço para trocar entre instanciação (verde) e renderização normal (vermelho)
building: use o cmake para gerar o projeto (configure CMAKE_BUILD_TYPE para "Release")
https://docs.google....dit?usp=sharing

-história do OpenGL
http://www.opengl.or...story_of_OpenGL

-wiki de Instanciação no OpenGL
http://www.opengl.or...ring#Instancing
http://www.opengl.or...nstanced_arrays
http://www.opengl.or...dback_rendering

-Tutoriais relacionados que achei
http://ogldev.atspac...tutorial33.html
http://sol.gfxile.net/instancing.html

-Culling de instancia usando feedback de transformações
http://rastergrid.co...ometry-shaders/


Este artigo foi originalmente postado em Gamedev.net
http://www.gamedev.n...mystified-r3226
  • Alan likes this



0 Comments