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 | editar código-fonte]Carregando os Shader
[editar | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]
É 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 | editar código-fonte]
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 | editar código-fonte]
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 | editar código-fonte]
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);
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