"""Janela de seleção (*window*) × janela de exibição (*viewport*). Aula 03 — Visualização 2D: mapeamento. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python janela_viewport.py A mesma cena é desenhada quatro vezes, em quatro regiões da janela. O que muda de um quadrante para o outro é **apenas** a chamada de ``gluOrtho2D`` que define a *window* — a geometria da cena é literalmente a mesma lista de vértices nos quatro casos. quadrante window o que ensina ------------------ ------------------------- ------------------------- superior esquerdo ajustada ao aspecto referência: sem distorção superior direito quadrada, sem ajuste a mesma cena esticada inferior esquerdo reduzida pela metade zoom inferior direito deslocada pan As bordas coloridas identificam cada quadrante; o programa imprime a legenda no terminal ao iniciar. Ideias para experimentar: mude ``ZOOM`` e ``DESLOCAMENTO``; troque a ordem dos quadrantes; use um único viewport ocupando a janela inteira e compare. """ from __future__ import annotations import math import sys from PySide6.QtCore import Qt from PySide6.QtGui import QSurfaceFormat from PySide6.QtWidgets import QApplication from PySide6.QtOpenGLWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import gluOrtho2D MUNDO = 50.0 # semilargura da janela de referência, em unidades do mundo ZOOM = 0.5 # fator da window do quadrante de zoom (0.5 = metade) DESLOCAMENTO = (22.0, 14.0) # deslocamento da window do quadrante de pan # Cor da borda de cada quadrante. Serve só para identificar os quadrantes na # hora de comparar; nada tem a ver com o conteúdo desenhado. COR_AJUSTADA = (0.35, 0.85, 0.45) COR_ESTICADA = (0.95, 0.70, 0.25) COR_ZOOM = (0.35, 0.55, 0.95) COR_PAN = (0.95, 0.35, 0.35) def desenhar_cena() -> None: """Uma cena fixa no Sistema de Referência do Universo (SRU). Nenhum vértice aqui depende do tamanho da janela nem do viewport: a cena existe no mundo, e cada quadrante escolhe *que pedaço dela* olhar. """ # Chão. glColor3f(0.30, 0.34, 0.40) glLineWidth(2.0) glBegin(GL_LINES) glVertex2f(-MUNDO, -20.0) glVertex2f(MUNDO, -20.0) glEnd() # Casa: parede e telhado. O mesmo objeto da figura de sistemas de # coordenadas — ele é criado uma vez e apenas observado de vários modos. glColor3f(0.92, 0.92, 0.95) glLineWidth(3.0) glBegin(GL_LINE_LOOP) glVertex2f(-16.0, -20.0) glVertex2f(16.0, -20.0) glVertex2f(16.0, 6.0) glVertex2f(-16.0, 6.0) glEnd() glBegin(GL_LINE_LOOP) glVertex2f(-20.0, 6.0) glVertex2f(20.0, 6.0) glVertex2f(0.0, 26.0) glEnd() # Porta. glColor3f(0.75, 0.55, 0.35) glBegin(GL_LINE_LOOP) glVertex2f(-5.0, -20.0) glVertex2f(5.0, -20.0) glVertex2f(5.0, -2.0) glVertex2f(-5.0, -2.0) glEnd() # Sol, fora da casa e bem à direita: é ele que aparece ou desaparece # conforme a window muda, e é por isso que ele está aqui. glColor3f(0.95, 0.80, 0.30) glLineWidth(2.0) glBegin(GL_LINE_LOOP) ang = 0.0 while ang < 2 * math.pi: glVertex2f(36.0 + 7.0 * math.cos(ang), 32.0 + 7.0 * math.sin(ang)) ang += math.pi / 12.0 glEnd() def desenhar_borda(cor: tuple[float, float, float]) -> None: """Contorna o viewport corrente. A borda tem de ser desenhada em uma projeção própria: se usássemos a window da cena, um contorno "colado no viewport" exigiria conhecer os limites dela. Com ``gluOrtho2D(-1, 1, -1, 1)``, as bordas do viewport são sempre +-1, quaisquer que sejam os limites do mundo. """ glMatrixMode(GL_PROJECTION) glPushMatrix() glLoadIdentity() gluOrtho2D(-1.0, 1.0, -1.0, 1.0) glColor3f(*cor) glLineWidth(3.0) glBegin(GL_LINE_LOOP) glVertex2f(-0.99, -0.99) glVertex2f(0.99, -0.99) glVertex2f(0.99, 0.99) glVertex2f(-0.99, 0.99) glEnd() glPopMatrix() glMatrixMode(GL_MODELVIEW) class JanelaViewport(QOpenGLWidget): """Quatro pares (viewport, window) sobre a mesma cena.""" def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) def resizeGL(self, w: int, h: int) -> None: # Ao contrário dos exemplos da Aula 02, aqui NÃO definimos a projeção # em resizeGL: cada quadrante tem a sua, e elas são escolhidas no # paintGL. O resizeGL só guarda o tamanho da área de desenho. self._largura = max(1, w) self._altura = max(1, h) def _quadrante(self, coluna: int, linha: int) -> tuple[int, int]: """Define o viewport de um dos quatro quadrantes. ``glViewport`` responde "ONDE na tela" — em pixels, com a origem no canto inferior esquerdo da área de desenho. """ largura = self._largura // 2 altura = self._altura // 2 glViewport(coluna * largura, linha * altura, largura, altura) return largura, altura def paintGL(self) -> None: # glClear NÃO obedece ao viewport: ele varre o framebuffer inteiro, # qualquer que seja o retângulo corrente. Quem o restringe é o teste # de tesoura (glScissor + GL_SCISSOR_TEST), que não usamos aqui. # Logo, uma única limpeza no início basta para os quatro quadrantes. glClear(GL_COLOR_BUFFER_BIT) glMatrixMode(GL_MODELVIEW) glLoadIdentity() # --- 1) window ajustada ao aspecto do viewport -------------------- # É a correção da Aula 02, agora aplicada ao viewport e não à janela # inteira: o que importa para a proporção é a razão do RETÂNGULO ONDE # SE DESENHA, não a da janela do sistema operacional. largura, altura = self._quadrante(0, 1) self._window_ajustada(largura, altura, 1.0, 0.0, 0.0) desenhar_cena() desenhar_borda(COR_AJUSTADA) # --- 2) window quadrada, sem ajuste ------------------------------- # A mesma cena, esticada: o viewport é 4:3 e a window é 1:1, logo a # razão unidade-de-mundo/pixel é diferente em X e em Y. largura, altura = self._quadrante(1, 1) glMatrixMode(GL_PROJECTION) glLoadIdentity() gluOrtho2D(-MUNDO, MUNDO, -MUNDO, MUNDO) glMatrixMode(GL_MODELVIEW) desenhar_cena() desenhar_borda(COR_ESTICADA) # --- 3) zoom: a mesma window, reduzida ---------------------------- largura, altura = self._quadrante(0, 0) self._window_ajustada(largura, altura, ZOOM, 0.0, 0.0) desenhar_cena() desenhar_borda(COR_ZOOM) # --- 4) pan: a mesma window, deslocada ---------------------------- largura, altura = self._quadrante(1, 0) self._window_ajustada(largura, altura, ZOOM, *DESLOCAMENTO) desenhar_cena() desenhar_borda(COR_PAN) @staticmethod def _window_ajustada(largura: int, altura: int, fator: float, cx: float, cy: float) -> None: """``gluOrtho2D`` centrado em (cx, cy), corrigido pelo aspecto. ``gluOrtho2D`` responde "O QUÊ do mundo" — que retângulo do SRU será mapeado sobre o viewport definido logo antes. """ meia = MUNDO * fator glMatrixMode(GL_PROJECTION) glLoadIdentity() if largura <= altura: gluOrtho2D(cx - meia, cx + meia, cy - meia * altura / largura, cy + meia * altura / largura) else: gluOrtho2D(cx - meia * largura / altura, cx + meia * largura / altura, cy - meia, cy + meia) glMatrixMode(GL_MODELVIEW) def keyPressEvent(self, event) -> None: if event.key() == Qt.Key.Key_Escape: self.close() def main() -> int: print(__doc__) fmt = QSurfaceFormat() fmt.setVersion(2, 1) fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) fmt.setDepthBufferSize(0) QSurfaceFormat.setDefaultFormat(fmt) app = QApplication(sys.argv) window = JanelaViewport() window.setWindowTitle("Mapeamento — window × viewport") window.resize(800, 600) window.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())