Universidade Federal de Santa Maria Departamento de Eletrônica e Computação Prof. Cesar Tadeu Pozzer Disciplina: Computação Gráfica Avançada pozzer@inf.ufsm.br 05/0/203 OpenGL Conceitos Avançados Neste módulo serão vistos conceitos mais avançados da programação em OpenGL, que tratam: Otimizações: Vertex Array, Display List, VAO, VBO e otimizações de pipeline; Realismo: Iluminação, material e textura; Pipeline OpenGL Tudo o que está vem vermelho faz parte da antiga API do OpenGL, mas ainda com suporte garantido. http://pyopengl.sourceforge.net/documentation/deprecations.html. Arquitetura Cliente-Servidor O OpenGL trabalha com a filosofia cliente-servidor. Isso significa que se pode trabalhar em um computador (cliente) e ver os resultados em outro (servidor). O cliente é responsável pela geração dos comandos OpenGL enquanto que o servidor é responsável pelo processamento (render) destes dados e exibição na tela. Caso não existirem computadores em rede, o mesmo computador exerce o papel de cliente e servidor (maior parte dos casos). Esse conceito é importante para melhor compreensão de conceitos avançados do OpenGL. Veja os comandos glflush() e glfinish() para uso em uma aplicação distribuída que faça uso de cliente e servidor em máquinas diferentes conectadas por uma rede. Client server 2. Vertex Array Usado para: Reduzir o número de chamada de funções, que podem causar redução de desempenho da aplicação o Desenhar um polígono com 20 lados requer 22 chamadas de funções: uma para cada vértice + glbegin() + glend(). Passando-se as normais, pode-se duplicar o número de chamadas. Evitar a replicação de vértices, resultando em economia de memória. o Para se especificar um cubo, gasta-se 24 vértices, sendo cada vértice replicado 3x. O mesmo cubo poderia ser desenhado com apenas 8 vértices. Com o uso de vertex array (OpenGL.), os 20 vértices do polígono poderiam ser colocados em um vetor e passados em uma única chamada para o OpenGL. O mesmo poderia ocorrer com os vetores normais. Em relação ao número de vértices, pode-se facilmente definir vértices compartilhados por várias faces. Passos para se utilizar vertex array: Ativar o vetor de dados que se deseja utilizar. Os mais comuns são de vértices, normais e textura; Colocar os dados nos arrays. Estes dados, no modelo cliente-servidor, são armazenados no cliente; Desenhar o objeto com os dados dos arrays. Neste momento, os dados são transferidos para o servidor.
Para ativar os buffers de dados, deve-se utilizar o comando glenableclientstate(), que recebe como parâmetro constantes associadas a cada vetor: GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_INDEX_ARRAY, GL_NORMAL_ARRAY, GL_TEXTURE_COORD_ARRAY, e GL_EDGE_FLAG_ARRAY void glenableclientstate(glenum array); void gldisableclientstate(glenum array); glenableclientstate (GL_COLOR_ARRAY); glenableclientstate (GL_VERTEX_ARRAY); Para a definição dos dados, existem comandos específicos para cada um dos 6 tipos de dado. O comando glvertexpointer() é usado para indicar o vetor de vértices. O comando glnormalpointer() para especificação de normais. void glvertexpointer(glint size, GLenum type, GLsizei stride, const GLvoid *pointer); void glnormalpointer(glenum type, GLsizei stride, const GLvoid *pointer); void gltextcoordpointer(glenum type, GLsizei stride, const GLvoid *pointer); void glcolorpointer(glint size, GLenum type, GLsizei stride, const GLvoid *pointer); No comando glvertexpointer(): pointer especifica onde os dados das coordenadas podem ser acessados. type especifica o tipo de dados: GL_SHORT, GL_INT, GL_FLOAT, ou GL_DOUBLE de cada coordenada do array. size é o número de coordenadas por vértice (2, 3, ou 4). Não necessária para especificação de normais (sempre é 3). Para cor pode ser 3 ou 4. stride é o offset em bytes entre vértices consecutivos. Deve ser 0 se não existir espaço vago entre dois vértices. Para enviar os dados para o processamento (enviar para o servidor na arquitetura cliente-servidor do OpenGL), existem vários comandos. void gldrawelements(glenum mode, GLsizei count, GLenum type, void *indices); O comando gldrawelements() define uma seqüência de primitivas geométricas onde os parâmetros são: count: É o úmero de elementos do vetor indices: Vetor de índices dos elementos 2
type: Indica o tipo de dados do array indices. Deve ser GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT ou GL_UNSIGNED_INT. mode: indica o tipo de primitivas a serem geradas, como GL_POLYGON, GL_LINE_LOOP, GL_LINES, GL_POINTS, etc. Exemplo de uso: GLint allvertex[] = { 0,0,0,,0,0,,,0, 0,,0, 0,0,,,0,,,,, 0,,}; //GLint allnormals[8] = {Nesta configuração, define-se uma normal por vértice}; GLfloat allcolors[] = {,0,0, 0,,0,,,,,0,, 0,0,,,,, 0,,, 0,0,0}; GLubyte allindices[] = {4, 5, 6, 7,, 2, 6, 5, 0,, 5, 4, 0, 3, 2,, 0, 4, 7, 3, 2, 3, 7, 6}; glenableclientstate (GL_VERTEX_ARRAY); glenableclientstate (GL_COLOR_ARRAY); glvertexpointer(3, GL_INT, 0, allvertex); glcolorpointer( 3, GL_FLOAT, 0, allcolors); //glnormalpointer(gl_float, 0, allnormals); 4 5 gldrawelements(gl_quads, 6*4, GL_UNSIGNED_BYTE, allindices); 7 3 2 0 6 Face 0 Face Face N 4 5 6 7 2 6 5 0 5 6 V 0 V V 2 V 3 V 4 V 5 V 6 V 7 C 0 C C 2 C 3 C 4 C 5 C 6 C 7 Para a especificação de um único vértice por vez de um vertex array pode-se utilizar o comando glarrayelement(glint). O seguinte trecho de código tem o mesmo efeito que o comando gldrawelements() apresentado anteriormente. glbegin (GL_QUADS); for (i = 0; i < 24; i++) glarrayelement(allindices[i]); glend(); Observações: Existe um mapeamento direto de para entre o vetor de vértices com os vetores de normais, cores, etc. O vetor de índices é GLubyte pois como existem apenas 24 = 6*4 índices, usar um tipo maior como GLuint seria um desperdício de memória. Se um vértice compartilhado tiver normais diferentes em cada face, deve-se replicar os vértices e as normais (o mesmo vale para as cores). Neste caso, perde-se a otimização de memória mas continua- 3
se ganhando com a redução de chamadas em relação a definição do objeto utilizando-se os comandos convencionais como glvertex*() e glnormal*(). Para o caso do cubo, seriam necessários 24 vértices (4 por face x 6 faces), 24 normais e 24 cores. [http://www.songho.ca/opengl/gl_vertexarray.html] Este recurso do GL, quando usado com normais, é melhor aplicado em superfícies planares, como terrenos. Para objetos volumétricos fechados como cubos, não faz sentido ter-se normais compartilhadas entre as 3 faces adjacentes, o que exige a replicação de vértices. O mesmo se aplica para cilindros. 3. Display List É um grupo de comandos OpenGL que são armazenados para execução futura. Quando um display list é chamado, os comandos são executados na ordem que foram definidos. Nem todos os comandos OpenGL podem ser armazenados em um display list. Não podem ser utilizados especialmente comandos relacionados com vertex array, comandos como glflush(), glfinish(), glgenlists(), dentre outros. Deve ser utilizada sempre que for necessário desenhar o mesmo objeto mais de uma vez. Neste caso, deve-se guardar os comandos que definem o objeto e então exibir a display list toda vez que o objeto precisar ser desenhado. No caso de um carro que possui 4 rodas, pode-se gerar um display list com a geometria de uma roda e invocar a renderização, uma vez para cada roda, lembrando de aplicar as devidas transformações para posicionar cada roda no devido lugar. Os dados de um display list residem no servidor, reduzindo deste modo o trafego de informações. Dependendo da arquitetura do servidor, o display list pode ser armazenado em memória especial para agilizar o processamento. Para criar um display list deve-se utilizar os seguintes comandos: glgenlists(), glnewlist() e glendlist() como mostrado no seguinte exemplo: GLuint id_objeto; void init() { id_objeto = glgenlists(); glnewlist(id_objeto, GL_COMPILE); //comandos de desenho do objeto //vértices, transformações, etc. glendlist(); } void display() {... glrotated(0,, 0, 0); glcalllist(id_objeto); } gltranslated(0, 3, 0); glcalllist(id_objeto);... glswapbuffers(); GLuint glgenlists(glsizei range): Aloca um intervalo contínuo de índices. Se o parâmetro range for igual a, gera um único índice que é o valor retornado pela função. O índice retornado é usado pelas funções glnewlist() e glcalllist(). O retorno zero significa erro. 4
void glnewlist (GLuint list, GLenum mode): Especifica o início do display list. Todos os comandos válidos especificados antes do comando glendlist() são guardados no display list. o List: identificador do display list. o Mode: aceita dois argumentos: GL_COMPILE_AND_EXECUTE e GL_COMPILE (indica que os comandos não devem ser executados à medida que são inseridos no display list). void glendlist (void): indica o fim do display list. void glcalllist (GLuint list): Executa o display list referenciado por list. Os comandos são executados na ordem que foram especificados, do mesmo modo caso não fosse utilizado este recurso. Além de reduzir o número de chamada de funções, bem como a transferência de dados para o servidor, reduz-se também processamento para geração dos objetos. Supondo que fosse necessário gerar um círculo (ou uma esfera) por coordenadas polares, diversos cálculos de ângulos podem ser realizados uma única vez, visto que apenas comandos OpenGL são armazenados na display list para processamento futuro. Entretanto, uma vez gerada o display list, os valores das coordenadas do círculo não podem mais ser alterados. Comandos GLUT também pode ser inseridos, uma vez que são desmembrados em comandos básicos do OpenGL. Pode-se criar display list hierárquicos, onde um display list invoca outros.... id_objeto = glgenlists(); glnewlist(id_objeto, GL_COMPILE); glbegin(gl_polygon); for(int i=0; i<000; i++) { x = cos(ang)*radius; y = sin(ang)*radius; ang+=(360.0/000); glvertex2f(x,y); } glend(); glendlist();... Para maiores detalhes dos benefícios do uso de display list e recursos adicionais, consulte []. 4. Vertex Buffer Object e Vertex Array Object Por Cícero Pahins e Guilherme Schardong, 203 Buffer Objects são espaços de memória alocados no contexto do OpenGL que servem para armazenar dados. Vertex Buffer Objects (VBO) são usados para armazenar dados de vértices (coordenadas, cores, normais e outros dados relevantes), os quais são necessários para desenhar geometria na tela. A maior vantagem de usar VBOs é que os dados ficam armazenados na GPU, o que evita que o programador tenha que enviá-los para a GPU a cada ciclo de renderização. Os VBOs foram concebidos para aliar as funcionalidades dos Vertex Arrays e Display Lists, enquanto evitam suas desvantagens: Os Vertex Arrays reduzem o número de chamadas de funções do OpenGL e proporciona o uso redundante de vértices compartilhados. No entanto, a desvantagem dessa técnica é que as funções do Vertex Array estão no cliente e os dados devem ser reenviados para o servidor 5
a cada vez que o array for referenciado, diminuindo o desempenho da aplicação como um todo. A Display List é uma função do servidor, logo os dados não precisam ser reenviados a cada renderização. O problema surge quando há a necessidade de alterar os dados já enviados, uma vez que uma Display List compilada não admite alterações, causando a necessidade de recriá-la para cada alteração a ser executada. Os VBOs permitem que o usuário defina como os dados serão armazenados na memória da GPU, ou seja, permite que, por exemplo, haja vários buffers, cada um contendo um dado relevante para a renderização, ou que tudo seja armazenado em um buffer apenas. Deve-se notar que a GPU não sabe como estes dados estão organizados, para isso há a necessidade de uma estrutura que descreva como interpretá-los para a correta renderização da geometria. Abaixo iremos construir um exemplo utilizando VBO. Note que o código está adequado ao OpenGL 3.2+. A partir do OpenGL 3.2 foi inserido o conceito de profile na criação de contextos, sendo eles o core e o compatibility. Enquanto que o primeiro expõem as últimas e mais modernas funcionalidades da API, tornando a utilização de shaders uma exigência, o segundo inclui as funções relativas ao pipeline fixo e depreciadas, ou seja, aquelas que possuem seu uso desaconselhado. A escolha do profile a ser utilizado é feita no momento da criação do contexto OpenGL, desta maneira, depende diretamente do sistema operacional ou APIs utilizadas para este fim. O código abaixo é utilizado na API GLFW para que o contexto OpenGL suporte ao mínimo a versão 3.2 da API e exclusivamente as funcionalidades definidas no core (a criação de um contexto não suportado pelo driver de vídeo resultará em erro): glfwwindowhint(glfw_context_version_major, 3); glfwwindowhint(glfw_context_version_minor, 2); glfwwindowhint(glfw_opengl_profile, GLFW_OPENGL_CORE_PROFILE); O uso das funcionalidades (extensões) mais recentes do OpenGL é facilitada pelo uso da API GLEW. Desta maneira, procure sempre fazer a linkagem dela em seu projeto. Veja mais em []. Primeiramente precisamos definir a lógica da estrutura de dados que será armazenada na GPU. Como dito mais acima, há duas opções: utilizar vários buffers, cada um contendo um tipo de dado, ou, somente um buffer para todos os dados. A escolha depende diretamente de como o programador armazena, cria ou importa os seus dados, uma vez que diferentes API s para importação de modelos 3D oferecem diferentes interfaces. Outro fator a ser considerado é o tamanho do VBO resultante [2]: Pequeno Mais facilmente gerenciado, uma vez que cada objeto da cena pode ser representado por um único VBO. Se o VBO tiver poucos dados o ganho de performance em relação a outras técnicas pode não ser aparente. Muito tempo irá ser perdido na troca de buffers. Grande A renderização de vários objetos pode utilizar somente uma chamada de função, melhorado a performance. 6
Caso o gerenciamento separado de objetos seja muito complicado, isto irá gerar um overhead desnecessário. Se os dados do VBO forem muito grandes é possível que não possam ser movidos para a VRAM (Video RAM) A formatação dos dados (ordem de armazenamento) também pode influenciar no desempenho final, a wiki oficial do OpenGL fornece informações das melhores práticas em [3]. Para este exemplo iremos armazenar três informações a respeito do vértice: posição (3 floats), normal (3 floats) e coordenada de textura (2 floats), como demonstra a Figura. Figura. Formatação de dados utilizada no exemplo de VBO. Note que o tamanho total de cada informação (posição + normal + coordenada de textura) é de 32 bytes, ou seja, se alocarmos n informações a respeito do vértice (n * sizeof(informação)) encontraremos a primeira tupla nos bytes 0 à 3, a segunda nos bytes 32 à 63, e assim por diante. Este modo de armazenamento é dito intercalado, e pode ser representado pela seguinte sequência de caracteres: (VVVNNNTT) (VVVNNNTT) (VVVNNNTT)..., onde V = vertex, N = normal e T = texture coordinate. Observe no Código a etapa de declaração de variáveis e definição da estrutura de dados e ordem de armazenamento. GLuint id_vbuffer = 0; GLuint id_ibuffer = 0; struct Vertex { GLfloat vertex_xyz[3]; GLfloat normal_xyz[3]; GLfloat texcoord_uv[2]; }; Vertex vertexdata[num_verts] = {... }; GLuint indexdata[num_indices] = { 0,, 2,... }; Código. Exemplo VBO. Etapa de declaração de variáveis e definição da estrutra de dados. Semelhante a utilização de Vertex Array, é criado um vetor de índices (indexdata) que irá coordenar a ordem de acesso ao vetor de dados (vertexdata). As variáveis id_vbuffer e id_ibuffer serão utilizadas para armazenar os ids dos VBOs a serem criados, neste exemplo um para dados e outro para índices, e posteriormente utilizadas para referenciá-los. O OpenGL não permite a mistura de dados de vértices aos de índices, por isso a necessidade da criação de dois buffers. 7
A criação de uma VBO requer 3 passos:. Geração de um novo buffer com glgenbuffers() void glgenbuffers(glsizei n, GLuint * buffers); Parâmetros: n: número de buffers a serem criados. buffers: endereço de uma variável ou array do tipo GLuint para armazenamento do id de cada buffer. 2. Vinculação do buffer criado ao OpenGL com glbindbuffer() void glbindbuffer(glenum target, GLuint buffer); Parâmetros: target: especifica o tipo de dado a ser armazenado, pode ser: dado de vértice: GL_ARRAY_BUFFER dado de índice: GL_ELEMENT_ARRAY_BUFFER buffer: id do buffer. 3. Cópia dos dados com glbufferdata() void glbufferdata(glenum target, GLsizeiptr size, const GLvoid * data, GLenum usage); Parâmetros: target: deve ser o mesmo utilizado em glbindbuffer(). size: tamanho em bytes do buffer a ser criado (corresponde ao tamanho total em bytes dos dados). data: ponteiro para o array de dados a ser copiado. Caso NULL, o VBO irá somente reservar o espaço de memória. usage: especifica o padrão de uso esperado do armazenamento de dados, pode ser qualquer combinação entre os grupos abaixo (9 possibilidades): Grupo A ( o conteúdo do armazenamento será: ) STATIC: especificado uma vez e utilizado muitas vezes STREAM: especificado e utilizado repetidamente DYNAMIC: especificado uma vez e utilizado uma vez Grupo B ( os dados serão movidos do(a): ) DRAW: aplicação para OpenGL READ: OpenGL para aplicação COPY: OpenGL para OpenGL (DRAW + READ) 8
A fim de descobrir o melhor local para armazenar os dados dos VBOs criados a implementação do OpenGL (driver de vídeo) utilizará o parâmetro usage da função glbufferdata() como dica e, por isso, ele é muito importante na busca por maior desempenho. Observe atentamente o Código 2 e faça a correlação dos parâmetros de função e a estrutura de dados utilizados. // Criação do VBO de dados de vértices glgenbuffers(, &id_vbuffer); glbindbuffer(gl_array_buffer, id_vbuffer); glbufferdata(gl_array_buffer, sizeof(vertex) * NUM_VERTS, vertexdata, GL_STATIC_DRAW); // transferência do dados de vértices // Criação do VBO de dados de índices glgenbuffers(, &id_ibuffer); glbindbuffer(gl_element_array_buffer, id_ibuffer); glbufferdata(gl_element_array_buffer, sizeof(gluint) * NUM_INDICES, indexdata, GL_STATIC_DRAW); // transferência do dados de índices Código 2. Exemplo VBO. Etapa de transferência de dados para GPU. Criados os VBOs e transferidos os dados, resta a etapa de renderização, a qual requer 3 passos (semelhante ao utilizado em Vertex Array):. Vinculação do buffer a ser utilizado pelo OpenGL com glbindbuffer(). 2. Ativação e definição dos atributos dos vértices void glenablevertexattribarray(gluint index); void glvertexattribpointer(gluint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer); Parâmetros: index: índice do atributo do vértice (correspondente ao vertex shader) size: especifica o número de componentes do atributo, podendo ser, 2, 3 ou 4 type: especifica o tipo de dado de cada componente do array stride: especifica o offset (intercalação) entre atributos consecutivos pointer: especifica o ponto de início (definido como um offset, e não como um ponteiro) para o primeiro atributo do tipo definido no array 3. Chamada da função de desenho com gldrawelements() ou outra. Um atributo do vértice (posição, índice, normal, coordenada de textura, etc.) é habilitado e definido com as funções glenablevertexattribarray() e glvertexattribpointer(), respectivamente. Através da função glvertexattribpointer() a formatação dos dados é enviada ao OpenGL. Note que no Código 3 existem três chamadas a essa função, cada uma definindo a estrutura de dados utilizada neste exemplo. Observe estas funções e compare-as com a Figura. // Bind dos VBOs a serem utilizados glbindbuffer(gl_array_buffer, id_vbuffer); glbindbuffer(gl_element_array_buffer, id_ibuffer); // Ativação e definição dos atributos dos vértices glenablevertexattribarray(0); // texcoord_uv glvertexattribpointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(sizeof(glfloat) * 6)); glenablevertexattribarray(); // normal_xyz 9
glvertexattribpointer(, 3, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(sizeof(glfloat) * 3)); glenablevertexattribarray(2); // vertex_xyz glvertexattribpointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(0)); // Chamada da função de desenho gldrawelements(gl_triangles, NUM_INDICES, GL_UNSIGNED_INT, (void*)0); Código 3. Exemplo VBO. Etapa de renderização. Informações referentes à Figura estão sublinhadas. Note que o índice utilizado para a habilitação e definição de um determinado atributo através das funções glenablevertexattribarray() e glvertexattribpointer() deve ser o mesmo, no exemplo é utilizado o índice 0 para as coordenadas de textura, o índice para as normais e o índice 2 para os vértices. (Quando utilizado shader os índices devem ser correspondentes). Uma boa prática de programação a ser seguida é sempre retornar ao estado do OpenGL antes da ativação de atributos, buffers, flags ou outros, procurando desabilitar recursos utilizados pontualmente, como no caso da criação ou renderização de VBOs. O Código 4 ilustra a como desabilitar as funcionalidades utilizadas na etapa de renderização do VBO (Código 3) e deve sempre ser seguida para evitar problemas desnecessários em aplicações maiores ou mais complexas. gldisablevertexattribarray(2); gldisablevertexattribarray(); gldisablevertexattribarray(0); glbindbuffer(gl_array_buffer, 0); glbindbuffer(gl_element_array_buffer, 0); Código 4. Boa prática de programação em OpenGL. A modificação de um VBO pode ser dada utilizando a função glbuffersubdata(), caso em que parte dos dados são reenviados para o buffer (note que é preciso ter cópias válidas dos dados no cliente e no servidor), ou utilizando a função glmapbuffer(), a qual mapeia o buffer para o espaço de memória do cliente. O uso da segunda opção é preferível por não exigir cópia dos dados no cliente e servidor, mas há situações que o uso da primeira opção pode trazer maior desempenho [4]. No Código 5 é exibido o uso da função glmapbuffer(), a qual retorna um ponteiro que permite o programador modificar os dados do VBO. São parâmetros o tipo de buffer e de acesso, nesta ordem. glbindbuffer(gl_array_buffer, id_vbuffer); Vertex* ptr = (Vextex*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); if (ptr) { updatemyvbo(ptr,...); glunmapbuffer(gl_array_buffer); } Código 5. Exemplo VBO. Mapeamendo do VBO para o espaço de memória do cliente. Vertex Array Objects (VAOs) são objetos OpenGL que encapsulam todos os estados necessários para a definição de dados de vértices. Eles definem o formato desses dados bem como seus arrays fonte [5], ou seja, descrevem como estão armazenados os atributos de vértices. Note que o VAO não contém os dados dos vértices, estes estão armazenados em Vertex Buffer Objets, mas somente referencia aqueles já existentes no buffer. Para usar um VAO ele deve ser criado e definido como ativo (semelhante ao VBO). Feito isso, os atributos para os VBOs devem ser habilitados e passados para que o VAO saiba onde estão os
dados e como eles estão armazenados. De maneira simplificada, um VAO registra os estados (flags) necessárias para a configuração do VBO. No Código 6 é exibido o uso de um VAO em conjunto com o exemplo de VBO anterior. Note que toda a etapa de renderização do VBO (Código 3) é registrada na etapa de configuração do VAO, com exceção da chamada a função de desenho (gldrawelements()). No Código 7 a etapa de renderização é realizada. Compare com a mesma etapa do VBO. // Bind do VAO GLuint id_vao; glgenvertexarrays(, &id_vao); glbindvertexarray(id_vao); // início do registro dos estados pelo VAO // Bind dos VBOs a serem utilizados glbindbuffer(gl_array_buffer, id_vbuffer); glbindbuffer(gl_element_array_buffer, id_ibuffer); // Ativação e definição dos atributos dos vértices glenablevertexattribarray(0); // TexCoordPointer glvertexattribpointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(sizeof(glfloat) * 6)); glenablevertexattribarray(); // NormalPointer glvertexattribpointer(, 3, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(sizeof(glfloat) * 3)); glenablevertexattribarray(2); // VertexPointer glvertexattribpointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(glfloat) * 8, (void*)(0)); // Chamada da função de desenho NÃO DEVE SER CHAMADA gldrawelements(gl_triangles, NUM_INDICES, GL_UNSIGNED_INT, (void*)0); glbindvertexarray(0); // fim do registro dos estados pelo VAO Código 6. Exemplo VAO. Etapa de configuração. glbindvertexarray(id_vao); gldrawelements(gl_triangles, NUM_INDICES, GL_UNSIGNED_INT, (void*)0); glbindvertexarray(0); Código 7. Exemplo VAO. Etapa de renderização. Um VAO possui um determinado número de atributos que devem ser associados e habilitados para que possam ser usados pelo OpenGL. Alguns exemplos de atributos são: dados de vértices, índices, vetores normais e cores. A tentativa de renderização de um VAO vazio irá resultar em erro. Para consultar os protótipos de função do OpenGL veja [6]. 5. Exibição de FPS Frames Per Second Passos: Calcular o FPS Guardar o estado de cada atributos ativo com o comando glpushattrib(glbitfield mask). Este comando aceita diversas constantes como argumentos. Os dois mais genéricos são GL_ALL_ATTRIB_BITS e GL_ENABLE_BIT. Este comando tem o mesmo efeito do glpushmatrix(), só que este opera sobre pilha de atributos. Desabilitar todos os atributos com gldisable(), para não interferirem na impressão do valor do FPS, que é tratado como uma string: o Cor material (GL_COLOR_MATERIAL) o Coordenadas de textura (GL_TEXTURE_2D) o Nevoeiro (GL_FOG);
o Iluminação (GL_LIGHTING) o Z-buffer (GL_DEPTH_TEST) o Composição (GL_BLEND) Setar a matriz de projeção como ortográfica Habilitar a matriz de Modelview, Desenhar os caracteres com glrasterpos2f(), glutbitmapcharacter() e glutbitmapwidth(); Restaurar os atributos com glpopattrib(); Para mais detalhes, veja o demo FPS. 6. Textura Para dar as superfícies características mais realistas, o OpenGL oferece além de recursos de iluminação, recurso de texturização. Faz-se uso da técnica de texture mapping para aplicar a textura sobre a superfície dos polígonos. A idéia é mapear cada face do modelo em coordenadas de textura, geralmente representada por uma imagem, como na seguinte figura. A definição de coordenadas de textura, para cada triângulo do modelo, que neste exemplo é um modelo de personagem animado 3D (Formato MD2), é feita pelo modelador (artista gráfico). A textura faz parte do estágio de rasterização do pipeline gráfico do OpenGL. A rasterização é a conversão de dados geométricos (polígonos) e pixels (textura) em fragmentos. Cada fragmento corresponde a um pixel no framebuffer. O mapeamento da textura para os fragmentos nem sempre preserva as proporções. Dependendo da geometria do objeto, podem haver distorções da textura ao ser aplicada. Também pode acontecer de um mesmo texel (texture element) ser aplicado a mais de um fragmento como um fragmento mapear vários texels. Para isso operações de filtragem são aplicadas. Uma textura é um mapa retangular de dados (geralmente cores como em uma imagem). O OpenGL oferece algumas maneiras de se especificar textura: pela especificação de coordenadas para cada vértice do modelo ou pela geração automática de coordenadas de textura. Como o assunto de textura é muito amplo, nesta seção são apresentados alguns comandos com alguns parâmetros possíveis. Para maiores detalhes, consulte []. Restrições da textura: A textura deve ter preferencialmente dimensões em potência de 2, como por
exemplo: 64x28, 52x52, 28x64, etc. A função glteximage2d(), como será visto, não funciona se a textura não seguir esta restrição. Outra função equivalente, a glubuild2dmipmaps(), por sua vez, opera com textura de qualquer dimensão. Se a textura não for potência de 2, deve-se garantir que a posição de memória do pixel que representa o início de uma linha seja múltipla de 4 (mesmo formato adotado pelo padrão de imagem BMP para imagens que não são potência de 2). Por exemplo, uma imagem em RGB de 3x4 pixels, vai ocupar 2 bytes por linha, ao invés de 9, como esperado. Ver demo de textura. Para se aplicar textura deve-se inicialmente habilitar a textura, criar a textura, definir parâmetros e aplicar a textura. Para habilitar o recurso de textura utilizam-se os comandos glenable(gl_texture); glenable(gl_texture_2d); Cada textura esta associada a um ID que é usado pelo OpenGL para definição da textura ativa. A cada momento existe uma única textura ativa, que é aplicada sobre todos os objetos que possuem coordenadas de textura. Para criar um ID utiliza-se o comando glgentextures(,&id) que indica que serão criados índice de textura, e que este índice será armazenado na variável intera id. Par se criar, habilitar e desabilitar a textura, utiliza-se o comando void glbindtexture(glenum target, GLuint texturename); O parâmetro texturename corresponde ao ID da textura. O parâmetro target pode assumir GL_TEXTURE_D ou GL_TEXTURE_2D. Quando este comando for chamado com um ID de textura inteiro diferente de zero pela primeira vez, um novo objeto de textura é criado com o nome passado. Se a textura já havia sido criada, ela é então habilitada. Se o parâmetro texturename for zero, desabilita o uso de textura do OpenGL. Após a criação da textura (nome), deve-se configurar parâmetros de aplicação da textura sobre um fragmento. void gltexparameter{if}(glenum target, GLenum pname, TYPE param); target: pode assumir GL_TEXTURE_D ou GL_TEXTURE_2D. pname e param: formam uma dupla. Para cada valor de pname, existem possíveis valores para param. Pname GL_TEXTURE_WRAP_S GL_TEXTURE_WRAP_T GL_TEXTURE_MAG_FILTER GL_TEXTURE_MIN_FILTER param GL_CLAMP, GL_REPEAT GL_CLAMP, GL_REPEAT GL_NEAREST, GL_LINEAR GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR Os parâmetros GL_TEXTURE_WRAP_S/T representam como a textura será aplicada sobre objeto, caso o objeto for maior que a textura. GL_CLAMP usa a cor da borda e GL_REPEAT replica a textura.
GL_NEAREST indica que deve ser usado o texel com coordenadas mais próximas do centro do pixel. GL_LINEAR faz uma média dos 2x2 texels vizinhos ao centro do pixel. Os parâmetros GL_TEXTURE_MAG/MIN_FILTER indicam como a textura será filtrada. Isso ocorre porque após a projeção, pode ocorrer de parte de um texel aplicado sobre um pixel (magnification) ou de vários texels serem aplicados sobre um único pixel (minification). O argumento passado vai indicar como será realizada a média ou interpolação dos valores. A última etapa consiste na definição dos dados da textura. Para isso existem dois comandos: glteximage2d() e glubuild2dmipmaps(). Mipmapping é uma técnica usada para criar várias copias da textura em diferentes resoluções (/4+/6+... /3, ou seja, aumento de 33% do uso de memória), facilitando assim a escolha da melhor textura a ser aplicada sobre o objeto em função da sua área de projeção (http://en.wikipedia.org/wiki/mipmap): void glteximage2d(glenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels); level: comumente deve ser 0. Ver função glubuild2dmipmaps() para geração automática de textura em várias resoluções. internalformat: indica como estão armazenados os dados no array pixels (imagem). Pode assumir vários valores. O parâmetro mais comum é GL_RGB, para indicar que o array é um vetor de coordenadas RGB. width: largura da imagem em pixels height: altura da imagem em pixels border: indica a largura da borda. Valor 0 significa sem borda. format: Formato dos dados. Use GL_RGB. type: Geralmente deve ser GL_UNSIGNED_BYTE para indicar que cada componente RGB utiliza byte não sinalizado. pixels: contém os dados da imgem-textura. Ver restrições de dimensão de textura. int glubuild2dmipmaps(glenum target, GLint components, GLint width, GLint height, GLenum format, GLenum type, void *data); Esta função cria uma série de mipmaps e chama a função glteximage*d() para carregar as imagens. Por isso é uma função glu. Vários argumentos são os mesmos da função glteximage2d(). Exemplo de Mipmap: textura original e cópias em escaladas de 2 (separada em RGB).
Para se especificar as coordenadas de textura para cada vértice utiliza-se o comando gltexcoord*(). void gltexcoord{234}{sifd}(type coords); void gltexcoord{234}{sifd}v(type *coords); Este comando seta as coordenadas de textura corrente (s, t, r, q). Vértices definidos na seqüência com glvertex*() fazem uso do valor corrente de textura. Geralmente utiliza-se o comando gltexcoord2f() para definir as coordenadas s e t. Coordenadas de textura são multiplicadas pela matriz de textura antes que o mapeamento ocorra. Por default esta matriz é a identidade. A matriz de textura pode conter vários tipos de transformações como rotações, translações, perspectivas, etc. Para aplicar transformações nesta matriz deve-se setar a matriz de textura como corrente com o comando glmatrixmode(gl_texture). Com isso pode-se simular a rotação de uma textura sobre uma superfície. (0,) (,) (0,0) (,0) Textura (0,3) (5,3) (0,0) (5,0) Mapeamento da textura. Adaptada de [3] Um programa que usa textura tem a seguinte estrutura. Para maiores detalhes, veja o demo de textura. int tex_id; void init() { glgentextures(, &tex_id ); glbindtexture( GL_TEXTURE_2D, tex_id ); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_S, GL_CLAMP);...
glteximage2d(gl_texture_2d,...); } glenable( GL_TEXTURE ); glenable( GL_TEXTURE_2D ); void render() { glbindtexture( GL_TEXTURE_2D, tex_id ); } glbegin(gl_quads); gltexcoord2f(0, 0); glvertex3f(v[0].x, v[0].y, v[0].z); gltexcoord2f(3, 0); glvertex3f(v[].x, v[].y, v[].z); gltexcoord2f(3, 3); glvertex3f(v[2].x, v[2].y, v[2].z); gltexcoord2f(0, 3); glvertex3f(v[3].x, v[3].y, v[3].z); glend(); Exemplos de geração de textura: GLuint textureid[max_textures]; glgentextures(, &textureid[0] ); glbindtexture( GL_TEXTURE_2D, textureid[0] ); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_S, GL_REPEAT); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_T, GL_REPEAT); gltexparameteri(gl_texture_2d, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); gltexparameteri(gl_texture_2d, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glubuild2dmipmaps(gl_texture_2d, GL_RGB, img->getwidth(), img->getheight(), GL_RGB, GL_UNSIGNED_BYTE, data); glgeneratemipmap(gl_texture_2d); glgentextures(, &textureid[] ); glbindtexture( GL_TEXTURE_2D, textureid[] ); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_S, GL_CLAMP); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_T, GL_CLAMP); gltexparameteri(gl_texture_2d, GL_TEXTURE_MAG_FILTER, GL_NEAREST); gltexparameteri(gl_texture_2d, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glteximage2d(gl_texture_2d, 0, GL_RGB, img->getwidth(), img->getheight(), 0, GL_RGB, GL_UNSIGNED_BYTE, data);
Geração automática de coordenadas de textura O OpenGL permite que as coordenadas de textura sejam criadas automaticamente. Isso é muito útil em diversas aplicações como por exemplo na geração de terrenos onde os vértices formam um reticulado. Considere o caso de um malha poligonal com dimensão 0x0, com origem em (0,0). Independente do número de faces que vai ter, deseja-se aplicar uma única textura sobre toda a malha, sem replicação. Neste caso define-se uma escala /0 e especifica-se parâmetros de projeção da textura nos eixos x e z, como no seguinte exemplo. (0,0) float escala =.0 / 0; float p[4] = { escala, 0, 0, 0}; float p2[4] = { 0, 0, escala, 0}; gltexgenfv(gl_s, GL_OBJECT_PLANE, p); gltexgenfv(gl_t, GL_OBJECT_PLANE, p2); Como o quadrado tem dimensão de 0, com coordenada inicial em (0,0), se escala valesse /20, seria aplicada apenas ¼ da textura. Se escala = /5, seriam usadas 4 cópias da textura (com filtro GL_REPEAT) para preencher o quadrado. glgentextures(, &id ); glbindtexture( GL_TEXTURE_2D, id); float escala =.0 / 0; float p[4] = { escala, 0, 0, 0 }; float p2[4] = { 0, escala, 0, 0 }; gltexgenfv(gl_s, GL_OBJECT_PLANE, p); gltexgenfv(gl_t, GL_OBJECT_PLANE, p2); gltexgeni(gl_s, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); gltexgeni(gl_t, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_S, GL_REPEAT); gltexparameteri(gl_texture_2d, GL_TEXTURE_WRAP_T, GL_REPEAT); gltexparameteri(gl_texture_2d, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); gltexparameteri(gl_texture_2d, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glubuild2dmipmaps(gl_texture_2d, GL_RGB, img->getwidth(), img->getheight(), GL_RGB, GL_UNSIGNED_BYTE, data); Deve-se habilitar a geração automática de textura e após o uso desabilitar. Maiores detalhes podem ser vistos em []. glenable(gl_texture_gen_s); glenable(gl_texture_gen_t); 7. Pipeline de Renderização Uma corrente não é mais forte que o seu elo mais fraco. Anônimo O pipeline gráfico de uma aplicação gráfica que faz uso do OpenGL (tradicional sem shanding) é composto por 3 etapas: aplicação, geometria e rasterização, como mostrado na seguinte figura.
Os processos do nível de aplicação executam na CPU e os dois demais na GPU. Os 3 estágios executam simultaneamente. Antes do surgimento de placas aceleradoras, todo este trabalho era executado exclusivamente pela CPU. Hoje em dia há uma separação das tarefas, o que permite aliviar o uso da CPU para outras tarefas como: processamento da IA (visto que ainda não existem placas destinadas a este propósito), interação com o usuário, simulação física (por pouco tempo, visto que estão surgindo placas dedicadas ou incorporadas à placa de vídeo destinadas a este propósito), Algoritmos de remoção de objetos não visíveis (culling), etc. Aplicação Geometria Rasterização Entrada do usuário Transformações do modelo e de câmera Conversão vetorial matricial - rasterização Cinemática dos objetos e teste de colisão Processamento da IA Descarte de objetos fora do campo de visão Iluminação Projeção Recorte Operações de fragmentos: textura, fog, blending, etc. Frame buffer de cor e profundidade Extração da geometria a ser visualizada Mapeamento para tela Etapas do processo de renderização. Adaptada de [3]. Como mostrado na seguinte figura, o pipeline de geometria opera sobre vértices, enquanto o pipeline de rasterização opera sobre pixels (fragmentos). Operações por vértice Aplicação Transformação Modelagem e Visualização Iluminação Projeção Clipping Mapeamento de Tela Rasterização Mapeamento de Textura Combinação Flog/Blend Teste α, s, z Frame Buffer Operações por pixel Aplicação Geometria Rasterização Estágios funcionais do pipeline. Extraído de [3] O Framebuffer é composto por: Color buffers: guarda a cor dos pixels (fragmentos). Pode existir o left- e right-buffer (para visão estéreo), e cada um destes pode ser single ou double (front- e back-buffer).
Depth buffer: guarda a profundidade dos fragmentos em relação à posição da câmera. Podem haver vários fragmentos que projetam em um mesmo pixel. Somente o mais próximo será o visível (ver algoritmo z-buffer). Accumulation Buffer: semelhante ao buffer de cor, com a diferença que este pode ser usado para acumular uma série de imagens. Isso é útil em operações de super-amostragem, antialiasing e motion blur. Stencil Buffer: buffer usado para restringir o desenho a certas partes da tela. Pode ser usado para desenhar o interior do carro (painel, direção, etc). Somente a área na região do pára-brisa é que pode ser atualizada com informações da cena. Para cada buffer, existem comandos de limpeza, como glclearcolor(), glcleardepth(), glclearaccum() e glclearstencil(), ou glclear(gl_color_buffer_bit...). Para maiores detalhes de uso destes buffers, consulte []. O buffer de profundidade é alterado pelo algoritmo z-buffer. Este buffer tem o mesmo tamanho e forma que o color buffer, onde cada pixel armazena a distância da câmera até a primitiva corrente mais próxima. Quanto uma primitiva estiver sendo renderizada para um certo pixel (amostragem), compara-se a distância deste pixel em relação à câmera. Se a distância for menor que a distância já armazenada, significa que a primitiva corrente, para o dado pixel, está mais próxima que a última primitiva avaliada. Neste caso deve-se atualizar o buffer de profundidade, juntamente com o buffer de cor com informações da nova primitiva. Se a distância for maior, mantêm-se os buffers inalterados. O custo computacional do z-buffer é O(n), onde n é número de primitivas sendo renderizadas. 7.. Otimização do Pipeline Como os 3 estágios conceituais (aplicação, geometria e rasterização) executam simultaneamente, o que for mais lento (gargalo) vai determinar a velocidade de renderização (throughput). Observações: Antes de tudo deve-se localizar o gargalo; Deve-se procurar otimizar o estágio mais lento para aumentar o desempenho do sistema; Se o estágio da aplicação for o mais lento, não se terá nenhum ganho otimizando o estágio de geometria, por exemplo; Ao se otimizar o estágio mais lento, pode ocorrer de outro estágio se tornar o novo gargalo; Caso o gargalo não puder ser otimizado, pode-se melhor aproveitar os demais estágios para se obter maior realismo. A estratégia mais simples para se detectar o gargalo é realizar uma série de testes, onde cada teste afeta somente um único estágio. Se o tempo total for afetado, ou seja, reduzir o frame rate, então o gargalo foi encontrado. Para isso pode-se aumentar ou reduzir o processamento de um estágio. Teste no estágio da Aplicação: A estratégia mais simples neste caso é reduzir o processamento dos outros estágios, pela simples substituição dos comandos glvertex*() por glcolor*(). Neste caso os estágios de geometria e rasterização não vão ter o que processar. Se o desempenho não aumentar, o gargalo está na aplicação. Outra estratégia é incluir um laço de repetição que realiza N cálculos matemáticos. Se o desempenho geral piorar, o gargalo era a aplicação. Teste no estágio de geometria: É o estágio mais difícil de se testar pois se a carga de trabalho neste estágio for alterada, será alterada também em outros estágios. Existem duas estratégias de teste: aumentar ou reduzir o número de fontes luminosas, o que não vai afetar os demais estágios, ou verificar se o gargalo está no estágio da aplicação ou rasterização. Teste do estágio de Rasterização: É o estágio mais fácil de ser testado. Isso pode ser feito simplesmente aumentando-se ou reduzindo-se o tamanho da janela onde as imagens estão sendo renderizadas. Isso não interfere nem na aplicação e nem na geometria. Quanto maior for a janela,
maior será a quantidade de pixels a serem desenhados. Outro modo de testar é desabilitando operações de textura, fog, blending. Existem várias estratégias para se otimizar os estágios do pipeline. Algumas abordagens comprometem a qualidade em troca de desempenho e outras são apenas melhores estratégias de implementação: Otimização do Estágio da Aplicação: otimizações para tornar o código e acesso à memória mais rápidos. Isso pode ser obtido ativando-se flags de compilação e conhecendo detalhes das linguagens de programação. Algumas dicas gerais de código/memória são listadas. Para maiores detalhes consulte [2]: o Evite divisões; o Evite chamar funções com pouco código. Use funções inline; o Reduzir uso de funções trigonométricas; o Usar float ao invés de double; o Usar const sempre que possível, para o compilador melhor otimizar o código; o Evitar uso excessivo de funções malloc/free. Otimização do Estágio de geometria: Pode-se aplicar otimizações apenas sobre transformações e iluminação. o Em relação a transformações, se o objeto precisar ser movido para uma posição estática, ao invés de aplicar a transformação a cada renderização, pode-se aplicar as transformações em um pré-processamento antes da renderização iniciar; o Outra estratégia é reduzir ao máximo o número de vértices, com o uso de vertex array, ou primitivas do tipo STRIP ou FAN; o Outra estratégia muito eficiente é aplicar culling, no estágio da aplicação; o Uso de LOD; o Reduzir o número de fontes luminosas; o Verificar quais partes da cena necessitam iluminação; o Iluminação flat é mais rápida que Gourdaud; o Avaliar se é necessário iluminar os dois lados das superfícies; o Uso de Light maps. Otimização do Estágio de Rasterização: o Habilitar backface culling para objetos fechados. Isso reduz o número de triângulos para serem rasterizados em aproximadamente 50%; o Desenhar objetos mais próximos do observador primeiro. Isso evita gravações sucessivas no z-buffer; o Desabilitar recursos como fog, blending ou uso de filtros de textura mais baratos, como o bilinear ao invés de mipmapping. Otimizações gerais: o Reduzir a geometria da cena; o Reduza a precisão em vértices, normais, cores e coordeandas de textura. Isso reduz uso de memória e uso de barramento; o Desabilitar recursos não utilizados; o Minimizar mudança de estados; o Evitar swap de texturas; o Use display lists para objetos estáticos. 7.2. Avaliando o Desempenho do Pipeline A velocidade de renderização depende do estágio mais lento, e é medida em FPS (frames por segundo). Para este cálculo, deve-se desabilitar o recurso de double-buffer, visto que a troca de buffer neste caso ocorre em sincronismo com o refresh do monitor. 2
Com o double buffer ativado, enquanto o front buffer está sendo exibido, o back buffer está sendo gerado. Somente ocorre a troca de buffers (glutswapbuffers) quando o monitor acabar de desenhar o frame corrente. Considerando-se o monitor com uma taxa de atualização de 72 Hz, significa que a cada t = 3. 8 milisegundos a tela é atualizada. Com o recurso de double-buffer ativado, a taxa de atualização deve ser múltiplo deste valor. Se o tempo de renderização consome 20 ms (50 Hz) em single-buffer, a taxa cairá para 2*3. 8 = 36 Hz com o double-buffer ativo. Neste caso, o pipeline gráfico fica inativo por 3. 8*2 20 = 7.78 ms para cada frame [2]. Monitor 3.8 ms 3.8 ms 3.8 ms 3.8 ms 3.8 ms Pipeline 20 ms 7.78 ms 20 ms 7.78 ms 20 ms Uma forma de contornar os problemas do uso do double-buffer é o uso de tiple-buffer. Neste caso existem dois back buffers e um front buffer. Neste caso, enquanto o monitor está exibindo o font buffer, podem estar sendo gerados ou 2 back buffers. No caso do uso de double-buffer, o o refresh do monitor for de 60 Hz e o tempo de geração das cenas for menor que /60 s, então mantém-se uma taxa de 60 fps. Porem se o tempo de geração for pouco maior que /60 s, a taxa de atualização cai para 30 fps. Com o uso de triple-buffer, a taxa se mantém próxima de 60 fps. O inconveniente do uso de triple-buffer é que o tempo de resposta para ações do usuário é um pouco maior, especialmente se o fps for baixo. 8. Tópicos adicionais Vários tópicos suportados pelo OpenGL não foram tratados neste material. Para detalhes sobre assuntos como blending, fog, antialiasing, bitmaps, quádricas, NURBS, picking, e extensões OpenGL, consulte []. Para técnicas de renderização como sombras, efeitos especiais e técnicas de otimização, consulte [2]. 9. Referências Bibliográficas [] "GLEW," 22 07 203. [Online]. Available: http://glew.sourceforge.net/. [2] "opengl - How many VBOs do I use?," 28 2 20. [Online]. Available: http://stackoverflow.com/questions/866629/how-many-vbos-do-i-use. [3] "Vertex Specification Best Practices," 22 0 202. [Online]. Available: http://www.opengl.org/wiki/vertex_specification_best_practices. [4] "Modifying only a specific element type of VBO buffer data?," 3 07 20. [Online]. Available: http://stackoverflow.com/questions/66887/modifying-only-a-specific-element-type-of-vbo-bufferdata. [5] "Vertex Specification," 8 8 202. [Online]. Available: http://www.opengl.org/wiki/vertex_specification. [6] "OpenGL 4 Reference Pages," 9 09 202. [Online]. Available: http://www.opengl.org/sdk/docs/man/xhtml/. [7] Woo, M.; Neider, J.; Davis, T.; Shreiner, D. OpenGL: Programming Guide. 5. ed. Massachusetts : Addison-Wesley, 2005. [8] Moller, T.; Haines, E.; Akenine, T. Real-Time Rendering. 2 ed. AK Peters, 2002. [9] Celes, W. Notas de Aula. PUC-Rio, 2006. [0] OpenGL.org, Vertex Specification. Disponível em: http://www.opengl.org/wiki/vertex_specification [] ozone3d.net, OpenGL Tutorials. Disponível em: http://www.ozone3d.net/tutorials/opengl_vbo_p2.php#part_26 2