3. Sistemas Gráficos

Nessa aula vamos apresentar alguns recursos típicos encontrados em sistemas gráficos usados para gerar imagens por computador.

3.1. Elementos de um sistema gráfico

Um sistema gráfico computacional possui os elementos típicos de um computador de mesa ou portátil comum, como ilustrado na Figura Fig. 3.1, ou seja, possui dispositivos de entrada (como teclado, mouse e joystick) e saída (como monitor, impressora e plotter), um processador central (CPU), um processador gráfico (GPU) e memória.

Elementos de um sistema gráfico.

Fig. 3.1 Elementos de um sistema gráfico.

3.2. Dispositivos de entrada e saída

Os dispositivos de entrada, como teclado e mouse, são necessários para que o usuário possa interagir com computadores usando interfaces gráficas do tipo WIMP (Windows, Icons, Menus, and Pointg), ou seja, interfaces formadas por janelas, ícones, menus e um cursor para apontamento dos elementos gráficos. Outros dispositivos, como mesas digitalizadoras e controles para jogos, são comuns em muitos aplicativos gráficos interativos, para criação de desenhos e edição de imagens.

A saída gráfica pode ser feita por meio de impressoras e plotters, mas o dispositivo mais comum são os monitores de vídeo por permitirem interação em tempo real e a visualização de animações. Os monitores mais utilizados hoje são do tipo raster, ou seja, que representam informações gráficas por meio de uma matriz de pixels. Cada monitor pode utilizar uma tecnologia distinta na sua construção e funcionamento. Os tipos mais comuns de monitores são:

  • Tubo de raios catódicos: consistem em uma tela com revestimento de fósforo, que permite que cada pixel seja iluminado momentaneamente ao ser atingido por um feixe de elétrons. Um pixel está iluminado (branco) ou não (preto). O nível de intensidade pode ser variado para atingir valores de cinza arbitrários. Como o fósforo mantém sua cor apenas brevemente, a imagem é repetidamente digitalizada, a uma taxa de 25 a 30 vezes por segundo. Essa tecnologia é mais antiga e não é mais utilizada devido ao seu grande peso, volume e alto consumo de energia quando comparados aos monitores LCD e LED.
  • Telas de cristal líquido (LCD’s): usa um campo elétrico para alterar a polarização das moléculas cristalinas em cada pixel. Considere que a luz que atravessa um pixel já está polarizada em alguma direção. Alterando a polarização do pixel, é possível variar a quantidade de luz que atravessa o cristal, controlando assim sua intensidade.
  • Diodos emissores de luz (LED ou OLED): cada pixel é iluminado por um diodo emissor de luz (ou conjunto tricromático RGB correspondente). Como esses monitores não precisam de iluminação de fundo como o LCD, eles tendem a ser mais eficientes, simples de fabricar e mais finos (tanto que podem ser colocados sobre membranas flexíveis e até dobráveis).

A resolução mais comum para monitores atualmente é chamada de “full HD”, que corresponde a 1920\(\times\)1080 pixels. Resoluções cada vez maiores estão se tornando comuns também, como monitores 4K (ou “ultra HD”) por exemplo, que possuem 4 vezes mais pixels que um monitor full HD, ou seja, 3840\(\times\)2160 pixels.

3.3. Framebuffer

Em sistemas mais simples, o framebuffer armazena a imagem a ser exibida no monitor. O Controlador de Vídeo é um módulo responsável para ler a imagem do framebuffer e gerar o sinal de vídeo para o monitor.

Para exibir uma imagem RGB full HD é necessário reservar um framebuffer com cerca de 6 MB (mega bytes). Essa quantidade de memória era indisponível em computadores mais antigos e por isso várias alternativas foram desenvolvidas para contornar essa limitação.

Uma forma para economizar memória em sistemas gráficos é reduzir a profundidade das imagens utilizadas, por exemplo, utilizando uma tabela ou palete de cores para imagens coloridas. Imagine, por exemplo, que nossas imagens possam ser representadas por um conjunto de 256 cores de 24 bits cada cor. Essas cores podem ser codificadas em uma tabela com 256 entradas, onde cada entrada corresponde a uma cor de 24 bits. Esses 256 códigos correspondem aos índices das linhas da tabela que precisam de 8 bits (1 byte) para serem codificadas. Esse código de 1 byte pode ser colocado em cada pixel, ao invés dos 3 bytes da cor, economizando assim 2/3 (dois terços) da memória necessária para armazenar a imagem original com 24 bits por pixel. Nesse caso, a imagem full HD pode ser agora representada com cerca de 2 MB, além da memória necessária para armazenar a tabela com os códigos das cores.

Uma alternativa capaz de reduzir ainda mais o consumo de memória é o uso de imagens vetoriais ao invés de imagens raster. Imagens vetoriais são apropriadas para representar desenhos formados por segmentos de linha (ou simplesmente linhas), onde um segmento é representado por dois pontos na tela do monitor. Assim, um desenho formado por centenas ou até mesmo milhares de linhas pode ser representado de forma bem mais compacta (em termos de memória) que uma imagem raster. Além de serem facilmente escaláveis, esses desenhos também podem ser exibidos de forma relativamente simples em monitores analógicos que utilizam tubo de raios catódicos, fazendo o feixe de elétrons do tubo varrer a lista de segmentos de forma contínua.

Observe que, para desenhar uma linha em monitores raster a partir de 2 pontos, é necessário calcular a posição de cada pixel interior ao segmento. Algoritmos de rasterização servem para calcular os pixels internos entre os pontos, preenchendo a imagem raster.

Em sistemas mais complexos, o framebuffer pode armazenar outras informações também, como profundidade ou opacidade na representação RGBA. Além disso, com mais memória disponível, não é mais necessário limitar cada cor a 256 níveis por exemplo. Framebuffers modernos permitem representar cada pixel usando 12 ou mais bits por cor, permitindo a renderização de imagens com larga amplitude dinâmica (HDR ou high dynamic range).

3.4. Processadores e Memória

Em sistemas mais simples, o processador gráfico ou GPU (Graphics Processing Unit) fica integrado ao processador central ou CPU (Central Processing Unit). Em sistemas mais complexos o processamento gráfico fica em um circuito ou placa dedicada, com memória própria. Tipicamente o framebuffer faz parte da memória dedicada ao processamento gráfico e por isso faz parte da memória utilizada pela GPU ou, em sistemas sem GPU, como parte da memória do sistema.

Embora a maioria dos monitores trabalhem com frequências de 60 Hz ou mais, a taxa de geração de imagens em animações costuma ser bem inferior. No entanto, para manter uma sensação de continuidade, uma animação precisa manter uma taxa de aproximadamente 15 novos quadros por segundo (fps – frames per second). Abaixo de 10 fps, a sensação de descontinuidade da animação começa a ser notada pela maioria das pessoas. Para garantir uma boa qualidade, os projetores antigos nos cinemas exibiam filmes a 24 fps e na TV a 30 fps.

Chamamos de renderização o processo de geração dos pixels (posição e cor) no framebuffer a partir de um modelo de cena (representação geométrica dos objetos). Grande parte desse processo ocorre atualmente dentro da GPU que, para obter um alto desempenho, utilizam uma arquitetura paralela conhecida como pipeline gráfico. A Figura Fig. 3.2 ilustra os principais módulos do pipeline gráfico usado pelo OpenGL.

Estágios do pipeline gráfico.

Fig. 3.2 Estágios do pipeline gráfico do OpenGL.

Para executar um comando simples como “desenhe um triângulo em 3D”, o pipeline primeiramente recebe o conjunto de vértices que definem o triângulo. O processador de vértices usa os parâmetros da câmera virtual para transformar as coordenadas dos vértices para um sistema centrado na câmera. As transformações são representadas por matrizes e a mudança de coordenadas envolve a multiplicação dessas matrizes.

Após a transformação de coordenadas, alguns vértices podem ficar fora do campo visual da câmera. O módulo de clipping (recorte) remove esses vértices e, caso alguma parte dos segmentos ainda sejam visíveis, esses segmentos são redefinidos na forma de novos vértices.

Os segmentos visíveis são passados para o módulo de rasterização, que usa os vértices visíveis para pintar os pixels da parte visível do triângulo, que são chamados de fragmentos. Observe que a cena pode ser composta por outros objetos que resultam em mais fragmentos. Esses fragmentos são combinados pelo processador de fragmentos para calcular a cor final de cada pixel do framebuffer.

3.5. Pipelines programáveis

As placas gráficas mais antigas possuíam um pipeline rígido com poucos recursos. As placas mais recentes permitem programar o processador de vértices e o processador de fragmentos. Com isso podemos implementar vários efeitos em tempo real que não eram possíveis com as placas mais antigas (como bump mapping). Chamamos de shaders esses programas que são executados pela GPU.

  • Vertex shaders: executado pelo processador de vértices e usado para aplicar transformações nos vértices e nas suas propriedades;
  • Fragment shaders: Esse shader é usado para processar fragmentos, permitindo aplicar texturas e outros efeitos de iluminação e computações específicas para cada tipo de fragmento.

Nas placas gráficas mais antigas era também comum avaliar o desempenho das placas pelo número de primitivas geométricas processadas por segundo (front end) ou então pela taxa de bits que podiam ser movidos para o framebuffer (back end). Hoje as GPUs mais poderosas podem usar ponto flutuante em todo o pipeline, até no framebuffer, um recurso útil para representar e manipular imagens HDR (high dynamic range). Essas GPUs são chamadas de unified shading engines pois fazem o processamento dos vértices e fragmentos concorrentemente. Hoje, esse grande poder computacional é aproveitado por muitas aplicações não relacionadas à geração de imagens.

3.6. Bibliotecas gráficas

Mesmo sendo muito eficiente, programar o pipeline para criar 30 imagens por segundo necessárias para gerar boas animações ainda é um desafio.

Dizemos que cada redesenho faz parte de um ciclo de atualização (refresh cycle) pois o programa deve atualizar o conteúdo da imagem. O programa se utiliza de uma biblioteca ou API (application programming interface) para se comunicar com o sistema gráfico. Existem várias APIs diferentes usadas em sistemas gráficos modernos, cada uma fornecendo alguns recursos distintos em relação às outras. De um modo geral, as APIs gráficas podem ser classificadas em duas classes gerais:

  • Modo Retido (retained mode): A biblioteca mantém o estado da computação em suas próprias estruturas de dados internas. A cada ciclo de atualização, esses dados são transmitidos à GPU para renderização.
    • Por conhecer o estado completo da cena, a biblioteca pode realizar otimizações globais automaticamente.
    • Este método é menos adequado para conjuntos de dados que variam no tempo, uma vez que a representação interna dos dados precisa ser atualizada com frequência.
    • Isso é funcionalmente análogo à compilação de um programa.
    • Exemplos: Java3d, Ogre, Open Scenegraph.
  • Modo Imediato (immediate mode): O aplicativo fornece todas as primitivas a cada ciclo de exibição. Em outras palavras, seu programa transmite comandos diretamente para a GPU para serem executados.
    • A biblioteca só pode realizar otimizações locais, pois não conhece o estado global. É responsabilidade do programa do usuário realizar otimizações globais.
    • Isso é adequado para cenas altamente dinâmicas.
    • Isso é funcionalmente análogo à interpretação do programa.
    • Exemplos: OpenGL, DirectX.

3.7. OpenGL

O OpenGL se tornou uma API padrão largamente utilizada. Ela está disponível em praticamente todas as plataformas e pode ser acessada pelas linguagem de programação mais populares como C/C++, Java e Python. Para que possam funcionar em plataformas tão diferentes, ela precisa ser genérica, em contraste por exemplo com o DirectX, que foi desenvolvido para funcionar principalmente em sistemas da Microsoft.

Para facilitar essa generalidade e tornar o OpenGL independente do sistema operacional e do sistema que controla as janelas da interface, o OpenGL não fornece recursos de entrada e saída para interação com usuárias(os).

O OpenGL trabalha, em sua maior parte, no modo imediato. Isso significa que cada chamada de função resulta no envio de um comando diretamente para a GPU. No entanto, há alguns elementos retidos como, por exemplo, transformações, iluminação e texturização que precisam ser configurados previamente para que possam ser aplicadas no cálculo. Por exemplo, o OpenGL não oferece recursos para criar uma janela, redimensionar uma janela, determinar as coordenadas atuais do mouse ou detectar se uma tecla do teclado foi pressionada. Para atingir esses outros objetivos, é necessário usar um kit de ferramentas adicional. Nesse curso vamos adotar a versão WebGL, que permite criar aplicações gráficas que fazem uso do OpenGL a partir de qualquer navegador moderno.

3.8. Onde estamos e para onde vamos

Nessa aula continuamos a cobrir conceitos da computação gráfica, muitos deles associados ao desenvolvimento histórico dos sistemas gráficos. Com a evolução desses sistemas, temos muito mais recursos computacionais a disposição (tanto de hardwares quanto de software) e GPUs muito poderosas mas otimizadas para tratar de elementos geométricos básicos.

O OpenGL é uma API gráfica genérica que permite o desenvolvimento de aplicativos gráficos para praticamente qualquer plataforma. A versão WebGL permite usar o OpenGL dentro de um navegador. Do ponto de vista pedagógico, isso facilita bastante o ensino pois reduz a necessidade de instalação de ferramentas de desenvolvimento e estimula a prática pois os exercícios que você vai desenvolver podem ser executados em qualquer navegador.

Na próxima aula vamos começar a introduzir alguns elementos básicos de HTML e JavaScript para desenvolver nossas primeiras aplicações. Logo em seguida vamos introduzir o WebGL, usando esse ambiente Web de programação com HTML+JavaScript.

3.9. Exercícios

  1. Considere um segmento de linha definido por dois pontos no espaço 2D e uma janela de exibição retangular. Mostre que basta redefinir um ou dois desses pontos para recortar a parte visível do segmento, ou seja, a parte do segmento interna à janela de exibição.
  2. Filmes de cinema eram tipicamente filmados em películas de 35 mm com uma resolução de aproximadamente \(3000 \times 2000\) pixels. Que implicação isso resulta quando esses filmes eram exibidos em TVs antigas? E nas modernas?
  3. Em um vídeo full HD, cada imagem possui resolução \(1920\times1080\) pixels, com profundidade de 24 bits. Se o vídeo exibe 30 imagens por segundo, qual a frequência necessária para transmitir esse vídeo de forma serial?

3.10. Para saber mais

Recomendamos a seguinte leitura para complementar essas notas: