Aula Macedonia


Curso de Programación Gráfica en OpenGL


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





Capitulo 4
Interacción. Dispositivos de entrada..

Ya lo sé...¡¡¡¡ya lo sé!!!!...exámenes y más exámenes...pero ahora ha llegado el momento de relajarnos totalmente y qué mejor que.....¡¡¡¡el curso de gráficos!!!!...claro si es que lo teniáis en la punta de la lengua ¿¿¿verdad???...pués vamos a ello.

En la pasada edición de la revista comenté ampliamente un programa de ejemplo. Esto nos sirvió para familiarizarnos con la forma de la función MAIN en OpenGL, así como con diversas funciones de GLUT. Hablaremos aún más de éste último en relación con lo que se llama "programación orientada al evento" ( ¿os suena a los usuarios de Power Builder, Visual Basic o Delphi?...¡¡¡si si es que va por ahí!!! ).

Si queréis ir haciendo "boca" pasaros por el manual de la API de GLUT aquí.

Permitidme por eso un pequeño cambio sobre la marcha. Hablaré de lo mismo que en su día propuse en el temario inicial del curso pero lo haré en otro orden. Empezemos pués:

Programación guiada por eventos

Dejadme definirla primero y aplicarla después a nuestro caso, OpenGL.

Un evento es "algo que el usuario puede hacer" como por ejemplo maximizar una ventana, redimensionarla, pulsar el botón izquierdo del ratón, o usar una determinada combinación de teclas. En todos estos casos deberemos "ejecutar algo de código" dentro de nuestro programa, si es que estaba previsto así. Para los "más informáticos" diré que se trata de algo así como una interrupción que provoca la ejecución de una determinada rutina cuando se la activa.

En OpenGL, y gracias a GLUT, se le permite al usuario "jugar" pulsando botones del ratón, moviéndolo por la pantalla, apretando teclas, cambiando la ventana de la aplicación. Cada vez que éste provoque alguno de estos eventos deberemos llamar a una determinada rutina o función para que se haga cargo de la acción a tomar.

Las más comunes funciones que OpenGL llama automáticamente al detectar uno de estos eventos son:

Son las llamadas "Callbacks" o funciones controladoras de eventos.

Analizemos algunos casos:

El ratón

Lo más normal es querer controlar lo que debe hacerse cuando el usuario pulsa uno de sus botones. Si definimos lo siguiente en la función MAIN de nuestro programa...

glutMouseFunc( ControlRaton );

OpenGL entiende que cada vez que se pulse uno de los botones del ratón debe llamar a una rutina llamada ControlRaton, que por supuesto tenemos que crear y definir nosotros mismos. Lo haremos de esta forma:

void ControlRaton( int button, int state, int x, int y ){

.....................................................
.....................................................
<código que deseemos se ejecute>
.....................................................
.....................................................

}

donde los parámetros que la función nos da (automáticamente y sin tener que hacer nada) son los siguientes:

Y entonces hacemos lo que queramos con toda esta información !!!!

Es importante aclarar que GLUT espera que los parámetros sean éstos en el caso de este callback y no otros !!!

Veamos un ejemplo:

void ControlRaton( int button, int state, int x, int y ){

    if (button==GLUT_LEFT_BUTTON && state==GLUT_DOWN){
        printf( "Cerramos la aplicación.../n");
        exit(-1);
    }

}

En este caso, cuando el usuario pulse el botón izquierdo del ratón, sacaremos un mensaje diciendo que se cierra la aplicación y entonces la cerraremos. La función exit(-1) pertenece a ANSI C, no a OpenGL, y provoca el fin de la ejecución.
¿Lo váis viendo más claro?

En el caso de la función:

glutMotionFunc( ControlMovimientoRaton );

GLUT llamará a ControlMovimientoRaton a intervalos discretos, es decir de tanto en tanto, mientras el ratón se esté moviendo por la pantalla. La definimos así:

void ControlMovimientoRaton( GLsizei x, GLsizei y ){

.....................................................
.....................................................
<código que deseemos se ejecute>
.....................................................
.....................................................

}

teniendo en cuenta que X e Y son las coordenadas de pantalla por las que el ratón está pasando. Así podríamos usar esta función para indicar nuestra situación en pantalla de la siguiente forma:

void ControlMovimientoRaton( GLsizei x, GLsizei y ){

    printf( "La posición del ratón en coordenadas de ventana es:/n");
    printf( " X = %f/n", (GLfloat)GLsizei x);
    printf( " Y = %f/n", (GLfloat)GLsizei y);

}

De manera que mientras movemos el ratón se imprimen estas tres líneas en la ventana desde la que hemos ejecutado el programa, una ventana de DOS o UNIX (no en la ventana de visualización, esa es para los gráficos!!!). Y claro, los valores de X e Y se irán actualizando según nos vayamos moviendo ya que se llamará a ControlMovimientoRaton sucesivamente.

GLsizei es un tipo de variable numérica de OpenGL comparable a un real. Fijaros en que hago un casting para convertirla en los printf.

¿Qué os parece?

El teclado

El control del teclado se realiza mediante:

glutKeyboardFunc( ControlTeclado );

esto lo añadimos a nuestra función de MAIN y entonces definimos a parte la función de control propiamente dicha:

void ControlTeclado( unsigned char key, int x, int y ){

.....................................................
.....................................................
<código que deseemos se ejecute>
.....................................................
.....................................................

}

Por ejemplo supongamos que podemos "movernos" por nuestro mundo virtual. De una forma un tanto simple y primitiva, y asumiendo que el plano del suelo se corresponde con Y=0, tendríamos una situación como ésta:

Eventos en OpenGL, figura 1


y queremos movernos según esta tabla de comportamiento:

Eventos en OpenGL, figura 2

Para implementar este comportamiento necesitamos definir dos variables, XPOS y ZPOS, que contienen nuestra posición (X,Z) en el mundo. Tan sólo tendremos que incrementar/decrementar estas variables según la tecla que el usuario pulse. Por otra parte será la rutina de render ( dibujado ) la que nos dibujará en otra posición según nos movamos, cuando detecte que XPOS y ZPOS han cambiado. ¿ Lógico no?

Una primera aproximación medio codificada podría ser algo así:
 

/* Definición e inicialización de variables globales */  
/* Partimos de la posición X=0 y Z=0 en el mundo */  

GLfloat xpos=0, zpos=0;  

/* Rutinas de Render (Dibujado) */   
void DibujarMundo( ){  

/*  
........  
........  
esta rutina dibujaría todos los polígonos que componen  
nuestro mundo virtual !  
........  
........  
*/  
}  

void Dibujar( ){  
    /* Dibujo el mundo que me rodea */  
    DibujarMundo( );  

    /* Representaré a mi personaje con una esfera amarilla */  
    /* Activo el color amarillo */  
    glColor3f(1.0, 1.0, 0.0);  

    /* Las funciones referidas a matrices que se observan las comentaré   
    ampliamente en el siguiente capítulo, no os preocupéis por ellas */  
    glPushMatrix();  

        /* Me trasladó a la posición concreta en el mundo */  
        glTranslatef(xpos, 0.0, zpos);  

        /* Dibujo una esfera de radio 2 unidades, y dividida en 16 trozos */  
        glutSolidSphere(2.0, 16, 16);  
    glPopMatrix();  

    /* Esta función la explico más adelante en este capítulo */  
    glutSwapBuffers( );  
}  
/* Rutina de control del teclado */   
void ControlTeclado(unsigned char key,int x,int y ){  
    /* Según la tecla pulsada incremento una u otra variable de movimiento */  
    switch(key){  
        case "o":  
            xpos++;  
            break;  
        case "p":  
            xpos--;  
            break;  
        case "q":  
            zpos++;  
            break;  
        case "a":  
            zpos--;  
            break;  
    }  
    /* Le digo a OpenGL que dibuje de nuevo cuando pueda */  
    glutPostRedisplay( );  
}  
/* Función MAIN del programa */   
int main(int argc, char** argv){  
    int id;  
    /* Definición típica de la ventana de visualización */   
  
    glutInit(&argc, argv);  
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);  
    glutInitWindowSize(500, 500);  
    glutInitWindowPosition(0, 0);  
    id=glutCreateWindow("Ejemplo de control de movimiento");  
    /* Definición de los Callbacks que controlaremos */  
    /* Cuando haya que dibujar llamaré a ... */  
    glutDisplayFunc(Dibujar);  
    /* Cuando el usuario pulse una tecla llamaré a ... */  
    glutKeyboardFunc(ControlTeclado);  
    /* Cuando no esté haciendo nada también dibujaré ... */  
    glutIdleFunc(Dibujar);  
    glutMainLoop( );  
  
    return 0;  
} 
 


Una cuestión importante. Al iniciar el programa, OpenGL ejecuta la función Dibujar, es decir renderiza por defecto. Cuidado porque después tenemos que forzar nosotros que se dibuje de nuevo. En nuestro caso obligamos a OpenGL a dibujar cuando se pulsa una tecla, con glutPostRedisplay( );. También le obligamos cuando nada esté pasando, es decir cuando el usuario no pulse nada. En ese caso se llamará a la función indicada por glutIdleFunc, que es precisamente Dibujar( ); !!!!!

Quisiera que todos/as hicierais el esfuerzo de entender este pequeño ejemplo a la perfección. Si tenéis dudas ya sabéis, mi dirección de Email está por ahí !!!

La ventana ( Window )

El evento más importante que puede darse en nuestra ventana de visualización es un cambio de tamaño, es decir un Reshape. Para controlarlo deberemos usar:

glutReshapeFunc( ControlVentana );

que como siempre añadimos a nuestra función MAIN. Falta definir la función de control:

void ControlVentana( GLsizei w, GLsizei h ){

.....................................................
.....................................................
<código que deseemos se ejecute>
.....................................................
.....................................................

}

Los parámetros que nos llegan a la función se refieren al nuevo ANCHO ( Width, w) y al nuevo ALTO ( Height, h ) de la ventana tras ser redimensionada por el usuario.

En esta función deberemos asegurarnos de que la imagen no se distorsione, de si cambiamos o no el tamaño de lo que contiene según sus nuevas medidas, de si dibujamos todos los polígonos o por contra recortamos una parte...

Todos estos casos ya dependen de la aplicación en concreto.

¿ Qué es MUI ?libreria MUI

Un trabajador de SGI ( Silicon Graphics ) llamado Tom Davis codificó una pequeña librería a partir de GLUT llamada MUI (Micro User Interface). Lo hizo para usarla él mismo en un proyecto interno de empresa pero dada su facilidad y versatilidad de uso la incluyó de forma totalmente gratuita con GLUT.

Se trata de una serie de funciones que podemos usar fácilmente para crear ventanas con botones, barras de desplazamiento, casillas de selección y verificación....todo al estilo Motif/Windows que tanto nos gusta y sabemos manejar.

Esta librería puede obtenerse conjuntamente con GLUT a partir de la versión 3.5 de éste. Ahora van por la 3.6. Lo podéis encontrar aquí.

Es tremendamente sencilla y perfecta para proyectos pequeños/medios. Para más información conectaros aquí.

Menús

GLUT nos permite crear menús jerárquicos de varios niveles. Se activan mediante la presión de uno de los botones del ratón, el que elijamos (normalmente el derecho), y cuando estemos sobre la ventana de visualización.

Si lo que queremos son menús que cuelguen de la ventana (típicos de cualquier aplicación Windows) deberemos recurrir a algo más sofisticado como XWindows, Motif o MUI, que también los implementa.

Vamos a crear un sencillo menú asociado a la presión del botón derecho de nuestro ratón. Todo lo haremos desde el MAIN del programa:

Queremos crear este menú:

Menús en OpenGL

Como véis tenemos dos niveles de menú en la primera opción mientras que el resto son opciones de un sólo nivel. Esto lo codificaríamos así:
 
 

/* Funciones de Control del menú seleccionado */  
/* Se ejecutan cuando el usuario utilize los menús */  

void menu_nivel_2(int identificador){  

    /* Según la opción de 2o nivel activada, ejecutaré una rutina u otra */  

    switch( identificador){  
        case 0:  
            ControlVertices( );  
            break;  
        case 1:  
            ControlNormales( );  
            break;  
        case 2:  
            ControlAristas( );  
            break;  
    }  
}  

void menu_nivel_1(int identificador){  

    /* Según la opción de 1er nivel activada, ejecutaré una rutina u otra */  

    switch( identificador){  
       case 0:  
            ControlLuces( );  
            break;  
       case 1:  
            ControlColisiones( );  
            break;  
       case 2:  
            ControlSonido( );  
            break;  
       case 3:  
            exit( -1 );  
    }  
}  

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

    int submenu, id;  

    /* Definición de la ventana */  

    glutInit(&argc, argv);  
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGB|GLUT_DEPTH);  
    glutInitWindowSize(500, 500);  
    glutInitWindowPosition(0, 0);  
    id=glutCreateWindow("Ventana con menú contextual");  

    /* Creación del menú */  
    submenu = glutCreateMenu(menu_nivel_2);  

    glutAddMenuEntry("Vértices", 0);  
    glutAddMenuEntry("Normales", 1);  
    glutAddMenuEntry("Aristas", 2);  
    glutCreateMenu(menu_nivel_1);  
    glutAddSubMenu("Ver", submenu);  
    glutAddMenuEntry("Luces on/off", 0);  
    glutAddMenuEntry("Colisiones on/off", 1);  
    glutAddMenuEntry("Sonido on/off", 2);  
    glutAddMenuEntry("Salir", 3);  
    glutAttachMenu(GLUT_RIGHT_BUTTON);  
    ........  
    ........  
    aquí vendrían los callbacks, el Loop, el retorno del entero....lo de siempre!!!  
    ........  
    ........  
}

Las rutinas asociadas a cada opción de menú no las defino, claro. Eso ya dependería de la aplicación. Tan sólo me interesa la pura creación del menú sin importarme las acciones que se llevan a cabo si se activa, ok?

Yo creo que el código ya se entiende bastante bién de por sí pués es muy lógico. Voy a comentarlo un poco.

Fijaros que con glutCreateMenu, genero un nuevo menú y además le asocio la rutina que tendrá que llamarse cuando este menú se active. En el caso del menú de primer nivel se llamará a menu_nivel_1, mientras que el menú de segundo nivel llamará a menu_nivel_2.

Al menú de primer nivel le asociamos 5 posibles opciones a activar usando glutAddMenuEntry. La primera, Ver, desplegará otro menú mientras que las restantes cuatro deben procesarse en menu_nivel_1. Es por eso que a cada opción se le asocia un identificador (integer), de manera que en la función menu_nivel_1 se hace una cosa u otra dependiendo de este entero. Éste nos indica que opción se activó. Lo controlamos con un Switch de ANSI C (analizador de casos posibles).

Para el menú de segundo nivel todo es idéntico excepto que se "engancha" al de primer nivel mediante...

glutAddSubMenu("Ver", submenu);

que le dice al menú de primer nivel que la primera de sus opciones se llama Ver y debe llamar a un menú asociado al entero submenu. Mirad que este entero lo hemos asociado usando:

submenu = glutCreateMenu(menu_nivel_2);

y por supuesto este segundo nivel también dispone de sus propias opciones, creadas de igual manera que antes. Por útimo le decimos a GLUT que este menú debe asociarse a pulsar el botón derecho del ratón con:

glutAttachMenu(GLUT_RIGHT_BUTTON);

Igual que siempre os digo, leedlo detenidamente, pensadlo....si podéis provadlo...y ya me comentareis que tal !!!......8)

Arquitectura Cliente-Servidor

En una red contamos con ordenadores servidores (servers) que llevan a cabo acciones demandadas por ordenadores clientes (clients). Si yo quiero imprimir un documento y la impresora se encuentra físicamente ubicada en otro edificio, tendré que dialogar usando mi ordenador (cliente), con otro ordenador (servidor) para que me permita mandarle mi documento y lo imprima por mí.

El hecho de contar con diversos servidores y muchos clientes configura una red (network) donde todos los esfuerzos se comparten, se distribuyen y así se consigue un comportamiento óptimo, veloz y barato !!!

Una estación de trabajo que cuente con una pantalla, un teclado, un ratón ... puede actuar perfectamente como un servidor de gráficos. Esta máquina nos provee de servicios de salida gráfica en pantalla, y de entrada gracias al teclado y al ratón. Estos servicios podrán ser requeridos por cualquier cliente que pertenezca a la red.

Nuestras aplicaciones OpenGL son clientes que usan al servidor de gráficos para ejecutarse y visualizarse. Pensad que deberíamos ser siempre capaces de ejecutar la misma aplicación en diferentes servidores de la red. Esta es la filosofia interna de trabajo de nuestra querida librería.

Para la realización de grandes producciones cinematográficas plagadas de gráficos (simulación física) como Titanic, Spawn o Perdidos en el Espacio, se necesitaron decenas de ordenadores conectados en paralelo, compartiendo memoria y procesadores, y calculando como locos 24 horas al día. Estaciones Silicon, Alpha o Sun, sistemas operativos Irix, Motif, Linux... y todo a la vez. Y es que la unión hace la fuerza no??

"Display lists" en OpenGL.

Las display lists de OpenGL ilustran muy claramente como podemos utilizar la filosofia cliente-servidor para nuestros gráficos.

Supongamos que tenemos un ordenador dedicado única y exclusivamente a dibujar primitivas en el tubo de rayos catódicos (CRT) de nuestro monitor. Supongamos que esta máquina cuenta con un muy limitado juego de instrucciones de programación que sabe ejecutar. Tan sólo sabe dibujar lo que le manden. Otro ordenador compila y ejecuta el programa, genera unos resultados y le manda las correspondientes instrucciones para dibujarlos al Display Processor ( ordenador dedicado en exclusiva a renderizar por pantalla ). Estas instrucciones se almacenan en una memoria de render (display memory) en forma de fichero/lista de primitivas (display file o display list).

Así cada vez que le mandemos la display list al display processor, éste ya se encargará de dibujar cuando lo crea conveniente y nosotros quedamos libres para dedicarnos a otros quehaceres.

El display processor mandará dibujar la display list a una frecuencia lo suficientemente alta como para evitar parpadeo en pantalla, y lo hará solito. Así nosotros le decimos lo que tiene que dibujar una sola vez y él ya lo hace repetidamente. ¿No os parece que nos ahorramos así mucho trabajo?

Fijaros en la figura:

Display Lists en OpenGL

Tal como decía, nosotros asumimos el papel cliente ejecutando el programa, generamos una display list con lo que hay que dibujar, se lo enviamos a la DPU (Display Processor Unit - Unidad de Proceso de Render) y que dibuje!!!

Hoy por hoy lo que se llamaba DPU se ha sustituido por un ordenador servidor de gráficos mientras que lo que en la figura menciono como host es nuestro ordenador cliente. Los problemas que nos encontramos con esta arquitectura se refieren a la velocidad a la que podemos trabajar, ya sabeis que las redes no "corren" a veces a la velocidad que deberían. Por otra parte y gracias a utilizar hardware específico gráfico, conseguimos equilibrar la balanza, al aumentar la velocidad a la que trabajamos.

Pues bién, usaremos entonces las display lists en OpenGL para todo aquello que tengamos que dibujar siempre, de forma contínua, y no queramos ir recalculando cada vez. Lo calcularemos al principio, lo mandaremos en forma de lista y llamaremos a una función de tanto en tanto para recordarle al servidor que debe dibujar!!!.

Defino una display list con OpenGL:

glNewList(CUADRADO, GL_COMPILE);
    glBegin(GL_POLYGON);
        glColor3f(0.0, 0.0, 1.0);
        glVertex3f(-10.0, -10.0, 0.0);
        glVertex3f(10.0, -10.0, 0.0);
        glVertex3f(10.0, 10.0, 0.0);
        glVertex3f(-10.0, 10.0, 0.0);
    glEnd( );
glEndList( );

Es un polígono que consiste en un cuadrado de lado 20 y de color azul que se encuentra situado sobre el plano Z=0. Inicio la lista con glNewList pasándole el nombre que tendrá esta lista, en nuestro caso usamos CUADRADO, y también el parámetro GL_COMPILE. Éste se encargará de que se envie la lista al servidor para que la guarde pero que no la dibuje hasta que se lo ordenemos.

Finalizamos la lista con glEndList, de manera que OpenGL ya sabe que el código que escribamos a continuación no forma parte de la display list.

Ahora cada vez que deseemos que se dibuje de nuevo nuestro cuadrado, sólo tendremos que ejecutar esta línea:

glCallList(CUADRADO);

que avisa al servidor y le obliga a dibujar lo que contenga la lista llamada CUADRADO que ya se le envió anteriormente. Así nosotros ya no tendremos que dibujar por nosotros mismos, el servidor lo hará cuando se lo mandemos. Resultado: un considerable aumento de la velocidad de ejecución de nuestro programa en tiempo real. Y si trabajamos localmente y no contamos con red ni con servidor de gráficos, también notaremos un gran aumento de velocidad pues la lista queda ya almacenada en el pipeline gráfico, justo antes de su salida por pantalla. No vuelve a pasar por todo el pipeline nunca más, que es lo que ocurriría si mandaramos a dibujar el cuadrado nosotros, cada vez.

Uno de los grandes ejemplos de uso de display lists en OpenGL es la generación de un alfabeto, de una fuente (font). Para escribir texto en OpenGL, se suelen crear las letras 3D con polígonos, y después se dibujan en pantalla. Lo que puede hacerse es crear cada letra (trabajo duro sin duda...), almacenarla en una display list y después tan sólo llamarla cuando quieras dibujarla....

Un ejemplillo

En uno de los libros que siempre os he recomendado, Interactive Computer Graphics de Edward Angel, hay un ejemplo muy interesante que ilustra el uso de callbacks, display lists e interacción con el usuario. Es un programa que implementa un pequeño editor gráfico que es capaz de dibujar puntos, líneas, cuadrados y triangulos con el simple uso del ratón.

Para acceder al código en C de este ejemplo pulsad aquí.

Que lo disfrutéis!!!

Doble buffer

¿Qué ocurre cuando ejecuto mi programa?, tengo una escena rotando y...hay parpadeo!!!...se vé fatal!!!....¿y esto a qué se debe?

El parpadeo o flicker se produce ya que estamos utilizando un frame buffer simple para renderizar y no uno doble que nos ahorraría este problema. El programa regenera la ventana de visualización a una determinada frecuencia (entre 50 y 75 Hz) y esto es problemático en el sentido de que a veces aún no se ha terminado de renderizar la ventana y ya empezamos a dibujar la siguiente, con lo cuál nuestro ojo percibe un molesto parpadeo. Pero no podemos disminuir la frecuencia a menos de 50 Hz porque entonces también percibimos parpadeo debido a que nuestro cerebro nota el cambio sucesivo de una imagen tras otra.

Para evitarlo utilizaremos el doble buffering, es decir, cada vez que deseemos renderizar de nuevo, lo haremos sobre una porción del frame buffer que aún no está activa. La otra porción es la que se está dibujando. Cuando ésta ya esté acabada, pasaremos a la porción remanente con la siguiente imagen ya preparada para ser dibujada. Así sucesivamente, vamos intercambiando las dos porciones y una siempre se está dibujando mientras la otra está recibiendo lo siguiente que deberemos renderizar.

De hecho tenemos dos frame buffer's. Se les llama Front y Back buffer. Se muestra el front, luego el back, luego el front de nuevo, el back.....y así sucesivamente. Debemos por tanto activar esta forma de trabajo y además debemos intercambiar los buffers de tanto en tanto. Usaremos:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

en la función MAIN, en la definición de la ventana. La primera constante activa el doble buffering.

Por otra parte y cada vez que hayamos dibujado de nuevo, llamaremos a:

glutSwapBuffers( );

para intercambiar los buffers y dejarlos listos para la próxima vez. OpenGL hará el resto!!!

Buuuuf...parecía que no iba a acabar nunca, ¿verdad?...bueno ya termino por esta edición. Nos vemos en dos meses de nuevo!!




AULA MACEDONIA
a
MACEDONIA Magazine