3.1 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.3 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).

Rasterização de um círculo e triângulo.

Figura 3.3: Rasterização de um círculo e triângulo.

Observação

A figura 3.3 é 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 ou m em SVG8) 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 ou l) 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 ou z) 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="none" />
</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 no espaço, além de atributos 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 (numa 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.4 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.

Formando diferentes primitivas do OpenGL com um mesmo arranjo de vértices.

Figura 3.4: Formando diferentes primitivas do OpenGL com um mesmo arranjo de vértices.

A figura 3.5 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.

A ordem dos vértices no arranjo altera a geometria das primitivas.

Figura 3.5: A ordem dos vértices no arranjo altera a geometria das primitivas.

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.

Observação

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++):

glColor3f(0.83f, 0.83f, 0.83f); // Light gray color

glBegin(GL_TRIANGLES);
  glVertex2i(-1, -1);
  glVertex2i( 1, -1);
  glVertex2i( 0,  1);
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 preto9.

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:

glBegin(GL_TRIANGLES);
  glColor3f(1.0f, 0.0f, 0.0f); // Red
  glVertex2f(0.0f, 0.5f);
  glColor3f(1.0f, 0.0f, 1.0f); // Magenta
  glVertex2f(0.5f, -0.5f);
  glColor3f(0.0f, 0.0f, 1.0f); // Green
  glVertex2f(-0.5f, -0.5f);
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 paintGL de nossa classe derivada de abcg::OpenGLWindow. Internamente o OpenGL utilizaria o 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 já não é suportado em vários drivers e plataformas. 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.6 mostra uma imagem digital e um detalhe ampliado.

Imagem digital de 300x394 pixels e detalhe ampliado de 38x38 pixels.

Figura 3.6: Imagem digital de 300x394 pixels e detalhe ampliado de 38x38 pixels.

Observação

Embora os pixels ampliados da figura 3.6 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.7 mostra uma imagem binária em formato BMP, que é um formato simples e muito utilizado para armazenar mapas de bits.

Imagem binária.

Figura 3.7: Imagem binária.

A imagem da figura 3.7 foi gerada a partir de outra de maior profundidade de cor (figura 3.11) 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 bandas de cor (color banding) resultantes da quantização da cor. A figura 3.8 mostra esse efeito. 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.

Redução de bandas de cor com dithering. Esquerda: imagem original de 24 bits/pixel. Centro: redução para 4 bits/pixel. Direita: redução para 4 bits/pixel usando dithering.

Figura 3.8: Redução de bandas de cor com dithering. Esquerda: imagem original de 24 bits/pixel. Centro: redução para 4 bits/pixel. Direita: redução para 4 bits/pixel usando dithering.

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.9).

Imagem monocromática de 8 bits por pixel.

Figura 3.9: Imagem monocromática de 8 bits por pixel.

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 os controladores gráficos só conseguiam 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.10 exibe uma imagem GIF e sua paleta correspondente de 256 cores.

Imagem de 8 bits com cores indexadas (esquerda) e paleta utilizada (direita).

Figura 3.10: Imagem de 8 bits com cores indexadas (esquerda) e paleta utilizada (direita).

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.11). Isso possibilita a exibição de \(2^{24}\) (16.777.216) cores simultâneas.

Imagem de 24 bits por pixel.

Figura 3.11: Imagem de 24 bits por pixel.

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.

Geralmente, os valores de intensidade de cor de um pixel são representados por números inteiros. Entretanto, em sistemas gráficos que usam imagens HDR (high dynamic range), cada componente de cor pode ter até 32 bits em formato de ponto flutuante, permitindo alcançar uma faixa muito superior 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

Floyd, Robert W., and Louis Steinberg. 1976. An Adaptive Algorithm for Spatial Greyscale.” Proceedings of the Society of Information Display 17 (2): 75–77.

  1. 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.↩︎

  2. Para desenhar o contorno preto poderíamos duplicar o código, mudando glColor3f para (0.0f, 0.0f, 0.0f) (cor preta) e chamando glBegin com GL_LINE_LOOP.↩︎