Clase 5 — Primer dibujo 3D en OpenGL y pila de matrices Model View
Resumen Ejecutivo
Primera clase de 3D real en OpenGL. Se introduce el control de profundidad (GL_DEPTH_TEST), el sistema de ejes de mano derecha, el dibujo de un cubo con sus 8 vértices y 6 caras de colores, el módulo auxiliar IGV_Utils que se usará en el examen, y — concepto crítico — la pila de matrices Model View con glPushMatrix / glPopMatrix. Este último punto es la clave para que los objetos de una escena 3D no se "contaminen" entre sí. ⚠️ EXAMEN: el examen será un problema 3D (en pasadas convocatorias se planteó parte 2D + 3D, pero ya es solo 3D) y el módulo IGV_Utils estará disponible.
Anuncios importantes
- Cambio de temario año que viene: se elimina OpenCV; la asignatura será solo OpenGL + iluminación, texturas, animaciones. ⚠️ Recomendación del profesor: aprobar este año, porque los cambios de temario perjudican al alumno que repite.
- Tema 6 (eliminación de superficies ocultas) es casi puramente divulgativo en este curso (~10 min). Servirá de colchón si se retrasa la programación.
- Examen en 3D. El módulo IGV_Utils será usable también en examen.
Conceptos Clave
- Máquina de estados: OpenGL mantiene variables internas (color, grosor,
GL_DEPTH_TEST, etc.) que se activan o desactivan conglEnable/glDisable. Estas variables persisten hasta que se cambien. - Control de profundidad (
GL_DEPTH_TEST): OpenGL calcula qué caras están delante y cuáles detrás según la posición de la cámara, de modo que las caras ocultas no se pinten encima de las visibles. Sin él, OpenGL pinta en el orden en que se declaran los polígonos, provocando artefactos visuales. ⚠️ EXAMEN: siempre habilitarlo al pintar en 3D. - Sistema de ejes de mano derecha: X hacia la derecha, Y hacia arriba, Z saliendo de la pantalla hacia el espectador. Z negativo va hacia el fondo. ⚠️ EXAMEN
- Punto de vista por defecto: cámara situada en el origen mirando hacia
-Z. Por eso las caras "frontales" se definen respecto a ese eje. Se estudiará cómo cambiarlo en el tema 4. - Proyección gabinete: proyección 3D → 2D que OpenGL usa por defecto en estos ejercicios. Se estudia en el tema 5; por ahora se acepta como está.
- Matriz Model View: matriz 4×4 que acumula todas las transformaciones (traslaciones, rotaciones, escalados) aplicadas a un objeto. Es acumulativa — y ese es el problema central a resolver.
- Pila de matrices Model View: estructura LIFO que OpenGL mantiene por cada tipo de matriz. La cima es la matriz activa.
glPushMatrix()duplica la cima;glPopMatrix()descarta la cima. ⚠️ EXAMEN - Módulo IGV_Utils: módulo Python con funciones reutilizables (
cube,color_cube,axis,printMatrix,drawText,drawText3D). Disponible en examen.
Desarrollo del Temario
1. Primera modificación al pasar de 2D a 3D
En initGL, las dos instrucciones de 2D (glMatrixMode(GL_PROJECTION) + gluOrtho2D(...)) se sustituyen por:
glEnable(GL_DEPTH_TEST)
Mac:
glEnable(GL_DEPTH_TEST)puede no funcionar por el abandono de OpenGL por parte de Apple. En clase los alumnos de Mac comprobaron que sí funciona con la invocación estándar. Si apareciera el problema, existe una secuencia alternativa (compartida por el profesor por el foro).
2. Función cube(h) — dibujar un cubo unitario
Cubo centrado en el origen, lado 1. Parámetro h = 0.5 (semilado) para simplificar las coordenadas de los 8 vértices.
Por qué 8 vértices y no 4: un cubo tiene 6 caras, cada una con 4 esquinas, pero las esquinas se comparten entre 3 caras cada una. El total son 8 vértices únicos.
Orientación: el cubo se nombra asumiendo que se mira desde +Z hacia -Z. "Cara frontal" = cara más cercana al espectador (plano Z = +0.5).
def cube(h):
# Cara frontal (z = +h), color rosa
glColor3f(1, 0.5, 0.5)
glBegin(GL_POLYGON)
glVertex3f(-h, -h, h)
glVertex3f( h, -h, h)
glVertex3f( h, h, h)
glVertex3f(-h, h, h)
glEnd()
# ... 5 caras más
3. Función axis(xp, xn, yp, yn, zp, zn, ticks) — ejes 3D
Pinta los 6 semiejes con código de colores fijo: ⚠️ EXAMEN
| Semieje | Color |
|---|---|
| X+ | Azul oscuro |
| X− | Azul claro |
| Y+ | Verde oscuro |
| Y− | Verde claro |
| Z+ | Rojo oscuro |
| Z− | Rojo claro |
Parámetro ticks (bool): dibuja marcas cada 1 unidad.
4. Efecto del GL_DEPTH_TEST
Sin control de profundidad, OpenGL dibuja las caras en el orden en el que aparecen en el código. Si la cara frontal (primera) se dibuja primero y las siguientes se pintan encima, se ven superpuestas de forma irreal.
Con glEnable(GL_DEPTH_TEST), OpenGL calcula la profundidad Z de cada fragmento y solo pinta los que están "delante" según la cámara, produciendo la escena correcta.
5. El problema de la Model View acumulativa
Escenario del cuaderno: una escena con dos objetos —pila de cubos vertical (apilados en Y) y cola de cubos horizontal (alineados en X).
def stack(N, colorV):
for i in range(N):
glTranslatef(0, 2, 0) # cada cubo 2 unidades arriba
color_cube(colorV)
def queue(N, colorV):
for i in range(N):
glTranslatef(2, 0, 0) # cada cubo 2 unidades a la derecha
color_cube(colorV)
def display():
# ... proyección
axis(...)
stack(4, 'gris')
queue(4, 'rojo')
Resultado (incorrecto): la cola aparece "flotando" en lo alto de la pila. ¿Por qué? La Model View acumula traslaciones. Tras stack(4, ...), la matriz contiene una traslación global de 8 unidades en Y. Cuando se ejecuta queue(4, ...), arranca desde esa traslación heredada.
Imprimiendo la matriz con printMatrix('m', 'después de stack') se ve claramente el 8 acumulado en la posición de traslación Y.
Intuición clave: la Model View es "global". Todo lo que uno escribe se acumula. En una escena con varios objetos, cada objeto contamina al siguiente.
6. Solución: pila de matrices Model View
OpenGL no mantiene una matriz por tipo, sino una pila. La cima es la matriz activa.
Operaciones (ojo al matiz): ⚠️ EXAMEN
glPushMatrix()— duplica la cima. Hace una copia. NO apila un elemento externo. La nueva cima es idéntica a la anterior.glPopMatrix()— descarta la cima. NO extrae hacia ningún sitio, solo la elimina. La matriz activa vuelve a ser la anterior.
Diferencia con la pila de toda la vida: en DS clásica
pushañade un elemento nuevo ypopsaca y devuelve. Aquípushduplica ypopsolo borra.
Selección de la pila activa: antes de usar push/pop hay que asegurar que la pila activa es la Model View:
glMatrixMode(GL_MODELVIEW)
7. Patrón: salvar y restaurar la Model View por objeto ⚠️ EXAMEN
Regla de oro para dibujar objetos independientes:
def objeto(...):
glMatrixMode(GL_MODELVIEW)
glPushMatrix() # salva el estado de entrada
# ... transformaciones y dibujo del objeto ...
glPopMatrix() # restaura el estado al salir
Con esto, el siguiente objeto encuentra la Model View como estaba antes de entrar en este.
Aplicación al ejemplo:
def stack(N, colorV):
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
for i in range(N):
glTranslatef(0, 2, 0)
color_cube(colorV)
glPopMatrix()
def queue(N, colorV):
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
for i in range(N):
glTranslatef(2, 0, 0)
color_cube(colorV)
glPopMatrix()
Resultado: pila en su sitio, cola en su sitio, sin contaminación mutua.
Tras el
glPopMatrix()dequeue, imprimiendo la Model View se obtiene la identidad (estado inicial de OpenGL, porque no se ha cargado nada en la raíz de la pila).
8. Objetos complejos y push/pop anidados
Un objeto complejo (un personaje con piezas: torso, brazo, pierna) puede necesitar transformaciones locales a cada pieza que no deben arrastrarse a las hermanas. Solución: glPushMatrix / glPopMatrix anidados internamente.
Regla invariante: cada glPushMatrix debe emparejarse exactamente con un glPopMatrix. Si hay N push dentro del dibujo de un objeto, debe haber N pop.
9. Módulo IGV_Utils
Disponible en la documentación de la asignatura y en el examen. Contiene:
| Función | Uso |
|---|---|
printMatrix(m, texto) |
Imprime la matriz activa (m = ModelView, p = Projection) con un texto descriptivo. |
cube(h) |
Cubo centrado en origen, semilado h, caras multicolor. |
color_cube(color) |
Como cube, pero todas las caras del mismo color (para la actividad grupal tipo Minecraft). |
axis(xp, xn, yp, yn, zp, zn, ticks) |
Ejes 3D con código de colores y marcas opcionales. |
drawText(x, y, texto) |
Texto 2D. |
drawText3D(x, y, z, texto) |
Texto plano posicionado en una escena 3D. |
Importar: from IGV_Utils import * al inicio de cada cuaderno.
10. Ejercicios hechos en clase
- Cubo centrado en origen.
cube(0.5)trasglLoadIdentity(). - Cubo trasladado.
glTranslatef(5, 5, 5)+cube(0.5)→ cubo desplazado a(5,5,5). - Cubo escalado.
glScalef(3, 3, 3)+cube(0.5)→ cubo de lado 3 alrededor del origen.
Preguntas de Autoevaluación
- ¿Qué efecto tiene
glEnable(GL_DEPTH_TEST)? Habilita el control de profundidad: OpenGL decide qué caras son visibles según la cámara en lugar de pintar por orden de declaración. - ¿Cuántos vértices tiene un cubo y por qué? 8, porque cada esquina se comparte entre 3 caras.
- ¿En qué dirección sale el eje Z por defecto en OpenGL? Hacia el espectador (+Z saliendo de la pantalla; convención de mano derecha).
- ¿Cuál es el punto de vista por defecto? Desde el origen mirando hacia -Z.
- ¿Qué hace
glPushMatrix()exactamente? Duplica la matriz de la cima de la pila activa. - ¿Y
glPopMatrix()? Descarta la cima de la pila activa (no extrae hacia ningún sitio). - ¿Por qué la Model View causa problemas si no se gestiona con push/pop? Porque es acumulativa: las transformaciones de un objeto persisten y afectan al siguiente.
- Patrón canónico para dibujar un objeto.
glMatrixMode(GL_MODELVIEW); glPushMatrix(); [...dibujo...]; glPopMatrix(). - ¿Qué se obtiene al imprimir la Model View justo después de haber equilibrado todos los
push/popal arrancar OpenGL? La matriz identidad. - ¿Qué código de color asigna IGV_Utils al eje X positivo y al Z positivo? Azul oscuro (X+) y rojo oscuro (Z+).
- Si hago 4
glPushMatrix()dentro del dibujo de un objeto, ¿cuántosglPopMatrix()tengo que hacer? 4, emparejados 1 a 1. - ¿Qué herramienta de OpenGL es una "máquina de estados" y cómo se cambian sus estados? OpenGL entera. Sus estados se cambian con
glEnable,glDisabley funciones específicas comoglColor,glLineWidth, etc.
Referencias a cuadernos
tema3_primer_dibujo_3D.ipynb— base del primer ejemplo.tema3_transformaciones_3D.ipynb— pendiente de ver próxima clase.ejemplo_pila_matrices_ModelView_3D.ipynb— ejemplo stack + queue usado en clase.IGV_Utils.py— módulo obligatorio; guardar en el directorio de trabajo.