Pular para o conteúdo principal

Aula 02 — Primitivas de Desenho 2D

Objetivos

Ao final desta aula você deve ser capaz de:

  • Escolher e aplicar a primitiva adequada (GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_QUADS, GL_POLYGON) para desenhar uma forma 2D.
  • Prever que figura resulta de um mesmo conjunto de vértices quando se troca apenas o argumento de glBegin.
  • Explicar o conceito de estado corrente no modo imediato e o efeito de glColor3f, glPointSize e glLineWidth sobre os vértices emitidos em seguida.
  • Definir o sistema de coordenadas do mundo com gluOrtho2D e corrigir a distorção causada pela proporção da janela (aspect ratio).
  • Justificar por que a projeção é definida em resizeGL e não em paintGL.
  • Distinguir polígonos convexos de côncavos e explicar por que GL_POLYGON exige convexidade.
  • Construir uma interface com QMainWindow, menu e botões que controlem o conteúdo de um canvas OpenGL, respeitando os limites de onde é seguro chamar funções gl*.
  • Localizar na referência do OpenGL recursos não apresentados em aula — como glLineStipple e glPolygonMode — e aplicá-los a partir de um enunciado.

Conteúdo

Retomando: o que muda em relação à Aula 01

Na Aula 01 desenhamos um triângulo e um cubo com um QOpenGLWidget mínimo. Agora o foco sai do "como abrir uma janela" e vai para o que se pode desenhar dentro dela. Três mudanças de estrutura acompanham essa virada:

AspectoAula 01 (helloworld2d.py)Aula 02
Onde a projeção é definidapaintGL (simplificação didática)resizeGL — depende da janela
Sistema de coordenadas(-1, 1, -1, 1) fixocorrigido pelo aspect ratio(-50, 50) nos exemplos de primitivas
PrimitivaGL_TRIANGLESTodas as do modo imediato
Por que mover a projeção para resizeGL?

A matriz de projeção só precisa ser recalculada quando a janela muda de tamanho — não a cada quadro. Recalculá-la dentro de paintGL funciona, mas desperdiça trabalho e mistura responsabilidades. O resizeGL é chamado pelo Qt uma vez antes do primeiro paintGL e depois a cada redimensionamento, o que é exatamente o momento certo. A partir desta aula, use sempre esse padrão.


O Modo Imediato e o Estado Corrente

Todo desenho no pipeline fixed-function é uma sequência:

glBegin(<primitiva>)
# emite vértices com glVertex2f / glVertex3f
glEnd()

O OpenGL do modo imediato é uma máquina de estados. Comandos como glColor3f, glPointSize e glLineWidth não desenham nada: eles alteram o estado corrente, que passa a valer para todos os vértices emitidos a partir dali, até ser alterado de novo — inclusive de um quadro para o seguinte.

glBegin(GL_TRIANGLES)
glColor3f(1.0, 0.0, 0.0) # a partir daqui, vermelho
glVertex2f(-35, -14) # vermelho
glVertex2f(-21, 14) # vermelho
glVertex2f( -7, -14) # vermelho
glColor3f(0.0, 0.0, 1.0) # a partir daqui, azul
glVertex2f( 7, 14) # azul
glVertex2f( 21, -14) # azul
glVertex2f( 35, 14) # azul
glEnd()

Se cada vértice de um mesmo triângulo receber uma cor diferente, a GPU interpola as cores no interior da face (o Gouraud shading visto na Aula 01). Uma única chamada de glColor3f antes do trio produz cor sólida.

glPointSize e glLineWidth ficam fora do glBegin

Nem todo comando é aceito entre glBegin e glEnd. glPointSize, glLineWidth, glPolygonMode e as funções de matriz devem ser chamados antes do glBegin. Chamá-los dentro gera o erro GL_INVALID_OPERATION — que o OpenGL registra silenciosamente, sem interromper o programa.


As Primitivas do Modo Imediato

A tabela abaixo mostra, para uma mesma sequência de vértices v0,v1,v_0, v_1, \dots, como cada primitiva os agrupa:

PrimitivaAgrupamentoResultado com 6 vértices
GL_POINTScada vértice isolado6 pontos
GL_LINESpares: (v0,v1),(v2,v3),(v_0,v_1), (v_2,v_3), \dots3 segmentos soltos
GL_LINE_STRIPv0v1v2v_0 \to v_1 \to v_2 \to \dots1 polilinha aberta (5 segmentos)
GL_LINE_LOOPcomo STRIP + fecha v5v0v_5 \to v_01 contorno fechado (6 segmentos)
GL_TRIANGLEStrios: (v0,v1,v2),(v3,v4,v5)(v_0,v_1,v_2), (v_3,v_4,v_5)2 triângulos
GL_TRIANGLE_STRIPcada novo vértice + os 2 anteriores4 triângulos
GL_TRIANGLE_FANtodos compartilham v0v_04 triângulos em leque
GL_QUADSquartetos: (v0,v1,v2,v3)(v_0, v_1, v_2, v_3), ...1 quadrilátero (sobram 2 vértices)
GL_POLYGONtodos os vértices1 polígono de 6 lados

Vértices que sobram são descartados, sem aviso: com 5 vértices e GL_TRIANGLES, sai 1 triângulo e 2 vértices ignorados. É uma fonte comum de "desapareceu metade do desenho".

Um mesmo conjunto de vértices produz cinco figuras diferentes: só muda o argumento de glBegin.

O Sistema de Coordenadas do Mundo e o Aspect Ratio

gluOrtho2D(left, right, bottom, top) declara que retângulo do mundo é visível na janela. Na Aula 01 usamos (-1, 1, -1, 1); nos exemplos de primitivas desta aula a faixa vai de -50 a 50, mais confortável para escrever coordenadas "em unidades de desenho" em vez de frações.

A escala em si é arbitrária: gui2d.py, o último exemplo da aula, mantém a faixa de -1 a 1 e funciona igualmente bem — por isso seu triângulo é escrito com vértices como -0.5. O que não é arbitrário é a correção descrita a seguir, presente em todos os exemplos.

O problema: a janela quase nunca é quadrada. Se mapearmos um retângulo do mundo quadrado sobre uma janela de 600×400 pixels, tudo fica esticado na horizontal — um círculo vira elipse. A correção padrão é estender o retângulo do mundo no eixo mais longo da janela:

if w <= h:
gluOrtho2D(-50.0, 50.0, -50.0 * h / w, 50.0 * h / w)
else:
gluOrtho2D(-50.0 * w / h, 50.0 * w / h, -50.0, 50.0)

O efeito é que a razão entre unidade de mundo e pixel fica igual nos dois eixos: a forma é preservada e a janela mais larga simplesmente mostra mais mundo na horizontal, em vez de deformar o que já estava lá.

JanelagluOrtho2D resultanteEfeito
400×400(-50, 50, -50, 50)quadrado exato
600×400(-75, 75, -50, 50)mostra mais mundo na horizontal
400×600(-50, 50, -75, 75)mostra mais mundo na vertical
Divisão por zero

Ao minimizar a janela, w ou h podem chegar a 0 — e as contas acima dividiriam por zero. Por isso todos os exemplos começam o resizeGL com w, h = max(1, w), max(1, h).


Exemplo 1 — GL_POINTS: um círculo de pontos

Os vértices são calculados por um laço que percorre a circunferência de raio 20 em passos de π/7\pi/7 radianos — 14 pontos na volta completa.

Janela intitulada "Primitivas 2D — GL_POINTS" com fundo cinza-escuro. Quatorze pequenos quadrados brancos, igualmente espaçados, formam a circunferência de um círculo centrado na janela; o interior fica vazio.
Saída de pontos2d.py. Cada ponto é rasterizado como um quadrado de 5 pixels de lado — o arredondamento exigiria GL_POINT_SMOOTH.

📥 pontos2d.py

pontos2d.py (trechos principais)
RAIO = 20.0
PASSO = math.pi / 7.0

def resizeGL(self, w: int, h: int) -> None:
w, h = max(1, w), max(1, h)

# 1) Onde desenhar, em pixels.
glViewport(0, 0, w, h)

# 2) Que retângulo do mundo é visível.
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
if w <= h:
gluOrtho2D(-50.0, 50.0, -50.0 * h / w, 50.0 * h / w)
else:
gluOrtho2D(-50.0 * w / h, 50.0 * w / h, -50.0, 50.0)

glMatrixMode(GL_MODELVIEW)
glLoadIdentity()

def paintGL(self) -> None:
glClear(GL_COLOR_BUFFER_BIT)

glColor3f(0.92, 0.92, 0.95)
glPointSize(5.0) # lado do ponto, em PIXELS

glBegin(GL_POINTS)
ang = 0.0
while ang < 2 * math.pi:
glVertex2f(RAIO * math.cos(ang), RAIO * math.sin(ang))
ang += PASSO
glEnd()

O que observar. glPointSize é medido em pixels, não em unidades do mundo — por isso os pontos não crescem quando a janela aumenta, enquanto o raio do círculo cresce. Essa distinção entre grandezas do mundo (afetadas pela projeção) e grandezas de rasterização (em pixels) vale também para glLineWidth.

Por padrão, um ponto é rasterizado como um quadrado. Para obtê-lo arredondado é preciso glEnable(GL_POINT_SMOOTH) — e o resultado depende do suporte do driver.


Exemplo 2 — GL_LINES, GL_LINE_STRIP e GL_LINE_LOOP

Este exemplo usa exatamente os mesmos vértices do anterior. A única mudança é o argumento de glBegin — e é essa comparação que interessa.

Janela intitulada "Primitivas 2D — GL_LINES" com fundo cinza-escuro. Sete traços brancos curtos e desconectados, dispostos ao longo de uma circunferência, deixando vãos entre si — o contorno do círculo aparece interrompido, como uma linha pontilhada irregular.
Saída de linhas2d.py com GL_LINES: os mesmos 14 vértices da Figura 2, agora consumidos aos pares — daí os sete vãos.

A Figura 3 mostra o resultado com GL_LINES: os 14 vértices foram consumidos aos pares, e por isso metade das ligações simplesmente não existe. Trocando a constante, os mesmos vértices fecham a figura.

📥 linhas2d.py

linhas2d.py (trecho principal)
# Experimente: GL_LINES | GL_LINE_STRIP | GL_LINE_LOOP
PRIMITIVA = GL_LINES

def paintGL(self) -> None:
glClear(GL_COLOR_BUFFER_BIT)

glColor3f(0.92, 0.92, 0.95)
glLineWidth(3.0)

glBegin(PRIMITIVA)
ang = 0.0
while ang < 2 * math.pi:
glVertex2f(RAIO * math.cos(ang), RAIO * math.sin(ang))
ang += PASSO
glEnd()

O que observar ao trocar a constante:

São 14 vértices — o mesmo laço do exemplo anterior:

PrimitivaFigura resultanteSegmentos
GL_LINES7 traços soltos, como uma estrela incompleta — os vértices são consumidos aos pares7
GL_LINE_STRIPuma polilinha aberta: falta o traço que voltaria ao primeiro vértice13
GL_LINE_LOOPos mesmos vértices, agora fechados — a melhor aproximação de círculo14

Aumentar o número de vértices (reduzindo PASSO) aproxima cada vez mais a figura de um círculo. Esse é o princípio geral da computação gráfica: curvas são aproximadas por segmentos de reta; o que muda é quantos.


Exemplo 3 — GL_TRIANGLES: a primitiva fundamental

Janela intitulada "Primitivas 2D — GL_TRIANGLES" com fundo cinza-escuro. Dois triângulos preenchidos lado a lado: à esquerda um vermelho apontando para cima, à direita um azul apontando para baixo. Cada um tem cor sólida e uniforme.
Saída de triangulos2d.py. Seis vértices, dois triângulos independentes — e uma cor por trio, sem o degradê da Aula 01.

Os seis vértices viraram dois triângulos — e como cada trio recebeu uma única chamada de glColor3f, cada face saiu com cor sólida, sem o degradê da Aula 01.

📥 triangulos2d.py

triangulos2d.py (trecho principal)
glBegin(GL_TRIANGLES)

glColor3f(0.95, 0.30, 0.30) # 1o. triângulo, vermelho
glVertex2f(-35.0, -14.0) # v0
glVertex2f(-21.0, 14.0) # v1
glVertex2f( -7.0, -14.0) # v2

glColor3f(0.35, 0.55, 0.95) # 2o. triângulo, azul
glVertex2f( 7.0, 14.0) # v3
glVertex2f( 21.0, -14.0) # v4
glVertex2f( 35.0, 14.0) # v5

glEnd()

O triângulo é a primitiva fundamental do hardware gráfico: é a menor figura que define um plano de forma não ambígua, é sempre convexa e é sempre planar — três propriedades que simplificam enormemente a rasterização. Toda malha 3D, por mais complexa, acaba decomposta em triângulos antes de chegar à GPU.

GL_TRIANGLE_STRIP e GL_TRIANGLE_FAN existem por eficiência: em uma malha, vértices vizinhos são compartilhados por vários triângulos. Um STRIP com nn vértices produz n2n-2 triângulos, contra n/3n/3 do GL_TRIANGLES — ou seja, transmite menos dados para o mesmo resultado.


Exemplo 4 — GL_QUADS e a ordem dos vértices

Janela intitulada "Primitivas 2D — GL_QUADS" com fundo cinza-escuro. Dois quadrados cinza-claros preenchidos, de mesmo tamanho, lado a lado e separados por um vão central.
Saída de quadrados2d.py com WIREFRAME = False. Os oito vértices foram percorridos como contorno; inverter dois deles produz a gravata-borboleta descrita abaixo.

📥 quadrados2d.py

quadrados2d.py (trecho principal)
glBegin(GL_QUADS)

glVertex2f(-45.0, -15.0) # v0, início do 1o. quadrilátero
glVertex2f(-45.0, 15.0) # v1
glVertex2f(-15.0, 15.0) # v2
glVertex2f(-15.0, -15.0) # v3

glVertex2f( 15.0, -15.0) # v4, início do 2o. quadrilátero
glVertex2f( 15.0, 15.0) # v5
glVertex2f( 45.0, 15.0) # v6
glVertex2f( 45.0, -15.0) # v7

glEnd()

Os quatro vértices devem ser dados percorrendo o contorno — horário ou anti-horário, tanto faz, desde que seja um percurso. Se você trocar v2 por v3 na ordem, o quadrilátero sai cruzado, em forma de gravata-borboleta. Para conferir visualmente como os vértices foram ligados, o exemplo traz uma constante WIREFRAME; com ela ativada:

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) # desenha só o contorno
GL_QUADS no OpenGL moderno

GL_QUADS e GL_POLYGON foram removidos do core profile 3.2+. Usamos aqui porque estamos no perfil de compatibilidade (2.1), mas vale saber: em OpenGL moderno, um quadrilátero é sempre montado como dois triângulos.


Exemplo 5 — GL_POLYGON e a restrição de convexidade

Janela intitulada "Primitivas 2D — GL_POLYGON" com fundo cinza-escuro. Um pentágono cinza-claro totalmente preenchido ocupa o centro da janela, com o vértice mais alto voltado para cima.
Saída de poligonos2d.py com a lista VERTICES. É o caso que GL_POLYGON garante: polígono convexo e simples.

Os cinco vértices de VERTICES formam um pentágono convexo — e o preenchimento sai correto. Trocar a lista pela CONCAVO é o que revela o problema descrito a seguir.

📥 poligonos2d.py

poligonos2d.py (trecho principal)
VERTICES = [
(-20.0, -35.0),
(-35.0, 5.0),
( 0.0, 35.0),
( 35.0, 5.0),
( 20.0, -35.0),
]

glBegin(GL_POLYGON)
for x, y in VERTICES:
glVertex2f(x, y)
glEnd()

Um polígono é convexo quando, para quaisquer dois pontos internos, o segmento que os une fica inteiramente dentro da figura — de forma equivalente, quando todos os seus ângulos internos são menores que 180°. Uma estrela e um "L" são côncavos.

GL_POLYGON só garante resultado correto para polígonos convexos e simples (sem lados que se cruzam). Com um polígono côncavo, o comportamento é indefinido: normalmente a GPU preenche a região errada, sem emitir erro algum. O arquivo do exemplo traz uma lista CONCAVO (uma estrela de 4 pontas) justamente para você observar isso na prática.

Como desenhar formas côncavas então? Decompondo-as em triângulos — triangulação. Manualmente, para formas simples; ou via gluTess* (o tessellator da GLU), para o caso geral. É o que qualquer motor gráfico faz internamente.


Elementos de Interface com PySide6

Até aqui, todas as janelas foram um QOpenGLWidget solto. Aplicações reais precisam de menus, botões e barra de status ao redor do canvas. O Qt resolve isso com a QMainWindow, que já traz as áreas prontas:

As áreas prontas da QMainWindow. O canvas OpenGL entra como um widget qualquer, dentro dos layouts do widget central.

Widgets e layouts. No Qt, você não posiciona widgets por coordenadas: você os coloca em layouts (QVBoxLayout, vertical; QHBoxLayout, horizontal), que recalculam posições e tamanhos automaticamente quando a janela muda. Layouts podem ser aninhados: um QHBoxLayout de botões dentro de um QVBoxLayout que também contém o canvas.

Sinais e slots. A interação em Qt funciona pelo mecanismo de signals and slots: um widget emite um sinal quando algo acontece (clicked, triggered) e você conecta esse sinal a uma função:

botao_fechar = QPushButton("Fechar")
botao_fechar.clicked.connect(self.close)

Quem emite não precisa saber quem recebe: o botão apenas anuncia que foi clicado, e o Qt encaminha a chamada para todos os slots conectados. Um mesmo sinal pode alimentar vários slots, e o mesmo slot pode atender a vários sinais — é o que permite que o menu e um atalho de teclado disparem exatamente a mesma ação.

A regra mais importante: onde é seguro chamar gl*

Nunca chame funções gl* dentro de um slot de menu ou de botão. O contexto OpenGL só está garantidamente ativo dentro de initializeGL, resizeGL e paintGL. Um slot deve:

  1. alterar um atributo do canvas (o estado da cena);
  2. chamar update(), que agenda um novo paintGL.
def definir_cor(self, r, g, b):
self.cor = (r, g, b) # só muda o estado
self.update() # o Qt chama paintGL no momento certo

Note que update() não desenha imediatamente: ele marca o widget como "sujo" e devolve o controle. Chamar gl* fora da hora costuma "funcionar por acaso" em uma máquina e falhar silenciosamente em outra.

O exemplo completo traz um menu Cor com as três cores (atalhos R, G e B), um separador e um item Sair (Ctrl+Q); um botão Fechar; e uma barra de status que reporta as coordenadas do clique no canvas. A tecla Esc também fecha a janela — é para isso que serve o setFocusPolicy do construtor, sem o qual o canvas nunca receberia o evento de teclado.

Janela intitulada "PySide6 + OpenGL — elementos de GUI". No topo, uma barra com o botão "Fechar" alinhado à esquerda; ao centro, o canvas OpenGL de fundo escuro com um triângulo vermelho preenchido apontando para cima; embaixo, a barra de status exibindo a mensagem "Pronto — use o menu Cor ou clique no canvas".
Saída de gui2d.py. O canvas OpenGL é o widget do meio; a barra de botões e a barra de status são Qt puro.

O canvas OpenGL é apenas um widget entre outros dentro da QMainWindow: o botão, o menu e a barra de status são widgets Qt comuns, e nenhum deles desenha na cena — todos apenas alteram o estado do canvas e pedem um novo paintGL.

📥 gui2d.py

gui2d.py (trechos principais)
class Canvas(QOpenGLWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.cor = (0.95, 0.30, 0.30) # estado da cena
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # para receber teclado

def definir_cor(self, r, g, b):
self.cor = (r, g, b)
self.update() # agenda o paintGL

def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT)
glColor3f(*self.cor)
glBegin(GL_TRIANGLES)
glVertex2f(-0.5, -0.5)
glVertex2f( 0.0, 0.5)
glVertex2f( 0.5, -0.5)
glEnd()


class JanelaPrincipal(QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas(self)

botao_fechar = QPushButton("Fechar")
botao_fechar.clicked.connect(self.close)

linha_botoes = QHBoxLayout()
linha_botoes.addWidget(botao_fechar)
linha_botoes.addStretch()

coluna = QVBoxLayout()
coluna.addLayout(linha_botoes)
coluna.addWidget(self.canvas, stretch=1)

central = QWidget()
central.setLayout(coluna)
self.setCentralWidget(central)

menu_cor = self.menuBar().addMenu("&Cor")
for rotulo, atalho, rgb in (
("&Vermelho", "R", (0.95, 0.30, 0.30)),
("V&erde", "G", (0.30, 0.95, 0.30)),
("&Azul", "B", (0.35, 0.55, 0.95)),
):
acao = QAction(rotulo, self)
acao.setShortcut(atalho)
# o default c=rgb "congela" o valor desta iteração
acao.triggered.connect(lambda _=False, c=rgb: self._trocar_cor(c))
menu_cor.addAction(acao)

No arquivo, a montagem do menu está separada em um método _montar_menu e a troca de cor em _trocar_cor — aqui foram reunidas para caber em um trecho só. _trocar_cor faz duas coisas: chama canvas.definir_cor(...) e atualiza a barra de status; note que nenhuma delas é uma função gl*.

Eventos de mouse e teclado. Sinais e slots servem para ações de widgets (um botão clicado, um item de menu acionado). Já os eventos de entrada bruta chegam por sobrescrita de métodos da classe base:

MétodoQuando o Qt chamaObservação
paintGL(self)a cada repintarúnico lugar onde se desenha
resizeGL(self, w, h)ao redimensionarviewport e projeção
mousePressEvent(self, event)ao pressionar um botão do mouseevent.position() dá a posição no widget
keyPressEvent(self, event)ao pressionar uma teclaexige setFocusPolicy(Qt.FocusPolicy.StrongFocus)

O detalhe fácil de esquecer é o foco: um QOpenGLWidget não recebe eventos de teclado até que uma política de foco seja definida — por isso o Canvas chama setFocusPolicy no construtor.

Origem do Y: Qt × OpenGL

O Qt entrega a posição do mouse com a origem no canto superior esquerdo, com o eixo Y crescendo para baixo. O OpenGL usa o canto inferior esquerdo, com Y crescendo para cima. Para converter:

y_opengl = self.height() - y_qt

O mousePressEvent do gui2d.py mostra as duas convenções lado a lado na barra de status — clique perto do topo do canvas e compare os dois valores de y. Esquecer essa conversão é a causa clássica de "o objeto aparece espelhado verticalmente em relação ao clique".


Atividades de Laboratório

Aquecimento

Antes das atividades, use os exemplos da aula para experimentar:

  1. Em linhas2d.py, troque PRIMITIVA pelas três constantes e descreva a diferença; depois reduza PASSO até a figura parecer um círculo.
  2. Em pontos2d.py, redimensione a janela e explique por que os pontos não mudam de tamanho enquanto o círculo muda.
  3. Em quadrados2d.py, ative WIREFRAME e depois inverta a ordem de dois vértices de um quadrilátero — explique a figura resultante.
  4. Em poligonos2d.py, troque VERTICES por CONCAVO e explique o preenchimento incorreto. Em seguida, desenhe a mesma estrela corretamente com GL_TRIANGLE_FAN — de que ponto o leque precisa partir, e o que isso obriga a acrescentar à lista de vértices?
  5. Em gui2d.py, acrescente um menu "Primitiva" que alterne entre GL_TRIANGLES, GL_LINE_LOOP e GL_POINTS no mesmo conjunto de vértices.

As duas atividades

São duas atividades independentes, cada uma com seu próprio arquivo de ponto de partida. Ambas usam Python com PySide6 e PyOpenGL, no pipeline fixed-function. Regras que valem para as duas:

  • Trabalhe a partir do arquivo de ponto de partida; não comece do zero nem cole código pronto de outra fonte.
  • Todo TODO do arquivo corresponde a um passo do roteiro.
  • Comente as decisões não óbvias no próprio código. Quem lê seu código depois precisa entender por que você escolheu aquela primitiva, não só o que ela desenha.

Atividade 1 — Mira 2D

Janela de fundo amarelo com uma mira vermelha centrada: um quadrado externo de contorno tracejado, um quadrado interno menor de contorno contínuo, e quatro braços retos partindo em direção ao centro — para cima, para baixo, para a esquerda e para a direita — que se interrompem antes de alcançá-lo, deixando um vazio central. Os braços são mais longos que o quadrado externo e ultrapassam suas bordas.
Alvo visual da Atividade 1. As proporções são indicativas; as coordenadas ficam por sua conta.

Objetivo. Reproduzir a mira da Figura 9 usando exclusivamente primitivas de linha, sem transformações geométricas — apenas coordenadas escritas diretamente.

Ponto de partida. O programa já abre a janela e configura a projeção; o paintGL está vazio.

📥 mira2d_base.py

Roteiro

Passo 1 — Entenda o espaço antes de desenhar. Leia o resizeGL do arquivo base e determine, no papel, qual faixa de coordenadas está visível quando a janela tem 640×480. Anote os limites em X e em Y. Você vai precisar deles para dimensionar a mira — e para saber se um vértice caiu fora da tela.

Passo 2 — Estabeleça o fundo. Ajuste a cor de fundo para o amarelo da referência. Rode o programa: se a janela abrir amarela, seu ciclo de vida está funcionando e o resto é desenho.

Passo 3 — Comece pelo elemento mais simples. Desenhe apenas o quadrado interno, com contorno contínuo. Escolha a primitiva que liga os vértices em sequência e fecha o contorno sozinho — consulte a tabela de primitivas desta aula. Defina cor e espessura antes de começar a emitir vértices.

Ao rodar, o quadrado apareceu do tamanho que você esperava? Se ficou grande ou minúsculo demais, releia o que você anotou no Passo 1: o problema está nas coordenadas ou no seu entendimento do espaço visível?

Passo 4 — Acrescente o quadrado externo, tracejado. O tracejado não é uma primitiva diferente: é um estado do OpenGL. Procure na referência do OpenGL 2.1 por GL_LINE_STIPPLE, glLineStipple e o par glEnable/glDisable.

glLineStipple(fator, padrao) recebe um padrão de 16 bits: cada bit em 1 desenha, cada bit em 0 pula; o fator multiplica a largura de cada bit.

Antes de testar: desenhe no papel o traço que 0xAAAA produz com fator 1 e com fator 4. Depois confira na tela se sua previsão estava certa.

Passo 5 — Descubra o efeito colateral. Rode o programa com o quadrado externo tracejado desenhado antes do interno, sem nenhum glDisable. O que aconteceu com o quadrado interno? Explique o resultado usando o conceito de estado corrente e corrija.

Passo 6 — Desenhe os quatro braços. Repare bem na referência: os braços não tocam o centro — há um vazio ao redor da origem. Cada braço é um segmento com início e fim.

Desafio: os quatro braços cabem em um único bloco glBegin/glEnd. Qual primitiva permite isso, e quantos vértices ela vai consumir? Faça primeiro com quatro blocos separados, depois reduza a um só e verifique que o resultado é idêntico.

Passo 7 — Teste o redimensionamento. Estique a janela na horizontal e depois na vertical. A mira continua com proporções corretas — quadrados quadrados, braços simétricos? Explique, em um comentário no código, qual trecho do programa garante isso.

Checklist de conclusão

  • A janela abre com o fundo amarelo e a mira centrada.
  • O quadrado externo é tracejado e o interno é contínuo.
  • Existe vazio entre o centro e o início de cada braço.
  • Os braços ultrapassam as bordas do quadrado externo.
  • Redimensionar a janela não deforma a mira.
  • Nenhuma chamada inválida entre glBegin e glEnd.

Para responder

Registre as respostas em um comentário no topo do arquivo:

  1. Quantos blocos glBegin/glEnd seu programa usa? Seria possível usar menos? E menos seria melhor? Justifique.
  2. Se você trocasse a primitiva dos quadrados por GL_POLYGON, o que mudaria na tela? E se trocasse por GL_LINE_STRIP?
  3. A espessura do traço está em pixels. Ao maximizar a janela, a mira cresce e a espessura não. Isso é um defeito ou o comportamento esperado?

Atividade 2 — Preenchido × Wireframe

Dois painéis lado a lado sobre fundo escuro. À esquerda, rotulado 'preenchido', um triângulo vermelho totalmente pintado. À direita, rotulado 'wireframe', o mesmo triângulo na mesma posição, mas mostrando apenas as três arestas, com o interior vazio. Uma seta entre os painéis, rotulada 'botão', indica que a interface alterna entre os dois estados.
Os dois estados da Atividade 2. A geometria é a mesma nos dois painéis: só muda como o OpenGL rasteriza o polígono.

Objetivo. Acrescentar um botão que alterne a mesma geometria entre preenchida e apenas com suas arestas, sem alterar nenhum vértice.

Ponto de partida. Janela com o triângulo preenchido e uma barra de botões vazia.

📥 wireframe2d_base.py

Roteiro

Passo 1 — Coloque o botão na tela. Crie o botão e adicione-o ao layout da barra. Rode: ele deve aparecer, mesmo sem fazer nada ainda. Escolha um rótulo que descreva o que vai acontecer ao ser clicado, não o estado atual.

Passo 2 — Decida onde mora o estado. A cena tem dois modos possíveis. Esse dado pertence ao canvas ou à janela? Antes de escrever, responda: se amanhã existirem dois canvas na mesma janela, sua escolha ainda funciona?

Passo 3 — Ligue o botão ao estado. Conecte o sinal do botão a um método que inverta o modo. Consulte gui2d.py para lembrar qual sinal um QPushButton emite.

Cuidado com a armadilha central desta atividade: esse método não pode chamar funções gl* nem paintGL() diretamente. Reveja a seção "onde é seguro chamar gl*" desta aula e use a chamada correta.

Passo 4 — Aplique o modo no desenho. Em paintGL, informe ao OpenGL como os polígonos devem ser rasterizados. Procure por glPolygonMode(face, modo) na referência do OpenGL 2.1 e descubra: que valores o parâmetro face aceita e qual serve aqui? Que constante corresponde a cada um dos dois modos?

Passo 5 — Teste o estado que ficou para trás. Implemente o ramo do wireframe (sem o else). Clique para ativar e clique de novo para desativar. O triângulo voltou a ser preenchido? Explique o que você observou e conserte.

Passo 6 — Feche o ciclo com o usuário. O rótulo escolhido no Passo 1 anuncia o que vai acontecer — logo, ele precisa mudar a cada clique. Faça isso, e use a barra de status para informar o modo atual. Depois pergunte-se: se amanhã você acrescentar um item de menu que alterna o mesmo modo, o rótulo continua correto? Se não, o que precisa mudar na estrutura do código?

Checklist de conclusão

  • O botão alterna entre os dois modos, indefinidamente.
  • Nenhuma função gl* é chamada fora de initializeGL, resizeGL ou paintGL.
  • Os vértices do triângulo não foram alterados.
  • O rótulo do botão nunca contradiz o que está na tela.

Para responder

Registre as respostas em um comentário no topo do arquivo:

  1. O que acontece se você aplicar o botão desta atividade sobre a mira da Atividade 1? Teste e explique o resultado. O que isso revela sobre o alcance de glPolygonMode?
  2. Qual a diferença entre alternar o modo com glPolygonMode e alternar trocando a primitiva de GL_TRIANGLES para GL_LINE_LOOP? Existe alguma situação em que as duas abordagens produzem resultados diferentes?
  3. Por que glPolygonMode é chamado dentro de paintGL, e não uma única vez em initializeGL?

Exercícios

Questões dissertativas

Q1

Explique o que significa dizer que o OpenGL do modo imediato é uma 'máquina de estados'. Use glColor3f como exemplo.

Q2

Dado o mesmo conjunto de 6 vértices, descreva a figura resultante com GL_LINES, GL_LINE_STRIP e GL_LINE_LOOP. Quantos segmentos cada uma gera?

Q3

Por que a definição da projeção com gluOrtho2D precisa levar em conta a proporção (aspect ratio) da janela? O que acontece se ela for ignorada?

Q4Difícil

O que caracteriza um polígono convexo? Por que GL_POLYGON não funciona corretamente com polígonos côncavos, e como se desenha uma forma côncava em OpenGL?

Q5

Um colega escreveu um slot de menu que chama glColor3f e glBegin diretamente para trocar a cor do desenho, e o programa às vezes não redesenha nada. Explique o erro e a forma correta de implementar.

Questões objetivas

Quiz5 questões

1. Um trecho emite 8 vértices entre glBegin(GL_TRIANGLES) e glEnd(). Quantos triângulos são desenhados?

  • a)8 triângulos
  • b)6 triângulos
  • c)2 triângulos, e 2 vértices são descartados
  • d)3 triângulos, com o último incompleto
  • e)Nenhum — o OpenGL gera erro por número inválido de vértices

2. Por que glPointSize e glLineWidth devem ser chamados FORA do bloco glBegin/glEnd?

  • a)Porque só funcionam depois de glEnd
  • b)Porque entre glBegin e glEnd apenas alguns comandos são válidos; os demais geram GL_INVALID_OPERATION
  • c)Porque eles alteram a matriz de projeção
  • d)Porque exigem que o depth test esteja habilitado
  • e)Não há restrição: podem ser chamados em qualquer lugar

3. Em uma janela de 800×400 pixels, qual chamada preserva a proporção das formas mantendo 50 unidades de mundo no eixo mais curto?

  • a)gluOrtho2D(-50, 50, -50, 50)
  • b)gluOrtho2D(-100, 100, -50, 50)
  • c)gluOrtho2D(-50, 50, -100, 100)
  • d)gluOrtho2D(-25, 25, -50, 50)
  • e)gluOrtho2D(0, 800, 0, 400)

4. Qual é a forma correta de fazer um item de menu trocar a cor do desenho em um QOpenGLWidget?

  • a)Chamar glColor3f diretamente dentro do slot conectado ao menu
  • b)Alterar um atributo do canvas com a nova cor e chamar update()
  • c)Recriar o QOpenGLWidget a cada troca de cor
  • d)Chamar paintGL() diretamente a partir do slot
  • e)Chamar glClearColor no slot e aguardar o próximo evento de resize

5. O mouse é clicado no ponto (100, 30) de um canvas com 400 pixels de altura. Qual a coordenada Y equivalente na convenção do OpenGL?

  • a)30
  • b)130
  • c)370
  • d)400
  • e)-30

Referências

Principais (essenciais)

  • AZEVEDO, E.; CONCI, A.; LETA, F. R. Computação Gráfica. Rio de Janeiro: Elsevier, 2003–2008. 2 v. (004.92 A994c)
  • FOLEY, J. D. et al. Computer Graphics: Principles and Practice. 3. ed. Addison-Wesley, 2013. (004.92 C738)
  • The Khronos Group. OpenGL 2.1 Reference Pages — glBegin. Disponível em: registry.khronos.org/OpenGL-Refpages/gl2.1

Aprofundamento (opcionais)

  • COHEN, M.; MANSSOUR, I. H. OpenGL: uma abordagem prática e objetiva. São Paulo: Novatec, 2006. (004.92 C678o)
  • HEARN, D.; BAKER, M. P.; CARITHERS, W. R. Computer Graphics with OpenGL. 4. ed. Pearson Prentice Hall, 2011. (004.92 H436co)
  • Qt for Python (PySide6). Layout Management e QMainWindow. Disponível em: doc.qt.io/qtforpython-6
  • Qt for Python (PySide6). Signals and Slots. Disponível em: doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html