3.2 Vetorial x matricial
Em computação gráfica, é comum trabalharmos com dois tipos de representações de gráficos: a representação vetorial, utilizada na descrição de formas 2D e 3D compostas por primitivas geométricas, e a representação matricial, utilizada em imagens digitais e definição de texturas.
O processo de converter representações vetoriais em representações matriciais desempenha um papel central no pipeline de processamento gráfico, uma vez que a representação matricial é a representação final de uma imagem nos dispositivos de exibição. Essa conversão matricial, também chamada de rasterização (raster conversion ou scan conversion), é implementada em hardware nas GPUs atuais.
A figura 3.17 ilustra o resultado da conversão de uma representação vetorial em representação matricial. As formas geométricas à esquerda estão representadas originalmente no formato SVG (Scalable Vector Graphics), que é o formato padrão de gráficos vetoriais nos navegadores Web. A imagem à direita é um arranjo bidimensional de valores de cor, resultado da renderização das formas SVG em uma imagem digital (neste caso, uma imagem de baixa resolução).
A figura 3.17 é apenas ilustrativa. Rigorosamente falando, a imagem da esquerda também está no formato matricial. O navegador converte automaticamente o código SVG em comandos da API gráfica que fazem com que a GPU renderize a imagem que vemos na tela. A rasterização ocorre durante este processamento. A imagem à direita não precisa passar pelo processo de renderização pois já é uma imagem digital em seu formato nativo.
Representação vetorial
Na representação vetorial, os gráficos são descritos em termos de primitivas geométricas. Por exemplo, o formato SVG é um formato de descrição de gráficos vetoriais 2D através de sequências de comandos de desenho. Uma forma 2D pode ser descrita através da definição de um “caminho” (path) composto por uma sequência de passos de movimentação de uma caneta virtual sobre um plano. Os principais passos utilizados são comandos do tipo MoveTo, LineTo e ClosePath:
- MoveTo (denotado por
M
oum
em SVG11) move a caneta virtual para uma nova posição na área de desenho, como se ela fosse levantada da superfície e posicionada em outro local; - LineTo (
L
oul
) traça um segmento de reta da posição atual da caneta até uma nova posição, que passa a ser a nova posição da caneta; - Em uma sequência de comandos LineTo, o comando ClosePath (
Z
ouz
) traça um segmento de reta que fecha o caminho da posição atual da caneta ao ponto inicial.
Observe o código SVG a seguir que resulta no desenho do triângulo visto mais abaixo:
<svg width="250" height="210">
<path d="M125 0 L0 200 L250 200 Z" stroke="black" fill="lightgray" />
</svg>
No rótulo <svg>
, os atributos width="250"
e height="210"
definem que a área de desenho tem largura 250 e altura 210. Por padrão, a origem fica no canto superior esquerdo. O eixo horizontal (\(x\)) é positivo para a direita, e o eixo vertical (\(y\)) é positivo para baixo.
O atributo d
do rótulo <path>
contém os comandos de desenho do caminho. M125 0
move a caneta virtual para a posição (125,0). Em seguida, L0 200
traça um segmento da posição atual até a posição (0, 200), que passa a ser a nova posição da caneta. L250 200
traça um novo segmento até (250, 200). O comando Z
fecha o caminho até a posição inicial em (125, 0), completando o triângulo. O atributo stroke="black"
define a cor do traço como preto, e fill="lightgray"
define a cor de preenchimento como cinza claro:
O formato SVG também suporta a descrição de curvas, arcos, retângulos, círculos, elipses, entre outras primitivas geométricas. Comandos similares são suportados em outros formatos de gráficos vetoriais, como o EPS (Encapsulated PostScript), PDF (Portable Document Format), AI (Adobe Illustrator Artwork) e DXF (AutoCAD Drawing Exchange Format).
Representação vetorial no OpenGL
No OpenGL, a representação vetorial é utilizada para definir a geometria que será processada durante a renderização. Todas as primitivas geométricas são definidas a partir de vértices que representam posições em um espaço, e atributos adicionais definidos pelo programador (por exemplo, a cor do vértice). Esses vértices são armazenados em arranjos ordenados que são processados em um fluxo de vértices no pipeline de renderização especificado pelo OpenGL.
Os vértices podem ser utilizados para formar diferentes primitivas. Por exemplo, o uso do identificador GL_TRIANGLES
na função de renderização glDrawArrays
faz com que seja formado um triângulo a cada grupo de três vértices do arranjo de vértices. Assim, se o arranjo tiver seis vértices (em uma sequência de 0 a 5), serão formados dois triângulos: um triângulo com os vértices 0, 1, 2, e outro com os vértices 3, 4, 5. Para o mesmo arranjo de vértices, GL_POINTS
faz com que o pipeline de renderização interprete cada vértice como um ponto separado, e GL_LINE_STRIP
faz com que o pipeline de renderização forme uma sequência de segmentos (uma polilinha) conectando os vértices. A figura 3.18 ilustra a formação dessas primitivas para um arranjo de seis vértices no plano. A numeração indica a ordem dos vértices no arranjo.
A figura 3.19 mostra como a geometria das primitivas pode mudar (com exceção de GL_POINTS
) caso os vértices estejam em uma ordem diferente no arranjo.
Veremos com mais detalhes o uso de primitivas no próximo capítulo quando abordaremos as diferentes etapas de processamento do pipeline de renderização do OpenGL.
Até a década de 2010, a maneira mais comum de renderizar primitivas no OpenGL era através de comandos do modo imediato de renderização, como a seguir (em C/C++):
(0.83f, 0.83f, 0.83f); // Light gray color
glColor3f
(GL_TRIANGLES);
glBegin(-1, -1);
glVertex2i( 1, -1);
glVertex2i( 0, 1);
glVertex2i(); glEnd
Nesse código, a função glColor3f
informa que a cor dos vértices que estão prestes a ser definidos será um cinza claro, como no triângulo desenhado com SVG. O sufixo 3f
de glColor3f
indica que os argumentos são três valores do tipo float
.
Entre as funções glBegin
e glEnd
é definida a sequência de vértices. Cada chamada a glVertex2i
define as coordenadas 2D de um vértice (o sufixo 2i
indica que as coordenadas são compostas por dois números inteiros). Como há três vértices e a primitiva é identificada com GL_TRIANGLES
, será desenhado um triângulo cinza similar ao triângulo desenhado com SVG, porém sem o contorno preto12.
O sistema de coordenadas nativo do OpenGL não é o mesmo da área de desenho do formato SVG. No OpenGL, a origem é o centro da janela de visualização, sendo que o eixo \(x\) é positivo à direita e o eixo \(y\) é positivo para cima. Além disso, para que a primitiva possa ser vista, as coordenadas dos vértices precisam estar entre -1 e 1 (em ponto flutuante).
Para desenhar o triângulo colorido do exemplo “Hello, World!”, como visto na seção 1.5, poderíamos utilizar o seguinte código:
(GL_TRIANGLES);
glBegin(1.0f, 0.0f, 0.0f); // Red
glColor3f(0.0f, 0.5f);
glVertex2f(1.0f, 0.0f, 1.0f); // Magenta
glColor3f(0.5f, -0.5f);
glVertex2f(0.0f, 0.0f, 1.0f); // Green
glColor3f(-0.5f, -0.5f);
glVertex2f(); glEnd
Observe que, antes da definição de cada vértice, é definida a sua cor. Quando o triângulo é processado na GPU, as cores em cada vértice são interpoladas bilinearmente (em \(x\) e em \(y\)) ao longo da superfície do triângulo, formando um gradiente de cores.
Em nossos programas usando a ABCg, bastaria colocar esse código na função membro onPaint
de nossa classe derivada de abcg::OpenGLWindow
. Internamente o OpenGL utilizaria um pipeline de renderização de função fixa (pipeline não programável) para desenhar o triângulo. No entanto, se compararmos com o código atual do projeto no subdiretório abcg\examples\helloworld
, perceberemos que não há nenhum comando glBegin
, glVertex*
ou glColor*
. Isso acontece porque o código acima é obsoleto. As funções do modo imediato foram retiradas do OpenGL na versão 3.1 (de 2009). Ainda é possível habilitar um “perfil de compatibilidade” (compatibility profile) para usar funções obsoletas do OpenGL, mas esse perfil não é recomendado para código atual. Por isso, não o utilizaremos neste curso.
Atualmente, para desenhar primitivas com o OpenGL, o arranjo ordenado de vértices precisa ser enviada previamente à GPU juntamente com programas chamados shaders que definem como os vértices serão processados e como os pixels serão preenchidos após a rasterização.
Desenhar um simples triângulo preenchido no OpenGL não é tão simples como antigamente, mas essa dificuldade é compensada pela maior eficiência e flexibilidade obtida com a possibilidade de programar o comportamento da GPU.
Representação matricial
Na representação matricial, também chamada de representação raster, as imagens são compostas por arranjos bidimensionais de elementos discretos e finitos chamados de pixels (picture elements). Um pixel contém uma informação de amostra de cor e corresponde ao menor elemento que compõe a imagem. A resolução da imagem é o número de linhas e colunas do arranjo bidimensional. Esse é o formato utilizado nos arquivos GIF (Graphics Interchange Format), TIFF (Tag Image File Format), PNG (Portable Graphics Format), JPEG e BMP. A figura 3.20 mostra uma imagem digital e um detalhe ampliado.
Embora os pixels ampliados da figura 3.20 sejam mostrados como pequenos quadrados coloridos, um pixel não tem necessariamente o formato de um quadrado. Um pixel é apenas uma amostra de cor e pode ser exibido em diferentes formatos de acordo com o dispositivo de exibição.
Uma imagem digital pode ser armazenada como um mapa de bits (bitmap). A quantidade de cores que podem ser representadas em um pixel – a profundidade da cor (color depth) – depende do número de bits designados a cada pixel. Em uma imagem binária, cada pixel é representado por apenas 1 bit. Desse modo, a imagem só pode ter duas cores, como preto (para os bits com estado 0) e branco (para os bits com estado 1). A figura 3.21 mostra uma imagem binária em formato BMP, que é um formato simples utilizado para armazenar mapas de bits.
A imagem da figura 3.21 foi gerada a partir de outra imagem de maior profundidade de cor (figura 3.25) usando o algoritmo Floyd-Steinberg de dithering (Floyd and Steinberg 1976). Dithering é o processo de introduzir um ruído ou padrão de pontilhado que atenua a percepção de artefatos no formato de bandas resultantes da quantização da cor (color banding). A figura 3.22 mostra esse efeito em uma imagem colorida. A imagem da esquerda é a imagem original, com 24 bits de profundidade de cor. A imagem do centro teve a profundidade de cor reduzida para 4 bits (16 cores). É possível perceber as bandas de cor no gradiente do céu. Na imagem da direita, a profundidade de cor também foi reduzida para 4 bits, mas o uso de dithering reduz a percepção das variações bruscas de tom.
Em imagens com profundidade de cor de 8 bits, cada pixel pode assumir um valor de 0 a 255. Esse valor pode ser interpretado como um nível de luminosidade para, por exemplo, descrever imagens monocromáticas de 256 tons de cinza (figura 3.23).
Uma outra possibilidade é fazer com que cada valor corresponda a um índice de uma paleta de cores que determina qual será a cor do pixel. Em imagens de 8 bits, a paleta de cores é uma tabela de 256 cores, sendo que cada cor é definida por 3 bytes, um para cada componente de cor RGB (vermelho, verde, azul). Esse formato de cor indexada foi o formato predominante em computadores pessoais na década de 1990, quando o hardware gráfico só conseguia exibir um máximo de 256 cores simultâneas no modo VGA (Video Graphics Array). O formato GIF, criado em 1987, utiliza cores indexadas. A figura 3.24 exibe uma imagem GIF e sua paleta correspondente de 256 cores.
Atualmente, as imagens digitais coloridas usam o formato true color no qual cada pixel tem 24 bits (3 bytes, um para cada componente de cor RGB), sem o uso de paleta de cor (figura 3.25). Isso possibilita a exibição de \(2^{24}\) cores simultâneas (aproximadamente 16 milhões).
Em arquivos de imagens, também é comum o uso de 32 bits por pixel (4 bytes), sendo 3 bytes para as componentes de cor e 1 byte para definir o nível de opacidade do pixel. Isso permite realizar composição de imagens sobrepostas, por exemplo, misturando cores de uma imagem A sobre uma imagem B, usando o valor de opacidade como peso da combinação.
Geralmente, os valores de intensidade de cor de um pixel são representados por números inteiros. Entretanto, imagens podem ser especificadas em um formato HDR (high dynamic range) no qual cada componente de cor pode ter até 32 bits em formato de ponto flutuante, permitindo alcançar uma faixa mais ampla de intensidades.
As GPUs atuais fornecem suporte a um variado conjunto de formatos de bits, incluindo suporte a mapas de bits compactados e tipos de dados em formato de ponto flutuante de 16 e 32 bits.
Referências
Na especificação de um caminho em SVG, letras maiúsculas correspondem a comandos dados em coordenadas absolutas, enquanto que letras minúsculas correspondem a comandos dados em coordenadas relativas à posição atual da caneta.↩︎
Para desenhar o contorno preto poderíamos duplicar o código, mudando
glColor3f
para(0.0f, 0.0f, 0.0f)
(cor preta) e chamandoglBegin
comGL_LINE_LOOP
.↩︎