Programação com OpenGL/Modern OpenGL Tutorial 02

Agora que nos temos um exemplo de trabalho que entendemos, nos poderemos adicionar novos recursos e mais robustez a ele.

Nossos shaders anteriores tinham a intenção ter um formato mínimo e mais fácil possível, mas no mundo real, usaremos mais códigos que nos auxiliarão.

Gerenciando Shader

editar

Carregando os Shader

editar

A primeiro coisa a acrescentar é uma maneira mais conveniente de carregar os shader: Seria muito mais fácil para nós carregarmos um arquivo externo( ao invés de copiarmos e colocarmos a String em C em nosso código). Além disso, eles nós permitira modificar nosso código GLSL sem precisar recompilar o código C!

Primeiro, nos adicionaremos uma função para carregar um string. é basicamente um código em C, que lera os conteúdos e alocando em buffer de tamanho no arquivo.

/**
 * Gravando todo o conteúdo do arquivo na memória, e passando tudo para o shaders
 * código fonte para OpenGL
 */
/* Problemas:
 * Nós deveríamos fechar o arquivo de entrada depois do return NULL; mas estas declarações levaria ao monte de repetições
 * Você pode resolver usar goto ou abusar de switch/for/while + break ou fazer alguns if else bagunçados.
 * Melhor solução: vamos usar o identificar File: char* file_read(const FILE* input)
*/
char* file_read(const char* filename)
{
  FILE* input = fopen(filename, "rb");
  if(input == NULL) return NULL;
  
  if(fseek(input, 0, SEEK_END) == -1) return NULL;
  long size = ftell(input);
  if(size == -1) return NULL;
  if(fseek(input, 0, SEEK_SET) == -1) return NULL;
  
  /* Se o compilador do C: não lançar o valor de retorno do malloc´s*/
  char *content = (char*) malloc( (size_t) size +1  ); 
  if(content == NULL) return NULL;
  
  fread(content, 1, (size_t)size, input);
  if(ferror(input)) {
    free(content);
    return NULL;
  }

  fclose(input);
  content[size] = '\0';
  return content;
}

Depurando os Shaders

editar

Poderá acontecer alguns erros com nossos shader, o programa pode parar sem nenhuma explicação com alguns erros particulares. Então nós poderemos obter mais informações do OpenGL criando um infolog:

/**
 * Mostrando na tela os erros de compilação do compilador de Shader do OpenGL
 */
void print_log(GLuint object)
{
  GLint log_length = 0;
  if (glIsShader(object))
    glGetShaderiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else if (glIsProgram(object))
    glGetProgramiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else {
    fprintf(stderr, "printlog: Não é um shader ou programa\n");
    return;
  }

  char* log = (char*)malloc(log_length);

  if (glIsShader(object))
    glGetShaderInfoLog(object, log_length, NULL, log);
  else if (glIsProgram(object))
    glGetProgramInfoLog(object, log_length, NULL, log);

  fprintf(stderr, "%s", log);
  free(log);
}

Abstraindo as diferenças entre OpenGL e GLES2

editar

Quando você usar somente as funções da GLES2, seu aplicativo é quase portável para desktop e dispositivos móveis. Existe ainda algumas questões para abordar:

  • A GLSL #version é diferente.
  • GLES2 requer alguns hints de precisão que não são compatíveis com a OpenGL 2.1

O #version precisa estar na primeira linha em alguns compiladores GLSL (por exemplo na PowerVR SGX540), assim não podemos usar a diretivas #ifdef para abstração dos shader GLSL, em vez disso, vamos preceder assim na versão em código C++:

  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    // Note: OpenGL ES define automaticamente este:
    // #define GL_ES
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);

Uma vez que usaremos esta versão em todos nossos tutoriais de GLSL, é uma uma solução simples.

Nós colocaremos #ifdef e os hints de precisão na próxima seção.

Uma função reutilizável para criar shader

editar

Com novas funções utilitárias e conhecimento, nos podemos fazer outra função para carregar e depurar um shader:

/**
 * Compilando o shader pelo arquivo 'filename', com erros
 * Compile the shader from file 'filename', com tratamento de erros
 */
GLuint create_shader(const char* filename, GLenum type)
{
  const GLchar* source = file_read(filename);
  if (source == NULL) {
    fprintf(stderr, "Erro abrindo: %s: ", filename); perror("");
    return 0;
  }
  GLuint res = glCreateShader(type);
  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    "#define GLES2\n",
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);
  free((void*)source);

  glCompileShader(res);
  GLint compile_ok = GL_FALSE;
  glGetShaderiv(res, GL_COMPILE_STATUS, &compile_ok);
  if (compile_ok == GL_FALSE) {
    fprintf(stderr, "%s:", filename);
    print_log(res);
    glDeleteShader(res);
    return 0;
  }

  return res;
}

Agora nós podemos compilar nossos shader simplesmente usando:

  GLuint vs, fs;
  if ((vs = create_shader("triangle.v.glsl", GL_VERTEX_SHADER))   == 0) return 0;
  if ((fs = create_shader("triangle.f.glsl", GL_FRAGMENT_SHADER)) == 0) return 0;

bem como mostrar os erros de link-edição; as well as display link errors:

  if (!link_ok) {
    fprintf(stderr, "glLinkProgram:");
    print_log(program);

Colocar as novas funções em um arquivo separado

editar

Nós colocaremos as novas funções no arquivo shader_utils.cpp

Note que nossa intenção ao escreve algumas das funções possíveis: O objetivo deste livro é entender como o OpenGL trabalha, não utilizar um conjunto de ferramentas que desenvolvemos.

Vamos criar um cabeçalho, shader_utils.h:

#ifndef _CREATE_SHADER_H
#define _CREATE_SHADER_H
#include <GL/glew.h>
char* file_read(const char* filename);
void print_log(GLuint object);
GLuint create_shader(const char* filename, GLenum type);
#endif

Referenciamos ele no arquivo triangle.cpp:

#include "shader_utils.h"

colocamos no Makefile:

triangle: shader_utils.o

Usando Vertex Buffer Objects (VBO) para maior eficiência

editar

É uma boa pratica, guarda nossas vértices diretamente na placa gráfica, usando um Vertex Buffer Objetc (VBO).

Além disso, as "client-side array" suportadas são removidas desde do OpenGL 3.0, e é lento, por isto vamos usar os VBOs por agora, mesmo sendo uma vértice simples é bom conhecer eles, por causa que eles é muito usado em qualquer código OpenGL que você encontrar.

Nos implementaremos em dois passos:

  • Criando um VBO com nossos vértices.
  • Vincular nosso VBO para serem chamadas pela função glDrawArray

Criar uma variavel global (abaixo do #include) para gravar nosso manipulador de VBO:

GLuint vbo_triangle;

no init_resources, nós mudaremos a definição triangle_vertices , criando um (1) buffer de dados e fazendo dele o buffer atual:

  GLfloat triangle_vertices[] = {
     0.0,  0.8,
    -0.8, -0.8,
     0.8, -0.8,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);

Nós agora podemos puxar nossas vértices para o buffer. Nós especificaremos como os dados são organizados, e com que frequência será usado. O GL_STATIC_DRAW indica como será a frequência do buffer, e que a GPU deve manter uma cópia do mesmo em sua memoria. É sempre possível escrever novos valores para o VBO. se os dados mudando uma vez por quadro ou mais, você poderia usar o GL_DYNAMIC_DRAW ou GL_STREAM_DRAW.

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


A qualquer momento podemos remover um buffer ativo como este:

  glBindBuffer(GL_ARRAY_BUFFER, 0);

Lembrando sempre, de desativar um buffer ativo toda vez que você quiser passar uma array C diretamente.

No onDisplay, nós adaptamos ligeiramente o código. Nós chamaremos glBindBuffer, e modificaremos os últimos dois parâmetros pelo glVertexAttribPointer:

  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glEnableVertexAttribArray(attribute_coord2d);
  /* Descreva nossas ordenação(array) de vértice para o OpenGL (ele não reconhece o formato automaticamente) */
  glVertexAttribPointer(
    attribute_coord2d, // atributo
    2,                 // numero de elementos por vértices, aqui é (x,y)
    GL_FLOAT,          // o tipo de cada elemento
    GL_FALSE,          // Como nossos valores estão.
    0,                 // sem dados extras em cada posição
    0                  // descolocamento do primeiro elemento
  );

Não vamos esquecer de limpar ao sair:

void free_resources()
{
  glDeleteProgram(program);
  glDeleteBuffers(1, &vbo_triangle);
}

Agora toda vez que desenharmos nossas cenas, o OpenGL automaticamente colocara nossa vértice na GPU. para cenas grandes, que tem muitos polígonos, teremos um aumento enorme de velocidade.

Verificar a versão do OpenGL

editar

Alguns usuário podem não possuir uma placa gráfica que suporte o OpenGL 2. Isto provavelmente fará que nosso programa faça termine inesperadamente ou mostre cenas incompletas. Isto pode ser verificado usando o GLEW(depois de chamarmos com sucesso o glewInit()):

  if (!GLEW_VERSION_2_0) {
    fprintf(stderr, "Erro: Sua placa de vídeo não suporta OpenGL 2.0\n");
    return 1;
  }

Note que alguns tutorais só poderão trabalhar com placas que suportem a versão próximas a 2.0, como a Intel 945GM com um suporte limitado nos shader mas com suporte oficial da OpenGL 1.4.

Alternativa para o GLEW

editar

Você pode encontra os seguintes cabeçalhos em outro código com OpenGL:

#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>

Se você não precisa carregar as extensões para OpenGL, e se cabeçalhos são bem recentes, você usar isto ao invés do GLEW Nosso teste mostraram que os usuários do Windows podem ter cabeçalhos desatualizados, e faltando simbolos como o GL_VERTEX_SHADER, então vamos usar o GLEW nestes tutoriais (estaremos assim prontos para carregar as extensões).

Veja uma comparação entre o GLEW e o GLee a seção APIs, Biblioteca e Acronimos.

Um usuário relatou que a utilização desta técnica, em vez de GLEW em uma Intel 945GM GPU permitido ignorar o suporte OpenGL 2.0 parcial para tutoriais simples. pelo GLEW isto pode ser feito adicionando um suporte parcial através de glewExperimental = GL_TRUE; depois chamamos o glutInit.

Ativando a transparência

editar

Nosso programa tem uma manutenção mais facil agora, mas não é exatamente isto que queríamos! Vamos então experimente uma pequena transparência, e mostraremos nosso triângulo com um efeito de "TV antiga".

Primeiro, inicializamos o GLUT com alfa:


glutInitDisplayMode(GLUT_RGBA|GLUT_ALPHA|GLUT_DOUBLE|GLUT_DEPTH);

Depois vamos declara o suporte a transparência(que está desligado por padrão) no OpenGL:

// Enable alpha
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 
A renderização do triângulo parcialmente transparente

e por ultimo, nós modificaremos nosso fragment shader para ter a transparência alfa:

  gl_FragColor[0] = gl_FragCoord.x/640.0;
  gl_FragColor[1] = gl_FragCoord.y/480.0;
  gl_FragColor[2] = 0.5;
  gl_FragColor[3] = floor(mod(gl_FragCoord.y, 2.0));

O mod e o floor são operadores aritméticos comuns, usados para determinar se estamos dentro ou fora da linha. Por isto uma das linhas é transparente e a outra opaca.

Navegue e baixe os códigos completos