"""Zoom e pan em 3D: mexendo na câmera, não na cena. Aula 05 — Câmera Sintética e Projeção em Perspectiva. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python zoom_pan_3d.py Compare com zoom_pan.py (Aula 03): lá o zoom mudava os limites de gluOrtho2D e o pan deslocava o centro da window, sem tocar em nenhum vértice da cena. Aqui a ideia é a mesma, adaptada a duas peças novas desta aula: * **zoom**: encolhe o ângulo de abertura (``fovy``) de ``gluPerspective`` — o mesmo efeito de "aproximar com uma lente", sem mover a câmera; * **pan**: desloca olho e alvo JUNTOS, na direção lateral da câmera — a cena passa a ser vista de outro lugar, mas com o mesmo enquadramento. Teclas: + - zoom (estreita ou alarga o fovy) setas pan (desloca olho + alvo lateralmente e na vertical) R volta ao estado inicial Esc fecha O título mostra o fovy e a posição corrente do olho. """ 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 gluPerspective, gluLookAt FOVY_INICIAL = 55.0 FOVY_MIN, FOVY_MAX = 10.0, 100.0 FATOR_ZOOM = 1.15 PASSO_PAN = 0.6 OLHO_INICIAL = (0.0, 2.0, 9.0) ALVO_INICIAL = (0.0, 0.5, 0.0) UP = (0.0, 1.0, 0.0) TAMANHO_CHAO = 8.0 PASSO_GRADE = 1.0 BLOCOS = [ (-2.0, 0.5, -1.5, 0.5, (0.86, 0.35, 0.35)), (1.8, 0.4, -2.2, 0.4, (0.35, 0.65, 0.86)), (0.2, 0.6, 1.6, 0.6, (0.45, 0.80, 0.45)), ] class ZoomPan3D(QOpenGLWidget): """Chão e blocos fixos; fovy, olho e alvo mudam por tecla.""" def __init__(self, parent=None): super().__init__(parent) self.fovy = FOVY_INICIAL self.olho = list(OLHO_INICIAL) self.alvo = list(ALVO_INICIAL) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self._largura, self._altura = 1, 1 def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) glEnable(GL_DEPTH_TEST) def resizeGL(self, w: int, h: int) -> None: dpr = self.devicePixelRatioF() self._largura = max(1, round(w * dpr)) self._altura = max(1, round(h * dpr)) glViewport(0, 0, self._largura, self._altura) def paintGL(self) -> None: # Como em zoom_pan.py (Aula 03): a projeção depende de estado que o # usuário controla (fovy), então é refeita a cada quadro aqui, e # não só no resizeGL. glMatrixMode(GL_PROJECTION) glLoadIdentity() aspecto = self._largura / float(self._altura) gluPerspective(self.fovy, aspecto, 0.1, 100.0) glMatrixMode(GL_MODELVIEW) glLoadIdentity() gluLookAt(*self.olho, *self.alvo, *UP) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self._desenhar_chao() for cx, cy, cz, s, cor in BLOCOS: self._desenhar_bloco(cx, cy, cz, s, cor) self.setWindowTitle( f"Zoom e pan 3D — fovy={self.fovy:.1f}° olho=(" f"{self.olho[0]:.1f}, {self.olho[1]:.1f}, {self.olho[2]:.1f})" ) @staticmethod def _desenhar_chao() -> None: glColor3f(0.30, 0.32, 0.38) glLineWidth(1.0) glBegin(GL_LINES) v = -TAMANHO_CHAO while v <= TAMANHO_CHAO: glVertex3f(v, 0.0, -TAMANHO_CHAO) glVertex3f(v, 0.0, TAMANHO_CHAO) glVertex3f(-TAMANHO_CHAO, 0.0, v) glVertex3f(TAMANHO_CHAO, 0.0, v) v += PASSO_GRADE glEnd() @staticmethod def _desenhar_bloco(cx: float, cy: float, cz: float, s: float, cor) -> None: x0, x1 = cx - s, cx + s y0, y1 = cy - s, cy + s z0, z1 = cz - s, cz + s faces = [ ((x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)), ((x1, y0, z0), (x0, y0, z0), (x0, y1, z0), (x1, y1, z0)), ((x0, y1, z1), (x1, y1, z1), (x1, y1, z0), (x0, y1, z0)), ((x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1)), ((x1, y0, z1), (x1, y0, z0), (x1, y1, z0), (x1, y1, z1)), ((x0, y0, z0), (x0, y0, z1), (x0, y1, z1), (x0, y1, z0)), ] glColor3f(*cor) glBegin(GL_QUADS) for face in faces: for v in face: glVertex3f(*v) glEnd() def _vetor_lateral(self): """Vetor unitário 'para a direita' da câmera, no plano xz. Perpendicular ao vetor de mira (alvo - olho) e ao up — é o mesmo produto vetorial que gluLookAt usa por baixo dos panos para montar o SRC. Aqui ele serve só para o pan andar "de lado" em vez de para frente ou para trás. """ mira = [self.alvo[i] - self.olho[i] for i in range(3)] lateral = ( mira[1] * UP[2] - mira[2] * UP[1], mira[2] * UP[0] - mira[0] * UP[2], mira[0] * UP[1] - mira[1] * UP[0], ) norma = math.sqrt(sum(c * c for c in lateral)) or 1.0 return [c / norma for c in lateral] def keyPressEvent(self, event) -> None: tecla = event.key() if tecla == Qt.Key.Key_Escape: self.close() return elif tecla in (Qt.Key.Key_Plus, Qt.Key.Key_Equal): self.fovy = max(FOVY_MIN, self.fovy / FATOR_ZOOM) elif tecla == Qt.Key.Key_Minus: self.fovy = min(FOVY_MAX, self.fovy * FATOR_ZOOM) elif tecla in (Qt.Key.Key_Left, Qt.Key.Key_Right): lateral = self._vetor_lateral() sinal = -1.0 if tecla == Qt.Key.Key_Left else 1.0 for i in range(3): desloc = sinal * PASSO_PAN * lateral[i] self.olho[i] += desloc self.alvo[i] += desloc elif tecla in (Qt.Key.Key_Up, Qt.Key.Key_Down): sinal = 1.0 if tecla == Qt.Key.Key_Up else -1.0 self.olho[1] += sinal * PASSO_PAN self.alvo[1] += sinal * PASSO_PAN elif tecla == Qt.Key.Key_R: self.fovy = FOVY_INICIAL self.olho = list(OLHO_INICIAL) self.alvo = list(ALVO_INICIAL) else: return self.update() def main() -> int: fmt = QSurfaceFormat() fmt.setVersion(2, 1) fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) fmt.setDepthBufferSize(24) QSurfaceFormat.setDefaultFormat(fmt) app = QApplication(sys.argv) janela = ZoomPan3D() janela.setWindowTitle("Zoom e pan 3D") janela.resize(800, 600) janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())