Antes que você possa começar a desenhar coisas com o OpenGL, precisa inicializa-lo. Isso é feito pela criação de um contexto, que é essencialmente um estado de máquina que armazena todos os dados relacionados à renderização de sua aplicação. Quando a sua aplicação é fechada, o contexto é destruído e tudo que ele armazena é apagado da memória.
O problema é que a criação de uma janela e de um contexto OpenGL não faz parte da especificação do OpenGL. Isso significa que isso é feito de formas diferentes em cada plataforma existente. Ao se desenvolver aplicações usando OpenGL, portabilidade é tudo, desta forma isso é a última coisa que queremos. Por sorte, existem bibliotecas que abstraem esse processo, de forma que você possa manter o mesmo código em todas as plataformas suportadas.
Se por um lado todas as bibliotecas disponíveis possuem vantagens e desvantagens, elas possuem um fluxo comum. Você começa especificando as propriedades da janela do jogo, como o título e o tamanho, e as propriedades do contexto OpenGL, como nível de anti-aliasing. Sua aplicação então inicia o loop de eventos, que contém um conjunto importante de tarefas que precisam ser completadas repetidamente até a janela ser fechada. Essas tarefas normalmente lidam como eventos como cliques do mouse, atualização do estado de renderização e o desenho das formas.
O fluxo do programa deve se parecer com algo como o pseudo-código abaixo:
#include <libraryheaders>
int main()
{
createWindow(title, width, height);
createOpenGLContext(settings);
while (windowOpen)
{
while (event = newEvent())
handleEvent(event);
updateScene();
drawGraphics();
presentGraphics();
}
return 0;
}
Ao renderizar um quadro, os resultados serão armazenados em um buffer off-screen conhecido como back buffer para que o usuário só veja o resultado final. A chamada para presentGraphic() irá copiar o resultado do back buffer para o buffer vísivel da janela, o front buffer. Todas as aplicações que fazem uso de gráficos em tempo real terão um fluxo de programa como esse, não importando se use uma biblioteca ou código nativo.
Por padrão, as bibliotecas criarão um contexto OpenGL que suporte funções legadas. Isso não é desejável, porque não estamos interessados nessas funções e elas podem se tornar indisponíveis em algum ponto do futuro. A boa notícia é que é possível informar aos drivers que nossa aplicação está pronta para o futuro e não depende de funções antigas. A notícia ruim é que nesse momento apenas a bibliotecas GLFW permite que façamos isso. Esse pequemo porém não terá nenhuma consequência negativa agora, assim mão permita que isso influencie a sua escolha de biblioteca, mas a vantagem de usar um contexto dessa forma é que chamar acidentalmente uma das funções antigas resultará em um erro de operação inválida que você terá que tratar.
O suporte a janelas redimensionáveis com o OpenGL introduz algumas complexidades, como arquivos de recursos que precisam ser recarregados e buffer que precisam ser recriados para caber no novo tamanho de janela. É mais conveniente para o processo de aprendizagem não se preocupar como esses detalhes ainda, assim só lidaremos como janelas de tamanho fixo.
Configuração
A primeira coisa a se fazer ao se iniciar um projeto OpenGL é lincar dinamicamente com o OpenGL.
- Windows: Adicione
opengl32.lib
na entrada de seu linker - Linux: Inclua
-lGL
nas opções do compilador - OS X: Adicione
-framework OpenGL
nas opções do compilador
Certifique-se de não incluir
opengl32.dll
junto com sua aplicação. Esse arquivo já vem incluso no Windows e problemas causados por diferenças de versões podem ocorrer.
Os passos restantes dependem de qual biblioteca você escolher para a criação da janela e do contexto.
Bibliotecas
Existem diversas bibliotecas disponíveis para a criação de uma janela e um contexto OpenGL para você. Não existe uma que seja melhor que as outras, pois todas tem necessidades e ideais diferentes. Nesse artigo, vamos ver o processo para as três bibliotecas mais populares, mas você pode encontrar guias detalhados em seus respectivos websites. Todo o código dos próximos artigos desta série será independente da sua escolha de biblioteca.
SFML
SFML é uma biblioteca multimídia escrita em C++ que fornece acesso à funções de gráficos, entrada de dados, áudio, rede e sistema. A desvantagem de usar essa biblioteca é que ela tenta ser um solução tudo-em-um. Você tem muito pouco controle sobre a criação do contexto OpenGL, já que ela é projetada para usar suas próprias funções de desenho.
SDL
SDL também é uma biblioteca multimídia multi-plataforma, mas escrita em C. Isso a torna um pouco mais dificil para programadores C++, mas é uma alternativa excelente ao SFML. Suporta mais plataformas exóticas e mais importante, oferece mais controle sobre a criação de um contexto OpenGL do que SFML.
GLFW
GLFW, como o nome sugere, é uma biblioteca C projetada especificamente para uso com o OpenGL. Ao contrário de SDL e SFML vem apenas com funções absolutamente necessárias: criação de uma janela e de um contexto e gerenciamento de entrada. Ela oferece o maior controle sobre a criação de um contexto OpenGL dessas três bibliotecas.
SFML
O contexto OpenGL é criado implicitamente ao abri uma nova janela com o SFML, de forma que isso é tudo que você precisa fazer. O SFML possui também um pacote gráfico, mas como iremos usar o OpenGL diretamente, não precisaremos dele.
Compilação
Após baixar o pacote binário do SFML ou compilar o código fonte, você irá encontrar os arquivos necessários nas pastas lib e include.
- Adicione a pasta
lib
ao seu caminho de bibliotecas e link comsfml-system
esfml-window
. Como o Visual Studio no Windows, link com os arquivossfml-system-s
esfml-window-s
delib/vc2008
. - Adicione a pasta
include
a sua pasta de arquivos de cabeçalho.
As bibliotecas SFML possuem uma convenção de nomenclatura simples para diversas configurações. Se você quiser lincar dinâmicamente, simplesmente remova o
-s
do nome, definaSFML_DYNAMIC
e copie as bibliotecas compartilhadas. Se você quiser usar os binários para depuração, adicione-d
ao nome.
Para verificar que você fez tudo certo, tente compilar e executar o seguinte código:
#include <SFML/System.hpp>
int main()
{
sf::sleep(sf::seconds(1.f));
return 0;
}
Isso deve exibir uma aplicação de linha de comando que será terminada após uma segundo. Se você encontrar algum problema, pode encontrar informações detalhadas para Visual Studio, Code::Blocks e gcc nos tutoriais do site do SFML.
Código
Começe pela inclusão do pacote window e definindo um ponto de entrada para sua aplicação.
#include <SFML/Window.hpp>
int main()
{
return 0;
}
Uma janela pode ser aberta pela criação de uma nova instância de sf:Window. O construtor básico usa uma estrutura sf:VideoMode, uma titulo para a janela e um estilo para a janela. A estrutura sf:VideoMode especifica a largura, altura e opcionalmente a profundidade de pixel da janela. Finalmente, para criar uma janela de tamanho fixo é necessário sobrepor o estilo padrão Style::Resize|Style::Close
. É também possível criar uma janela em tela cheia usando o estilo Style::Fullscreen
.
sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.antialiasingLevel = 2; // Optional
sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Close, settings);
O construtor também usa uma estrutura sf:WindowSettings que permite que você especifique o nível de anti-aliasing e a precisão dos buffers de profundidade e stencil. Esses dois últimos serão discutidos em um outro artigo no futuro. Na última versão do SFML, você precisa requisitar estes manualmente com o código acima.
Quando executar esse código, você irá notar que a aplicação é fechada automaticamente depois que a janela é criada. Vamos adicionar um loop de eventos para lidar com isso.
bool running = true;
while (running)
{
sf::Event windowEvent;
while (window.pollEvent(windowEvent))
{
}
}
Quando alguma coisa acontece com a sua janela, um evento é postada na fila de eventos. Existem uma ampla variedade de eventos, incluindo mudança no tamanho da janela, movimento do mouse e pressionamento de teclas. Cabe a você decidir quais eventos precisam de ação adicional, mas existe pelo menos um que precisa de tratamento para fazer com que sua aplicação execute direito.
switch (windowEvent.type)
{
case sf::Event::Closed:
running = false;
break;
}
Quando o usuário tentar fechar a janela, o evento Closed é disparado e tomamos ação em relação a isso pelo fechamento da aplicação. Tente remover essa linha e você verá que é impossível fechar a janela pelos meios normais. Se você preferir uma janela em tela cheia, você deve adicionar o pressionamento da tecla Esc como uma meio de fechar a janela:
case sf::Event::KeyPressed:
if (windowEvent.key.code == sf::Keyboard::Escape)
running = false;
break;
Você tem agora uma janela e os eventos importantes são tratados, de modo que você está pronto para colocar algo na tela. Depois de desenhar algo, você pode alternar entre back buffer e o front buffer como o comando window.display().
Quando você executar essa aplicação, você verá algo como isso:
Note que o SFML permite que você tenha múltiplas janelas. Se quiser fazer uso desse recursos, certifique-se de chamar window.setActive() para ativar uma janela específica para operações de desenho.
Agora que você criou uma janela e um contexto, pode pular para a última seção desse artigo para ver mais uma coisa que precisa ser feita.
SDL
O SDL vem com diversos módulos diferentes, mas para a criação de uma janela com um contexto OpenGL estamos interessados apenas no módulo de vídeo. Ele irá lidar com tudo o que precisamos, então vamos ver como usa-lo.
Compilação
Depois que você tiver baixado os binários da SDL ou compilado ela do código fonte, você encontrará os arquivos necessários nas pastas lib e include.
- Adicione a pasta
lib
no caminho de bibliotecas e link comSDL2
eSDL2main
. - O SDL usa lincagem dinâmica, de forma que você deve se certificar que as bibliotecas compartilhadas (
SDL2.dll
,SDL2.so
) estejam na mesma pasta de seu executável. - Adicione a pasta
include
na sua pasta de arquivos de cabeçalhos.
Para verificar que tudo esteja no seu lugar, tente compilar e executar o seguinte código:
#include <SDL.h>
int main(int argc, char *argv[])
{
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Delay(1000);
SDL_Quit();
return 0;
}
Isso deve exibir uma aplicação de linha de comando que será terminada após uma segundo. Se você encontrar algum problema, pode encontrar informações detalhadas para todas as plataformas e compiladores em tutoriais na Web.
Código
Comece pela definição de um ponto de partida na sua aplicação e inclua os arquivos de cabeçalho para o SDL.
#include <SDL.h>
#include <SDL_opengl.h>
int main(int argc, char *argv[])
{
return 0;
}
Para usar o SDL em sua aplicação, você precisa informar ao SDL quais módulos vai precisar e quando descarrega-los. Você pode fazer isso com essas duas linhas de código:
SDL_Init(SDL_INIT_VIDEO);
...
SDL_Quit();
return 0;
A função SDL_Init usa como parâmetro um bitfield com o módulo a ser carregado. O módulo vídeo inclui tudo que você precisa para criar uma janela e um contexto OpenGL.
Antes de fazer qualquer coisa, primeiro deve-se informar ao SDL que você quer um contexto compatível com o OpenGL 3.2:
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
Depois disso, crie uma janela usando a função SDL_CreateWindow.
SDL_Window* window = SDL_CreateWindow("OpenGL", 100, 100, 800, 600, SDL_WINDOW_OPENGL);
O primeiro argumento especifica o título da janela, os dois seguintes as posições X e Y e as duas últimas a largura e altura da janela. Se a posição não importar, você pode especificar SDL_WINDOWPOS_UNDEFINED ou SDL_WINDOWSPOS_CENTERED para o segundo e terceiro argumento. O parâmetro final especifica propriedades da janela como:
- SDL_WINDOW_OPENGL – Cria uma janela para OpenGL.
- SDL_WINDOW_RESIZABLE – Cria uma janela redimensionável.
- Opcional SDL_WINDOW_FULLSCREEN – Cria uma janela em tela cheia.
Depois de ter criado a janela, você pode criar o contexto OpenGL:
SDL_GLContext context = SDL_GL_CreateContext(window);
...
SDL_GL_DeleteContext(context);
O contexto deve ser destruído antes da chamada à SDL_Quit() para liberar os recursos.
Em seguida vem a parte mais importante do programa, o loop de eventos:
SDL_Event windowEvent;
while (true)
{
if (SDL_PollEvent(&windowEvent))
{
if (windowEvent.type == SDL_QUIT) break;
}
SDL_GL_SwapWindow(window);
}
A função SDL_PollEvent irá verificar se existe algum evento novo a ser tratado. Um evento pode qualquer coisa de um clique do mouse a movimentação da janela pelo usuário. Nesse momento, o único evento que você precisa responder é o pressionamento do botão X no canto da janela. Ao sair do loop principal, é chamada a função SDL_Quit() e a janela é destruída. Aqui, SDL_GL_SwapWindows cuida de alternar entre o back buffer e o front buffer depois que coisas novas são desenhadas pela aplicação.
Se você estiver usando uma janela em tela cheia, é preferível usar a tecla Esc para fechar a janela.
if (windowEvent.type == SDL_KEYUP &&
windowEvent.key.keysym.sym == SDLK_ESCAPE) break;
Quando você executa a sua aplicação, deve visualizar algo como isso:
Agora que você criou uma janela e um contexto, pode pular para a última seção desse artigo para ver mais uma coisa que precisa ser feita.
GLFW
GLFW foi desenvolvida especificamente para uso como o OpenGL, então é de longe a forma mais fácil de uso para nosso propósito.
Compilação
Depois de baixar os binários GLFW do website ou compilar o código fonte, você encontrará os arquivos de cabeçalho na pasta include e as bibliotecas para seu compilador em uma das pastas lib.
- Adicione a pasta
lib
apropriada ao seu caminho de bibliotecas e linque comGLFW
. - Adicione a pasta
include
no seu caminho de arquivos de cabeçalho.
Você pode lincar dinamicamente como a GLFW se quiser. Simplesmente linque com
GLFWDLL
e inclua a biblioteca compartilhada junto com seu executável.
Abaixo segue um trecho de código para verificar a sua configuração de compilação:
#include <GLFW/glfw3.h>
#include <thread>
int main()
{
glfwInit();
std::this_thread::sleep_for(std::chrono::seconds(1));
glfwTerminate();
}
Este código deve exibir uma aplicação de linha de comando e encerrar após um segundo. Se encontrar algum problema, deixe um comentário nesse artigo para receber alguma ajuda.
Código
Comece simplesmente incluindo o arquivo de cabeçalho do GLFW e define um ponto de partida na sua aplicação.
#include <GLFW/glfw3.h>
int main()
{
return 0;
}
Para usar o GLFW, ele precisa ser inicializado quando o programa é iniciado e você precisa liberar os recursos quando o programa é encerrado. As funções glfwInit e glfwTerminate cumprer esse papel.
glfwInit();
...
glfwTerminate();
A próxima coisa a ser feita é criar e configurar uma janela. Antes de chamar glfwCreateWindow, precisamos ajustar algumas opções.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL", nullptr, nullptr); // Windowed
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL", glfwGetPrimaryMonitor(), nullptr); // Fullscreen
Você irá notar de imediato que as três primeiras linhas do código acima são relevantes apenas para essa biblioteca. Elas especificam que desejamos que o contexto suporte OpenGL 3.2 no mínimo. A opção GL_GLFW_PROFILE especifica que queremos que o contexto suporte apenas a funcionalidade principal.
Os primeiros dois parâmetros de glfwCreateWindow especificam a largura e altura da superfície de desenho e o terceiro parâmetro o título da janela. O quarto parâmetro deve ser NULL para modo em janela e glfwGetPrimaryMonitor() para modo em tela cheia. O último parâmetro permite que você especifique um contexto OpenGL existente para compartilhar recursos como texturas. A função glfwWindowHint é usada para especificar requisitos adicionais para a janela.
Depois de criar a janela, o contexto OpenGL precisa ser ativado:
glfwMakeContextCurrent(window);
Em seguida vem o loop de eventos, que no caso do GLFW funciona de forma um pouco diferente das outras bibliotecas. O GLFW usa um loop de eventos dito como fechado, o que significa que você apenas precisa tratar os eventos quando precisa de fato faze-lo. Isso significa que o loop de eventos é bem simples:
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
As únicas funções necessárias no loop são glfwSwapBuffers para alternar entre o back buffers e o front buffer depois que você tiver terminado os desenhos e glfwPollEvents para recuperar eventos da janela. Se você estiver criando uma aplicação em tela cheia, deve tratar o pressionamento da tecla Esc para retornar à área de trabalho.
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
Se quiser aprender mais sobre como tratar a entrada, pode visitar a documentação oficial.
Agora que você criou uma janela e um contexto, pode pular para a última seção desse artigo para ver mais uma coisa que precisa ser feita.
Mais uma coisa…
Infelizmente, nós não podemos chamar as funções que precisamos ainda. Isso se deve ao fato de que é obrigação do fabricante da placa de vídeo implementar funcionalidades do OpenGL em seus drivers baseado no que a placa suporta. Você não irá querer que seu programa seja compatível com uma única versão do driver ou placa de vídeo, então teremos que fazer algo esperto.
Seu programa precisa checar quais funções estão disponíveis durante a execução e linca-las dinamicamente. Isso é feito encontrando os endereços das funções, associando elas a ponteiros de funções e chamando esses ponteiros. Isso deve se parecer com isso:
// Specify prototype of function
typedef void (*GENBUFFERS) (GLsizei, GLuint*);
// Load address of function and assign it to a function pointer
GENBUFFERS glGenBuffers = (GENBUFFERS)wglGetProcAddress("glGenBuffers");
// or Linux:
GENBUFFERS glGenBuffers = (GENBUFFERS)glXGetProcAddress((const GLubyte *) "glGenBuffers");
// or OSX:
GENBUFFERS glGenBuffers = (GENBUFFERS)NSGLGetProcAddress("glGenBuffers");
// Call function as normal
Gluint buffer;
glGenBuffers(1, &buffer);
Deixe eu dizer que é perfeitamente normal ficar assustado com esse pedaço de código. Você pode não estar familiarizado com o conceito de ponteiros de funções ainda, mas ao menos tente entender de forma rudimentar o que está acontecendo aqui. Você pode imaginar que passar por esse processo de definição de protótipos e encontrar endereços de funções é muito tedioso e no final pode ser uma completa perda de tempo.
A boa notícia é que existem bibliotecas que resolvem esse problema para nós. A biblioteca mais popular e melhor mantida no momento é a GLEW e não há nenhuma razão para isso deixar de acontecer tão cedo.
Se você ainda não compilou a GLEW, faça agora. Nós iremos agora adiciona-la ao nosso projeto:
- Comece lincando seu projeto com a biblioteca estática GLEW na pasta
lib
. Essa biblioteca pode serglew32s.lib
ouGLEW
dependendo de sua plataforma. - Adicione a pasta
include
ao seu caminho de arquivos de cabeçalho.
Agora apenas inclua o arquivos de cabeçalhos ao programa, mas certifique-se que sejam incluídos antes dos cabeçalhos do OpenGL ou da biblioteca que você está usando para criar a janela.
#define GLEW_STATIC
#include <GL/glew.h>
Não esqueça de definir GLEW_STATIC seja usando a diretiva de pré-processador acima ou adicionando -DGLEW_STATIC na linha comando ou configurações do projeto.
Se você prefere lincar dinamicamente com a GLEW, deixe de fora a diretiva define acima e linque com
glew32.lib
ao invés deglew32s.lib
no Windows. Não esqueça de incluirglew32.dll
oulibGLEW.so
junto com o seu executável!
Agora tudo que temos que fazer é chamar glewInit() depois da criação de nossa janela e do contexto OpenGL. A linha glewExperimental é necessária para forçar o GLEW a usar métodos modernos do OpenGL para checar se uma funções está disponível.
glewExperimental = GL_TRUE;
glewInit();
Certifique-se que você configurou seu projeto corretamente chamando a função glGenBuffers, que foi carregada pelo GLEW para você!
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
printf("%u\n", vertexBuffer);
Seu programa deve ser compilado e executado sem problemas e exibir o número 1 no terminal. Se você precisar de mais informações sobre o uso do GLEW, pode visitar este website.
Fonte: open.gl/context