MC102 Introdução à Programação de Computadores Programação Gráfica 2D com OpenGL (Aulas 22, 24 e 25) Felipe P.G. Bergo 1 Introdução OpenGL é uma biblioteca de funções utilizada para programar gráficos. O objetivo do OpenGL é uniformizar os comandos e primitivas geométricas usados para fazer desenhos, de forma que os fabricantes de hardware possam projetar processadores de vídeo (GPUs Graphical Processor Unit) otimizados para as primitivas de desenho usadas pelos programadores. Hoje a programação de gráficos é dominada por duas bibliotecas: OpenGL (mais antiga, compatível com vários sistemas operacionais e ambientes de programação) e DirectX (proprietária da Microsoft, usada apenas no Windows e no videogame XBox, da própria Microsoft). OpenGL não tem funções para abrir uma janela, ajustar o tamanho da janela ou obter toques no teclado, pois isto ainda é dependente do sistema operacional. Uma tentativa de deixar o OpenGL totalmente independente do sistema é a biblioteca GLUT, que define funções simples para realizar estas operações de forma compatível com todos os sistemas. As duas principais referências para OpenGL são os livros vermelho (Programming Guide) e azul (Reference Guide). Ambos os livros estão disponíveis online no site do OpenGL (http://www. opengl.org), na seção de documentação (Documentation). 2 Inicialização da GLUT e OpenGL O programa abaixo abre uma janela de 640 por 480 pixels. #include <GL/glut.h> int main(int argc, char **argv) { glutinit(&argc,argv); glutinitdisplaymode(glut_rgba GLUT_DOUBLE); glutcreatewindow("gl Exemplo 1"); glutreshapewindow(640,480); 1
glviewport(0.0,0.0,640.0,480.0); glmatrixmode(gl_projection); glloadidentity(); gluortho2d(0.0,640.0,480.0,0.0); glmatrixmode(gl_modelview); glloadidentity(); glutmainloop(); return 0; } O comando include pode variar entre ambientes de programação. Na seção final deste documento há explicações de como compilar programas OpenGL em vários ambientes. A primeira chamada, glutinit, inicializa a GLUT, avisando que nosso programa utilizará a biblioteca. Os parâmetros de linha de comando argc e argv precisam ser passados para glutinit (ponteiro para argc e o próprio argv). A segunda chamada, glutinitdisplaymode, ajusta o modo de exibição, que será com cor RGBA (RGB mais um canal de transparência, que é a forma mais eficiente pois já é implementada no hardware da maioria das placas de vídeo) e usando double-buffering. Double-buffering é uma técnica de atualização de tela na qual sempre temos duas telas no OpenGL: a tela que está sendo exibida e a tela que está sendo desenhada. Quando terminamos de desenhar, faremos uma operação de Swap (troca), exibindo a tela que era de desenho e usando a outra tela para desenhar. Esta técnica evita que os passos do desenho sejam vistos pelo usuário antes de terminar cada quadro. As chamadas glutcreatewindow e glutreshapewindow criam uma janela e a redimensiona para o tamanho dado. Em vez de glutreshapewindow, poderíamos chamar glutfullscreen(); para usar a tela toda do computador (como a maioria dos jogos). As seis chamadas seguintes estabelecem o sistema de coordenadas de desenho e sua relação com a janela ou tela do computador. Neste curso usaremos OpenGL apenas para desenhos 2D simples, e estas chamadas estabelecem um sistema cartesiano ortogonal com origem no canto superior esquerdo da janela (ou tela), indo de 0 a 640 (eixo X) e 0 a 480 (eixo Y). Para maiores informações sobre transformações de vista, veja o capítulo 3 do livro vermelho. A última chamada, glutmainloop, não retorna: ela executa o loop de eventos da GLUT, que espera eventos (a janela ser redimensionada, precisar ser repintada, algum temporizador ser chamado, uma tecla ser apertada, o mouse ser movido, etc.) e executa ações baseadas nos eventos. Para que nosso programa faça algo de útil devemos registrar tratadores de eventos antes de chamar glutmainloop. O principal tratador é o evento display, que é o evento que desenhará o conteúdo da janela. Para registrar o evento de display, usamos a função glutdisplayfunc: 2
glutdisplayfunc(display); Onde display (o nome pode ser qualquer um) deve ser uma função previamente declarada, sem parâmetros e com retorno void, que será chamada para desenhar a tela sempre que necessário. 3 Primitivas de Desenho A função que trata o evento display deve limpar a tela de desenho com a chamada glclear(gl_color_buffer_bit); desenhar o conteúdo da janela ou tela o mais rápido possível (sem realizar operações demoradas, como acesso a arquivos) e então chamar glutswapbuffers(); para trocar a tela exibida com a tela de desenho. Se, em algum momento o seu programa precisar forçar a tela a ser redesenhada, você não deve chamar a função de display diretamente. Em vez disso, chame glutpostredisplay();, que fará o laço de eventos da GLUT repintar a tela assim que possível. Chamar esta função de dentro da função de display pode causar um loop infinito, cuidado. 3.1 Primitivas Vetoriais A forma mais simples de desenhar com OpenGL é com primitivas vetoriais. Estas primitivas traçam algum tipo de desenho a partir de um conjunto de vértices. No nosso caso, os vértices terão coordenadas de duas dimensões, e serão criados com as funções void glvertex2i(int x,int y); void glvertex2f(float x,float y); A única diferença entre as duas é a especificação das coordenadas como inteiros ou como valores de ponto flutuante. Cada vértice tem uma cor associada. Ao ser criado, o vértice assume a cor atual. A cor atual pode ser modificada com as chamadas void glcolor3f(float r,float g,float b); void glcolor3ub(unsigned byte r, unsigned byte g, unsigned byte b); 3
A versão 3f usa valores entre 0.0 e 1.0 para as componentes R (Red), G (Green) e B (Blue) da cor. A versão 3ub usa valores entre 0 e 255. O Capítulo 5 do livro vermelho tem uma tabela com os intervalos de valores para outras versões de glcolor. As primitivas de desenho são delimitadas por comandos glbegin e glend: glcolor3f(1.0,0.0,0.0); glbegin(gl_points); glvertex2i(10,10); glvertex2i(20,20); glend(); A primitiva GL POINTS apenas pinta os pontos de cada vértice isoladamente. OpenGL oferece 10 primitivas de desenho (Fig. 1). Figura 1: Primitivas de desenho geométrico do OpenGL. Caso os diferentes vértices de uma figura tenha cores diferentes, o OpenGL realizará uma interpolação de cores no seu interior. A Fig. 2 mostra o resultado gerado por glbegin(gl_triangles); glcolor3f(1.0,0.0,0.0); glvertex2i(10,200); glcolor3f(0.0,1.0,0.0); glvertex2i(500,20); 4
glcolor3f(0.0,0.0,1.0); glvertex2i(400,400); glend(); Figura 2: Triângulo com vértices de cores diferentes (vermelho, verde e azul). 4 Tratadores de Eventos Alguns outros eventos que vamos querer tratar via GLUT: teclas normais do teclado (glutkeyboardfunc), teclas especiais do teclado (glutspecialfunc), botões do mouse (glutmousefunc) e temporizações (gluttimerfunc). Os protótipos abaixo indicam os tipos de retorno e dos parâmetros da função a ser passada. A sintaxe para ponteiros de funções em protótipos é estranha mesmo. void glutmousefunc(void (*func)(int button, int state, int x, int y)); void glutkeyboardfunc(void (*func)(unsigned char key, int x, int y)); void glutspecialfunc(void (*func)(int key, int x, int y)); void gluttimerfunc(unsigned int msecs, void (*func)(int value), value); Na glutmousefunc, a função passada deve receber 4 parâmetros: button, state, x, e y.button terá um dos valores GLUT LEFT BUTTON, GLUT MIDDLE BUTTON ou GLUT RIGHT BUTTON, de acordo com o botão acionado. state será ou GLUT UP ou GLUT DOWN, indicando se o botão está sendo pressionado ou solto. A função será chamada em ambos os casos (quando o botão é apertado e quando ele é solto). Os parâmetros x e y contêm as coordenadas da janela em que o evento ocorreu. Na glutkeyboardfunc, a função recebe o caractere gerado pela tecla pressionada. Os parâmetros x e y recebem a posição do mouse na janela no momento do pressionamento da tecla. A função registrada com glutspecialfunc é semelhante, e recebe eventos de teclas não-imprimíveis, como 5
teclas de função e setas de direção. Uma lista das constantes de teclas está listada na documentação da GLUT (http://www.opengl.org/resources/libraries/glut/spec3/spec3.html), mas são constantes do tipo GLUT KEY F1, GLUT KEY LEFT, etc. Na gluttimerfunc, a função passada será chamada após msecs milissegundos. Se a própria função se rearmar (chamando gluttimerfunc para si mesma), isto fará com que a função seja chamada periodicamente. Isto é a forma ideal de fazer animações e verificações periódicas. Caso a função temporizada precise forçar o redesenho da tela, deve fazê-lo com glutpostredisplay() (e nunca chamando a função de display diretamente). Na maioria dos computadores, dificilmente será possível usar tempos abaixo de 50 milissegundos. 5 Listas OpenGL permite que gravemos listas de comandos muito utilizados na placa de vídeo, evitando ter que transferir uma grande quantidade de dados toda vez que os comandos forem repetidos. Para isso usamos Listas. Cada lista é identificada por um número inteiro. Cada lista deve ser declarada entre um par de chamadas glnewlist e glendlist. Uma vez declarada, uma lista pode ser executada com glcalllist. Um conjunto de listas pode ser apagado da placa de vídeo com gldelelelists. Exemplo de definição de lista: glnewlist(1,gl_compile); // lista #1 glcolor3f(1.0,0.0,0.0); glbegin(gl_triangles); glvertex2i(0,0); glvertex2i(10,0); glvertex2i(0,10); glend(); glendlist(); Para chamar a lista criada: glpushmatrix(); gltranslatef(20.0,40.0,0.0); glcalllist(1); glpopmatrix(); No exemplo acima chamamos a lista 1 com uma matriz de translação que adiciona 20 às coordenadas X e 40 às coordenadas Y dos vértices da lista. As funções glpushmatrix e glpopmatrix salvam e restauram o sistema de coordenadas para que a translação não se aplique a outras chamadas. Um grupo de listas com identificadores adjacentes pode ser deletado como no exemplo abaixo: gldeletelists(1,10); // apaga listas de 1 a 10 gldeletelists(400,10); // apaga listas de 400 a 409 6
O primeiro parâmetro é a primeira lista ser apagada, e o segundo é o número de listas. 6 Texto Para escrever texto, o ideal é definirmos uma fonte de bitmaps, a serem carimbados na tela para compor texto. No OpenGL, um bitmap é um padrão de bits onde cada pixel ou é pintado (com a cor atual) ou não. O comando que desenha bitmaps é glbitmap. Seu protótipo é void glbitmap(int largura, int altura, float xorig, float yorig, float xmove, float ymove, void *dados); Esta função desenha um bitmap de largura altura pixels com o canto inferior esquerdo na atual posição raster e move a posição raster por (xmove, ymove). O desenho do bitmap é definido pela memória apontada por dados. A Fig. 3 tem um exemplo de uma letra S. O exemplo P4 disponível na página do curso contem uma fonte inteira. Figura 3: Definição de bitmap. A posição raster pode ser modificada com glrasterpos2i. Maiores detalhes podem ser consultados no capítulo 8 do livro vermelho. 7 Imagens De forma semelhante a bitmaps, podemos carimbar imagens coloridas com a função gldraw- Pixels: gldrawpixels(int largura, int altura, GLenum formato, GLenum tipo, void *dados); 7
Largura e altura definem o tamanho da imagem. O formato que nos interessa é GL RGB, onde nossos dados serão compostos de sequências de valores R,G,B para cada pixel. No campo tipo podemos especificar GL UNSIGNED BYTE para usar valores entre 0 e 255 para cada componente (É possível usar valores de ponto flutuante de 32 bits com GL FLOAT, por exemplo, mas isto é incomum. O capítulo 8 do livro vermelho tem uma tabela com todos os tipos permitidos). Com GL RGB e GL UNSIGNED BYTE, os dados devem apontar para uma imagem definida tal como no exercício com imagens P6 (R,G,B,R,G,B,etc.), exceto que OpenGL armazena imagens a partir da linha inferior (canto inferior esquerdo). gldrawpixels desenha a imagem com o canto inferior esquerdo na atual posição raster. 8 Compilando com OpenGL 8.1 Linux No Linux, caso OpenGL e glut estejam devidamente instalados, basta incluir a GLUT com #include <GL/glut.h> e compilar os programas com gcc programa.c -o programa -lglut Em alguns Linux pode ser necessário adicionar -lglu -lgl. 8.2 Windows: GLUT O Windows já inclui OpenGL, mas não a GLUT. O primeiro passo no Windows é baixar a GLUT de http://www.xmission.com/~nate/glut.html (arquivo glut-3.7.6-bin.zip) e descompactá-lo em um diretório qualquer. Figura 4: Arquivos da GLUT para Windows. 8
8.3 Windows: Microsoft Visual Studio Configuração inicial: selecione Tools, Options. Selecione Projects, VC++ Directories. Exibindo os diretórios para Include Files, adicione o diretório que contem o arquivo glut.h. Exibindo os diretórios para Library Files, adicione o diretório que contem o arquivo glut32.lib. Copie o arquivo glut32.dll para C:\Windows\System32 (ou terá que copiá-lo para o diretório do seu.exe toda vez que fizer um projeto). Criando um projeto OpenGL:File, New, Project. Em Visual C++ Projects escolha Win32 Project, escolha o nome e o diretório do seu projeto e clique Ok. Clique em Application Settings e escolha Console Application e Empty Project. Expanda Source Files no lado esquerdo da janela, abra o menu com o botão direito e escolha Add Existing Item para adicionar um arquivo C já existente, ou Add Item para adicionar um arquivo C vazio. Seus programas devem ter os includes abaixo: #include <windows.h> #include <glut.h> Há algumas pequenas diferenças no Windows. O math.h por exemplo não tem a definição de pi (M PI). Para compilar, Build, Build Solution (ou F7 no teclado). Procure o executável gerado (estará dentro de um diretório Debug no diretório do seu projeto, que foi escolhido na tela de criação de novo projeto). Figura 5: Telas do Visual Studio. 9
8.4 Windows: Dev C++ Configuração Inicial: Selecione Tools, Compiler Options. Na aba Directories, adicione o diretório que contem o arquivo glut32.lib aos diretórios de Libraries, e adicione o diretório que contem o arquivo glut.h aos diretórios de C Includes. Clique Ok. Criando um novo projeto: File, New Project. Escolha Basic, Console Application. Dê um nome ao projeto e clique Ok. Escreva seu programa C ou adicione um já existente ao projeto. Abra Project, Project Options. Selecione a aba Parameters. No campo do Linker, escreva em uma mesma linha: -lopengl32 -lglu32 -lglut32 Nos seus programas, escreva a linha #define GLUT_DISABLE_ATEXIT_HACK antes de todos os #include, e inclua #include <windows.h> #include <glut.h> Figura 6: Tela de configuração do Dev C++. 10