Programação com OpenGL/Modern OpenGL Tutorial 03

Attributes: Passando informações adicionais de vértices

editar

Será necessário mais algumas coordenadas em nosso programa. Por exemplo: cores. Vamos passar cores em RGB para o OpenGL.

Nós passaremos as coordenadas usando um attribute, assim iremos colocar novos atributos para as cores. Vamos modificar nossas variáveis globais:

GLuint vbo_triangle, vbo_triangle_colors;
GLint attribute_coord2d, attribute_v_color;

e nosso init_resources:

  GLfloat triangle_colors[] = {
    1.0, 1.0, 0.0,
    0.0, 0.0, 1.0,
    1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle_colors);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_colors), triangle_colors, GL_STATIC_DRAW);

  [...]

  attribute_name = "v_color";
  attribute_v_color = glGetAttribLocation(program, attribute_name);
  if (attribute_v_color == -1) {
    fprintf(stderr, "Could not bind attribute %s\n", attribute_name);
    return 0;
  }

Agora em nosso onDisplay, nós iremos passar 1(uma) cor RGB para cada 3 vértices. vou escolher amarelo, azul e vermelho, mas fique a vontade para escolher suas cores favoritas :)

  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glVertexAttribPointer(
    attribute_v_color, // atributos
    3,                 // numero de elemenos por vértice, aqui é (r,g,b)
    GL_FLOAT,          // o tipo de cada elementos
    GL_FALSE,          // estado dos nossos valores.
    0,                 // sem dados extras em cada posição.
    0                  // deslocamento do primeiro elemento.
  );

Vamos chamar o OpenGL apenas para completar o atributo, e terminar nossa função:

  glDisableVertexAttribArray(attribute_v_color);

Por ultimo, declararemos isto ao nosso vertex shader:

attribute vec3 v_color;

Neste ponto, ao rodarmos o programa, teremos isto:

Could not bind attribute v_color

Isto acontece porque não usamos o v_color ainda. [1]

O problema é: queremos cor no fragment shader, não no vertex shader! Vamos ver agora como fazer...

Varyings: Passando informações do Vertex Shader para o Fragment Shader

editar

Em vez de usar um atributo, vamos usar uma variável varying. Assim:

  • Uma variável output(saida) para o vertex shader
  • Um variavel input(entrada) para o fragment shader
  • Isto tudo interpolado.

assim teremos um canal de comunicação entre os dois shader. para entender porque ele é interpolado, vamos ao seguinte exemplo.

Nós vamos declarar nossa nova varying, dizendo f_color, no


 A varying declaration needs to be identical in the vertex shader and in the fragment shader.

em triangle.v.glsl:

attribute vec2 coord2d;
attribute vec3 v_color;
varying vec3 f_color;
void main(void) {
  gl_Position = vec4(coord2d, 0.0, 1.0);
  f_color = v_color;
}

em triangle.f.glsl:

varying vec3 f_color;
void main(void) {
  gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, 1.0);
}

(Nota: se estamos usando a GLES2, verifique a seção de portabilidade abaixo.)

Vamos ver o resultado:

 
Triângulo

nossa, temos realmente mais de três cores!

O OpenGL interpola os valores dos vertices em cada pixel. Isto explica o nome varying(algo como variando): ele varia para cada vértice(vertex), e depois varia mais em cada fragmento(fragment).

Nós não podemos declarar um varying no código C - que não tem uma interface que suporte varying em C.

Intercalando coordenadas e cores

editar

Para entender melhor a função glVertexAttribPointer, vamos misturar vários atributos em uma única array(ordenação):

  GLfloat triangle_attributes[] = {
     0.0,  0.8,   1.0, 1.0, 0.0,
    -0.8, -0.8,   0.0, 0.0, 1.0,
     0.8, -0.8,   1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

No glVertexAttribPinter o 5 elemento é uma stride, para dizer ao OpenGL quando cada atributo será configurado - em nosso caso 5 floats:

  glEnableVertexAttribArray(attribute_coord2d);
  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glVertexAttribPointer(
    attribute_coord2d,   // atributo
    2,                   // numero de elementos por vértice, aqui é (x,y)
    GL_FLOAT,            // o tipo de cada elemento
    GL_FALSE,            // como o valor está.
    5 * sizeof(GLfloat), // depois do coord2d aparece a cada 5 floats
    0                    // deslocamento do primeiro elemento
  );
  glVertexAttribPointer(
    attribute_v_color,      // atributo
    3,                      // numero de elementos por vértice, aqui é (r,g,b)
    GL_FLOAT,               // o tipo de cada elemento
    GL_FALSE,               // como os valores estão
    5 * sizeof(GLfloat),    // a próxima cor aparecerá a cada 5 floats.
    (GLvoid*) (2 * sizeof(GLfloat))  // descolocamento do primeiro elemento.
  );

Ele funciona da mesma forma!

Note que para cores. nós começamos no 3º elemento(2 * sizeof(GLfloat) da array, onde a primeira cor que é o offset(deslocamento) do primeiro elemento.

Porque (GLvoid*)? Vimos que, em algumas versões do OpenGL, era possivel passar um ponteiro para array C diretamente(em vez de buffer de objeto). Isto agora é obsoleto, mas o prototipo glVertexAttribPointer manteve inalterado, por isto passamos como se fosse um ponteiro, mas passamos realmente um offset.

Um alternativa por esporte:

struct attributes {
    GLfloat coord2d[2];
    GLfloat v_color[3];
};
struct attributes triangle_attributes[] = {
    {{ 0.0,  0.8}, {1.0, 1.0, 0.0}},
    {{-0.8, -0.8}, {0.0, 0.0, 1.0}},
    {{ 0.8, -0.8}, {1.0, 0.0, 0.0}},
};
...
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

...

glVertexAttribPointer(
  ...,
  sizeof(struct attributes),  // stride
  (GLvoid*) offsetof(struct attributes, v_color)  // offset
  ...

Note que usamos offsetof para especificar a primeira cor do offset.

Uniforms: Passando a informação global

editar

O Oposto de uma variável attribute é as variáveis uniform: Elas são as mesmas para todas as vértices. Note que podemos mudar-los em uma forma de base regular pelo código C - mas cada vez que um conjunto de vértices é mostrada na tela, a uniform será constante.

Vamos dizer que nós queremos definir a transparência global do triângulo pelo código C. tal qual acontece com os attributes, nós precisamos declaro isto. em uma global no código C:

GLint uniform_fade;

Depois de declararmos isto no código C (ainda após a linkagem do programa)

  const char* uniform_name;
  uniform_name = "fade";
  uniform_fade = glGetUniformLocation(program, uniform_name);
  if (uniform_fade == -1) {
    fprintf(stderr, "Could not bind uniform %s\n", uniform_name);
    return 0;
  }

Note: Poderiamos até atingir um array especifica de elementos no código de shader com uniform_name, e.g. "my_array[1]"!

Além disto, para o uniform, nó também definiremos explicitamente o valor da non-varying. Vamos requisitar, em onDisplay, que o triangulo deve ser um pouco opaco:

glUniform1f(uniform_fade, 0.1);

Agora nós podemos usar esta variável em nosso fragment shader:

varying vec3 f_color;
uniform float fade;
void main(void) {
  gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, fade);
}

Nota: se você não usar uniform em seus código, o glGetUniformLocation não vai enxergar nada, e vai falhar.

Portando para OpenGL ES 2

editar

Na seção anterior, foi mencionado que o GLES2 precisa de precision hints, ou seja sugestões de precisão, esses hints fala para o OpenGL qual é a precisão que queremos de nossos dados. Um precisão pode ser:

  • lowp
  • mediump
  • highp

Por exemplo, o lowp pode normalmente ser usado para cores, e recomendados usar highp para as vértices.

Nós podemos especificar a precisão para cada variável.

varying lowp vec3 f_color;
uniform lowp float fade;

ou, nós podemos declarar a precisão padrão:

precision lowp float;
varying vec3 f_color;
uniform float fade;

Infelizmente os precision hints não funciona no OpenGL 2.1 tradicional, então nós vamos incluir apenas no GLES2.

O GLSL inclui um preprocessador(preprocessor), similar aos precessadores do C, nós podemos usar directivas como #define ou #ifdef

Apenas o fragment shader, exige que seja especificado uma precisão para os floats. A precisão foi especificada como highp para o vertex shader. para o Fragment shader, o highp não deve estar disponivel, que pode ser experimentado usado o macro GL_FRAGMENT_PRECISION_HIGH[2]

Nós já pré-especificamos um directiva de processamento para o macro através da função utilitária create_shader:

#define GLES2

Agora podemos melhorar nosso carregamento de shader para isto definiremos um precisão padrão no GLES2, e ignoraremos o identificador de precisão no OpenGL 2.1(assim ainda podemos definir um precisão para uma variável especifica se precisarmos):

  GLuint res = glCreateShader(type);
  const GLchar* sources[] = {
    // Define GLSL version
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
#else
    "#version 120\n"
#endif
    ,
    // especificação de precisão do GLES2
#ifdef GL_ES_VERSION_2_0
    // Define default float precision for fragment shaders:
    (type == GL_FRAGMENT_SHADER) ?
    "#ifdef GL_FRAGMENT_PRECISION_HIGH\n"
    "precision highp float;           \n"
    "#else                            \n"
    "precision mediump float;         \n"
    "#endif                           \n"
    : ""
    // Nota: OpenGL ES automaticamente definido aqui:
    // #define GL_ES
#else
    // Ignore os especificador do GLES2:
    "#define lowp   \n"
    "#define mediump\n"
    "#define highp  \n"
#endif
    ,
    source };
  glShaderSource(res, 3, sources, NULL);

Lembrando que o compilador GLSL poderá contar as linhas pré-especificados enquanto mostra mensagens de erro, Ajustando a #line 0 infelizmente não reiniciara o contagem de linha do compilador

Atualizando a Tela

editar

Agora seria bem legal se a transparência pudesse continuar variando e voltando. Para fazer isto,

  • Precisamos verificar o numero de segundo desde que o usuário iniciou o aplicativo; o glutGet(GLUT_ELAPSED_TIME)/1000 dá que
  • Aplicando um função aritmética sin em nossa(a função sin volta ao primeiro entre -1 and +1 a cada 2.PI=~6.28 unidades de tempo)
  • Pergunte ao aplicativo que atualizará o triângulo(com novo valor de transparência); isto é o glutPostRedisplay()
  • O GLUT uma função ociosa "idle" que é chamada sempre que o GLUT não tem mais nada o que fazer. (ex. quando terminamos de mostrar o triângulo), nós podemos colocar nosso código lá.

No main, vamos declarar a função "idle", depois glutDisplayFunc(...):

glutIdleFunc(idle);

Vamos adicionar a nova função ociosa.

void idle()
{
  float cur_fade = sinf(glutGet(GLUT_ELAPSED_TIME) / 1000.0 * (2*M_PI) / 5) / 2 + 0.5; // 0->1->0 a cada 5 segundos
  glUseProgram(program);
  glUniform1f(uniform_fade, cur_fade);
  glutPostRedisplay();
}

também removeremos a chamada para glUniform1f da onDisplay

e por ultimo, nós vamos fazer uma declaração de um biblioteca aritmética do C.

#include <math.h>

no Makefile deixe assim:

LDLIBS=-lglut -lGLEW -lGL -lm

Compile e Rode...

 
Um triângulo animado, parcialmente transparente

Temos nossa primeira animação!

É comum que uma implementação do OpenGL aguarde a atualização vertical da tela depois atualize o buffer da tela física = isto é chamado de vertical sync. Quem neste caso, o triângulo será renderizado aproximadamente 60 vez por segundo (60 FPS). se você desativar o vertical sync, o programa manterá atualizando o triangulo continuamente, resultado em um alto uso da CPU. Vamos nós encontrar denovo com o vertical sync, quando nós criarmos aplicativos com alteração de perspectiva.

Referências

editar
  1. É um pouco confuso tendo apenas um único exemplo nos diferentes conceitos de atributos e variantes. Vamos tentar procurar dois exemplos separados para uma melhor explicação.
  2. Cf. OpenGL ES Shading Language 1.0.17 Specification. Khronos.org (2009-05-12)., seção 4.5.3 Default Precision Qualifiers
Navegue e baixe os códigos completos