|
4. Pipeline do OpenGL |
Vértices e Primitivas
No OpenGL, a maior parte dos objectos geométricos são
desenhados, englobando uma série de conjuntos de coordenadas que especificam
vértices ou opcionalmente as normas (comprimento de um vector), coordenadas das
texturas e cores entre o par de comandos glBegin/glEnd. Por exemplo, para
especificar um triangulo com os vértices (0, 0, 0) e, (1, 0, 1), podíamos fazer
da seguinte maneira:
glBegin(GL_POLYGON);
glVertex3i(0,0,0);
glVertex3i(0,1,0);
glVertex3i(1,0,1);
glEnd();
Os dez tipos de objectos geométricos que são desenhados desta
maneira estão sumariados na Tabela 1. Este grupo particular de objectos é
seleccionado porque a geometria destes é especificada por uma simples lista de
vértices, uma vez que cada um admite um algoritmo eficiente de renderização, e
foi determinado que estes objectos combinados satisfazem as necessidades de
quase todas as aplicações gráficas.
Cada vértice pode ser especificado por duas, três ou quatro
coordenadas (quatro coordenadas indicam uma localização tridimensional
homogénea). Adicionalmente, a norma, as coordenadas da textura e a cor
correntes, podem ser usadas para processar cada vértice. O OpenGL usa normais no
cálculo de iluminações; a normal corrente é um vector tridimensional que pode
ser definido enviando três coordenas que a especifiquem. As cores podem
consistir em vermelho, verde, azul e valores alfa (quando o OpenGL foi
inicializado para o modo RGBA), ou através do valor que define a cor. Uma, duas,
três ou quatro coordenadas de texturas determinam como a textura de uma imagem é
mapeada para uma primitiva.
Objecto |
Interpretação dos Vertices |
point |
cada vértice descreve a localização de um ponto |
line strip |
séries de segmentos de recta ligados; cada vértice posterior descreve primeiro o ponto final do próximo segmento |
line loop |
igual ao line strip mas o segmento final é adicionado do ultimo vértice para o primeiro vértice |
separate line |
cada par de vértices descreve um segmento de linha |
polygon |
o line loop é formado por vértices da fronteira de um polígono convexo |
triangle strip |
cada vértice posterior aos dois primeiros descreve um triangulo dado por esse vértice, mais os dois anteriores |
triangle fan |
cada vértice posterior aos dois primeiros descreve um triangulo dado por esse vertice, mais o anterior e pelo primeiro vértice |
separate triangle |
cada trio de vértices descrevem um triangulo |
quadrilateral strip |
cada par de vértices posterior aos dois primeiros, descreve um quadrilátero, dado por esse par e pelo par anterior |
independent quad |
cada grupo consecutivo de 4 vértices descreve um quadrilátero |
Tabela 1 - Objectos pertencentes ao glBegin/glEnd [1].
Cada comando que especifique as coordenadas, a normal, as cores,
ou a textura de um vértice é disponibilizado em vários formatos para acomodar
aos diferentes formatos de aplicações de dados e a números de coordenadas. Os
dados podem ser transferidos para estes comandos como listas de argumentos ou
como ponteiros para blocos de armazenamento que contenham dados. As variantes
são distinguidas por (na linguagem C) mnemónicas de sufixos.
A maior parte dos comandos do OpenGL que não especifiquem vértices
ou informação associada não podem aparecer entre o glBegin e o glEnd. Esta
restrição permite às implementações executar de um modo optimizado enquanto são
processadas simultaneamente as especificações de primitivas, de modo a garantir
um processamento o mais eficiente possível [4].
Figura 2 - Associação de valores correntes com um vértice
Quando um vértice é especificado, a sua cor, a normal e as
coordenadas da textura são usadas para obter valores que mais tarde serão
associados ao vértice (Figura 2). O próprio vértice é transformado pelo matriz
de visualização do modelo (model-view matrix), a qual pode representar
transformações translacionais e lineares. A cor é obtida através de cálculos da
cor pelas iluminações ou, caso as iluminações estejam desactivadas, obtida pela
própria cor. As coordenadas das texturas são passadas pela função de texture
coordinate generation. As coordenadas das texturas resultantes são transformadas
pela matriz de texturas (texture matrix) (esta matriz pode ser usada para
redimensionar ou rodar a textura que é para ser aplicada a uma primitiva).
Existem grupos de comandos que controlam os valores dos parâmetros
usados no processamento de vértices. Um desses grupos de comandos manipula
transformações de matrizes. Estes comandos são desenhados de modo a formar uma
maneira eficiente de gerar e manipular transformações que ocorrem numa
hierarquia de cenários 3D. Uma matriz pode ser carregada ou multiplicada através
de redimensionamento, rotação, translação ou por uma outra matriz. Outro comando
controla qual a matriz afectada por uma manipulação: a matriz de visualização do
modelo, a matriz de texturas ou a matriz de projecção (projection matrix). A
cada um destes três tipos de matrizes está associada uma pilha, na qual as
matrizes podem ser inseridas ou removidas (push ou pop).
Os parâmetros de iluminação são agrupados em três categorias:
parâmetros de material, que descrevem as características dos reflexos da
superfície iluminada; os parâmetros da fonte de iluminação, que descrevem as
propriedades de emissão de cada fonte de luz; e os parâmetros do modelo de luz,
que descrevem propriedades globais do modelo de luz. As iluminações são
aplicadas com base nos vértices; os resultados das iluminações são interpolados
por um segmento de recta ou por um polígono. A forma geral de uma equação de
iluminação inclui termos constantes, de difusão e de iluminação focal, cada um
deles pode ser atenuado pela distância do vértice da fonte de luz. O programador
pode sacrificar o realismo em favor de cálculos para efeitos luminosos mais
rápidos, indicando que quem vê a imagem, os pontos de luz ou ambos, estão a uma
grande distância do cenário.
Projecção e Corte (Projection and Clipping)
Assim que uma primitiva é construída a partir de um grupo de
vértices, esta é sujeita a um processo de corte através de superfícies de corte
(clip planes). As posições destas superfícies (cada implementação em OpenGL tem
de conter pelo menos seis) são especificadas usando o comando glClipPlane. Cada
superfície pode ser activada ou desactivada individualmente.
No caso de um ponto, as superfícies de corte não têm qualquer
efeito no ponto ou então simplesmente elimina-o, dependendo se o ponto está
dentro ou fora da intersecção dos half-spaces determinados pelas superfícies de
corte. No caso de se tratar de um segmento de recta ou de um polígono, as
superfícies de corte podem, não ter qualquer efeito, eliminar, ou alterar o
valor original da primitiva. No último caso, os novos vértices podem ser criados
entre os cantos descritos pelos vértices originais; a cor e os valores das
coordenadas das texturas para estes novos vértices são determinados interpolando
os valores atribuídos relativamente aos vértices originais.
Após o processo das superfícies de corte ter sido aplicado (caso
tenha sido necessário), as coordenadas dos vértices das primitivas resultantes
são transformadas pela matriz de projecção. Ocorre então o view frustum clipping.
O view frustum clipping é parecido à aplicação das superfícies de corte, mas
para superfícies fixas: se as coordenadas depois de uma transformação são dadas
por (x, y, z, w), então os seis half-spaces definidos por estas superfícies são
-w ≤ x, x ≤ w, -w ≤ y, y ≤ w, -w ≤ z, z ≤ w.
Com a operação de view frustum clipping terminada, cada grupo de
coordenadas de vértices é projectada calculando x/w, y/w e z/w. O resultado (que
tem que estar entre [-1, 1]) é multiplicado e compensado pelos parâmetros que
controlam o tamanho do viewport, onde as primitivas são desenhadas. Os comandos
glViewport (para x/w e y/w) e glDepthRange (para z/w) controlam estes
parâmetros.
Rasterização
O processo de rasterização converte uma primitiva
viewport-scaled numa série de fragmentos. Cada fragmento contém a localização de
um pixel no framebuffer, a cor, as coordenadas das texturas e a profundidade
(Z). Quando um segmento de linha ou um polígono são rasterizados, os dados
associados são interpolados através de primitivas de forma a obter o valor para
cada fragmento [4].
A rasterização de cada tipo de primitiva é controlada por um
correspondente grupo de parâmetros. Uma largura afecta a rasterização de um
ponto e a outra afecta a rasterização de um segmento de linha. Adicionalmente,
uma stipple sequence pode ser especificada para segmentos de recta e, um stipple
pattern pode ser especificado para polígonos.
O antialiasing pode ser activado ou desactivado individualmente
para cada primitiva. Quando for activado, um valor de cobertura é computado para
cada fragmento descrevendo uma porção desse fragmento que é coberto pela
primitiva projectada. Este valor de cobertura é usado depois do processo de
texturização ter terminado, para modificar o valor do fragmento alfa (quando em
modo RGBA) ou o valor do índice da cor (quando em modo de indexação de cor).
Texturização e Nevoeiro (Texturing and Fog)
O OpenGL fornece meios gerais para gerar primitivas mapeadas de
texturas. Quando a texturização está activada, cada fragmento das texturas
coordena a indexação de textura das imagens, gerando uma texel. Esta texel pode
ter entre um a quatro componentes, de modo que a textura da imagem possa
representar, por exemplo, apenas a intensidade (uma componente), cor RGB (três
componentes), ou RGBA (quatro componentes). Quando uma texel é obtida, ela
modifica a cor do fragmento de acordo com a especificação do ambiente de
texturização.
A imagem da textura é especificada usando o comando glTexImage, que
recebe argumentos semelhantes aos do comando glDrawpixels de modo a que o mesmo
formato da imagem possa ser usada com o framebuffer ou a memória de texturas
(todas as placas gráficas têm alocada uma fracção de memória para as texturas).
Adicionalmente, o glTexImage pode ser usado para especificar mipmaps, de modo
que a textura possa ser filtrada à medida que é aplicada a uma primitiva. A
função responsável pelo filtro é controlada por um número específico de
parâmetros usando o comando glTexParameter. O ambiente da textura é seleccionado
com o glTexEnv.
Finalmente, após o processo de texturização, a função do nevoeiro
(se estiver activada) é aplicada a cada fragmento. Esta função mistura as cores
que recebe com uma cor constante do nevoeiro de acordo com um factor ponderado
computado. Este factor é a função da distância (ou uma aproximação à distância)
do ponto de vista de quem vê até ao ponto tridimensional que corresponde ao
fragmento. Nevoeiro exponencial simula nevoeiro atmosférico e neblina, enquanto
o nevoeiro linear pode ser usado para produzir uma perda gradual de qualidade
com a distância (depth-cueing).
O Framebuffer
O destino dos fragmentos rasterizados é o framebuffer, onde os
resultados da renderização do OpenGL podem ser mostrados. No OpenGL, o
framebuffer consiste num tabela rectangular de pixels correspondentes à janela
destinada para a renderização OpenGL. Cada pixel é simplesmente um conjunto de
alguns números de bits. Bits correspondentes a cada pixel no framebuffer são
agrupados juntamente num bitplane, onde cada bitplane contém apenas um bit de
cada pixel [4, 5].
Os bitplanes são agrupados em vários buffers lógicos: de cor,
profundidade, stencil, e de acumulação. O buffer de cor é onde a informação
sobre o fragmento da cor é colocado. No buffer de profundidade é colocado
informação sobre o fragmento de profundidade. No buffer de stencil existem
valores que são sempre actualizados quando o correspondente fragmento chega ao
framebuffer. Os valores de stencil são úteis em algoritmos multi-passo, onde
cada cenário é renderizado várias vezes, até atingir um efeito como o das
operações CGS (união, diferença e intersecção) num dado número de objectos e
capping de objectos cortados por superfícies de corte.
O buffer de acumulação também é útil para algoritmos multi-passo e,
pode ser manipulado de modo a calcular a média dos valores armazenados no buffer
de cores. Isto pode aplicar efeitos como antialiasing no écran inteiro (agitando
o ponto de vista a cada passagem), profundidade de campo (agitando o ângulo de
visão), suavização de movimento (avançando o cenário no tempo). Algoritmos
multi-passo são simples de implementar em OpenGL, simplesmente porque um pequeno
número de parâmetros têm de ser manipulados antes de cada passagem, e alterar os
valores destes parâmetros é eficaz e não traz efeitos secundários aos outros
parâmetros que têm de permanecer constantes.
O OpenGL suporta buffering duplo e em estéreo (double-buffering e
stereo-buffering), de modo que o buffer de cor seja subdividido em quatro buffer:
os buffers de frente, esquerdo e direito e os buffers traseiros, esquerdo e
direito. Os buffers frontais são aqueles que são tipicamente mostrados enquanto
os buffers traseiros (numa aplicação com double-buffering) são usados para
compor a próxima frame. Uma aplicação monoscopic usaria apenas buffers
esquerdos. Adicionalmente, podem existir buffers auxiliares (que nunca são
mostrados) para onde os fragmentos podem ser renderizados. Cada um dos buffers
pode ser activado ou desactivado individualmente para escrita de fragmentos [4].
Uma cópia particular do OpenGL pode não dispor de buffers de
profundidade, stencil, acumulação ou buffers auxiliares. Apenas alguns
subconjuntos do buffer frontal esquerdo e direito e do buffer traseiro esquerdo
e direito é que podem estar presentes. Diferentes tipos de buffers podem estar
disponíveis (dependendo cada um do número de bits) dependendo da plataforma e do
sistema de janelas onde o OpenGL está a executar. Todos os sistemas de janelas
têm, no entanto, de fornecer pelo menos uma janela com um buffer frontal
(esquerdo) de cor, de profundidade, stencil e de acumulação. Isto garante um
mínimo para a configuração que um programador pode assumir que está presente
independentemente onde o programa OpenGL se encontra.
Operações por fragmento
Antes ser colocado na localização correspondente do framebuffer,
um fragmento é sujeito a vários testes e modificações, as quais podem ser
individualmente activados, desactivados ou controlados. Os testes e modificações
incluem o teste de stencil, o teste de profundidade do buffer (é tipicamente
utilizado para remover superfícies escondidas), e misturas.
O teste de stencil, quando activado, compara o valor no stencil
buffer correspondente ao fragmento com o valor de referência. Se a comparação é
bem sucedida, então o valor de stencil guardado pode ser alterado por uma função
que incrementa, decrementa ou limpa, avançando o fragmento para o teste
seguinte. Se o teste falhar, o valor guardado é actualizado por uma função
diferente e o fragmento é descartado. De forma similar, o teste de profundidade
do buffer compara o valor da profundidade do fragmento com o correspondente
valor guardado no buffer de profundidade. Se a comparação tiver sucesso, o
fragmento, é passado para a fase seguinte sendo os valores do buffer de
profundidade substituídos pelo valor guardado no buffer de profundidade (se este
buffer estiver activo para escritas). Caso a comparação falhe, o fragmento é
descartado, não havendo alterações no buffer de profundidade.
A mistura (blending) une a cor de um fragmento com a cor
correspondente já guardada no framebuffer (a mistura ocorre uma vez sempre que
um buffer de cor é activado para escrita). A função de mistura pode ser
especificada por glBlendFunction.
Misturar é uma operação que permite alcançar o antialiasing para cores RGBA. Se
lembrarmos que a computação de cobertura apenas altera o valor alfa do
fragmento, vemos que este valor deve ser usado para misturar a cor do fragmento
com a cor de fundo já guardada, de forma a obter o efeito de antialiasing. Este
efeito é também usado para alcançar transparências