|
|
Artículo realizado por Parece que ya vamos empezando a meternos
de lleno en el mundillo del 3D. Ya estamos hablando en términos
bastante más serios que al principio y en poco tiempo seremos
capaces de programar alguna que otra aplicación 3D de calidad. En este número os presento varias
capturas de un ejemplo hecho a medida para Macedonia, o
sea para vosotros. Al final os daré también el código
para que podáis compilarlo, ejecutarlo y alterarlo a vuestro
gusto. Vamos pues con más materia y.....ya
que aún no os había visto, ¡ Feliz Año
Nuevo ! Todos estamos acostumbrados a utilizar coordenadas
cartesianas para representar los vértices que definen a
nuestra geometría. Es decir un punto es algo así: P = ( x, y, z)
y representa una determinada localización en un espacio
3D. Pero cuando programamos Gráficos
hablamos de puntos y de vectores y pueden confundirse
en cuanto a representación. Si entendemos que un vector
es una resta entre dos puntos... Vector v = Punto1 - Punto2 = (x1, y1,
z1) - (x2, y2, z2) = (a, b, c) y acaso (a, b, c) no parece también
un punto ??? Por otra parte trabajaremos modelando
geometría para luego transformarla....trasladándola
a otra posición, rotándola respecto de un eje, escalándola
para cambiar su tamaño...Estas son las llamadas transformaciones
afines/rígidas/lineales. Dado que operamos usando matrices
para efectuar estas transformaciones necesitamos modificarlas
ligeramente por dos motivos: Es muy sencillo convertir un vector o un
punto cartesiano a su representación homogénea.
De hecho lo que se hace es añadir una nueva coordenada
a las típicas XYZ. Añadimos la componente W
de esta forma: Los valores típicos para la nueva
componente son: Por tanto el caso anterior queda modificado
de la siguiente manera: Veréis más adelante en este
capítulo como este convenio nos permite operar transformando
un punto en otro punto y un vector en otro vector....y nada de
cruces extraños entre uno y otro !!! Pero y si W es diferente de lo que dices
... ? En ese caso tendremos que efectuar una sencilla
operación para transformar un punto homogéneo en
uno cartesiano. Si tenemos el punto...: Punto P1 = (x1, y1, z1, w1) en homogéneas... ...entonces en cartesianas el punto es P1
= (x1/w1, y1/w1, z1/w1) Es decir que normalizamos cada una
de las componentes XYZ del punto por su componente W. Claro en
el caso de W = 1 no hay que hacer nada pues la división
es obvia pero puede pasar que nos interese variar W y entonces
no podremos usar las XYZ hasta haberlas normalizado según
os acabo de explicar. Según lo que os acabo de decir deberíais
ver clarísimo que: P = (1, 2, 3, 1) = (2, 4, 6, 2) = (5,
10, 15, 5).......estáis de
acuerdo conmigo ??? Utilizaremos el convenio típico
es decir, w=1 para puntos y w=0 para vectores, ok?.
Vamos a ver que podemos hacerle a un determinado objeto una vez
lo hemos modelado ("creado"). Una observación importante: podemos transformar de dos maneras distintas pero
totalmente equivalentes. Es exactamente lo mismo transformar un
vértice respecto de un sistema de referencia que transformar
en orden inverso el sistema de referencia dibujando después
el vértice en éste. Vaya lío de palabras
no?...Básicamente vengo a decir que el resultado final
es el mismo si yo voy andando a la tienda a comprar pipas que
si la tienda se mueve y se acerca a mi !!!...lo veis ahora?? Las transformaciones afines se llaman así
porque conservan las lineas. Mirad la figura:
Oscar García "Kokopus".
Capitulo 6
Transformaciones geométricas.
Coordenadas Homogéneas
Vengan las transformaciones afines!!!

Se observa que tan sólo transformando los vértices y uniéndolos de nuevo obtendremos como resultado final la linea que los une transformada. De esta forma queda claro que sólo tendremos que aplicar transformaciones a los puntos, vértices, de nuestra geometría, uniéndolos después con segmentos rectos.
Escalar
Gracias a la función de escalado podemos aumentar/disminuir un objeto en cuanto a tamaño. Tan sólo tendremos que multiplicar a cada uno de sus vértices por la matriz que sigue, uniéndolos después con líneas tal y como estaban al inicio:

En el ejemplo se observa el efecto claramente. He dibujado los ejes en amarillo para que sea posible orientar los cubos visualmente. El cubo original es el de color rojo y como veis está centrado en el origen. El cubo escalado es el de color verde. Es 2 veces mayor que el original al haber efectuado un escalado de 2.0 sobre todas sus componentes.
La matriz que se ha aplicado a cada uno de los vértices del cubo la tenéis en la figura 9.
Trasladar
Esta es precisamente una transformación afín imposible de realizar en cartesianas si no se incluye una suma de matrices. Pero nosotros no queremos sumar, tan sólo multiplicar. Y es que la mayoría de los "pipeline's" gráficos implementados vía hard en aceleradoras 3D o tarjetas de video esperan recibir matrices para concatenarlas y multiplicarlas.
Si queréis haced la prueba. Intentad aplicar la transformación que veis en la figura sin usar coordenadas homogéneas, es decir con un vector y una matriz de 3 x 3, y ya veréis como es imposible hacerlo. Necesitareis sumar "algo" al resultado para lograrlo.
En cuanto a la figura:

Al igual que anteriormente, el cubo original es de color rojo y está centrado en el origen. Al cubo verde se le ha aplicado una traslación de 30 unidades positivas siguiendo la dirección del eje X. Obviamente el tamaño se conserva pues no he aplicado ningún escalado. La matriz que implementa esta transformación la tenéis en la figura 9.
Rotar
La rotación debe realizarse siempre alrededor de un determinado eje de referencia. Podemos rotar alrededor del eje X, del eje Y o del eje Z, y según el caso la matriz a aplicar será una o será otra. En el caso de la figura:

Seguimos con el mismo criterio que antes. El cubo rojo sigue estático en el origen. El cubo verde tiene exactamente las mismas dimensiones pero se ha rotado 45 grados alrededor del eje vertical, que en este caso es el eje Y y no el Z al que tanto nos hemos acostumbrado desde siempre. La matriz a emplear está en la figura 9.
Pensad que en Gráficos se utiliza siempre la Z como unidad de profundidad y no de altura. Incluso se habla del "Z buffer" o buffer de profundidad que almacena el orden de profundidad de unos polígonos respecto de otros para que no se solapen (eliminación de superfícies ocultas).
Otra consideración importante es la convención en cuanto a "¿hacia donde se supone que roto si digo que el ángulo es positivo?...¿y si es negativo?....Obviamente existe una convención establecida para que cuando se hable de un ángulo positivo, sea el mismo para todo el mundo!!!...Hay que seguir el siguiente criterio:
"El ángulo de rotación se define como positivo si supone girar en dirección contraria a las manecillas del reloj (CCW-Counter Clockwise) al mirar el eje sobre el que se rota de fuera hacia a dentro (mirar hacia el origen)"
Y ahora es cuando me odiáis porque no os habéis enterado de nada...bueno lo tenéis en la figura:

En ella podéis ver como si miramos hacía el origen a través del eje de las X, una rotación contra-reloj es la indicada. Pues esa rotación se considera positiva. Así pues si digo que voy a rotar 30 grados CCW alrededor del eje X me refiero a que rotaré 30 grados siguiendo la dirección y el eje de la figura.
Deformar
Es la llamada transformación de "Shearing". Consiste en hacer que alguna de las componentes de un vértice varíe linealmente en función de otra. Me explico, se trata por ejemplo de alterar el valor de la X y de la Y en función del de la Z. Se consiguen efectos de distorsión muy interesantes para ciertas animaciones. Os dejo las matrices a aplicar:

El punto resultante es el de la izquierda. A la derecha tenemos la matriz a aplicar y un punto genérico al cuál se aplica ésta. En el primer caso variamos las componentes Y y Z en función de X, en el segundo X y Z en función de Y y en el tercero X e Y en función de Z. Efectuad las multiplicaciones y mirad como queda el punto transformado.
Se entiende que los valores Sxy, Sxz, Syx, etc...son escalares reales, es decir números, que vosotros mismos deberéis escoger para conseguir el efecto deseado.
Estaría bien que modificarais el ejemplo que os entrego con este capítulo para efectuar pruebas de deformación en el cubo.
Concatenación de transformaciones
Ahora supongamos que deseamos aplicar múltiples transformaciones a un determinado objeto geométrico. Para hacerlo tenemos que concatenar una detrás de otra todas las matrices por las que sus vértices deben multiplicarse. Para cada transformación creo una matriz, las multiplico todas y obtengo una matriz resultante más o menos compleja. Esa es la matriz que aplicaré a mis vértices para que se vean afectados "de golpe" por todas las transformaciones.
Pero cuidado con una cosa. La multiplicación de matrices no es conmutativa y eso implica que:
Lo cuál equivale a decir que el orden de las matrices afecta al resultado final, es decir, a la posición y orientación de nuestro objeto geométrico 3D. Una prueba viviente es la siguiente figura:
![]() |
![]() |
Se observa con toda claridad que el resultado de aplicar las mismas dos transformaciones pero con el orden cambiado da como resultados dos bien distintos. Como siempre el cubo rojo y centrado es el inicial y el verde el resultado final. En el primer caso he aplicado una traslación de 30 a lo largo de X y después una rotación de 45º alrededor del Y. En el segundo caso primero roto los 45º alrededor del mismo eje y después me traslado 30 unidades siguiéndo el eje X.
Las matrices a aplicar para cada vértice del cubo en ambos casos provienen de la concatenación de dos. Tenéis ambos casos resueltos en la figura 9.
Premultiplicación y postmultiplicación
Más convenciones !!!...si no nos podemos todos de acuerdo nos volveremos locos. ¿Pero en qué quieres que lleguemos un acuerdo ahora, Oscar?...¿no hay suficiente con el tema de los ángulos de rotación?...pues no !!!...Existe otra cosa muy importante por aclarar.
Existen dos convenciones en cuanto a uso de transformaciones geométricas: la de Robótica / Ingeniería y la de Gráficos. En ambos casos se realizan exactamente las mismas operaciones pues tanto puedo querer mover un brazo robot como un personaje sobre mi juego 3D. Pero en cada caso se sigue una metodología distinta.
En la convención de Gráficos, que es la que yo he estado asumiendo durante todo el artículo y en concreto en la figura 9, se postmultiplican las matrices. ¿Y eso qué eeeeeeehhhhhh?...pues que los puntos se toman como vectores en columna que se multiplican a las matrices por la derecha. Y además el orden de las transformaciones, de primera a última a aplicar, es de derecha a izquierda.
En cambio en Robótica se utilizan vectores de tipo fila, o renglón, que se multiplican por la izquierda. Las matrices se ordenan de izquierda a derecha en cuanto a orden de las transformaciones. Es decir, se premultiplica.
Aquí tenéis gráficamente lo que buenamente he intentado expresar escribiendo:

Dónde Pf es el punto transformado final, Pi el inicial del que parto, T1 la primera transformación a aplicar, T2 la segunda y así sucesivamente.
Pero ojo que en ambos casos tenemos que multiplicar las matrices como siempre nos han enseñado, es decir, de izquierda a derecha. Sólo hay que fijarse en la convención que se usa porque eso define que forma tienen nuestros puntos, por que lado los he de multiplicar y en que orden debo ir añadiendo las transformaciones. ¿Sí?
Aquí tenéis la famosa figura 9 a la que me he ido refiriendo varias veces ya:

Por cierto y antes de que se me olvide comentarlo. La figura siguiente os muestra cuales son las matrices genéricas a utilizar. Ahí están deducidas y vosotros os encargáis de darles valores a los números. Los escalares son todos números reales a definir por vosotros y los cos / sin asumen un ángulo cualquiera también a vuestro libre albedrío:

Bueno...y ¿cómo usa OpenGL todo esto?, ¿cómo le indico las transformaciones?, ¿a qué afectan éstas?....
En cualquier aplicación o paquete gráfico, también en OpenGL por supuesto, toda la geometría se ve afectada por la CTM (Current Transformation Matrix) o matriz de transformación actual. Esta guarda la información sobre todas las matrices que se han ido acumulando. Cualquier vértice que pase por el "pipeline" será multiplicado por esta matriz y consecuentemente transformado.
En OpenGL la CTM se compone de dos matrices. La "Model-View matrix" o matriz de transformación y la "Projection matrix" o matriz de proyección. Ambas se concatenan y de su producto se crea la CTM para el "pipeline" que controla OpenGL. De la segunda ya profundizaremos más adelante pues se encarga de la conversión 3D (mundo virtual) a 2D (pantalla) es decir de la proyección. La primera nos interesa mucho ahora pues almacena todas las transformaciones afines que definamos en nuestro código.
Lo primero que debe hacerse es inicializar la matriz. Esto se hace cargando en ella la matriz identidad que como ya sabréis es el elemento neutro de la multiplicación de matrices. Con esto me aseguro de "limpiarla" por completo antes de empezar a añadir transformaciones. Si no lo hiciera correría el riesgo de añadir transformaciones a otras ya existentes con lo cuál el resultado en pantalla sería de todo menos el esperado. Esto lo hago con:
glMatrixMode(GL_MODELVIEW); /* Activo la matriz */
glLoadIdentity(); /* La limpio antes de nada !!! */
Una vez hecho esto puedo acumular transformaciones sucesivas mediante las funciones:
glScalef(GLfloat sx, GLfloat sy, GLfloat sz); /* Escalar según sean los factores sx, sy y sz*/
glTranslatef(GLfloat tx, GLfloat ty, GLfloat tz); /* Trasladar según los factores tx, ty y tz */
glRotatef(GLfloat angulo, GLfloat vx, GLFloat vy, GLfloat vz); /* Rotar "angulo" según el eje que define el vector (vx,vy,vz) */
En el caso de la rotación podemos indicar cualquier eje que se nos ocurra. Lo típico es el eje X o (1,0,0), el Y o (0,1,0) o bien el Z que es (0,0,1).
A medida que vamos definiendo transformaciones se acumulan postmultiplicando en la matriz de transformación. Queda por tanto claro que OpenGL utiliza la convención de Gráficos como era de esperar. Cuidado porque la regla en esta librería es que la transformación que se ha definido última será la primera en aplicarse a la geometría. Eso quiere decir que si tengo 3 líneas de código tal que:
glScalef......
glRotatef....
glTranslatef....
...la primera en aplicarse será la transformación de la tercera línea, seguida por la de la segunda y finalizando con la primera. Eso "altera" un poco nuestra idea de ejecución secuencial al programar estructuradamente pero es así con OpenGL y debe tenerse en cuenta.
Concepto de "pila" o "stack"
La matriz de transformación, la "model-view" debe entenderse como una pila. Asumo que todos sabéis a lo que me refiero pues es un concepto fundamental de programación. Si alguien se pierde aquí que me lo comunique y gustoso le explico lo que es.
Pues bien, cada transformación que añadimos entra a la pila como la última y por tanto al salir será la primera. Ahí tenéis el porque OpenGL funciona tal y como os comentaba dos párrafos más arriba.
Podemos salvar el estado de la pila en cualquier momento para recuperarlo después. Esto la haremos mediante las funciones:
glPushMatrix( ); /* Salvamos el estado actual de la matriz */
glPopMatrix( ); /* Recuperamos el estado de la matriz */
Esto nos servirá en el caso de que tengamos que aplicar algunas transformaciones a una pequeña parte de la geometría. El resto no debiera verse afectado por esos cambios. Lo que se hace es definir las transformaciones generales que afectan a todos. Entonces se salva la matriz y se añaden otras. Se dibuja la geometría "especial" y inmediatamente después se recupera la matriz. Ahora podemos dibujar todo el resto estando tranquilos pues no se verá afectado por las transformaciones que hayamos definido entre el glPush... y el glPop...
Si lo queréis imaginar con código aquí va un mini-ejemplo:
.....
glRotatef... /* afectará a toda la geometría que dibuje a partir de ahora */
glTranslatef.... /* afectará a toda la geometría que dibuje a partir de ahora */
glPushMatrix( ); /* salvo el estado actual de la matriz, es decir, las 2 transformaciones anteriores */
glTranslatef.... /* afectará a sólo a la geometría que dibuje antes del glPop... */
glScalef..... /* afectará a sólo a la geometría que dibuje antes del glPop... */
dibujo_geometría_específica( ); /* Render de la geometría que pasará por 4 transformaciones */
glPopMatrix( ); /* recupero el estado de la matriz anterior */
dibujo_el_resto( ); /* Render de la geometría que pasará por 2 transformaciones */
.....
Espero que se entienda lo que pretendo deciros con este ejemplo. Son conceptos muy simples pero a veces abstractos al principio. Posteriormente todo es programar, equivocarse una y otra vez y acabar dominando el tema a la perfección!!!
Crear matrices "a medida"
Por último comentar que también podemos crearnos matrices "a mano" para después pasarlas a la matriz de transformación de OpenGL. No disponemos tan sólo de las funciones de traslación, rotación... que os he comentado sinó que también podemos usar:
glLoadMatrixf(puntero_a_matriz);
glMultMatrixf(puntero_a_matriz);
En el primer caso substituimos a la matriz actual con la que le pasamos precalculada por nosotros mismos. En el segundo caso multiplicamos a lo que ya haya en la matriz por lo que nosotros pasamos.
El puntero a una matriz se asume como variable del tipo:
GLfloat M[16];
o
GLfloat M[4][4];
y lo importantísimo es que OpenGL asume que la matriz que se le pasará está definida por columnas, es decir:
|a0 a4 a8 a12|
|a1 a5 a9 a13|
|a2 a6 a10 a14|
|a3 a7 a11 a15|
Primero definimos a0, después a1, a2, a3, a4 ... y así sucesivamente.
Si utilizáis estas funciones provad antes con ejemplos sencillos hasta entender perfectamente como pasar la matriz para que ocurra lo que esperáis !!!
El ejemplo del que os he mostrado capturas durante todo el artículo lo tenéis aquí. No pretende ser óptimo ni mucho menos. Podría mejorarse enormemente pero no pretendo eso. Lo que quiero es que sea docente y claro pues son los conceptos los que tenéis que entender para poder hacer virguerias programando más tarde. Espero que lo compiléis y ejecutéis y sobretodo que entendáis lo que hace. Modificadlo a vuestro gusto y probad tanto como os sea posible.
Nos vemos en el siguiente número
con más material !!!...hasta ahora !!!
ÚLTIMA REVISIÓN EN FEBRERO DE 1999
|
|