"""Atividade — as três pirâmides da Necrópole de Gizé. Aula 06 — Transformações Geométricas 3D em OpenGL. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python piramides_gize_base.py O objetivo é desenhar TRÊS instâncias de um mesmo modelo — Miquerinos, Quéfren e Quéops —, cada uma com sua própria translação e sua própria escala. É o instanciamento da Aula 04 (um modelo-base, uma tabela de parâmetros, várias cópias) levado para os três eixos. A função de desenho do modelo (``desenhar_piramide_aramada``) e o posicionamento do observador (``_posicionar_observador``) já estão prontos. Complete os dois TODOs sem alterar os vértices do modelo: o que diferencia uma pirâmide da outra é a matriz aplicada antes da chamada de desenho, nunca uma segunda lista de vértices. O arquivo já roda como está, mas a janela abre VAZIA — o título continua mostrando o estado da câmera, então dá para confirmar que o programa está de pé. Resolva os TODOs em ordem, rodando o programa depois de cada um: cada passo mostra na tela por que o próximo é necessário. Especificações do enunciado (não altere): Espessura de linha ....... 5.0 Cor de linha ............. (0.863, 0.796, 0.710) Cor de fundo ............. (0.0, 0.0, 0.0, 0.0) Observador inicial ....... obs = (1.75, 1.75, 8.5); rot_x = 0; rot_y = 45 Teclas: setas esquerda/direita giram o conjunto em torno de Y (rot_y) setas cima/baixo giram o conjunto em torno de X (rot_x) W / S aproximam / afastam o observador (obs_z) 0 restaura o estado inicial do observador Esc fecha """ from __future__ import annotations import sys from PySide6.QtCore import Qt from PySide6.QtGui import QSurfaceFormat from PySide6.QtOpenGLWidgets import QOpenGLWidget from PySide6.QtWidgets import QApplication from OpenGL.GL import * from OpenGL.GLU import gluLookAt, gluPerspective # --- especificações do enunciado ---------------------------------------- COR_LINHA = (0.863, 0.796, 0.710) COR_FUNDO = (0.0, 0.0, 0.0, 0.0) ESPESSURA_LINHA = 5.0 OBSERVADOR_INICIAL = (1.75, 1.75, 8.5) ROT_X_INICIAL = 0.0 ROT_Y_INICIAL = 45.0 ALVO = (0.0, 0.0, 0.0) UP = (0.0, 1.0, 0.0) # O campo de visão é mais aberto do que os 45-60° dos outros exemplos do # curso por uma razão concreta: a necrópole é rasa mas MUITO espalhada (10 # unidades de ponta a ponta em z) em relação à distância do observador, que # o enunciado fixa em obs_z = 8.5. Com fovy = 45°, Quéops — a maior e a # mais próxima — não cabe na tela depois que a cena é girada para os 45° # iniciais. Vale conferir trocando por 45.0 e rodando de novo: é a # pergunta 4 de "Para responder" da atividade, e liga direto com o fovy de # gluPerspective visto na Aula 05. FOVY = 78.0 PLANO_NEAR = 0.1 PLANO_FAR = 200.0 PASSO_ROTACAO = 4.0 PASSO_ZOOM = 0.4 # --- tabela de instanciamento ------------------------------------------- # Cada item: nome, translação (Tx, Ty, Tz) e escala uniforme. # TODO 1: complete com Miquerinos, Quéfren e Quéops, usando a tabela da # aula. Enquanto a lista estiver vazia, nada é desenhado. PIRAMIDES = [] # --- modelo-base --------------------------------------------------------- # Base quadrada no plano xz, ápice em +y, tudo em torno da PRÓPRIA origem # do modelo. As proporções seguem as das pirâmides reais: a base (1.6 de # lado) é mais larga do que a pirâmide é alta (1.0). ALTURA = 1.0 MEIO_LADO = 0.8 VERTICES = [ (0.0, ALTURA, 0.0), # 0: ápice (-MEIO_LADO, 0.0, -MEIO_LADO), # 1: base, -x -z (MEIO_LADO, 0.0, -MEIO_LADO), # 2: base, +x -z (MEIO_LADO, 0.0, MEIO_LADO), # 3: base, +x +z (-MEIO_LADO, 0.0, MEIO_LADO), # 4: base, -x +z ] ARESTAS = [ (0, 1), (0, 2), (0, 3), (0, 4), # quatro arestas do ápice à base (1, 2), (2, 3), (3, 4), (4, 1), # o quadrado da base ] def desenhar_piramide_aramada() -> None: """Desenha o modelo-base em wireframe, em torno da própria origem. Quem decide ONDE e com que TAMANHO esta pirâmide aparece é a matriz GL_MODELVIEW corrente no momento da chamada — nunca esta função. É o mesmo contrato de _desenhar_estrela (Aula 04) e de _desenhar_cubo_aramado (tg3d_cubo.py). """ glBegin(GL_LINES) for inicio, fim in ARESTAS: glVertex3f(*VERTICES[inicio]) glVertex3f(*VERTICES[fim]) glEnd() class PiramidesGize(QOpenGLWidget): """Três instâncias do mesmo modelo, cada uma com sua T e sua E.""" def __init__(self, parent=None): super().__init__(parent) self.obs = list(OBSERVADOR_INICIAL) self.rot_x = ROT_X_INICIAL self.rot_y = ROT_Y_INICIAL self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) def initializeGL(self) -> None: glClearColor(*COR_FUNDO) glEnable(GL_DEPTH_TEST) def resizeGL(self, w: int, h: int) -> None: dpr = self.devicePixelRatioF() w, h = max(1, round(w * dpr)), max(1, round(h * dpr)) glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) glLoadIdentity() gluPerspective(FOVY, w / float(h), PLANO_NEAR, PLANO_FAR) glMatrixMode(GL_MODELVIEW) # sempre voltar para a modelo-visão def _posicionar_observador(self) -> None: """Câmera da cena: gluLookAt e, depois, os ângulos de órbita. Equivale à PosicionaObservador do exemplo em C do enunciado. gluLookAt é a PRIMEIRA chamada depois de glLoadIdentity(); os glRotatef seguintes giram a cena INTEIRA diante da câmera — não um objeto em particular, e por isso ficam fora do push/pop de cada instância. """ glMatrixMode(GL_MODELVIEW) glLoadIdentity() gluLookAt(*self.obs, *ALVO, *UP) glRotatef(self.rot_x, 1.0, 0.0, 0.0) glRotatef(self.rot_y, 0.0, 1.0, 0.0) def _desenhar_cena(self) -> None: glColor3f(*COR_LINHA) glLineWidth(ESPESSURA_LINHA) # TODO 2: para cada instância em PIRAMIDES: # 1. salve a matriz corrente com glPushMatrix(); # 2. aplique a translação e a escala da instância; # 3. chame desenhar_piramide_aramada(); # 4. restaure a matriz com glPopMatrix(). # # Dois detalhes valem a atenção, e são o que esta atividade # exercita: # # ORDEM — a regra "T, R, S no código" da Aula 04 diz que a chamada # mais PRÓXIMA do desenho é a que age PRIMEIRO sobre o vértice. # Você quer que a pirâmide seja redimensionada em torno da # própria origem e só depois levada até a posição da tabela; a # ordem oposta faria a escala multiplicar também o deslocamento. # # ISOLAMENTO — experimente rodar uma vez SEM os passos 1 e 4 (só # transladar, escalar e desenhar). As transformações em OpenGL # são cumulativas: sem restaurar a matriz, a segunda pirâmide # herda a transformação da primeira e a terceira herda as duas # anteriores. Vale ver o estrago na tela antes de consertar. pass # apague este pass ao completar o TODO def paintGL(self) -> None: glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self._posicionar_observador() self._desenhar_cena() self.setWindowTitle( f"Pirâmides de Gizé — obs=({self.obs[0]:.2f}, {self.obs[1]:.2f}, " f"{self.obs[2]:.2f}) rot_x={self.rot_x:.0f}° " f"rot_y={self.rot_y:.0f}°" ) def keyPressEvent(self, event) -> None: tecla = event.key() if tecla == Qt.Key.Key_Escape: self.close() return if tecla == Qt.Key.Key_0: self.obs = list(OBSERVADOR_INICIAL) self.rot_x, self.rot_y = ROT_X_INICIAL, ROT_Y_INICIAL elif tecla == Qt.Key.Key_Left: self.rot_y -= PASSO_ROTACAO elif tecla == Qt.Key.Key_Right: self.rot_y += PASSO_ROTACAO elif tecla == Qt.Key.Key_Up: self.rot_x -= PASSO_ROTACAO elif tecla == Qt.Key.Key_Down: self.rot_x += PASSO_ROTACAO elif tecla == Qt.Key.Key_W: self.obs[2] = max(1.0, self.obs[2] - PASSO_ZOOM) elif tecla == Qt.Key.Key_S: self.obs[2] += PASSO_ZOOM else: return self.update() def main() -> int: formato = QSurfaceFormat() formato.setVersion(2, 1) formato.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) formato.setDepthBufferSize(24) QSurfaceFormat.setDefaultFormat(formato) app = QApplication(sys.argv) janela = PiramidesGize() janela.setWindowTitle("Pirâmides de Gizé") janela.resize(900, 600) janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())