| 
Después  de  un largo camino  por otros   temas, finalmente ha
llegado el momento de hablar de  gráficos en 3D  bajo OpenGL. No voy a
mentir diciendo  que es un tema fácil  porque no lo es. Cualquier buen
programador de   aplicaciones  3D  con   OpenGL, y  en   particular de
animaciones, debe  tener suficientes  conocimientos de álgebra lineal,
geometría analítica,  física  (mecánica)  y  por supuesto  dominar  el
análisis numérico.
 
Intentaré hacer  el resto de esta serie  lo mas accesible posible para
todo el mundo. Desafortunadamente, no hay forma de evitar la necesidad
de tener  conocimientos sobre matrices,  cómo  los planos y líneas  se
representan matemáticamente en el espacio 3D, vectores, aproximaciones
polinomiales a curvas, por mencionar solo unos pocos.
 
Durante la   última  semana he estado   pensando   como presentar este
material más complejo a   una  audiencia amplia. Los   libros clásicos
utilizan  una  metodología incremental paso  a  paso, más   o menos el
método que he seguido en los dos artículos anteriores.  He decidido no
seguir  con  esta  metodología  pues se  necesitaría  demasiado tiempo
(meses) para conseguir llevar al lector al  punto de poder escribir su
propio código.  En su lugar voy a aventurarme  a utilizar otro método,
que he  decidido llamarlo  "tratamiento  de  choque". Esta  vez voy  a
incluir una demostración   de una  de mis   simulaciones  3D  y  luego
explicare  bit  por bit  qué  hace  el código.   Finalmente, intentaré
explicar  con  más  detalle todas   las cuestiones  que normalmente se
tratan en  los libros  estándar de   OpenGL. Creo  que  saltando
directamente  al final  y dando al   lector  un ejemplo  de código con
algunas cosas interesantes incitará a los lectores  a experimentar y a
probar cosas, incluso aunque yo no haya dicho aun como funcionan todas
las cosas exactamente.  Espero que este  método funcione y la gente lo
encuentre rápido y más directo.
 
Así que pongamos  manos a la obra.  Durante  los 6 meses anteriores he
estado trabajado en la universidad de Pittsburgh en una herramienta OO
(Object  Oriented) para el desarrollo   de simulaciones de polímeros y
geles.    El  proyecto está    bastante  avanzado,  la física   es muy
interesante, incluso para  informáticos, porque un gel  es básicamente
una red  neuronal de polímeros y  muchas de las técnicas desarrolladas
para  redes neuronales se pueden aplicar  a la construcciones del gel.
He  elegido   unos cuantos  objetos   de  esta  herramienta y   los he
empaquetado    en        esta        sencilla        demo:          ../../common/May1998/example2.tar.gz.   Se   puede compilar bajo
Linux, cualquier UNIX o sobre Windows  95/NT (suponiendo que ya tienes
instalado GLUT). La demo muestra un polímero simple (una cadena lineal
de monómeros enlazados) moviéndose en suspensión en una solución a una
determinada  temperatura. La dinámica es  tentadora, se  asemeja a una
serpiente excitada.  La animación es  muy viva debido a las colisiones
de las moléculas   del solvente.  No  se puede  ver  el solvente, pero
influye en el movimiento  del polímero a través  de las ecuaciones del
movimiento.
 ![[model of polymer]](../../common/May1998/ogl1.jpg)  
El  modelo utilizado  para  dibujar el  polímero es bastante sencillo;
example2  controla  las coordenadas (x,  y,   z) de cada nodo
(monómero)   a  lo  largo  de  la  cadena polímero.    En  cada imagen
(frame) de la animación dibujamos una esfera en las coordenadas
del  monómero  y luego  los  unimos  utilizando  cilindros que  van de
monómero a monómero. Por tanto  tenemos dos primitivas elementales 3D:
una esfera   y un cilindro. Como en   cualquier molécula, la distancia
entre monómeros cambia  en el tiempo,  con lo que no podemos  utilizar
"un cilindro" para  dibujar todos los  enlaces, se ha de re-escalar de
acuerdo a la distancia entre cada par de monómeros.
  Primera pregunta:  Has dicho que  tienes dos
objetos  en 3D, una  esfera y un cilindro   unidad.  digamos que ambos
objetos están centrados  en el origen de coordenadas.  Si  todo lo que
sabemos sobre el polímero  es la secuencia de (x,  y, z) de los nodos,
¿cómo   podemos  escalar, rotar y   trasladar  las replicas  de los
cilindros para crear los enlaces de los polímeros?   
Por alguna razón que no logro comprender, los científicos informáticos
han  decidido   cambiar  el significado   clásico  de  las coordenadas
cartesianas:  x  es  horizontal,  y   es vertical  y
z va en dirección al observador. Ten  cuidado con esto porque
si vienes de una formación matemática te puede confundir bastante.
 
En la parte    superior de la  ventana  de  la  animación se   muestra
información sobre el estado de la  animación que te permitirá saber en
todo momento el tiempo, la temperatura del polímero, temperatura media
del polímero, temperatura de  la solución, la  fricción del solvente y
el ángulo de rotación  de la cámara exterior.   Para tener una  visión
más amplia del polímero desde todos los lados, la  cámara (tu punto de
vista) gira lentamente al rededor del centro de gravedad del polímero.
 
De  echo la longitud del polímero  que he elegido para  la demo es tan
corta que la rotación  de la cámara no es  realmente necesaria, con un
poco  de tiempo el polímero  llega a girar él   solo. Sigue adelante y
edita   el fichero example2.cxx y  modifica  la definición de
POLYMERLENGTH a un valor entre 2 y 100. La cámara gira porque
quiero que el lector se dé cuenta  de un aparente problema: cambia del
sistema  de coordenadas.  El sistema de  coordenadas  de los nodos es
utilizadas  por   las  ecuaciones del movimiento    y   por tato están
expresadas en coordenadas del mundo, independientes del punto de vista
concreto desde el que el  usuario observa la escena. Estas coordenadas
deben   proyectarse  a  las coordenadas   2D  x-y de   la pantalla del
ordenador.   Cada  vez  que  cambias el   punto de  vista, cambian las
formulas que  transforman  las coordenadas  internas  del polímero  en
coordenadas 2D de la ventana.
   Segunda  pregunta.   ¿Cómo solucionas este
problema?  cambiando las ecuaciones del   movimiento del mundo real  a
coordenadas 2D del  punto de vista no  es una solución,  pues requiere
demasiada álgebra, es  muy complicado de implementar  y es difícil  no
cometer errores.   
La respuesta  a la  segunda    pregunta   es  sencilla. Sólo hay  una opción:
realizar  toda  la dinámica  y la representación del  modelo 3D (polímero) en
las coordenadas del mundo y luego cambiar las coordenadas del mundo a las
coordenadas 2D del punto de vista de la cámara en el momento de dibujar
(render) la imagen. OpenGL es bastante eficiente realizando estas
transformaciones, incluso se pueden realizar por hardware (para aquellos que
posean una tarjeta gráfica con soporte OpenGL verán la diferencia). Pero
antes de entrar a describir cómo OpenGL resuelve este problema, consideremos
primero cuántas transformaciones de coordenadas hay desde el mundo real en
3D a las coordenadas finales 2D de la ventana.
   
 Primero  viene   la    transformación  de   coordenadas  del   modelo
 (Modelview),   para  proyectar las  coordenadas originales del
 mundo  a las coordenadas de la  vista (eye coordinates), estas
 son las coordenadas 3D  relativas a la  posición del ojo del que mira
 la escena (o    sea las   coordenadas  de   la  cámara).  Se    llama
 transformación Modelview porque
-----------------  It is called  Modelview  transformation  because it
really  involves   many    similar   though    distinct    operations.
----------------- 
Modelando   y viendo proyecciones,   lo  último  es análogo   a
posicionar una cámara de fotos en un estudio enfocando hacia la escena
que  se ha de  fotografiar; el  modelado de  la proyección es entonces
como posicionar el objeto a fotografiar en frente de la cámara.
 
Siguiendo  la secuencia de transformaciones,    las coordenadas de  la
vista   se  pasan   a   las   coordenadas de     transformación de  la
proyección.     El  propósito de   estas transformaciones puede
parecer un  poco esotérico a estas alturas.   Después de posicionar la
cámara en la  dirección  correcta y de posicionar   los objetos en  el
campo  de  la escena, OpenGL  quiere saber  qué cantidad  (volumen) de
campo debe  ser proyectado  sobre la ventana  2D de  la pantalla.  Por
ejemplo,  la  cámara  puede  estar  dirigida hacia  una   montaña  muy
distante, el campo que vemos define un  volumen de espacio muy grande.
Los ordenadores sólo pueden  trabajar con cosas  finitas, por ello hay
que especificar qué cantidad, de toda  la escena, ha de ser recortada.
Esta transformación también se encarga de eliminar las superficies que
no se  pueden ver.  Las  coordenadas finales obtenidas son las Clip
coordinates, recuerda  siempre    que no es   suficientes  que tus
objetos 3D estén en frente de la cámara, sino que deben estar situados
dentro  de los planos  de recorte  definidos  por la transformación de
proyección.  Las distintas  perspectivas 3D (como  cónica u ortogonal)
se definen en este nivel.
 
Por   el momento     no  entraremos  en  qué   es  una  perspective
division, ni  cuál   es la diferencia   entre las  coordenadas  de
recorte  y las coordenadas  normalizadas  de dispositivo. No es
necesario saberlo aun.
 
La  última    transformación   de   coordenadas    importante   es  la
transformación  del Viewport. Aquí las  coordenadas  3D que han
pasado por todo tipo de transformaciones 3D son finalmente proyectadas
en el área 2D de la ventana de tu ordenador.
 
Las  transformaciones   de  coordenadas se   representan  por matrices
(matrices de  dos dimensiones). Para cada  uno de los anteriores tipos
de  transformaciones  hay  una   matriz asociada.   Éstas  pueden  ser
especificadas en cualquier momento  del  programa antes de dibujar  la
imagen. OpenGL mantiene una pila  de matrices de transformación que se
han de aplicar sobre cada punto de la escena. Esta  es una técnica muy
eficiente y útil que exploraremos en futuros artículos. Por el momento
vayamos al   código   fuente,  donde se   definen  algunas   de  estas
transformaciones.  En el fichero example2.cxx encontramos las
ya familiares funciones reshape:
 
   
void mainReshape(int w, int h){  
  
  // VIEWPORT TRANSFORMATION
   glViewport(0, 0, w, h);  
  // PROJECTION TRANSFORMATION
  glMatrixMode(GL_PROJECTION);   
  glLoadIdentity();     
  glFrustum(wnLEFT, wnRIGHT, wnBOT, wnTOP, wnNEAR, wnFAR);   
  // MODELVIEW TRANSFORMATION
  glMatrixMode(GL_MODELVIEW);   
  
  ....
La directiva glViewport(x, y, width, height) especifica la
transformación del Viewport: x,  y son las coordenadas
de la  esquina  inferior izquierda  del  rectángulo de  la  ventana de
dibujo  y   width  y   height  las  dimensiones  del
viewport. Todos los números se expresan en pixels. 
Entonces    la    función     glMatrixMode(), utilizado  para
seleccionar    la matriz  actual,   es  invocada  con  los  parámetros
GL_PROJECTION para   comenzar     la  especificación   de  la
transformación  de  proyección.     Antes de  especificar    cualquier
transformación de matrices   es recomendable cargar  la matriz  unidad
(que no hace nada sobre los vértices de coordenadas), esto se hace con
glLoadIdentity(),   asigna la   matriz   unidad a  la  matriz
actual. Luego viene la declaración de la  perspectiva 3D; la sentencia
es glFrustum(left, right, bottom, top, near, far) declara los
planos de recorte en las posiciones izquierda, derecha, abajo, arriba,
cerca y  lejos.  Estos números están  especificados en coordenadas del
punto de vista (eye) y su magnitud determina la forma (la perspectiva)
del  volumen del espacio que  se va a  proyectar en el viewport
(pantalla del ordenador). Quizás parezca complicado,  a mí me llevo un
tiempo acostumbrarme.  La mejor  forma de entenderlo es experimentando
con varios  números, recuerda siempre   que has de  elegir números  de
forma que el objeto modelado-visto  caiga dentro  de los planos de
recorte o  no   verás nada en   la   pantalla.  Hay otras  formas   de
especificar la transformada de proyección.  Con el tiempo llegaremos a
verlas.
 
Finalmente      cambiamos  la matriz    actual     a  la  matriz   del
modelview, otra vez  con  la función glMatrixMode()  y
utilizando   GL_MODELVIEW    como   parámetro.   La   función
mainReshape() continua con otras cosas que no tienen nada que
ver y acaba. Lo que importa es que después de que la ventana principal
ha    sido  re-dimensionada,   esta   función    ha  especificado   el
viewport y la transformada de proyección y finalmente establece
como matriz actual, la matriz modelview.
 
Lo que sucede luego  es que la función mainDisplay() termina
la especificación del modelview y finalmente dibuja el polímero
con scene():
  
void mainDisplay(){  
  glutSetWindow(winIdMain); 
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
      // Limpiar los buffers de color y profundidad
      // Esto es como limpiar la pizarra.
        
  // Continuar con la transformación MODELVIEW: 
  // posicionar y orientar la cámara.
  glLoadIdentity();                // Cargar matriz unidad 
  glTranslatef(0.0, 0.0, -4.0);    // Mover la cámara 4 unidades atrás
      // dejamos la cámara apuntando en la dirección "-z". Realmente 
      // esto funciona moviendo la siguiente escena 4 pasos en el eje "-z".
      // Solo los elementos de la escena que queden dentro de el volumen
      // de representación (ver transformación de proyección más adelante)
      // se verán en la pantalla.
  // Dibujar el polímero
  glScalef(0.5, 0.5, 0.5); 
  glRotatef(Angle, 0, 1, 0); 
  scene();  
 
  glutSwapBuffers(); 
}; 
 
Espero no haber confundido  mucho al  lector  por haber  utilizado dos
sub-ventanas. No    estoy   explicando cuestiones  relativas    a  las
sub-ventanas   porque ya las  expliqué    en el artículo anterior  (
Gestión de  ventanas).  Si tienes alguna duda,   puedes ir a  este
artículo para refrescar la memoria.
 
Esta función es muy simple. Primero glClear borra los buffers
de color y profundidad. El  buffer de profundidad es importante  ahora
en 3D porque  la  coordenada z de   cada  vértice ha  de  ser
comparada    para   determinar     las      superficies    ocultas   y
eliminarlas.   Después cargamos la matriz   unidad en la matriz actual
modelview e invocamos las tres transformaciones de modelado:
 
  glTranslatef(xt, yt, zt) , esto  traslada el sistema de
coordenadas actuales por  el vector (xt,  yt, zt). En nuestro
caso, tiene el efecto  de mover la cámara 4  unidades hacia atrás a lo
largo de z, esto es, alejarse del modelo. Si no hacemos esto,
la cámara se quedaría en el origen, en medio de  la escena, con lo que
difícilmente podríamos ver nada.
  glScalef(xs, ys, zs) , como su nombre indica, su misión
es  la de escalar el  sistema de coordenadas  por los factores xs,
ys, zs a lo largo  de los ejes  x, y, z respectivamente.
Este escalado es necesario para que la escena expresada en coordenadas
del mundo (polímero) quepa en las coordenadas del volumen visible.
   glRotatef(angle, vx, vy,  vz) , gira el sistema actual
de coordenadas a lo largo del vector normalizado (vx, vy, vz)
el  ángulo  angle.   Este es  el  truco  que  utilizamos para
producir la ilusión de que la cámara gira al rededor de la escena.  De
hecho  es la escena la  que está girando.  Hay  muchas otras formas de
mover la cámara, pero por el momento está es la más sencilla.
 
Unas palabras de advertencia:  el  orden  en  el  que se aplican   las
transformaciones    de  modelado  es   muy  importante.   Es necesario
comprender qué es lo que sucede con la matriz de Modelview cada
vez  que  invocas    a   una transformación   de   coordenadas.   Cada
transformación Ti  se representa matemáticamente por
una matriz Mi.  La superposición de una secuencia de
transformadas Tn Tn-1...    T1
(por ejemplo: translación +  escalado  +  rotación ) es   representado
matemáticamente por  una única matriz  M = Mn
Mn-1 ....  M1. El orden es crucial
porque cuando  la transformación  compuesta  M actúa  sobre  un
vértice v, las transformaciones  son realmente aplicadas  en el
orden inverso:
M  v         =   Mn    Mn-1
.... M1 v 
Primero  M1, luego  M2, etc..    y
finalmente Mn.   En    nuestro código   ejemplo,  he
declarado la transformación en    el siguiente orden:  translación  ->
escalado ->  rotación.   Por tanto, cada   punto   del modelo en   las
coordenadas del mundo va a ser rotado ->  escalado -> trasladado antes
de ser proyectado sobre la pantalla gráfica. 
 
Siempre  has de  tener este orden  inverso  de transformaciones en  la
cabeza  cuando  escribas  código,  en caso  contrario   puedes obtener
resultados no deseados muy sorprendentes.
 
La  función  scene() sencillamente   ejecuta el   dibujado 3D
(render) del  objeto  polímero.  Para  entender  como  se construye el
modelo 3D, tenemos que ir al fichero Gd_opengl.cxx y echar un
vistazo a la función miembro draw(GdPolymer &p). Hay un bucle
principal que pasa   por cada  monómero  de  la cadena  del  polímero,
obtiene   sus  coordenadas x,y,z dibuja  una    esfera en esa
posición, y luego dibuja los  cilindros a lo  largo de los enlaces que
conectan cada  monómero  ¿Recuerdas la primera pregunta?  Aquí tenemos
una posible solución... Si encuentras otra más rápida dímelo.
 
Hay un cosa más  que el lector  debe saber para entender completamente
la rutina  de dibujado del   polímero ¿Para qué  sirven  lasfunciones:
glPushMatrix() y glPopMatrix()?
 
Hay sólo   dos primitivas geométricas  en el  modelo del polímero, una
esfera de  radio 0.40  centrada en el  origen  y un cilindro  superior
derecho de altura 1.0 y radio 0.4. El polímero se construye utilizando
dos primitivas y una serie de transformaciones para situar las esferas
y cilindros  en las posicies correctas.  Cada vez que se  ejecutan las
sentencias                 glCallList(MONOMER)              o
glCallList(CYLINDER) se dibuja una nueva esfera y cilindro en
el origen.  Para  mover las esferas   a las coordenadas x,y,z
necesitamos una translación (ver glTranslatef(x, y, z)); para
dibujar y posicionar un cilindro es  más complicado porque tenemos que
orientarlo  en la dirección    adecuada (en mi algoritmo   utilizo una
transformación de escalado->rotación).
 
Pero cualquiera que sea el  método que utilices para construir modelos
complejos 3D, no hay  duda que necesitarás varias transformaciones  de
translación,  rotación  y escalado.    Cuando  se  invoca  la  función
scene(), la matriz actual de la  máquina de estados OpenGL es
la  matriz MODELVIEW,  como he mencionado   anteriormente, ésta  es la
matriz que   representa la proyección del   modelo  de coordenadas del
mundo a coordenadas de recorte. Éste es un problema serio, mientras la
matriz   MODELVIEW  sea todavía   la matriz   activa,  cualquier nueva
transformación aplicada  para construir el modelo  3D  se añadirá a la
matriz   actual,  con  la  consecuencia   indeseable   de destruir  la
transformación MODELVIEW.  De forma   similar, algunas veces  queremos
aplicar determinadas transformaciones  3D a una  parte del modelo pero
no   a   otras (por   ejemplo,  escalar    un  cilindro   pero no   la
esfera). OpenGL resuelve este problema utilizando  una pila interna de
matrices.  Hay dos operaciones  básicas sobre esta  pila implementadas
mediante      glPushMatrix()   (meter   en   pila)          y
glPopMatrix() (sacar de pila).  Examina una vez más el código
fuente de scene() y observarás que antes de dibujar la esfera
de cada  nomómero  llamamos una vez   a "push", para  mover  la matriz
MODELVIEW  a  la pila, y   al final del  bucle llamamos   a "pop" para
restaurar la matriz MODELVIEW. El bucle interno que dibuja los enlaces
del polímero   tiene  sus propios  "push"   y "pop"   para  aislar las
transformaciones de  escalado  y  rotación de  las   translaciones que
afectan a ambos, esfera y cilindro.
 
Hay mucho más que decir sobre transformaciones 3D y pilas de matrices.
En este artículo sólo hemos arañado la superficie de estas cuestiones.
Por el momento  lo dejaremos así  y dejaremos que el lector interesado
explore el código fuente  de la demo e  intente construir  sus propios
modelos 3D.  El código example2  también utiliza unas cuantas
característica aun no estudiadas: materiales e iluminación. Dejamos la
presentación  de estos temas para artículos  futuros.  La próxima vez,
continuaremos explorando con mayor profundidad las transformaciones 3D
y las  pilas  de  matrices, también  mostraremos  como  utilizar ambas
características  de  OpenGL para  implementar   un robot  móvil. Hasta
entonces, pasatelo bien con OpenGL.
 |