A OC-Zone decidiu aprender um pouco de OpenGl e divertir-se as custas desta biblioteca que a maioria das placas gráficas suportam. Contudo criamos este Artigo para que os nossos leitores e principiantes da programação em C++, aprendam a utilizar e a criar pequenos cenários utilizando algumas figuras geométricas.
O Software utilizado para programar C++ foi o Borlan C++ Builder 6, no entanto, também funciona em Visual Studio C++. O processo é o mesmo portanto fica ao inquérito dos nossos leitores.
Introdução ao OpenGL
A OpenGL(OpenGraphicsLibrary), é uma biblioteca padrão de funções gráficas para o desenvolvimento de aplicações gráficas interactivas 2D e 3D com qualidade profissional e de alta performance, tendo sido criada em 1992 pela SiliconGraphics (actual SGI – www.sgi.com), na forma de uma API (ApplicationProgramming Interface), gráfica, independente dos dispositivos de exibição e dos sistemas operativos.
A OpenGL foi escrita em C e segue a convenção de chamada da linguagem C.
Existem diferentes implementações da OpenGL para diferentes sistemas operativos, por exemplo para Windows e Linux. As várias implementações usam os mesmos tipos de dados e os mesmos nomes de funções. Desse modo um programa escrito para Windows por exemplo, pode também ser compilado para Linux.
A OpenGL não contem funções especificas para interacção com os modernos sistemas de janelas (Windows, X Windowetc), pelo que as tarefas comuns de uma aplicação, como criar janelas, gerir eventos provenientes do rato e do teclado, assim como apresentação de menus, ficam a cargo de bibliotecas próprias de cada ambiente de programação. Existe no entanto uma extensão da biblioteca OpenGL conhecida por GLUT (OpenGLToolKit), que contem funções para a gestão de janelas e eventos e pode ser usada para o desenvolvimento de aplicações gráfica sem OpenGL, independentes do sistema de gestão de janelas dos sistemas operativos.
A OpenGL transformou-se num padrão extensamente utilizado pela indústria. O desenvolvimento desta biblioteca está hoje em dia a cargo de um consórcio formado pelas principais empresas de software e hardware do mundo (www.opengl.org).
A OpenGL promove a inovação e acelera o desenvolvimento de aplicações incorporando um grande conjunto de funções de renderização, de texturas, de efeitos especiais, e de outras poderosas funções de visualização.
1.1 Máquina de estados
A OpenGL é uma máquina de estados porque é possível colocá-la em vários estados (ou modos) que não são alterados, a menos que uma função seja chamada para isso. Por exemplo, a cor de desenho é uma variável de estado que pode ser definida como branca. Todos os objectos são então desenhados com a cor branca, até ao momento em que outra cor de desenho seja especificada. A OpenGL mantém uma série de variáveis de estado, tais como estilo de linhas, posições e características das luzes, propriedades do material dos objectos a desenhar, etc.
Muitas delas referem-se a modos que podem ser activados ou desactivados com os comandos glEnable()e glDisable().
1.2 O Pipeline da OpenGL
Os comandos da biblioteca OpenGL seguem uma série de estágios de processos ao serem executados a que chamamos Pipeline de Renderização da OpenGL. A sequencia dos processos desse pipeline é mostrada no diagrama da figura 1.
Figura 1 – Pipeline de Renderização da OpenGL
1.3 Funções gráficas da OpenGL
As funções gráficas da biblioteca OpenGL podem ser agrupadas de acordo com a função que desempenham. Segue-se a descrição genérica desses grupos de funções.
Buffer de acumulação: Trata-se de um buffer no qual múltiplos frames renderizados podem ser compostos para produzir uma única imagem. Este buffer é usado para efeitos tais como a profundidade de campo, “blur” de movimento, e de anti-aliasing da cena.
Alfa Blending: Proporciona mecanismos para criar objectos transparentes. Usando
a informação alfa, é possível escolher se um objecto é definido como algo totalmente transparente até algo totalmente opaco.
Anti-aliasing: Um método de renderização utilizado para suavizar linhas e curvas.
Esta técnica calcula a média da cor dos pixels junto à linha. Tem o efeito visual de suavizar a transição dos pixels na linha e daqueles junto à linha, gerando assim uma aparência mais suave.
Modo “Color-Index”: Buffer de Cores que armazena índices de cores das componentes vermelhas, verdes, azuis, e alfa das cores (RGBA), de modo a criar uma paleta de cores especifica.
DisplayLists: Uma lista nomeada de comandos de OpenGL. As funções inseridas numa Displaylist podem ser pré-processadas de modo a serem executadas mais eficientemente do que se fossem executadas no modo imediato.
Doublebuffering: Usado para fornecer uma animação suave dos objectos. Cada cena sucessiva de um objecto em movimento pode ser construída em “background” ou no buffer “invisível” e então apresentada. Isto permite que somente as imagens completas sejam sempre apresentadas no ecrã.
FeedBack: Um modo onde OpenGL retornará a informação geométrica processada (cores, posições do pixel, e assim por diante), à aplicação. GouraudShading: Interpolação suave das cores através de um segmento de polígono ou de linha. As cores são atribuídas em vértices e linearmente interpoladas através da primitiva para produzir uma variação relativamente suave na cor.
Modo Imediato: A execução de comandos OpenGL quando eles são chamados, possui resultado melhor do que os “DisplayLists”. Iluminação e sombreamento de materiais: A habilidade de computar exactamente a cor de algum ponto da superfície de um objecto dado as propriedades materiais para essa superfície.
Operações de pixel: Armazena, transforma, traça e processa aumento e redução de imagens.
Executores polinomiais: Para suportar as NURBS (non-uniformrationalBsplines). Primitivas: Um ponto, uma linha, um polígono, um bitmap, ou uma imagem. Primitivas da rasterização: bitmaps e retângulos de pixels.
Modo RGBA:Buffers de cores armazenam componentes vermelhos, verdes, azuis, e alfa da cor.
Selecção e colheita: Trata-se de um modo no qual a OpenGL determina se certa primitiva identificada do gráfico é renderizada numa dada região no buffer de frame.
Planos do stencil: Um buffer que é usado para mascarar pixels individuais no buffer de frame de cores.
Mapeamento de Texturas: O processo de aplicar uma imagem a uma primitiva gráfica. Esta técnica é usada para gerar imagens com realismo.
Transformações: A habilidade de mudar a rotação, o tamanho, e a perspectiva de um objecto no espaço 3D.
Z-buffering: O Z-buffer é usado remover as superfícies escondidas.
1.4 Sintaxe de comandos da OpenGL
Os comandos da biblioteca OpenGL obedecem a um padrão bem definido para especificação dos nomes de funções e constantes. A grande maioria dos nomes das funções contêm o prefixo “gl” em letras minúsculas e todos os nomes de constantes começam com as iniciais “GL_”, em letras maiúsculas, e usam um “underscore” para separar as palavras que formam o nome
(Ex. GL_COLOR_BUFFER_BIT ). Existem algumas funções para a plataforma Windows (evidenciados pelo prefixo "wgl") que facilitam a criação de um espaço de renderização numa janela comum Windows.
Os nomes de algumas funções contêm um sufixo formado por um algarismo e uma ou duas letras (Ex. glColor3f e glVertex3fv). O algarismo indica o número de argumentos da função e a letra indica o tipo desses argumentos. Este formato para o nome das funções ajuda a lidar com o grande número de variantes que uma dada função pode ter. A figura 2 ilustra o formato do nome das variantes para a função glVertex…().
Figura 2 - Variantes do nome da função glVertex…().
Para tornar o código “portátil”, foram definidos tipos de dados próprios para OpenGL. Estes tipos de dados são mapeados dos tipos de dados comuns do C, que também podem ser utilizados.
Como os vários compiladores e ambientes possuem regras diferentes para determinar o tamanho das variáveis em C, usando os tipos OpenGL é possível “isolar” o código das aplicações, destas alterações. A tabela 1 apresenta os sufixos das funções e os tipos de dados para os argumentos das funções.
Tab. 1 - Sufixos e tipos de dados para os argumentos das funções da OpenGL.
2 Instalação da OpenGL no Windows XP
2.1 C++ Builder e no Visual Studio C++
A implementação da biblioteca para uma dada plataforma e sistema operativo é composta por dois grupos de ficheiros: os ficheiros que formam a biblioteca propriamente dita e os ficheiros de interface com a biblioteca. No caso da implementação da biblioteca para o sistema operativo Windows, os ficheiros que forma a biblioteca são opengl32.dll(kernelOpenGL) e glu32.dll (biblioteca GLU) e fazem parte da distribuição desse sistema operativo (encontram-se na pasta System32). Os ficheiros de interface com a biblioteca vêm normalmente com o ambiente de desenvolvimento da linguagem de programação que quisermos usar. No caso do C++Builder, os ficheiros de interface são opengl32.lib(interface para o kernelOpenGL) e glu32.lib(interface para a biblioteca GLU) e os ficheiros de cabeçalho gl.h(com os protótipos para o kernelOpenGL) e glu.h(com os protótipos para a biblioteca GLU). Estes ficheiros encontram-se respectivamente nas pastas $(BCB)LIBPSDK e $(BCB)INCLUDEGL.
No caso do Visual C++, tanto os ficheiros de interface como os ficheiros de cabeçalho são os mesmos mencionados anteriormente, no entanto esses encontra-se respectivamente nas pastas $(VC++)LIBPSDK e $(VC++)INCLUDEGL.
Existem versões da biblioteca OpenGL para outros sistemas operativos e ambientes de programação. Os ficheiros correspondentes podem ser obtidos no site oficial da OpenGL (www.opengl.org).
2.1 Primeiros passos
Segue-se o desenvolvimento de uma pequena aplicação que exemplifica a instalação e o uso da biblioteca OpenGL em C++Builder. Os gráficos produzidos pelos comandos da biblioteca OpenGL podem ser visualizados em qualquer componente para o qual se possa obter um devicecontext. No exemplo que se segue, usaremos um componente TPanelpara esse efeito.
Crie um novo projecto e adicione um componente TPanelao formulário principal. Altere o nome do painel para PanelGLe elimine o texto na sua propriedade Caption. Altere as dimensões do painel para que fique apenas um pouco menor do que o formulário e centre-o em relação a este. Declare as seguintes variáveis na secção privateda classe do formulário, no ficheiro Unit.h:
private:
HDC hdc;
HGLRC hrc;
intpixel_format;
A primeira variável é o handle para o devicecontext do componente TPanel. A segunda guardará um handle para a janela de renderizaçãoOpenGLe a terceira é um identificador do “formato de pixel” que teremos que configurar. Declare as seguintes funções na secção public da classe do formulário:
Inclua no ficheiro Unit.cppos ficheiros cabeçalho necessários para usar a biblioteca OpenGL:
#include <gl/gl.h>
#include <gl/glu.h>
Crie a função Configurar_Pixel_Format()que configura o devicecontext do componente PanelGLpara que possa ser usado para renderizar as imagens geradas pela OpenGL:
void __fastcall TForm1::Configurar_Pixel_Format()
{
//Configuração da estrutura PixelFormatDescriptor com
//suporte para OpenGL, uso de DoubleBuffer e formato RGBA.
PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
24,
0,0,0,0,0,0,
0,0,
0,0,0,0,0,
32,
0,
0,
PFD_MAIN_PLANE,
0,
0,0,0
};
//Cria o devicecontext do painel com as características
//configuradas no PixelFormatDescriptor.
pixel_format = ChoosePixelFormat(hdc, &pfd);
SetPixelFormat(hdc, pixel_format, &pfd);
}
Crie a função Idle_Loop()que será chamada automaticamente no evento OnIdle da aplicação e que implementa o ciclo de renderização da OpenGL:
A função glViewport(0, 0, PanelGL->Width, PanelGL->Height)define a viewport de visualização, isto é, a área rectangular dentro do componente TPanel onde as imagens serão renderizadas. O protótipo desta função é: voidglViewport(GLint x, GLint y, GLsizeiwidth, GLsizeiheight).
Os seus parâmetros especificam o canto inferior esquerdo da viewport (x e y), dentro da área do painel, e a sua largura e altura em pixels (widthe height). Neste caso os parâmetros usados fazem com que a viewport coincida com a própria área do painel, mas é possível usar outros valores e definir uma área mais pequena (ou mais grande), do que a área do componente. Também é possível definir mais do que uma viewport permitindo assim renderizar mais do que uma cena, ou mais doq que uma vista da mesma cena.
A função glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)é usada para definir o volume de visualização e em consequência o tipo de projecção, que será usada para renderizar as imagens.
Os parâmetros lefte rightespecificam os limites mínimo e máximo no eixo X; analogamente, bottome top especificam os limites mínimo e máximo no eixo Y; zNeare zFar, por sua vez, especificam a coordenada mais próxima e mais distante do observador, respectivamente, no eixo de profundidade Z. Estes parâmetros definem um volume de visualização com a forma de um paralelepípedo rectangular como o mostrado na figura 1. Este volume de visualização é também designado por volume de recorte, pois só será renderizadoaquilo que estiver dentro deste volume. A projecção definida pela função é uma projecção ortográfica ou paralela.
Figura 3 – Volume de visualização da projecção ortográfica.
As funções glMatrixMode(GL_PROJECTION)e glMatrixMode(GL_MODELVIEW), servem para indicar qual é a matriz que será afectada pelas operações de transformações que possam ser executadas a seguir. A biblioteca OpenGL mantém internamente duas pilhas de matrizes distintas. Uma delas diz respeito às transformações da câmara (ou observador) e é representadapelo identificador GL_PROJECTION, a outra diz respeito às transformações geométricas dos modelos que formam a cena e é representada pelo identificador GL_MODELVIEW. A função glMatrixMode() permite indicar qual destas matrizes deve passar a estar activa num dado momento e portanto qual será afectada pelas transformações que possam ser executadas a seguir. A função glLoadIdentity()serve para inicializar (igualar à matriz identidade), a matriz que estiver activa no momento.
Por fim crie A função Desenhar_Cena().
void __fastcall TForm1::Desenhar_Cena()
{
//local onde vai desenhar o cenário propriamente dito
}
Contudo se compilamos o código podemos visualizar algo parecido a isto:
Como vê-mos criamos um modelo de aplicação predefinida para depois poder-mos desenhar os cenários e brincar um bocado com o OpenGL.
Portanto vamos lá então criar o nosso primeiro cenário. Iremos desenhar 2 sólidos, um cubo e uma esfera. Depois de desenhados os sólidos iremos aplicar as texturas para dar vida e para visualizarem o poder do OpenGl. Contudo existem muitos factores que fazem uma pessoa visualizar os sólidos, e um desses factores é a Luz.
A criação, configuração e posicionamento das luzes em OpenGL é feita com as
pname: especifica o parâmetro que está a ser configurado pela chamada desta função.
param ou params: é um valor ou um vector de valores a usar na configuração.
Nome do parâmetro
Valor Padrão
Significado
GL_LIGHT_MODEL_AMBIENT
(0.2, 0.2, 0.2, 1.0)
Intensidade RGBA; ambiente de toda a cena
GL_LIGHT_MODEL_LOCAL_VIEWER
0.0 ou GL_FALSE
Como o ângulo de reflexão especular e calculado
GL_LIGHT_MODEL_TWO_SIDE
0.0 ou GL_FALSE
Define entre a iluminação de um ou dois lados
Outros factores é o material de que é feito os objectos, as sombras que podemos dar aos objectos e as reflexões que os objectos podem criar perante outras superfícies como por exemplo um cubo a reflectir na água. Esses factores são importantes, mas no entanto não serão mencionados no nosso Artigo, embora estejam presentes na programação.
3 Criação da Cena.
Crie uma nova aplicação a partir do modelo de aplicações OpenGL definido em aulas anteriores. Altere o painel de modo a que fique com a dimensão de 500x500 pixels
Altere a função FormResize()de modo a que fique com o seguinte código:
O cubo será construído usando as primitivas básicas da OpenGL, isto é, através da definição de 8 polígonos (quadrados), a esfera será desenhada usando objectos quadráticos da biblioteca GLU (as texturas não podem ser aplicadas directamente nas primitivas da biblioteca GLUT).
As coordenadas do cubo são mostradas na figura 4.
Figura 4 – Coordenadas do cubo.
Na função Desenhar_Cena()coloque o seguinte código:
Execute o programa. Verá os dois objectos em rotação algo idênticos a Figura 5
Figura 5 – Esfera e Cubo em Rotação
4 Aplicar texturas aos objectos.
O uso de texturas em OpenGL é um assunto complexo e bastante extenso. Neste texto faremos apenas uma breve introdução ao tema.
Na sua forma mais simples, uma textura é uma imagem 2D e a aplicação de uma textura consiste no mapeamento dos pontos que formam essa imagem, nos pontos que forma a superfícies de um polígono 3D.
Na figura 6 as letras A, B, C e D definem os vértices de uma textura e os vértices A1, B1, C1 e D1 os vértices de um polígono 3D onde deve ser mapeada essa textura. O processo de mapeamento de texturas em OpenGL consiste em aplicar a imagem 2D sobre o polígono 3D de forma que os pontos A, B, C e D correspondam aos pontos A1, B1, C1 e D1.
Figura 6 – Correspondência entre os vértices de uma textura e os vértices de um polígono.
Acrescentando o seguinte código à função FormCreate()para criar duas texturas:
A função glEnable(GL_TEXTURE_2D)habilita o uso de texturas na aplicação. A função glGenTextures(2, Texturas)gera automaticamente identificadores para as texturas que vamos usar. O primeiro parâmetro indica quantos identificadores de texturas vamos gerar e o segundo parâmetro é um ponteiro para uma lista (array), que armazena os identificadores gerados.
Declare o arrayTexturas na classe do formulário como sendo do tipo unsignedint. Declare também a variável Imagem como sendo um ponteiro do tipo _AUX_RGBImageRec (este tipo está declarado no ficheiro glaux.h).
Voltando à criação das texturas, criamos os identificadores necessários (um para a textura do cubo, um para a textura da esfera), lê-mos as imagens a partir de ficheiros do tipo .BMP usando a função auxDIBImageLoadA()da biblioteca glaux.lib, ligamos o identificador de textura no qual iremos trabalhar e criamos a textura. A função glTexImage2D() recebe como parâmetros o tipo de textura a ser criada (uma textura bidimensional, portanto 2D), o nível de detalhe da imagem (utilizado na criação de BMP’s), o formato interno da imagem (quantos bytes cada pixel gastará), a largura e altura da imagem, se haverá ou não bordas na imagem, o formato da imagem a carregar, como os dados estão agrupados na imagem e finalmente o ponteiro para o local da memória onde está localizado o bitmap.
Tendo criado a textura, acertamos os filtros que serão usados quando o objecto estiver muito perto (GL_TEXTURE_MIN_FILTER) ou muito longe (GL_TEXTURE_MAG_FILTER) da câmara. Os valores mais comuns são GL_NEAREST(mais rápido, porém com menor qualidade) e GL_LINEAR(maior qualidade na produção do ponto da textura). Finalmente, após a imagem ter sido carregada e os parâmetros ajustados, liberamos a memória que foi alojada para armazenar a imagem (pois a OpenGL encarrega-se de manipular o conteúdo das texturas). As texturas estão criadas e prontas para serem usadas. Na função Desenhar_Cena(), imediatamente antes do desenho do cubo, executamos a função glBindTexture(GL_TEXTURE_2D, Texturas[0])que liga a textura desejada para que todos os vértices definidos daquele ponto em diante usem a textura identificada por Texturas[0]. Após isso, para cada vértice que enviarmos à OpenGL, precisamos também enviar um ponto de mapeamento de texturas, que indica o ponto na imagem da textura que o vértice actual utiliza.
Para permitir a construção desta correspondência entre a imagem 2D e o polígono 3D, usa-se a função glTexCoord2f()antes da definição do ponto 3D.
Por exemplo, considerandoa figura 61, o código:
glTexCoord2f(0.0f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
define que o ponto (0.0, 0.0) da textura 2D corresponde ao ponto (1.0, -1.0,1.0) do polígono 3D.
O sistema de coordenadas da textura tem como (0,0) o ponto inferior esquerdo da
imagem e como (1,1) o ponto superior direito. Ou seja, na imagem acima temos
as seguintes coordenadas para os pontos A, B, C e D:
Vértice da Textura
Coordenada
A
(0,1)
B
(1,1)
C
(1,0)
D
(0,0)
Supondo que o polígono 3D é a face lateral direita de um cubo de aresta 2 com o centro no ponto (0,0,0), teremos as seguintes coordenadas:
Vértice do Polígono 3D
Coordenada
A1
1.0, 1.0, 1.0
B1
1.0, 1.0, -1.0
C1
1.0, -1.0, -1.0
D1
1.0, -1.0, 1.0
Assim, substitua o código que desenha o cubo pelo seguinte:
Para a esfera o processo é o mesmo. Ligamos a textura que queremos usar com a chamada da função glBindTexture(GL_TEXTURE_2D, Texturas[1]), notar que indicámos o outro identificador de textura. Como a esfera é desenhada usando primitivas de objectos do tipo quadrático, não é necessário indicar os pontos de mapeamento, pois são determinados automaticamente.
Por último referir que a OpenGL aceita apenas texturas com largura e altura como potências de 2 (2, 4, 8, 16, 32, 64, 128, 256, etc) portanto é necessário cuidado na criação de imagens para usar como texturas.
As texturas utilizadas no nosso programa estão no formato BMP e vez das actuais Jpeg.
As Texturas aplicadas no nosso exemplo foram as seguintes:
Para a Esfera é:
E para o Cubo:
Execute novamente o programa, Deverá ver os objectos com as respectivas texturas a rodar.
Uma pequena Optimização que podemos fazer, é a seguinte:
Em vez de usar-mos a função Idle_Loop(), podemos substitui-la pelo evento Timer e programar o timer para 1000 milissegundos, assim liberta-se mais o processador para ele poder executar outras tarefas deixando este de funcionar a 100% Load.
Esperamos com este artigo ter respondido a muitas questões acerca do OpenGL bem como de outros assuntos com ele relacionado, já sabe que se possuir alguma dúvida tem à disposição o nosso fórum.