Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos da Categoria: Uncategorized

VAOs e VBOs

Quando você lê sobre os Vertex Array Objects na especificação do OpenGL, a coisa pode ficar meio confusa. Em alguns casos você é levado a acreditar que esse objeto mantém o estado apenas dos mapas de atributos de um array de vértices (daí o nome), mas, de fato, ele mantém o estado, inclusive, dos Buffer Objects usados no mapeamento. Até 16 conjuntos de mapas de atributos e até 16 VBOs podem ter seus estados mantidos num único VAO.

VBOs, como já descrevi, existem como containers de dados que serão hospedados no servidor OpenGL, por exemplo, na memória da sua placa de vídeo, ao invés da memória do sistema. Isso acelera um bocado o processamento gráfico, mas cria uma complicação: As posições onde cada componente de um vértice contido nesse buffer precisam ser informadas para o shader através de funções como glVertexAttribPointer. Ou seja, “shader, a coordenada x,y,z está na posição 0 do VBO nº 2 e é composta com 3 floats… mas a cor do vértice está na posição 0 do VBO nº 3 (ou na posição 12 da mesma VBO nº 2) e tem tamanho…”. Esses são os “atributos” do vértice que glVerterAttribPointer dispobiliza e armazena na VAO.

Eis a primeira parte da lista de itens que uma VAO mantém:

Estado Tipo Tamanho Default
VERTEX_ATTRIB_ARRAY_ENABLED bool 16 false
VERTEX_ATTRIB_ARRAY_NORMALIZED bool 16 false
VERTEX_ATTRIB_ARRAY_INTEGER bool 16 false
VERTEX_ATTRIB_ARRAY_LONG bool 16 false
VERTEX_ATTRIB_ARRAY_SIZE uint 16 4
VERTEX_ATTRIB_ARRAY_STRIDE uint 16 0
VERTEX_ATTRIB_ARRAY_TYPE GLenum 16 GL_FLOAT
VERTEX_ATTRIB_ARRAY_POINTER void * 16 NULL
VERTEX_ATTRIB_ARRAY_DIVISOR uint 16 0

Repare que até 16 itens podem ser armazenados. A mesma coisa acontece com outros estados, exceto para o ELEMENT_ARRAY_BUFFER_BINDING, que refere-se ao VBO que contém a lista de índices que montam as faces do objeto a ser desenhado.

Quanto aos VBOs ligados ao VAO, seus estados são mantidos por 5 possíveis estados:

Estado Tipo Tamanho Default
VERTEX_ATTRIB_BUFFER_BINDING uint 16 0
VERTEX_ATTRIB_BINDING* uint 16 ?
VERTEX_ATTRIB_REL_ACTIVE_OFFSET uint 16 0
VERTEX_BINDING_OFFSET uint 16 0
VERTEX_BINDING_STRIDE uint 16 16

O VERTEX_ATTRIB_BINDING é um caso especial e vale ler a documentação da função glVertexAttrib (note a ausência de “Pointer” no final), mas, em resumo, os VBOs usados na VAO são mapeados.

Isso significa que, numa inicialização, você fez o binding de um VAO recém criado, e ajustou os atributos da estrutura de seu vértice a ser passada para o vertex shader usando até 16 VBOs, não será necessário refazer os bindings dos VBOs quando você re-selecionar a VAO antes da chamada a glDrawElements, por exemplo. O VAO mantém o binding de cada uma das 16 possíveis locations.

PS:

Existe um estado único (não são 16) extra chamado LABEL que é uma string. Mas isso é primariamente usado para debugging e não tem lá grande importância.

Anúncios

O que é e o que não é o OpenGL

Antes de continuar debulhando algorítmos e funções é interessante definir o que OpenGL pode fazer o que não pode. Alguns de vocês pode ter o conteito de que OpenGL é uma biblioteca mágica que engloba todas as funcionalidades possíveis para o desenho, em tempo real, de qualquer efeito ou recurso necessário para obter uma animação realista tridimensional. Se for assim, ficará desapontado… Grande parte do que pode ser visto em jogos, por exemplo, é criado por rotinas independentes do OpenGL (ou do DirectX, no caso do Windows)…

Em resumo, OpenGL é uma biblioteca que permite posicionar pontos no espaço tridimensional e projetá-los no espaço bidimensional correspondente a uma janela ou a tela do computador. Ele também contém alguns recursos interessantes como texturas. Ele não contém rotinas para renderizar sombras, iluminação, detecção de colisões, “radiosidade”, oclusão, … Tudo isso é feito fora do contexto do OpenGL ou, no máximo, no interior de rotinas especializadas chamadas de shaders − mesmo assim, são rotinas definidas pelo programador!

Você viu, anteriormente, que existem diversos sistemas de coordenadas usadas pelo OpenGL. Mas, o que ocorre, na realidade, é que só existem 2 sistemas: World CoordinatesScreen Coordinates. O segundo é a projeção do primeiro no plano xy. Para facilitar a vida criam-se outros sistemas com o auxílio de transformações lineares:

Transformações lineares.

Transformações lineares.

No OpenGL anterior à versão 3, as matrizes de transformação mostradas acima eram codificadas em funções do próprio OpenGL… Isso não existe mais. Para posicionar e transformar os objetos definidos no object space para o Screen Space temos que, explicitamente, definir as matrizes ModelView e Projection e passá-las para o vertex shader. Isso significa que esses “espaços” são construções meramente matemáticas e já fazem parte do OpenGL. Um vertex shader típico seria:

#version 430 core

layout(location=0) in vec3 vpos;

uniform mat4 modelviewMatrix;
uniform mat4 projectionMatrix;

void main()
{
  gl_Position = projectionMatrix * modelviewMatrix * vec4(vpos, 1.0);
}

Onde modelviewMatrixprojectionMatrix são definidas no programa host. A primeira transforma o objeto definido no object space para o view space (ou camera space) e a segunda faz a projeção do espaço tridimensional do view space para o espaço bidimensional do Screen space.

Note também que OpenGL não lida, diretamente, com a figura geométrica como um todo… Ao definir um triângulo, definimos apenas as características dos vértices. As informações de preenchimento e contorno dos lados são interpolações lineares baseadas nos atributos dos vértices. Interpolações lineares seguem a equação abaixo:

\displaystyle \dot{p}'=(1-n)\dot{p}_1+n\dot{p}_2,\quad n \in [0,1]

Por causa dessa interpolação, OpenGL não garante que duas placas de vídeo desenharão triângulos do mesmo jeito. Praticamente tudo o que OpenGL garante é que a sequência dos comendos será obedecida… Ao mandar OpenGL desenhar um triângulo ele o fará, mas se as bordas serão desenhadas antes do preenchimento, ou vice-versa, isso não é garantido.

Outra garantia do OpenGL: Uma vez definido o viewport (a área retangular que será visível no plano da tela), os limites de coordenadas são padronizados em [-1, 1]. Ou seja, o canto inferior esquedo da área visível é (-1,-1) e o canto superior direito (1,1). Ainda, o centro da área visível é a origem do sistema de coordenadas (0,0)… Observe, de novo, como defini o triângulo no exemplo do post anterior… Lá, a sequência de coordenadas é definida no sentido anti-horário e a primeira coordenada é (0, 0.707, -1). Ou seja, o topo do triângulo está a pouco mais de 70% do caminho entre o centro da janela e o topo. Ao mesmo tempo, os outros dois pontos são definidos com suas coordenadas X como -0.5 e 0.5, respectivamente:

Saída do código de exemplo.

Saída do código de exemplo.

Assim, o escalonamento é importante. Ao definir seu mundo em uma escala, você será obrigado a modificá-la para caber no padrão do OpenGL (e isso pode ser feito na matriz modelview). E, é claro, dependendo do aspect ratio, esse exemplo ai em cima pode ficar distorcido… No exemplo criei uma janela de aspect ratio 16:9, já que esse é o formato de meu monitor de vídeo… Ou seja, nesse formato é garantido que o pixel terá o formato “quadrado” esperado. Se tivesse escolhido uma resolução de 640×480, ou seja, com aspect ratio de 4:3, o triângulo seria mais achatado, horizontalmente.

 

Uma diferença impressionante numa rotina básica.

A rotina de normalização de um vetor, aquela que transforma um vetor qualquer num vetor unitário, constitui-se em dividir os componentes x, y e z do vetor original pelo seu tamanho… Mas, essa é uma das rotinas fundamentais de todo o projeto e, por isso, tem que ser feita de forma rápida, bem rápida! Vejamos duas implementações possíveis:

void vec3_normalize_mul(float *v)
{
  float len = vec3_length(v);

  if (len != 0.0f)
  {
    len = 1.0f / len;

    v[0] *= len;
    v[1] *= len;
    v[2] *= len;
  }
}

void vec3_normalize_div(float *v)
{
  float len = vec3_length(v);

  if (len != 0.0f)
  {
    v[0] /= len;
    v[1] /= len;
    v[2] /= len;
  }
}

Essencialmente as duas rotinas fazem a mesma coisa. Mas, se você medir a quantidade de ciclos de clock gastos por ambas terá uma surpresa…  A primeira rotina gasta cerca de 350 ciclos, mas a segunda gasta cerca de 2500 ciclos! Ou seja, fazer uma divisão e 3 multiplicações é quase 615% mais rápido do que fazer três divisões!

Evite divisões!

Livro novo sendo feito….

Alguns de vocês já sabem que em 1994 eu publiquei um conjunto de textos que ficou sendo conhecido como “Curso de Assembly”. Esses textos são, hoje, meio que obsoletos – embora ainda sejam válidos. Desde essa época tenho pensado em escrever um livro “de verdade”, mas que seja um pouco mais abrangente e mais atualizado.

Well… estou escrevendo um com o título “C & Assembly para arquitetura x86-64” e o rascunho mais recente está disponível no grupo, de mesmo nome, no Facebook: aqui

Para quem não tem Facebook, a versão mais recente do rascunho está disponível no Google Drive: aqui.

Voltando atrás…

Voltei atrás e retirei a maioria das otimizações usando SSE. Especialmente com relação aos vetores. O motivo é que colocar o componente w nos vetores, embora facilite o uso de SSE, consome 4 bytes adicionais para cada vetor. Num objeto com 10000 vértices isso dá cerca de 400 kB adicionais.

As rotinas de vetores agora tém nomes como Vector3Normalize() e aceitam ponteiros para float.

Um interessante tutorial sobre CG básica, no Youtube

Eis uma playlist com alguns vídeos falando sobre triângulos e matrizes. Clique aqui para ver a playlist.

Não… eu não morri!

Peço desculpas aos pouquíssimos leitores desse blog. Andei sumido e vou andar um cadinho mais sumido. Dentre outras coisas estou estudando o OpenGL 3.3 (pq minhas máquinas só “pegam” essa versão) e portando meus códigos de teste para essa versão.

Assim que eu tiver novidades e melhores exemplos para mostrar pra vocês, posto por aqui.

[]s
Fred

A new begining…

Com o encerramento das atividades do Lost in the e-Jungle começo um novo projeto, bem mais técnico e menos filosófico. Há algum tempo venho estudando e criando módulos para um projeto que chamei pelo codinome Virtual Worlds — ênfase no codinome porque não quero ferir os direitos autorais de ninguém, caso um projeto como este já exista — trata-se de um engine 3D para criação de “jogos” totalmente escrito em C e, eventualmente, com algumas pequenas partes em assembly.

É claro que existem diversos engines por ai que são bastante robustos. Só que esses engines pecam em dois aspectos que são muito importantes para mim:

  1. Documentação pobre;
  2. São feitos em C++ ou alguma linguagem com recursos de orientação à objetos

Tentei mesmo usar ferramentas como o Crystal Space, Ogre 3D e outros, mas sempre esbarro no problema (1), acima. Quanto ao fato de serem desenvolvidos em C++, não existem grandes problemas com relação a isso, exceto que eu sou uma espécie de high performance junkie.

Este é um blog de acompanhamento do projeto e de coisas que eu for assimilando no caminho para o desenvolvimento do projeto. Para aqueles que estão acompanhando os textos publicados no Bit Is Myth, saibam que continuarei escrevendo por lá. Aqui a minha dedicação é ao meu projeto…

Como é de se esperar, dada as características de um projeto deste tamanho, passarei algum tempo estudando antes de criar algum post importante. Esperem um delay grande entre posts, ok?

[]s
Frederico Lamberti Pissarra