Aula 01 — Introdução ao OpenGL com Python
Objetivos
Ao final desta aula você deve ser capaz de:
- Descrever o papel da computação gráfica e as etapas do pipeline gráfico que transforma vértices em pixels.
- Explicar a jornada de um vértice pelos espaços de coordenadas (objeto → mundo → olho → recorte → NDC → tela) e o papel da coordenada homogênea
w. - Nomear e escolher a primitiva gráfica adequada do modo imediato (
GL_POINTS,GL_LINES,GL_TRIANGLES,GL_QUADSetc.) para cada situação. - Explicar o que é um contexto OpenGL e como criá-lo dentro de um
QOpenGLWidget(PySide6). - Identificar os três métodos do ciclo de vida de um widget OpenGL:
initializeGL,resizeGLepaintGL. - Diferenciar a projeção ortográfica (
gluOrtho2D) da projeção em perspectiva (gluPerspective), incluindo o papel de cada parâmetro do frustum. - Justificar a necessidade do teste de profundidade (depth buffer) e das normais para a iluminação em cenas 3D.
- Executar, ler linha a linha e modificar dois programas mínimos — um 2D e um 3D — no pipeline fixed-function.
Conteúdo
O que é Computação Gráfica?
Computação gráfica é a área que estuda a síntese de imagens a partir de descrições matemáticas de uma cena — geometria (pontos, retas, polígonos), cores, câmeras e luzes. O objetivo é responder à pergunta: dada uma cena descrita por números, que cor deve ter cada pixel da tela?
OpenGL (Open Graphics Library) é uma API padronizada para desenhar
gráficos 2D e 3D acelerados por hardware (GPU). Ela não é uma biblioteca de
janelas — apenas desenha dentro de um contexto que alguém precisa criar. Por
isso a combinamos com um toolkit de interface: aqui usamos Qt através do
PySide6, cujo QOpenGLWidget fornece o contexto pronto.
Vamos usar o pipeline fixed-function do OpenGL (versão 2.1, perfil de
compatibilidade): comandos como glBegin/glEnd, glVertex, glColor e a
iluminação embutida (GL_LIGHTING). É didático — cada passo é explícito e
fácil de ler. O OpenGL moderno (3.3+) substitui tudo isso por shaders escritos
em GLSL, que veremos mais adiante. Começar pelo fixed-function torna os
conceitos (projeção, câmera, iluminação) visíveis antes de introduzir a
programação da GPU.
O Pipeline Gráfico
Desenhar uma cena é uma sequência de transformações — o pipeline gráfico. De forma simplificada:
Dois estágios de matrizes importam nesta aula:
| Matriz | Comando p/ selecionar | Papel |
|---|---|---|
GL_MODELVIEW | glMatrixMode(GL_MODELVIEW) | Posiciona os objetos e a câmera no mundo (transformações e gluLookAt) |
GL_PROJECTION | glMatrixMode(GL_PROJECTION) | Define como o volume 3D é projetado no plano 2D (orto ou perspectiva) |
O viewport (glViewport) mapeia o resultado projetado para a área da janela,
em pixels. A seguir, detalhamos essa jornada — de onde o vértice "nasce" até o
pixel final.
Da Coordenada do Objeto ao Pixel: Espaços de Coordenadas
Cada vértice que você escreve no código (ex.: glVertex3f(1, 1, 1)) passa por
uma sequência de sistemas de coordenadas até virar um pixel colorido na
tela. Entender essa jornada é a base para tudo que vem depois — transformações,
câmera e projeção.
| Espaço | Também chamado | O que leva até aqui | Papel |
|---|---|---|---|
| Objeto | local / model space | — (é onde o vértice nasce) | Coordenadas relativas ao próprio objeto (ex.: cubo centrado na origem) |
| Mundo | world space | Transformações de modelo (translação, rotação, escala) | Posiciona o objeto na cena, em relação a uma origem comum |
| Olho | eye / view space | gluLookAt (parte da matriz GL_MODELVIEW) | Reescreve a cena do ponto de vista da câmera — a câmera passa a ser a origem |
| Recorte | clip space | Matriz GL_PROJECTION (gluOrtho2D / gluPerspective) | Define o volume visível (a view frustum); o que sai fora é descartado |
| NDC | normalized device coordinates | Divisão por w (perspective divide) | Cubo canônico de -1 a 1 em X, Y e Z, independente de resolução |
| Tela | window / screen space | glViewport | Mapeia o cubo -1..1 para os pixels reais da janela |
No código fixed-function, você não vê a divisão por w acontecer — ela é
feita automaticamente pela GPU, entre a etapa de GL_PROJECTION e o
glViewport. Mas é útil saber que ela existe: é justamente esse mecanismo que
faz objetos distantes parecerem menores na projeção em perspectiva (Exemplo 2).
O OpenGL representa cada vértice internamente como um vetor de 4
componentes (x, y, z, w), não 3. Quando você chama glVertex3f(x, y, z), o
OpenGL completa automaticamente w = 1. Essa quarta coordenada — a
coordenada homogênea — é o que permite representar translação, rotação e
projeção todas como multiplicação de matriz 4×4, de forma uniforme. Sem
ela, translação exigiria um tipo de operação (soma) diferente das demais
(multiplicação), complicando o pipeline. Vamos aprofundar coordenadas
homogêneas e matrizes de transformação nas próximas aulas.
Cores e Primitivas Gráficas: o Modo Imediato
Antes de ler o código dos exemplos, vale conhecer duas peças que aparecem em toda cena fixed-function: como se define uma cor e como se declara o que desenhar a partir de um conjunto de vértices.
Cor. O OpenGL representa cor como três (ou quatro) componentes de ponto
flutuante entre 0.0 e 1.0 — não de 0 a 255 como em imagens.
glColor3f(0.95, 0.30, 0.30) é um vermelho quase puro; glClearColor(0.10, 0.11, 0.13, 1.0) é um cinza-azulado quase preto, com o quarto valor (alfa,
opacidade) em 1.0 — totalmente opaco.
Primitivas. Todo desenho no modo imediato acontece entre um
glBegin(<tipo>) e um glEnd(). O <tipo> diz à GPU como agrupar os
vértices enviados entre essas duas chamadas:
| Primitiva | Como agrupa os vértices | Uso típico |
|---|---|---|
GL_POINTS | Cada vértice é um ponto isolado | Nuvens de pontos, estrelas |
GL_LINES | Pares de vértices viram segmentos independentes | Eixos, wireframes soltos |
GL_LINE_STRIP | Vértices conectados em sequência (linha aberta) | Trajetórias, gráficos |
GL_LINE_LOOP | Como LINE_STRIP, mas fecha o último vértice no primeiro | Contornos poligonais |
GL_TRIANGLES | Trios de vértices viram triângulos independentes | Malhas gerais (usado no Exemplo 1) |
GL_TRIANGLE_STRIP | Cada novo vértice forma um triângulo com os dois anteriores | Malhas contínuas, mais eficiente |
GL_TRIANGLE_FAN | Todos os triângulos compartilham o primeiro vértice | Formas em leque, círculos |
GL_QUADS | Quartetos de vértices viram quadriláteros | Faces planas (usado no Exemplo 2 — cubo) |
GL_POLYGON | Todos os vértices formam um único polígono convexo | Formas com N lados |
GL_QUADS e GL_POLYGON existem apenas no pipeline fixed-function
(removidos no core profile 3.2+, onde tudo vira triângulo). Isso não é um
problema aqui — é justamente por isso que esta aula usa a versão de
compatibilidade (2.1) — mas vale saber que, em OpenGL moderno, um quadrado é
sempre desenhado como dois triângulos.
O Ciclo de Vida de um QOpenGLWidget
Todo widget OpenGL implementa três métodos, chamados pelo Qt nos momentos certos:
| Método | Quando é chamado | Responsabilidade |
|---|---|---|
initializeGL() | uma vez, ao criar o contexto | Estado inicial: cor de fundo, glEnable(...) de recursos |
resizeGL(w, h) | a cada redimensionamento | Definir o glViewport e a matriz de projeção |
paintGL() | a cada quadro (redesenho) | Limpar buffers e desenhar a cena |
Além do widget, um detalhe de configuração acontece antes de criar a
janela: o formato da superfície (QSurfaceFormat) escolhe a versão do OpenGL,
o perfil e o tamanho do buffer de profundidade.
| Parâmetro | Exemplos nesta aula | Papel |
|---|---|---|
setVersion(major, minor) | (2, 1) | Versão do OpenGL solicitada ao driver |
setProfile(...) | OpenGLContextProfile.NoProfile | NoProfile/CompatibilityProfile mantém o fixed-function (glBegin, luzes prontas); CoreProfile (3.2+) exige shaders para tudo |
setDepthBufferSize(n) | 0 (2D) / 24 (3D) | Bits reservados para o depth buffer — 0 significa "não alocar" |
NoProfile pertence ao enum aninhado QSurfaceFormat.OpenGLContextProfile. Em
tempo de execução o PySide6 aceita a forma curta QSurfaceFormat.NoProfile,
mas ela não existe nos type stubs — e IDEs que checam tipos (PyCharm, VS Code
com Pylance) marcam a linha como referência não resolvida. Escrever o caminho
completo silencia o aviso e continua correto. A mesma regra vale para
Qt.Key.Key_Escape e Qt.FocusPolicy.StrongFocus.
Essa configuração precisa ocorrer antes de QApplication e da criação da
janela, pois o contexto gráfico do sistema operacional é criado nesse momento e
não pode ser trocado depois.
Exemplo 1 — Hello World 2D (projeção ortográfica)
O primeiro programa abre uma janela e desenha um triângulo com um vértice de
cada cor, usando projeção ortográfica (gluOrtho2D). Repare que a cena 2D
não habilita o teste de profundidade: a ordem de desenho já resolve a
sobreposição.

helloworld2d.py. O degradê não foi desenhado: são só três cores de vértice, interpoladas pela GPU no interior do triângulo.📥 Baixe o arquivo completo: helloworld2d.py
from __future__ import annotations
import sys
from PySide6.QtGui import QSurfaceFormat
from PySide6.QtWidgets import QApplication
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from OpenGL.GL import *
from OpenGL.GLU import gluOrtho2D
class HelloWorld2D(QOpenGLWidget):
"""Viewport 2D ortográfico com uma cena estática."""
def initializeGL(self) -> None:
# Cor de fundo (RGBA). NÃO habilitamos GL_DEPTH_TEST —
# a cena 2D não precisa; a ordem de desenho define a sobreposição.
glClearColor(0.10, 0.11, 0.13, 1.0)
def resizeGL(self, w: int, h: int) -> None:
# O viewport mapeia a cena para toda a área da janela (em pixels).
glViewport(0, 0, w, max(1, h))
def paintGL(self) -> None:
# Limpa apenas o buffer de cor (sem GL_DEPTH_BUFFER_BIT).
glClear(GL_COLOR_BUFFER_BIT)
# Projeção ortográfica 2D: retângulo do mundo visível
# (left, right, bottom, top) que preenche a janela.
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# Um triângulo com um vértice de cada cor (a cor é interpolada).
glBegin(GL_TRIANGLES)
glColor3f(0.95, 0.30, 0.30); glVertex2f(-0.6, -0.5)
glColor3f(0.30, 0.95, 0.30); glVertex2f( 0.6, -0.5)
glColor3f(0.30, 0.30, 0.95); glVertex2f( 0.0, 0.6)
glEnd()
def main() -> int:
fmt = QSurfaceFormat()
fmt.setVersion(2, 1) # OpenGL 2.1 (fixed-function)
# NoProfile é o perfil de compatibilidade, que mantém o fixed-function.
fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile)
fmt.setDepthBufferSize(0) # 2D não precisa de profundidade
QSurfaceFormat.setDefaultFormat(fmt)
app = QApplication(sys.argv)
window = HelloWorld2D()
window.setWindowTitle("Hello World 2D — gluOrtho2D")
window.resize(640, 640)
window.show()
return app.exec()
if __name__ == "__main__":
raise SystemExit(main())
Passo a passo
1. Configurando o contexto antes de a janela existir (main)
fmt = QSurfaceFormat()
fmt.setVersion(2, 1)
fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile)
fmt.setDepthBufferSize(0)
QSurfaceFormat.setDefaultFormat(fmt)
QSurfaceFormat.setDefaultFormat registra esse formato como padrão para
qualquer QOpenGLWidget criado a partir daqui — por isso é chamado antes
de QApplication(sys.argv). Só então a janela (HelloWorld2D()) e o laço de
eventos (app.exec()) entram em cena.
2. initializeGL — o estado que só existe uma vez
glClearColor(0.10, 0.11, 0.13, 1.0)
Define a cor usada por glClear sempre que o buffer de cor é limpo. É chamado
uma única vez, quando o contexto OpenGL é criado — por isso não faz sentido
colocar aqui algo que dependa do tamanho da janela (isso é papel do
resizeGL).
3. resizeGL — o viewport
glViewport(0, 0, w, max(1, h))
glViewport(x, y, largura, altura) diz à GPU em que retângulo de pixels
desenhar. Os dois primeiros parâmetros são o canto inferior-esquerdo — a
origem de tela do OpenGL fica embaixo, ao contrário de muitas APIs 2D que
começam no topo. max(1, h) evita passar altura 0 (o que pode acontecer
brevemente ao minimizar a janela), prevenindo problemas em contas de proporção
feitas a partir de h — como veremos já no Exemplo 2.
4. paintGL — limpar e desenhar
glClear(GL_COLOR_BUFFER_BIT)
GL_COLOR_BUFFER_BIT é uma máscara de bits: glClear aceita o "ou" bit a
bit (|) de várias constantes para limpar múltiplos buffers de uma vez só
(veremos isso no Exemplo 2, que também limpa profundidade).
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
glMatrixMode(GL_PROJECTION)seleciona qual pilha de matrizes os próximos comandos afetam — o fixed-function mantém pilhas separadas paraGL_PROJECTION,GL_MODELVIEW(eGL_TEXTURE). É como escolher a ferramenta certa antes de usá-la.glLoadIdentity()reseta a matriz do topo da pilha para a matriz identidade — o equivalente a "não transformar nada". Sem isso, transformações de quadros anteriores se acumulariam a cadapaintGL.gluOrtho2D(left, right, bottom, top)multiplica a matriz de projeção pela matriz que mapeia esse retângulo do mundo para o cubo NDC (-1..1). Com(-1, 1, -1, 1), mundo e NDC coincidem — por isso o triângulo, com vértices entre -0.6 e 0.6, aparece perto do centro da janela.
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
Troca para a pilha GL_MODELVIEW (onde entrariam câmera e transformações de
objeto) e a reseta também. Como este exemplo não move nem gira nada, a
identidade basta — a "câmera" implícita observa a origem de frente.
glBegin(GL_TRIANGLES)
glColor3f(0.95, 0.30, 0.30); glVertex2f(-0.6, -0.5)
glColor3f(0.30, 0.95, 0.30); glVertex2f( 0.6, -0.5)
glColor3f(0.30, 0.30, 0.95); glVertex2f( 0.0, 0.6)
glEnd()
Cada par glColor3f + glVertex2f define um vértice completo: a cor
"atual" no momento em que glVertex é chamado fica associada àquele vértice.
Como há exatamente 3 vértices entre glBegin(GL_TRIANGLES) e glEnd(), a GPU
monta um triângulo. Dentro dele, cada pixel recebe uma cor interpolada
a partir das cores dos três vértices, proporcional à distância de cada um —
esse é o Gouraud shading, responsável pelo degradê suave visível na janela.
Resumo rápido:
gluOrtho2Ddefine o sistema de coordenadas do mundo visível.- A cor é definida por vértice e interpolada pela GPU entre eles.
- Sem projeção em perspectiva, não há sensação de profundidade — o tamanho não muda com a distância.
Exemplo 2 — Hello World 3D (perspectiva + luz)
O segundo programa desenha um cubo colorido iluminado. As diferenças em relação ao 2D são exatamente os conceitos novos do 3D:
- Buffer de profundidade (24 bits) +
glEnable(GL_DEPTH_TEST)— resolve a oclusão: faces à frente escondem as de trás. - Projeção em perspectiva (
gluPerspective) — objetos distantes ficam menores. - Iluminação (
GL_LIGHTING+GL_LIGHT0) — cada face precisa de uma normal (glNormal3f), o vetor perpendicular usado no cálculo da luz. - Uma câmera posicionada com
gluLookAt.

helloworld3d.py. Três dos quatro conceitos novos do 3D aparecem de uma vez: oclusão, perspectiva e iluminação.Vale ler a Figura 3 com calma. Apenas três das seis faces aparecem: as
outras estão atrás e foram descartadas pelo teste de profundidade. E as três
visíveis têm brilhos diferentes apesar de usarem cores puras — é a
componente difusa reagindo ao ângulo entre cada normal e a luz. A face
vermelha, quase preta, está de perfil em relação à GL_LIGHT0.
📥 Baixe o arquivo completo: helloworld3d.py
# Os 8 vértices do cubo, de -1 a 1 em cada eixo.
_VERTS = [
(-1, -1, 1), (-1, 1, 1), ( 1, 1, 1), ( 1, -1, 1),
(-1, -1, -1), (-1, 1, -1), ( 1, 1, -1), ( 1, -1, -1),
]
# Cada face: (cor RGB, normal, índices dos 4 vértices anti-horário).
_FACES = [
((1.0, 0.0, 0.0), ( 1.0, 0.0, 0.0), (2, 3, 7, 6)), # +X vermelho
((0.0, 1.0, 0.0), ( 0.0, 1.0, 0.0), (1, 2, 6, 5)), # +Y verde
((0.0, 0.0, 1.0), ( 0.0, 0.0, 1.0), (0, 3, 2, 1)), # +Z azul
((1.0, 0.0, 1.0), ( 0.0, -1.0, 0.0), (3, 0, 4, 7)), # -Y magenta
((1.0, 1.0, 0.0), ( 0.0, 0.0, -1.0), (4, 5, 6, 7)), # -Z amarelo
((0.0, 1.0, 1.0), (-1.0, 0.0, 0.0), (5, 4, 0, 1)), # -X ciano
]
class HelloWorld3D(QOpenGLWidget):
def initializeGL(self) -> None:
glClearColor(0.12, 0.12, 0.14, 1.0)
glEnable(GL_DEPTH_TEST) # oclusão: faces à frente escondem as de trás
glEnable(GL_LIGHTING) # ativa o modelo de iluminação
glEnable(GL_LIGHT0) # com uma fonte de luz (LIGHT0)
glEnable(GL_COLOR_MATERIAL) # glColor* passa a valer como material
def resizeGL(self, w: int, h: int) -> None:
glViewport(0, 0, w, max(1, h))
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
# fov 45°, proporção da janela, planos near=0.1 e far=100.
gluPerspective(45.0, w / float(max(1, h)), 0.1, 100.0)
glMatrixMode(GL_MODELVIEW)
def paintGL(self) -> None:
# Limpa cor E profundidade (o depth buffer também precisa ser limpo).
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(4.0, 3.0, 5.0, # posição do olho
0.0, 0.0, 0.0, # ponto observado
0.0, 1.0, 0.0) # vetor "para cima"
glRotatef(20.0, 1.0, 0.0, 0.0)
glRotatef(30.0, 0.0, 1.0, 0.0)
glBegin(GL_QUADS)
for color, normal, face in _FACES:
glColor3f(*color)
glNormal3f(*normal) # normal usada pela iluminação
for vi in face:
glVertex3f(*_VERTS[vi])
glEnd()
Passo a passo
1. A geometria do cubo: vértices e faces
_VERTS = [
(-1, -1, 1), (-1, 1, 1), ( 1, 1, 1), ( 1, -1, 1),
(-1, -1, -1), (-1, 1, -1), ( 1, 1, -1), ( 1, -1, -1),
]
Um cubo tem 8 vértices; aqui cada um é uma combinação de -1/1 nos três
eixos. _FACES associa a cada uma das 6 faces uma cor, uma normal e os
índices (posições em _VERTS) dos 4 vértices que a formam:
((1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2, 3, 7, 6)), # +X vermelho
Repare que a ordem dos índices não é arbitrária: percorrendo 2 → 3 → 7 → 6 e olhando essa face de fora do cubo, os vértices aparecem em sentido
anti-horário. Essa convenção (counter-clockwise winding) é o que permite
ao OpenGL calcular automaticamente a orientação de uma face — usada por
recursos que não ativamos aqui, como back-face culling (descartar faces
voltadas para longe da câmera), mas que já vale adotar como boa prática.
2. initializeGL — três recursos novos em relação ao 2D
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_COLOR_MATERIAL)
GL_DEPTH_TESTativa a comparação de profundidade por pixel (detalhada adiante, em "Por que o depth buffer?").GL_LIGHTINGliga o modelo de iluminação do fixed-function — sem ele,glColorsozinho definiria a cor final de cada pixel, ignorando qualquer luz.GL_LIGHT0é a primeira de até 8 fontes de luz simultâneas (GL_LIGHT0...GL_LIGHT7) que o fixed-function suporta. Sem parâmetros extras,GL_LIGHT0já vem com valores padrão razoáveis (luz branca).GL_COLOR_MATERIALfaz o valor passado aglColor3fvaler também como a cor do material (componentes ambiente e difusa) — sem isso, seria preciso chamarglMaterialfvmanualmente para cada face.
3. resizeGL — a projeção em perspectiva
gluPerspective(45.0, w / float(max(1, h)), 0.1, 100.0)
gluPerspective(fovy, aspect, zNear, zFar) descreve um frustum (tronco de
pirâmide) — o volume de visão da câmera:
gluPerspective. Nada antes do plano near ou depois do far chega à tela.fovy(field of view, em graus): o ângulo de abertura vertical da câmera — quanto maior, mais "grande angular" a cena parece.aspect: a razão largura/altura da janela — por isso é recalculada a cadaresizeGL; se ficasse fixa, a cena distorceria ao redimensionar a janela.zNear/zFar: os planos de recorte próximo e distante. Nada mais perto que0.1ou mais longe que100é desenhado.zNearnunca pode ser0— a matemática da projeção em perspectiva envolve uma divisão porz.
4. paintGL — câmera, rotação e desenho
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
Agora dois bits são combinados com |: além da cor, o depth buffer
também precisa ser limpo a cada quadro (ver "Por que o depth buffer?" abaixo).
gluLookAt(4.0, 3.0, 5.0,
0.0, 0.0, 0.0,
0.0, 1.0, 0.0)
gluLookAt(eyeX,Y,Z, centerX,Y,Z, upX,Y,Z) recebe três vetores:
| Parâmetro | Valor aqui | Papel |
|---|---|---|
| olho (eye) | (4, 3, 5) | Onde a câmera está posicionada no mundo |
| centro (center) | (0, 0, 0) | Para onde a câmera está olhando |
| para-cima (up) | (0, 1, 0) | Define o "topo" da imagem — sem ele, a câmera não saberia se está de cabeça para baixo |
Internamente, gluLookAt monta uma matriz que multiplica a
GL_MODELVIEW — ela transforma a cena para o espaço do olho, onde a câmera
passa a ser a origem, olhando na direção -Z (convenção de mão direita do
OpenGL).
glRotatef(20.0, 1.0, 0.0, 0.0)
glRotatef(30.0, 0.0, 1.0, 0.0)
glRotatef(ângulo, x, y, z) gira em torno do eixo definido pelo vetor
(x, y, z), pelo ângulo em graus. Como essas chamadas vêm depois de
gluLookAt na mesma matriz GL_MODELVIEW, elas são aplicadas ao objeto
(o cubo), não à câmera — a ordem de multiplicação de matrizes importa, e
voltaremos a esse ponto com mais rigor matemático nas próximas aulas
(transformações geométricas e coordenadas homogêneas).
glBegin(GL_QUADS)
for color, normal, face in _FACES:
glColor3f(*color)
glNormal3f(*normal)
for vi in face:
glVertex3f(*_VERTS[vi])
glEnd()
Para cada face, o laço define a cor, a normal (que vale para os próximos
vértices, assim como a cor) e então emite os 4 vértices daquela face —
formando um quadrilátero (GL_QUADS). A normal é constante dentro de cada
face (as 4 chamadas de glVertex3f de uma face usam a mesma normal), o que
produz um sombreamento plano por face; normais por vértice — que
suavizariam as bordas — ficam para quando trabalharmos com modelos
importados.
Por que o depth buffer? Sem ele, a face desenhada por último sempre cobriria
as anteriores — mesmo estando atrás. O teste de profundidade compara, pixel a
pixel, a distância à câmera e mantém apenas o fragmento mais próximo. Por isso
paintGL precisa limpar o depth buffer a cada quadro
(GL_DEPTH_BUFFER_BIT).
O modelo de iluminação: ambiente, difusa e especular
O modelo de iluminação fixed-function do OpenGL (uma variante do modelo de Phong) soma três componentes para cada pixel iluminado:
| Componente | O que representa | Depende de |
|---|---|---|
| Ambiente | Luz indireta que "preenche" sombras (aproximação simples de luz que ricocheteou várias vezes no ambiente) | Nada — é constante |
| Difusa | Luz que bate diretamente na superfície e se espalha igualmente em todas as direções (superfícies foscas) | O ângulo entre a normal e a direção da luz (lei do cosseno de Lambert) |
| Especular | O brilho concentrado que aparece em superfícies polidas | O ângulo entre a direção de reflexo da luz e o observador |
Neste exemplo, só percebemos ambiente + difusa: o material padrão ativado por
GL_COLOR_MATERIAL não define um componente especular (fica preto por
padrão), então não há "brilho" visível no cubo. Como exercício, tente
adicionar glMaterialfv(GL_FRONT, GL_SPECULAR, ...) e glMaterialf(GL_FRONT, GL_SHININESS, ...) para observar o efeito.
Por que normais? A componente difusa — a mais perceptível aqui — é
calculada a partir do ângulo entre a normal da superfície e a direção da
fonte de luz: quanto mais alinhados, mais clara a face; quanto mais oblíqua,
mais escura. Faces voltadas para a luz têm normal quase paralela a ela e
recebem mais iluminação; faces de perfil recebem pouca. Sem glNormal3f,
todas as faces receberiam a mesma intensidade e o cubo pareceria "achatado",
sem volume aparente, mesmo estando corretamente ocluso pelo teste de
profundidade.
2D × 3D — quadro comparativo
| Aspecto | Hello World 2D | Hello World 3D |
|---|---|---|
| Projeção | Ortográfica (gluOrtho2D) | Perspectiva (gluPerspective) |
| Depth buffer | 0 bits (desligado) | 24 bits + GL_DEPTH_TEST |
glClear | Só cor | Cor e profundidade |
| Iluminação | Não | GL_LIGHTING + GL_LIGHT0 |
| Câmera | Implícita (origem) | gluLookAt |
| Vértices | glVertex2f | glVertex3f + glNormal3f |
| Sensação de profundidade | Não | Sim (perspectiva + luz) |
Atividade Prática
Baixe os dois arquivos, instale as dependências e execute cada um:
pip install PySide6 PyOpenGL
python helloworld2d.py
python helloworld3d.py
Depois, modifique os exemplos:
- No 2D: troque
GL_TRIANGLESporGL_LINE_LOOPouGL_POLYGONe observe a diferença; adicione um segundo triângulo; mude os limites degluOrtho2Dpara(-2, 2, -2, 2). - No 3D: varie os ângulos de
glRotatef; troque as cores das faces; anime a rotação usando umQTimerque chamaupdate()periodicamente. - Experimente desabilitar o
GL_DEPTH_TESTno exemplo 3D e explique o que acontece com o cubo. - Aumente
zNearno exemplo 3D para um valor maior (ex.:4.0) e observe faces do cubo sendo cortadas pelo plano near — relacione o resultado com o frustum descrito nesta aula.
Exercícios
Questões dissertativas
Qual a diferença entre OpenGL e um toolkit como o Qt/PySide6? Por que precisamos dos dois juntos para desenhar na tela?
Explique o papel de cada um dos três métodos initializeGL, resizeGL e paintGL. Por que a definição da projeção normalmente fica em resizeGL, e não em initializeGL?
O programa 2D usa buffer de profundidade de 0 bits e não habilita GL_DEPTH_TEST, enquanto o 3D usa 24 bits e habilita o teste. Explique por que essa diferença é necessária.
No exemplo 3D, cada face define uma normal com glNormal3f. O que é uma normal e qual seu papel no cálculo da iluminação?
Diferencie a projeção ortográfica da projeção em perspectiva quanto à aparência da cena. Dê um exemplo de aplicação em que cada uma é mais adequada.
Questões objetivas
1. No pipeline fixed-function, qual matriz deve ser selecionada com glMatrixMode antes de chamar gluPerspective ou gluOrtho2D?
- a)GL_MODELVIEW
- b)GL_PROJECTION
- c)GL_TEXTURE
- d)GL_VIEWPORT
- e)GL_COLOR
2. Por que o método paintGL do exemplo 3D chama glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT), enquanto o 2D limpa apenas GL_COLOR_BUFFER_BIT?
- a)Porque o 3D usa mais cores do que o 2D
- b)Porque o depth buffer precisa ser reiniciado a cada quadro para o teste de profundidade funcionar corretamente
- c)Porque limpar a profundidade acelera a renderização
- d)Porque o 2D não tem cor de fundo
- e)Porque gluPerspective exige a limpeza dupla
3. No trecho glColor3f(0.95, 0.30, 0.30); glVertex2f(-0.6, -0.5) do exemplo 2D, o que representa a cor definida antes do vértice?
- a)A cor de fundo da janela
- b)A cor de todos os vértices seguintes até que glColor seja chamado de novo — interpolada entre os vértices do triângulo
- c)A cor apenas do primeiro pixel
- d)Um valor ignorado pelo pipeline fixed-function
- e)A cor da borda do triângulo
4. Qual é o papel do QSurfaceFormat.setDepthBufferSize(24) no programa 3D?
- a)Definir a resolução da janela em 24 pixels
- b)Solicitar 24 bits de cor por pixel
- c)Pedir ao contexto OpenGL um buffer de profundidade de 24 bits, necessário para o teste de profundidade
- d)Limitar a cena a 24 objetos
- e)Configurar a taxa de quadros em 24 fps
5. A chamada gluLookAt(4.0, 3.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) no exemplo 3D configura o quê?
- a)A posição de uma fonte de luz
- b)A câmera: olho em (4,3,5), olhando para a origem, com 'para cima' no eixo Y
- c)As dimensões do cubo desenhado
- d)Os planos de recorte near e far
- e)A cor de três faces do cubo
Referências
Principais (essenciais)
- AZEVEDO, E.; CONCI, A.; LETA, F. R. Computação Gráfica. Rio de Janeiro: Elsevier, 2003–2008. 2 v.
- FOLEY, J. D. et al. Computer Graphics: Principles and Practice. 3. ed. Addison-Wesley, 2013.
- The Khronos Group. OpenGL 2.1 Reference Pages. Disponível em: registry.khronos.org/OpenGL-Refpages/gl2.1
Aprofundamento (opcionais)
- HEARN, D.; BAKER, M. P.; CARITHERS, W. R. Computer Graphics with OpenGL. 4. ed. Pearson Prentice Hall, 2011.
- COHEN, M.; MANSSOUR, I. H. OpenGL: uma abordagem prática e objetiva. São Paulo: Novatec, 2006.
- Qt for Python (PySide6). QOpenGLWidget. Disponível em: doc.qt.io/qtforpython-6
- PyOpenGL Documentation. Disponível em: mcfletch.github.io/pyopengl