OpenGL – Desenhando polígonos

Ao aprender OpenGL, você decidiu que queria fazer todo trabalho você mesmo. Isso inevitavelmente significa que você será jogado no fundo, mas uma vez que tenha entendido o essencial, verá que fazer as coisas da maneira mais difícil  não tem que ser difícil.

O diagrama abaixo, chamado de graphic pipeline, cobre todos os passos consecutivos no processamento dos dados de entrada até a imagem final. Esses passos serão explicados com a ajuda desse diagrama.
c2_pipeline
 
Tudo começa com o vértices, que são os pontos com os quais as formas serão construídas. Cada um desses pontos é armazenado com certos atributos e cabe a você decidir quais atributos você quer armazenar. Atributos usados com frequência são a posição 3D no mundo e as coordenadas da textura.
vertex shader é um pequeno programa que roda em sua placa de vídeo que processa cada um desses vértices individualmente. Aqui é onde a transformação de perspectiva ocorre, o que projeta os vértices de um mundo 3D em uma tela 2D! Também determina atributos importantes como cor e coordenadas da textura para a graphic pipeline.
Depois que os vértices de entrada terem sido processados, a placa de vídeo irá formas triângulos, linhas e pontos a partir deles. Essas formas são chamadas primitivas pois formam a base de formas mais complexas.  Existem alguns modos de desenho para se escolher, como triangle strips line strips. Esses modos reduzem o número de vértices que você precisa passar se quiser criar objetos onde cada primitiva está conectada à anterior, como uma linha continua que consiste de vários segmentos.
O passo seguinte, o geometry shader, é completamente opcional e foi introduzido bem recentemente. Ao contrário de vertex shader, o geometry shader pode retornar mais dados do que recebe. Ele pega as primitivas  do estágio de montagem das formas como entrada e pode ou retornar um primitiva para o restante do fluxo gráfico, modifica-la primeiro, descarta-la completamente ou mesmo substitui-la por outra(s) primitiva(s). Como a comunicação entre a GPU e o PC é relativamente lenta, essa etapa pode ajudar você a reduzir a quantidade de dados que precisam ser transferidos. Em um jogo, por exemplo, você pode passar vértices como pontos, junto com um atributo que guarde sua posição no mundo, cor e material e a forma desejada pode ser produzida neste shader como apenas um ponto como entrada.
Depois que  lista final de formas é composta e convertidas em coordenadas da tela, o rasterizador converter as partes visíveis das formas em fragmentos. Os atributos do vértice vindos do vertex shader ou do geometry shader são interpolados e passados como entrada para o fragment shader para cada fragmento. Como você pode ver na imagem, as cores são interpoladas suavemente pelo fragmento que forma o triângulo, mesmo que apenas 3 pontos tenham sido especificados.
fragment shader processa cada fragmento individualmente junto com seus atributos interpolados e deve retornar a cor final. Isso normalmente é feito através da leitura de uma amostra de uma textura usando os atributos do vértice da coordenada da textura ou simplesmente informando uma cor. Em cenários mais avançados, podem haver cálculos relacionadas a iluminação e sombra e efeitos especiais nesse programa. O shader também possui a habilidade para descardas certos fragmentos, o que pode significar que uma forma será transparente nessa região.
Finalmente, o resultado final é composto a partir de todos esses fragmentos pela mistura de todos e a execução de testes de profundidade e stencil. Tudo o que você precisa saber sobre esses dois últimos agora é que eles permitem que você use regras adicionais para descartar certos fragmentos e deixar outros passarem. Por exemplo, se um triângulo é obscurecido por outro, o fragmento do triângulo que está mais próximo deve estar na tela.
Agora que você sabe como sua placa gráfica  transforma um array de vértices em uma imagem na tela, vamos começar o trabalho!

Entrada de vértices

A primeiro coisa que você precisa decidir é quais dados a placa de vídeo irá precisar para desenhar a sua cena corretamente. Como mencionado acima, esses dados vem na forma de atributos de vértices. Você tem a liberdade de criar quaisquer tipo de atributo que quiser, mas tudo começa inevitavelmente com a posição no mundo. Esteja você criando um gráfico 2D ou 3D, este é o atributo que irá determinar onde os objetos e formas serão mostradas na tela.

Coordenadas do dispositivo
Quando seus vértices tiverem sido processados pelo fluxo destacado acima, suas coordenadas terão sido transformadas em coordenadas do dispositivo. As coordenadas X e Y do dispositivo são mapeadas na tela com os valores -1 e 1.
c2_dcc2_dc2
Assim como em um gráfico, o centro possui as coordenadas (0, 0) e o eixo y é positivo acima do centro. Isso soa não natural porque aplicações gráficas normalmente definem o (0, 0) no canto superior esquerdo e (largura, altura) no canto inferior direito, mas é uma excelente forma de simplificar cálculos 3D e permanecer independente da resolução.

O triângulo acima consiste de 3 vértices posicionados em (0, 0.5), (0.5, -0.5) e (-0.5, -0.5) em ordem horária. Fica evidente que a única variação entre os vértices aqui é a posição, assim este é o único atributo que precisamos. Como estamos passando as coordenadas do dispositivo diretamente, as coordenadas X e Y são suficientes para a posição.
O OpenGL espera que você envie todos os vértices em um único array, o que pode ser confuso no início. Para entender o formato deste array, vamos ver como ele deve se parecer para nosso triângulo.

float vertices[] = {
     0.0f,  0.5f, // Vertex 1 (X, Y)
     0.5f, -0.5f, // Vertex 2 (X, Y)
    -0.5f, -0.5f  // Vertex 3 (X, Y)
};

Como você pode ver, esse array deve simplesmente ser uma lista de todos os vértices com seus atributos empacotados juntos. A ordem em que os atributos aparecem não importa, desde que seja a mesma para cada vértice. O ordem dos vértices não tem que ser sequencial (isto é, a ordem em que a forma é formada), mas isso irá requerer o fornecimento de dados extras na forma de um element buffer. Isso será discutido no final desse capítulo e pode complicar as coisas nesse momento.
O próximo passo é carregar os dados dos vértices na placa de vídeo. Isso é importante porque a memória de sua placa é muito mais rápida e você não terá que enviar os dados novamente toda vez que sua cena precisar ser renderizada (aproximadamente 60 vezes por segundo).
Isso é feito pela criação de um Vertex Buffer Object (VBO):

GLuint vbo;
glGenBuffers(1, &vbo); // Generate 1 buffer

A memória é gerenciado pelo OpenGL, assim ao invés de um ponteiro você obtém um número positivo como referência. GLuint é simplesmente um substituto multi-plataforma para unsigned int, assim como GLint é para int. Você precisará desse número para tornar o VBO ativo e para destruí-lo quando terminar de usa-lo.
Para carregar os dados você precisa tornar o object ativo pela chamada à glBindBuffer:

glBindBuffer(GL_ARRAY_BUFFER, vbo);

Como GL_ARRAY_BUFFER dá a entender, existem outros tipos de buffers, mas eles não tem importância agora. Essa sentença torna ativo o VBO que acabamos de criar. Agora que ele está ativo, podemos copiar os dados do vértice para ele.

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

Note que essa função não faz referência para o id de nosso VBO, mas ao invés disso para o buffer ativo. O segundo parâmetro especifica o tamanho em bytes. O parâmetro final é muito importante e seu valor depende do uso dos dados dos vértices. Abaixo, destaco aqueles relacionados ao desenho:

  • GL_STATIC_DRAW: Os dados dos vértices serão carregados uma vez e desenhados muitas vezes (ex.: o mundo).
  • GL_DYNAMIC_DRAW: Os dados dos vértices serão alterados de tempos e tempos, mas desenhados muitas vezes mais do que isso.
  • GL_STREAM_DRAW: Os dados dos vértices serão alterados quase todas as vezes que forem desenhados (ex.: interface com o usuário).

Esse valor para o tipo de uso irá determinar em que tipo de memória os dados serão armazenados em sua placa de vídeo para maior eficiência. Por exemplo, VBOs com o tipo GL_STREAM_DRAW podem armazenar seus dados em uma memória que permita escrita mais rápida mas que desenhe-o mais lentamente.
Os vértices com seus atributos foram copiados para a placa de vídeo agora, mas eles não estão prontos para serem usados ainda. Lembre que podemos criar quaisquer tipo de atributo em quaisquer ordem que quisermos, então agora chegou o momento onde teremos que explicar para a placa de vídeo como tratar esses atributos. Aqui é onde veremos o quanto o OpenGL moderno realmente é.

Shaders

Como discutido anteriormente, existem três estágios de shader que seus dados de vértices irão passar. Cada estágio tem um propósito bem definido e em versão antigas do OpenGL você tinha pouco controle sobre o que acontecia e como acontecia em cada estágio. Como o OpenGL moderno cabe a você instruir a placa de vídeo o que ela deve fazer com os dados. É por isso que é possível decidir por aplicação quais atributos cada vértice deve ter. Você precisará implementar tanto o vertex shader quanto o fragment shader para exibir algo na tela, o geometry shader é opcional e será discutido futuramente.
Shaders são escritos em uma linguagem estilo C chamada GLSL (OpenGL Shading Language). O OpenGL irá compilar seu programa a partir do código fonte durante a execução e copia-lo para a placa de vídeo. Cada versão do OpenGL possui sua própria versão da linguagem de shader com a disponibilidade de certos recursos. Aqui estaremos usando a versão 1.50 do GLSL. Essa versão pode parecer estranha quando estamos usando o OpenGL 3.2, mas isso se deve ao fato de que shader foram introduzidos no OpenGL 2.0 como GLSL 1.10. A partir do OpenGL 3.3 esse problema foi resolvido e a versão do GLSL é a mesma do OpenGL.

Vertex shader

vertex shader é um programa que roda na placa de vídeo que processa cada vértice e seus atributos como eles aparecem no array de vértices. É seu trabalho retornar a posição final do vértice em coordenadas do dispositivo e retornar qualquer dado necessário ao fragment shader. Por isso a transformação 3D deve ser feita aqui. O fragment shader depende de atributos como a cor e coordenadas da textura, que normalmente irão ser passadas da entrada para saída sem qualquer cálculo.
Lembre que nossos vértices já foram especificados como coordenadas de dispositivos e nenhum outro atributo existe, assim o vertex shader será bem simples:

#version 150
in vec2 position;
void main()
{
    gl_Position = vec4(position, 0.0, 1.0);
}

A diretiva de pré-processador #version é usada para indicar que o código usa a GLSA 1.50. Em seguida, especificamos que existe apenas um atributo, position. Diferente aos tipos normais de C, GLSL tem tipos embutidos para vetores e matrizes, identificados por vec* mat*. O tipo dos valores dentre dessas estruturas é sempre float. O número após o vec especifica o número de componentes (x, y, z, w) e o número após mat especifica o número de linhas/colunas. Como o atributo posição consiste de apenas uma coordenada X e Y, vec2 é perfeito.

Você pode ser bastante criativo quando trabalhar com esses tipos de vértices. No exemplo acima, um atalho foi usado para configurar os dois primeiros componentes de vec4 para esses de vec2. Essas duas linhas são iguais:

gl_Position = vec4(position, 0.0, 1.0);
gl_Position = vec4(position.x, position.y, 0.0, 1.0);

Quando estiver trabalhando com cores, você podeacessar os componentes individuais com r, g, b e a ao invés de x, y, z e w. Isso não faz nenhuma diferença e pode ajudar a tornar o código mais claro.

A posição final do vértice é associada à variável especial gl_Position, porque a posição é necessário para a construção da primitiva e muitos outros processos. Para estes funcionarem corretamente, o último valor precisa ter um valor 1.0f. Fora isso, você está livre para fazer o que quiser com os atributos e veremos como retornar eles quando adicionarmos cores ao triângulo em um artigo futuro.

Fragment shader

O valor retorno pelo vertex shader é interpolado para todos os pixels da tela usados por uma primitiva. Esses pixels são chamados de fragmentos e é onde os fragment shader opera. Assim como o vertex shader possui uma saída obrigatória, este shader possui também: a cor final do fragmento. Cabe a você escrever o código para calcular essa cor a partir das cores do vértice, das coordenadas da textura e de qualquer dado vindo do vertex shader.
Nosso triângulo consiste apenas de pixels brancos, assim o fragment shader simplesmente retorna essa cor:

#version 150
out vec4 outColor;
void main()
{
    outColor = vec4(1.0, 1.0, 1.0, 1.0);
}

Você pode notar de imediato que não estamos usando uma variável embutida para retornar a cor, algo como gl_FragColor. Isso se deve ao fato de que o fragment shader poder retornar múltiplas cores e veremos como lidar com isso quando formos carregar esses shaders. A variável outColor usa o tipo vec4, por isso cada cor é formada pelos componentes redgreenblue alpha. As cores no OpenGL são normalmente representadas por número de ponto flutuante entre 0.0 e 1.0 ao invés de 0 e 255.

Compilando shaders

Compilar shaders é fácil uma vez que você tenha carregado o código fonte (seja de um arquivo ou uma string). Assim como os vertex buffers, começa pela criação de um object de shader e o carregamento de dados nele.

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSource, NULL);

Ao contrário dos VBOs, você pode simplesmente passar uma referência para uma função de shader ao invés de torna-la ativa ou algo do tipo. A função glShaderSource pode ler múltiplas string em um array, mas normalmente você armazenará seu código fonte em um array char. O último parâmetro pode conter um array com os tamanhos das string com o código fonte, usando NULL simplesmente faz com que a leitura pare quando chegar no NULL.
Tudo que resta agora é compilar o shader em um código que possa ser executado pela placa de vídeo:

glCompileShader(vertexShader);

Fique ciente que se o shader falha em compilar, no caso de um erro de sintaxe por exemplo, glGetError não irá reportar um erro! Veja o bloco abaixo para informações de como depurar os shaders.

Verificando se um shader foi compilado com sucesso

GLint status;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status);

Se status for igual a GL_TRUE, então seu shader foi compilador com sucesso.
Recuperando o log de compilação

char buffer[512];
glGetShaderInfoLog(vertexShader, 512, NULL, buffer);

Isso irá armazenar os primeiros 511 bytes mais a terminação NULL do log de compilação no buffer especificado. O log pode também reportar aviso úteis quando a compilação for bem sucedida, então é útil checa-lo de tempos em tempos quando estiver desenvolvendo seus shaders.

fragment shader é compilado exatamente da mesma maneira:

GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);

Novamente, certifique-se de que se seu shader foi compilado com sucesso, porque irá lhe salvar de muita dor de cabeça no futuro.

Combinando shaders em um program

Até agora o vertex shader  e o fragment shader tem sido dois objectos separados. Mesmo que eles tenham sido programados para trabalhar juntos, eles ainda não foram conectados. Essa conexão é feita pela criação de programa a partir desses dois shaders.

GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);

Como o fragment shader pode retornar múltiplos buffers, você precisa especificar quais retornos serão escritos e em quais buffers. Isso precisa ser feito antes de lincar o programa. Porém, como o valor 0 é o padrão e nesse exemplo só existe um valor sendo retornado, a seguinte linha de código não é necessária:

glBindFragDataLocation(shaderProgram, 0, "outColor");

Após anexar os dois shaders, a conexão é feito através da lincagem (linking) do program. É permitido fazer alterações aos shaders depois que eles terem sido adicionados ao programa (ou à múltiplos programas), mas o resultado final não será alterado até que o programa tenha sido lincado novamente. É possível também anexar múltiplos shaders para o mesmo estáfio (ex.: fragmento) se eles forem partes que formarem um shader completo. Um object de shader pode ser deletado com glDeleteShader, mas ele não será removido de fato antes de ser destacado de todos os programas com glDetachShader.

glLinkProgram(shaderProgram);

Para começar a usar de fato os shaders em um program, você tem que invocar a seguinte função:

glUseProgram(shaderProgram);

Assim como o vertex buffer, apenas um programa pode estar ativo ao mesmo tempo.

Criando uma ligação entre os dados dos vértices e os atributos

Apesar de agora termos nossos dados de vértices e shaders, o OpenGL ainda não sabe como os atributos são formatados e ordenados. Você primeiro precisa recuperar uma referência para a variável de entrada position do vertex shader:

GLint posAttrib = glGetAttribLocation(shaderProgram, "position");

A localização é um número que depende da ordem das definições da entraa. A primeira e única entrada position nesse exemplo terá sempre o valor 0.
Com a referência para a entrada, você pode especificar como os dados dessa entrada serão recuperados do array:

glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);

O primeiro parâmetro referencia a entrada. O segundo especifica o número de valores para essa entrada, que é o mesmo número de componentes de vec. O terceiro parâmetro especifica o tipo de cada componente e o quarto especifica se os valores da entrada devem ser normalizados para o intervalo -1.0 e 1.0 (ou 0.0 e 1.0 dependendo do formato) se eles não forem números de ponto flutuante.
Os últimos dois parâmetros são sem dúvida os mais importantes aqui pois eles especificam como os atributos estão dispostos no array de vértices. O primeiro número especifica o stride (passo), ou quantos bytes estão entre cada atributo no array. O valor 0 significa que não há nenhum dado entre eles. Este é o caso já que a posição de cada vértice é imediatamente seguida pela posição do próximo. O último parâmetro especifica o offset (deslocamento), ou a quantos bytes do início do array o atributo está. Como não há nenhum outro atributo, esse valor é 0 também.
É importante saber que essa função irá armazenar não apenas o passo e o deslocamento, mas também o VBO que está ligado ao GL_ARRAY_BUFFER. Isso significa que você não tem que  associar o VBO correto quando a função de desenho é chamado. Isso também significa que você pode usar um VBO diferente para cada atributo.
Não se preocupe se você ainda não entender isso completamente, pois iremos ver como alterar esse exemplo para adicionar mais atributos em breve.

glEnableVertexAttribArray(posAttrib);

Por último, o array de atributos de vértices precisa ser ativado.

Vertex Array Objects

Você pode imaginar que programas gráficos reais usam muitos shaders diferentes e layouts de vértices para cuidar de uma ampla variedade de efeitos especiais. Alterar o shader ativo é fácil o suficiente com uma chamada para glUseProgram mas seria inconveniente ter que configurar todos os atributos novamente todas as vezes.
Por sorte, o OpenGL resolve esse problema com os Vertex Array Objects (VAO). VAOs armazenam os links entre os atributos e seus VBOs com dados de vértices brutos.
Um VAO é criado da mesma forma que um VBO:

GLuint vao;
glGenVertexArrays(1, &vao);

Para começar a usa-lo, simplesmente ative-o:

glBindVertexArray(vao);

Assim que você tiver ativado um certo VAO, toda vez que chamar glVertexAttribPointer, essa informação será armazenada nesse VAO. Isso torna a alternância entre diferentes dados e formatos de vértices  tão fácil quanto ativar um VAO diferente! Só lembre que um VAO não armazena nenhum dado de vértice por si só, apenas faz referência aos VBOs que você criou e como recuperar os valores dos atributos deles.
Como apenas chamadas posteriores à ativação do VAO são consideradas, certifique de criar e ativar o VAO no início de seu programa.

Desenhando

Agora que você carregou os dados dos vértices, criou os programas de shader e lincou os dados aos atributos, você está pronto para desenhar o triângulo. O VAO que foi usado para armazenar os atributos já está ativo, assim você não precisa se preocupar com isso. Tudo que resta é simplesmente chamar glDrawArrays em seu loop principal:

glDrawArrays(GL_TRIANGLES, 0, 3);

O primeiro parâmetro especifica o tipo de primitiva (geralmente um ponto, linha ou triângulo), o segundo especifica quantos vértices devem ser pulado a partir do início e o último especifica o número de vértices (não de primitivas!) a serem processados.
Quando você executa o seu programa agora, deve visualizar o seguinte:
c2_window
 
Se você não visualizar nada, verifique se os shaders foram compilados corretamente, que o programa foi lincado corretamente, que o array de atributos e o VAO, que o VAO foi ativado antes da especificação dos atributos, que os dados dos vértices estão corretos e que glGetError retorna 0. Se você não conseguir encontrar o problema, compare o seu código com esse exemplo.

Uniforms

Até agora, a cor branca do triângulo foi codificado diretamente no shader, mas se você você quiser altera-la depois que ele foi compilado? Como esperado, atributos de vértices não são a única maneira de passar dados para os programas de shader. Existe uma outra maneira de passar dados para os shader chamad uniforms. Essas estruturas são basicamente variáveis globais, que possuem o mesmo valor para todos os vértices e/ou fragmentos. Para demonstrar como usa-las, vamos demonstrar como fazer para tornar possível alterar a cor do triângulo do próprio programa.
Ao tornar a cor no fragment shader em um unform, ele irá ficar parecido com isso:

#version 150
uniform vec3 triangleColor;
out vec4 outColor;
void main()
{
    outColor = vec4(triangleColor, 1.0);
}

O último componente da cor retornada é a transparência, que não é muito importante agora. Se você executar seu programa agora verá que o triângulo fica preto, porque nenhum valor para triangleColor foi informado.
Alterar o valor de um uniform é feito da mesma forma que configurar atributos dos vértices, você primeiro precisa recuperar a localização:

GLint uniColor = glGetUniformLocation(shaderProgram, "triangleColor");

Os valores de uniforms são alterados com qualquer uma das funções glUniformXY, onde X é o número de componentes e Y é o tipo. Os tipos mais comuns são f (float), d (double) e (integer).

glUniform3f(uniColor, 1.0f, 0.0f, 0.0f);

Se você executar seu programa agora, verá que o triângulo fica vermelho. Para tornar as coisas um pouco mais excitantes, tente variar a cor no tempo fazendo algo como isso em seu loop principal:

float time = (float)clock() / (float)CLOCKS_PER_SEC;
glUniform3f(uniColor, (sin(time * 4.0f) + 1.0f) / 2.0f, 0.0f, 0.0f);

Apesar desse exemplo não ser muito interessante, ele demonstra que uniforms são essenciais para controlar o comportamento dos shaders durante a execução. Atributos de vértices por outro lado são ideais para descrever um único vértice.


 
Veja esse código se tiver alguma problema fazendo isso funcionar.

Adicionando mais algumas cores

Apesar de uniforms terem o seu lugar, corres é algo que seria preferìvel especificar por canto do triângulo! Valos adicionar um atributo de cor aos vértices para conseguir isso.
Em primeiro lugar, precisamos adicionar atributos extras aos dados do vértice. Transparência não é importante agora, então vamos adicionar apenas os componentes redgreenblue.

float vertices[] = {
     0.0f,  0.5f, 1.0f, 0.0f, 0.0f, // Vertex 1: Red
     0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Vertex 2: Green
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f  // Vertex 3: Blue
};

Então temos que alterar o vertex shader para pegar essa entrada e passa-la para o fragment shader:

#version 150
in vec2 position;
in vec3 color;
out vec3 Color;
void main()/media/img/c2_window3.png
{
    Color = color;
    gl_Position = vec4(position, 0.0, 1.0);
}

Color é adicionado como uma entrada do fragment shader:

#version 150
in vec3 Color;
out vec4 outColor;
void main()
{
    outColor = vec4(Color, 1.0);
}

Certifique-se de que a saída do vertex shader e a entrada do fragment shader possuem o mesmo nome, ou os shaders não serão lincados apropriadamente.
Agora só precisamos alterar o código do ponteiro para o atributo um pouco para acomodar a nova ordem de atributos.

GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
                       5*sizeof(float), 0);
GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
                       5*sizeof(float), (void*)(2*sizeof(float)));

O quinto parâmetro é configurado para 5*sizeof(float) agora, pois cada vértice consiste de 5 valores ponto flutuante. O deslocamente de 2*sizeof(float) para o atributo cor é necessário porque cada vértice começa com 2 valores de ponto flutuante para a posição que precisam ser pulados.
E com isso está feito!
c2_window2
 
Você deve ter agora um conhecimento razoável de atributos de vértices e shaders. Se encontrar algum problema, pergunte nos comentários ou dê uma olha neste código fonte.

Element buffers

Até agora, os vértices são especificados na ordem em que eles são desenhados. Se você quiser adicionar outro triângulo, precisa adicionar 3 vértices adicionais no array de vértices. Existe uma maneira de controlar a ordem, que permite também que você reuse vértices existentes. Isso pode lhe economizar bastante memória mais para a frente quando estiver trabalhando com modelos 3D, pois cada ponto é normalmente ocupado por um canto de três triângulos!
Um array de elementos é preenchido com inteiros sem sinal que fazem referência aos vértices do GL_ARRAY_BUFFER. Se quisermos apenas desenha-los na ordem em que eles estão agora, o array deve se parecer com isso:

GLuint elements[] = {
    0, 1, 2
};

Eles serão então carregados na memória de vídeo através de um VBO da mesma forma que os dados dos vértices:

GLuint ebo;
glGenBuffers(1, &ebo);
...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
    sizeof(elements), elements, GL_STATIC_DRAW);

A única diferença é o alvo, que vai ser GL_ELEMENT_ARRAY_BUFFER dessa vez.
Para fazer uso desse buffer de fato, precisará alterar o comando de desenho:

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

O primeiro parâmetro é o mesmo do usado para glDrawArrays, mas todos os outros fazem referência ao element buffer. O segundo parâmetro especifica o número de índices a serem desenhados, o tericeiro especifica o tipo de dados de elementos e o último especifica o deslocamente. A única diferença real é que estamos falando de índices ao invés de vértices agora.
Para ver como um element buffer pode ser benéfico, vamos tentar desenhar um retângulo usando dois triângulos. Vamos começar fazendo isso sem esse buffer.

float vertices[] = {
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // Top-left
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // Top-right
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-right
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-right
    -0.5f, -0.5f, 1.0f, 1.0f, 1.0f, // Bottom-left
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f  // Top-left
};

Chamando glDrawArrays ao invés de glDrawElementi como antes, o element buffer será simplesmente ignorado:

glDrawArrays(GL_TRIANGLES, 0, 6);

O retângulo será renderizado como devia, mas a repetição de dados de vértices é um desperdício de memória. Usar um element buffer permite a você reutilizar os dados:

float vertices[] = {
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // Top-left
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // Top-right
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-right
    -0.5f, -0.5f, 1.0f, 1.0f, 1.0f  // Bottom-left
};
...
GLuint elements[] = {
    0, 1, 2,
    2, 3, 0
};
...
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

element buffer ainda especifica 6 vértices para formas 2 triângulos como antes, mas agora podemos reutilizar os vértices! Isso pode não parecer muito importante agora, mas quando sua aplicação gráfica carregar muitos modelos em uma memória gráfica relativamente pequena, element buffers serão uma área importante para otimização.
c2_window4
 
Se encontrar algum problema, dê uma olhada no código fonte completo.
Esse artigo cobriu todos os princípios gerais para desenhar coisa com o OpenGL e é absolutamente essencial para o entendimento de artigos futuros.
Fonte: open.gl/drawing