Aula Macedonia


Curso de Programación Gráfica en OpenGL


Artículo realizado por
Oscar García "Kokopus".





Capitulo 3
Programación gráfica (parte II).

Buenas a todos/as de nuevo.....espero que no os resultara demasiado complejo el listado de la última edición del cursillo. Seguro que con los comentarios que incluía y algo de intuición habéis podido controlarlo todo a la perfección.

De todas formas lo comentaré de principio a fin para vuestra tranquilidad.

Vamos a teclear un poco... ( Cont.)

Bueno para aquellos/as que lo hayáis compilado y ejecutado correctamente el resultado está bastante claro.....sólo hay que mirar la pantalla.

Se genera un cuadrado de lado 200 formado por bastantes colores diferentes que varían de vértice a vértice....no existe interacción ni tampoco hay movimiento pero todo llegara pasito a pasito. Primero analicemos la estructura del programa, típica de OpenGL, así como de cada una de sus funciones.

Función MAIN

Ya sabéis que cualquier programa en C se caracteriza por su función MAIN o principal, función que se llama automáticamente al iniciar la ejecución del programa. Veamos que hacemos con ella en el caso de OpenGL.

Para empezar definimos la "archiconocida" MAIN como siempre se hace.....notad que no la defino como VOID ( no retorna nada), sino como INT ya que en ANSI C, es decir C original y exportable a cualquier arquitectura, se definió originalmente que MAIN debía retornar un entero. Así pues:

int main (int argc, char **argv) {

Una vez declarada pasamos a la primera llamada de OpenGL. Fijaros que las llamadas que siguen son a GLUT, que como ya comenté es una librería que actúa sobre OpenGL. Esta nos facilita mucho las cosas en el momento de crear una ventana para observar nuestra aplicación. Lo comentaré en profundidad en el siguiente punto pero si estáis impacientes pulsad aqui.

Debemos recoger los parámetros de la línea de comandos, ARGC y ARGV, mediante glutInit(...). De momento no nos preocuparemos más por esto. Si queremos usar la línea de comandos como entrada de información inicial lo haremos de manera normal como en cualquier programa C y simplemente usaremos esta función para inicializar OpenGL....De hecho aseguramos interacción entre el sistema de ventanas y OpenGL llamándola:

glutInit (&argc, argv);

Ahora hay que decirle al motor gráfico COMO queremos "renderizar", es decir, si hay que refrescar la pantalla o no, que "buffers" hay que activar/desactivar y que modalidad de colores queremos usar. En este caso NO tenemos doble buffer (GLUT_DOUBLE) y por tanto definimos un buffer de render único con la constante GLUT_SINGLE. Por otra parte si recordáis os comenté que teníamos dos posibilidades en el momento de colorear. Podíamos usar colores indexados, es decir referirnos a ellos por un identificador y que el sistema los buscara en una tabla, o bien podíamos usar convención RGB. En nuestro caso le decimos al subsistema gráfico que cada color a aplicar será definido por tres valores numéricos, uno para el rojo (Red), otro para el verde (Green) y otro para el azul (Blue). Recordad la teoría aditiva del color.... para esto usamos la constante GLUT_RGB. Sino usaríamos GLUT_INDEX.

glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);

Con buffer simple contamos tan solo con un área de memoria que se redibuja constantemente. Esto no es factible para aplicaciones donde la velocidad de render es muy alta o el contenido gráfico es elevado. En nuestro caso sólo definimos un polígono y por lo tanto nos conformaremos con el buffer simple. De todas formas casi siempre utilizaremos el buffer doble, dos zonas de memoria que se alternan de manera que se dibuja primero una y en la siguiente iteración se dibuja la otra. Ampliaré esta información en el siguiente capítulo como podéis observar si miráis el índice del curso.

Ahora GLUT nos permite definir las medidas de nuestra ventana de visualización. Estamos definiendo literalmente el ANCHO y ALTO de nuestra "Window"... en pixels...

glutInitWindowSize (ANCHO, ALTO);

También hay que "colocar" la ventana en algún punto determinado de la pantalla. Después podremos moverla con el ratón, si no lo impedimos por código, pero siempre hay que indicar un punto de origen para la primera vez que ejecutamos la aplicación. Las coordenadas que suplimos a la función son las correspondientes al pixel que se encuentra en la esquina superior-izquierda de la ventana.

glutInitWindowPosition (ORIGENX, ORIGENY);

Una vez ya definido como "renderizar", con que medidas de ventana y en que posición física de la pantalla......creamos la ventana. Para ello le damos un nombre cualquiera que sera el título que aparecerá en esta. De hecho a la función se le pasa un array de caracteres, ya sea explícito o sea el nombre entre comillas, o bien implícito, es decir, una variable que contiene el nombre:

glutCreateWindow ("Cuadrado Multicolor");

Hasta aquí ya tenemos una ventanita flotando en la pantalla, con su titulito y su fondo negro por defecto ya que si os fijáis no hemos especificado aún otro. Ya podemos empezar a "generar" gráficos para colocarlos en ella. Creamos una función llamada Inicio() que se encargará de prepararnos el terreno...

inicio ();

Para observar lo que hace esta función, pasaros unos párrafos más abajo por el apartado Inicialización del sistema.

Tras inicializar el sistema le decimos al subsistema gráfico CUÁL es la función que debe llamarse cada vez que se requiera dibujar de nuevo en pantalla. Esta función la tendremos que crear nosotros por supuesto y será dónde le diremos a OpenGL QUÉ es lo que debe dibujarse en la ventana que hemos creado para tal fin. ¿ Que cuando tendremos que redibujar ?, el "evento" de "render" se produce cada vez que se cambia el tamaño de la ventana, o que ésta se cambia de posición, o bien cuando por programa nosotros le decimos a OpenGL que AHORA queremos que dibuje todo de nuevo. Lo observaremos más adelante en este mismo programa.

En resumidas cuentas le estamos diciendo a la librería que cada vez que note que se debe redibujar, llame a una función que hemos llamado "DIBUJAR"...normalmente y si miráis ejemplos de libros o web's, veréis que se la llama comúnmente "DISPLAY".

glutDisplayFunc (dibujar);

Bueno.....parece ser que ya sabemos dónde dibujar, en la ventana, y qué dibujar. Sólo falta entrar en el bucle infinito que domina cualquier aplicación OpenGL. Con la función que sigue, que siempre se pone al final del main, le decimos a la librería que espere eternamente a que se produzcan "eventos", es decir, que hasta que no ocurra algo se mantenga a la expectativa. En nuestro caso el único evento posible es el propio "render" pues aún no sabemos interactuar con el ratón, ni sabemos controlar que pasa cuando se mueve la pantalla o se redimensiona. Ya llegaremos a eso...

glutMainLoop ();

Por ultimísimo y como ya he comentado al principio de este punto, ANSI C requiere que la funcion main retorne un entero aunque no sirva literalmente para nada. Es una cuestión de consistencia y compatibilidad entre máquinas o sea que seamos buenos y respetemos a los creadores de este maravilloso lenguaje, OK???

return 0;
}

Retornamos un 0, que obviamente es un entero, como podríamos retornar cualquier otro valor numérico.

Inicialización del sistema

Bueno todo programador que se precie de no hacerlo muy mal sabrá que hay que ahorrarse el máximo de sentencias en el propio main e incluirlas en una rutina de inicialización que se llama desde éste. Si si.....todo eso de la estructuración, modularidad, claridad....seguro que os suena.....:-).

Analicemos pues nuestra función de inicialización Inicio(). Dado que no se le pasa ningún parámetro ni retorna ningún valor la declaramos tal como:

void inicio (void) {

Bueno bueno....ya empieza el follón con las matrices...nooooo...por favorrrr....tranquilos/as que no es para tanto !!!!. En primer lugar queremos "parametrizar" nuestra proyección, es decir, queremos decirle a OpenGL CÓMO debe proyectar nuestros gráficos en pantalla. Por ello y para empezar le decimos que active la matriz de proyección que es la que vamos a "retocar"....

glMatrixMode (GL_PROJECTION);

¿Qué es lo primero que debe hacerse SIEMPRE con una matriz cuando se va a trabajar con ella?, pues "limpiarla" para que no contenga ningún valor que pueda falsear los cálculos y por tanto nos haga obtener resultados inexactos.....o más bien que lo que salga en la pantalla no se parezca en nada a lo que esperamos....Para ello cargamos a la matriz activa con la matriz identidad....recordad que:
 

Si A es una matriz....

y la matriz identidad es I....

entonces A * I = A....

...es decir que cualquier matriz operada con la identidad de como resultado la misma matriz...no varía!!

 

Por lo tanto cargamos ( "load" ) la matriz identidad en la de proyección...

glLoadIdentity ();

Muy bien, tengo la matriz de proyección limpia.....pero ahora tengo que decirle a OpenGL de que manera quiero que proyecte mis gráficos en la ventana que he creado. Usaremos la función que ya comente en el pasado artículo, glOrtho(...). Así creo el volumen de visualización. Todo lo que esté dentro de este volumen será proyectado de la forma más simple, eliminando su coordenada Z, o de profundidad. Es la llamada proyección ortográfica y como ya dije no permite que se distinga la distancia de los objetos a la cámara pero para empezar no está mal !!!. Ya mejoraremos el asunto cuando usemos proyección perspectiva.

glOrtho (0, ANCHO, 0, ALTO, -10, 10);

que crea el siguiente volumen de visualización:

 

Ya hemos terminado con la matriz de proyección. El sistema ya sabe como debe proyectar en pantalla. Ahora pasemos a la matriz de modelado/visionado, o sea, la matriz que rota/escala/traslada....Dado que queremos operar sobre ella la seleccionamos primero....

glMatrixMode (GL_MODELVIEW);

y también la reseteamos claro...la "limpiamos" nada mas empezar... con la matriz identidad como acabamos de hacer:

glLoadIdentity ();

Originalmente el centro de coordenadas se asume en la esquina inferior-izquierda de la ventana, es decir...

 

pero nosotros lo queremos en el centro de la ventana, o sea...

 

y por lo tanto debemos aplicar una traslación al origen. ¿Cómo lo hacemos?, pues nada más "limpiar" la matriz de modelado/visionado, almacenamos la traslación en ella de manera que todo aquello que requiera ser dibujado será primero multiplicado por esta matriz y consiguientemente trasladado donde lo queremos con relación a un origen de coordenadas en el centro de la ventana. ¿Lo pilláis?, simplemente estamos efectuando un cambio de coordenadas a todo lo que se dibuje!!!. Aquí tenéis una figura para aclararos si os habéis liado:

Para ello utilizamos en el programa la siguiente línea de código:

glTranslatef ((GLfloat)ANCHO/2, (GLfloat)ALTO/2, 5.0);

Si os fijáis defino también que el centro se encuentre en Z=5.0....esto no es importante y ni lo notaremos pues recordad que la proyección ortográfica elimina la componente Z de todos los puntos a "renderizar"....de todas formas glTranslatef(...) lo requiere y se lo damos...

Observad que hago un "casting" de las dos primeras coordenadas. Para los mas neófitos decir que un "casting" permite hacer conversiones directas entre formatos de variables. En este caso aviso al compilador de que las dos primeras coordenadas deben tratarse como GLfloat ( numeros reales ) aunque ese no sea su formato inicial. ¿Por qué?, pues porque glTranslatef acaba en "f" porque espera valores GLfloat o float para sus parámetros de entrada.

Supongo que también habréis observado que utilizo algunas constantes como ANCHO, ALTO, ORIGENX, ORIGENY que yo he definido previamente en mi programa....estas no pertenecen a OpenGL claro...8-).

Bueno...para acabar con las inicializaciones le diremos a OpenGL con que color deseamos que se reinicialice el frame buffer cada vez que haya que volver a dibujar....o lo que es lo mismo, que color debe aparecer en todas aquellas áreas donde yo no ponga nada, vamos el color del fondo !!!. Definimos que queremos un color de fondo para nuestra ventana negro:

glClearColor (0.0, 0.0, 0.0, 0.0);
}

Y se acabó!!!....vamos a ver cómo y qué dibujamos!!!

"Render" del sistema

Vamos allá pués !!!....con el render. Sabemos que OpenGL llamara a la función DIBUJAR cada vez que necesite "renderizar" de nuevo. En nuestro caso hacemos lo siguiente:

void dibujar (void) {

Una vez declarada la función, que como veis ni espera ni retorna nada, le decimos a OpenGL que restaure el frame buffer. Esto es obligado para un correcto proceso de render. De hecho estamos "reinicializando" la ventana de visualización con el color definido anteriormente en glClearColor (0.0, 0.0, 0.0, 0.0);. De hecho nosotros definimos el negro como valor para el fondo pero podríamos no haberlo hecho ya que se adopta este color por defecto.

glClear (GL_COLOR_BUFFER_BIT);

¿Muy bien....y qué queremos que OpenGL dibuje?, podemos dibujar las caras frontales, o las traseras o ambas!!! y además podemos rellenarlas con color o no. En este segundo caso sólo se colorearan los bordes (aristas). Ya dije en su momento que OpenGL supone caras frontales aquellas cuyos vértices se definen en orden contrareloj. Casi siempre sólo dibujaremos las frontales porque las traseras "no se verán" y así ahorraremos tiempo de cálculo optimizando la velocidad de ejecución. Por tanto, en este caso, decidimos dibujar sólo las caras frontales (GL_FRONT) y rellenadas, es decir con color en su interior además de en los bordes (GL_FILL). Para dibujar sólo las caras traseras utilizaríamos GL_BACK y para dibujar ambas GL_FRONT_AND_BACK. En el caso del color de relleno, si deseamos evitarlo coloreando solo los bordes de cada polígono usaremos la constante GL_LINE.

glPolygonMode (GL_FRONT, GL_FILL);

Pues empezamos a definir un nuevo polígono...esto ya lo comenté....

glBegin (GL_POLYGON);

El primer vértice es de color azul y sus componentes en pantalla son los 3 enteros que observáis...

glColor3f (0.0, 0.0, 1.0);
glVertex3i (-100, -100, 5);

el segundo vértice es verde...

glColor3f (0.0, 1.0, 0.0);
glVertex3i (-100, 100, 5);

el tercero es rojo....

glColor3f (1.0, 0.0, 0.0);
glVertex3i (100, 100, 5);

y el cuarto es amarillo....

glColor3f (1.0, 1.0, 0.0);
glVertex3i (100, -100, 5);

y cerramos la estructura correspondiente al polígono que estabamos dibujando....un simple cuadrado con un color distinto en cada esquina.

glEnd ();
}

¿Muy bonito Oscar....así que cada vez que OpenGL redibuje la escena vendrá aquí y volverá a ejecutar este código no?, pues sí eso es. Y entonces si estoy poniendo un color distinto en cada vértice y le he dicho al principio de la función que me rellene el polígono, ¿con qué color lo va a rellenar?.....muy buena pregunta desde luego....lo que la librería hace es mejor verlo para entenderlo. De hecho interpola los colores punto a punto de manera que si un vértice es rojo y el siguiente amarillo pondrá en medio de ambos toda la escala de colores del rojo al amarillo, automáticamente. Para el interior del polígono también interpola punto a punto obteniéndose el curioso efecto que podéis observar si ejecutáis este sencillo programa.

Sistema de ventanas (GLUT)

Grácias a GLUT podremos controlar diversas ventanas a la vez mostrando diferente información en cada una de ellas. Tan sólo definiremos una función de "display" diferente para cada ventana así como respuestas distintas según sea la interacción del usuario con ellas.

Otro aspecto importante es el de las coordenadas. No son lo mismo las coordenadas de mundo que las de ventana. Por tanto una ventana tiene coordenadas 2D que definimos como habéis visto en el programa de ejemplo ( ALTO, ANCHO, ORIGENX, ORIGENY ) y en cambio el mundo virtual que estamos creando suele ser 3D y no tiene porque corresponderse con ella. Entonces se produce un curioso fenómeno. ¿Qué ocurre si yo quiero recuperar las coordenadas del ratón en el momento en que se produce un "click" en la ventana, para trabajar con ellas?, es decir, imaginad que el usuario debe poder mover una esfera por el mundo simplemente seleccionándola con el ratón y arrastrándola. Si resulta que cuando se mueve el ratón las coordenadas que el sistema operativo nos retorna son las de la posición de la esfera en la VENTANA y éstas no se corresponden con las del mundo.....nos volveremos locos para operar con ellas y calcular la nueva posición de la bolita!!!!!.

Solución: cambio de coordenadas !!...eso es...cada vez que deseéis recoger información del ratón para operar con ella en el mundo tendréis que convertir las coordenadas recibidas vía S.O. en coordenadas del mundo.

Se suele hacer de la siguiente forma:

 

Observad la figura atentamente. Podemos convertir coordenadas de mundo a pantalla o bién de pantalla a mundo. De hecho el proceso es obviamente el mismo pero en una dirección o en la contraria.

Imaginemos que queremos saber cuál es la coordenada de pantalla de un punto (Xm, Ym, Zm) cualquiera de nuestro mundo virtual. Queremos saber en que lugar de la pantalla será dibujado, en que pixel....

En primer lugar pasamos de un punto 3D en el mundo a un punto situado en una ventana imaginaria de lado unitario de manera que las coordenadas estarán acotadas entre 0 y 1. A esto se le llama "mapear coordenadas". Llamemos a las coordenadas 2D del punto en el cuadrado unitario (Xu, Yu). Entonces:

Xu = (Xm - X1) / (X2 - X1);

Yu = (Ym - Y1) / (Y2 - Y1);

donde como ya he dicho (Xm, Ym) son las coordenadas 2D del punto en el mundo y (X1,Y1), (X2, Y2) las observáis en la figura como límites de la escena real.

Entonces mapeamos de coordenadas unitarias a pantalla. Llamemos a las coordenadas de pantalla (Xp, Yp).....:

Xp = W * Xu;

Yp = H * (1 - Yu);

donde W y H son las medidas de la ventana de visualización como también podéis ver en la figura. Y ya está!!!!, ya sabemos en que punto de la pantalla se mapea el punto del mundo al cual nos referíamos. Si queremos pasar coordenadas de pantalla a mundo, pués sólo tenemos que repetir el proceso pero al revés claro está !!!....no es tan difícil no???....8-).

Resumamos un ejemplo típico de úso de este concepto. Supongamos lo que dije , es decir, quiero mover una esfera con el ratón cada vez que hago "click" sobre ella o bién cuando la arrastro con el botón derecho pulsado. En este caso tendré que:

  1. Tomar las coordenadas de pantalla. Veremos que eso nos lo da GLUT automáticamente.

  2. Convertirlas según he analizado, en coordenadas de mundo.
  3. Operar con ellas, ya convertidas, en mi programa. En este caso las puedo incrementar/decrementar según se mueva el ratón hacia un lado u otro. Obtengo nuevas coordenadas de posición para la esfera, en el mundo.

  4. Le digo a OpenGL que dibuje la escena de nuevo.....y ya está....repetitivamente veríamos que nuestra esfera se mueve a la vez que nuestra mano lo hace con el ratón. Es decir, se va "redibujando" en posiciones diferentes cada vez, lo que implica sensación de movimiento para nuestros ojos.

Vamos a otro efecto no muy deseable. Primero definiré lo que se entiende por "aspect ratio". El "aspect ratio" de una ventana es la relación que existe entre su anchura y su altura. Esta relación debería conservarse siempre aunque se varíe el tamaño de ésta pues de lo contrario distorsionaremos el contenido y la visualización será bastante ineficiente. Es el caso típico de coger una imagen y alargarla o ensancharla de manera que lo que acaba quedando al final es como muy irrisorio según el caso. Controlaremos este fenómeno modificando la ventana para acomodarla al ratio correcto y lo haremos en nuestro programa cuando sea el caso, por ejemplo al producirse un redimensionado de ésta.

Si me varian la altura varío la anchura que corresponda o al revés.

Si queremos dividir una ventana en varias porciones independientes podemos usar "Viewports". Un viewport es un área rectangular de la ventana de visualización. Por defecto es la ventana entera pero podemos variarlo a gusto. Se utiliza la función:

void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)

dónde (X,Y) es la esquina inferior izquierda del rectangulo o viewport. Esta coordenada debe especificarse con relación a la esquina inferior izquierda de la ventana. Claro está que W, H son la anchura y altura de nuestro viewport dentro de la ventana. Todos los valores son enteros. No hay decimales en coordenadas de ventana ya que un pixel no puede partirse a trozos.....;).

Tras activarlo o definirlo será dentro de éste donde dibujaremos.

Bueno no os estrujo más la cabeza con las ventanas...volveremos a ellas en el siguiente capítulo cuando os hable profundamente de los eventos.

Para acabar deciros que no incluyo a continuación el apartado dedicado a la función MAIN en OpenGL pues creo haberla comentado con creces en el programa de ejemplo. Así pués conclúyo este capítulo y os dejo con él hasta el próximo número de Macedonia!!!!!

Un abrazote y suerte compilando !!!!





AULA MACEDONIA
a
MACEDONIA Magazine