Vamos a entrar en la programación en Linux, concretamente con XWindow (X11) y OpenGL. Iremos poco a poco, como es habitual. Al terminar el capÃtulo seremos capaces de hacer un pequeño programa que visualiza una ventana en la que podemos pintar con OpenGL y capturamos eventos como el teclado y el ratón.
Recomiendo antes de nada, leer el artÃculo en que se explica el entorno en el que está hecho el pequeño programa que vamos a desarrollar. Es este.
En primer lugar una pequeña explicación de ciertos detalles que, a los que estamos acostumbrados a Windows, nos son extraños. XWindow es el sistema de ventanas de los sistemas UNIX. En los UNIX el sistema de ventanas es opcional. Imaginad un servidor que solo tocan por consola los técnicos; un sistema de ventanas solo servirÃa para cargar más el servidor. En las distribuciones Linux actuales, las XWindow se integran con el Linux propiamente dicho de forma que el usuario no distingue entre uno y otro. Al instalar se instalan por defecto las XWindow, es más, la instalación es gráfica. Para un PC de casa eso facilita mucho el uso y ningún usuario normal prescinde de un entorno gráfico como GNOME o KDE, y estos dos funcionan sobre las XWindows. La forma de llamar a las XWindow es con una X mayuscula, y si añadimos la versión será X11. En temas más concretos diré que en Linux (por lo menos en Ubuntu 10.04) los ficheros de inclusión de las librerÃas instaladas para C o C++ están en /usr/include/ y las librerÃas en si están en /usr/lib/ . En este último directorio están las librerÃas dinámicas y las estáticas. Las dinámicas tienen estensión .so y a veces detras llevan la versión (ejemplo: libreria.so, libreria.so.2.1). Para enlazar el programa con una de estas librerÃas dinámicas, no es necesario como en Windows una librerÃa intermedia ( en Windows para usar libreria.dll necesitamos una libreria.lib ), si no que se enlaza directamente, al linkar, con la extensión el sistema sabe de que tipo es. En caso de existir las dos versiones, una .so y una .a (estática), se puede indicar con el parámetro -static que use la estática, en caso contrario usará la dinámica. ¿He dicho ya que las estáticas tienen estensión .a?.
Tras la parte teórica podemos empezar con el código.
Comenzamos con los includes necesarios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
// Por si los necesitamos #include <stdio.h> #include <stdlib.h> // glx incluye los ficheros <X11/Xlib.h> y <X11/Xutil.h> // para las X11. #include <GL/glx.h> // Para OpenGL. #include <GL/gl.h> // Para la libreria de utilidad glu. // En caso de no usarla no sria necesario. #include <GL/glu.h> // Definiciones de X11. #include <X11/X.h> // Para el tema de teclado, definiciones. #include <X11/keysym.h>
|
Sobre esto. El fichero glx.h permite usar una serie de funciones análogas a las que usabamos en Windows que empezaban por wgl. Son para buscar el formato de pixel, crear un contexto de OpenGL, ... la inicialización dependiente del sistema operativo. Para Windosws son wgl y para Linux glx, logico. Este fichero incluye dentro, los ficheros necesarios para las XWindows (Xlib.h y Xutil.h) asà que no hace falta incluirlos ya. Incluimos glu para facilitar las cosas y luego dos ficheros para definiciones de X11.
Tras los includes vienen las definiciones globales.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
// Variable para indicar al programa // que se termine. bool SALIR = false; // Puntero para acceso a la pantalla. // En realidad es del servidor X // pero a efectos practicos es nuestra // pantalla. Display *Pantalla; // Identificador de la ventana que // vamos a crear. Window IDventana; // Estructura para las caracteristicas de // la pantalla XVisualInfo *visual_info; // Estructura para atributos de ventana. XSetWindowAttributes Atri_ventana; // Identificador del contexto de GLX GLXContext ContextoGLX; // Estructura para manejar eventos. XEvent evento; // Matriz para almacenar el estado de las teclas. // Los "KeyCodes", los codigos de tecla seran // el indice. bool TECLAS[255];
|
La mayoria son definiciones para crear la ventana al estilo XWindow. La primera variable "SALIR" la usaremos para poder indicar en cualquier momento al programa que termine, simplemente poniendola el valor true. La última matriz será para almacenar el estado del teclado (igual que haciamos en Windows).
A partir de aquà estaremos ya en la función main(...), la función principal de un programa C o C++, y por donde se empieza a ejecutar el programa.
1 2 3 4 5 6 7 8
|
// Funcion principal en C o C++. Aqui comienza a ejecutar. int main(int argc, char **argv) { // Variable auxiliar para lo que surja. int aux; // Inicializo la matriz del teclado a false; for(aux=0;aux<255;aux++) TECLAS[aux]=false;
|
A parte de ampezar main hemos inicializado toda la matriz de estado de teclado a false (sin pulsar).
Lo primero que hacemos para crear una ventana será encontrar un Display válido. XWindow actua como un servidor y puede controlar muchas pantallas, incluidas pantallas de otro PC a las que se conecta por red. En nuestro caso solo tenemos una y está claro que la pantalla de nuestro PC es la pantalla por defecto. Si algo sale mal terminamos el programa con error al estilo UNIX, no sin antes escribir lo que pasa en la salida de errores estandard. Al estilo UNIX también.
1 2 3 4 5 6 7 8 9
|
// Localizo una pantalla que usar. Normalmente, en un PC, // deberia haber almenos una. Si se llama con NULL // obtendremos la pantalla por defecto. Pantalla = XOpenDisplay(NULL); // Si no se puede, copio el texto del error a la // salida de error standard y termino el programa. if (Pantalla == NULL) { fprintf(stderr, "No puedo usar ninguna pantalla"); exit(1); }
|
Comprobamos que la pantalla que vamos a usar acepta las funciones glx. Si no, vamos a error.
1 2 3 4 5 6 7 8
|
// Comprobamos que se pueda usar las funciones GLX // en esta pantalla (con su driver). El segundo y // tercer parametro no los necesitamos asi que no // usaremos los valores que retornan. if(!glXQueryExtension(Pantalla, &aux, &aux)) { // Si no se puede vamos a error. fprintf(stderr, "No puedo usar GLX"); exit(1); }
|
Buscamos ahora un formato de pixel adecuado para lo que queremos, RGBA, con buffer de profundidad de 16 bits y con doble buffer, vamos, lo básico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
// En una matriz de int's definimos las caracteristicas del formato // de pixel que queremos. Lo ultimo debe ser "none". // Aqui indicamos que: use RGBA para cada pixel, que use 16 // bits para el buffer de profundidad y que use doble buffer. static int FormatoPixel[] = {GLX_RGBA, GLX_DEPTH_SIZE, 16, GLX_DOUBLEBUFFER, None}; // Buscamos el formato de pixel con las caracteristicas de arriba // para la pantalla en que trabajamos y cargamos la informacion // en una estructura que esta para eso. visual_info = glXChooseVisual(Pantalla, DefaultScreen(Pantalla), FormatoPixel); // Si no se pude elegir el formato que queremos vamos a error. if (visual_info == NULL) { fprintf(stderr, "No encuentro Formato de Pixel adecuado"); exit(1); }
|
Creamos un contexto OpenGL que sea compatible con el formato de pixel que hemos definido.
1 2 3 4 5 6 7 8
|
// Creamos un contexto para OpenGL. Indicamos la pantalla, // informacion visual, contextos con los que comparte display lists // (ninguno para nosotros) y si accede directamente al hardware. ContextoGLX = glXCreateContext(Pantalla, visual_info, None, GL_TRUE); // Si no se puede crear vamos a error. if (ContextoGLX == NULL) { fprintf(stderr, "No puedo crear el contexto OpenGL"); exit(1); }
|
Vamos ya a crear una ventana.
Primero indicamos ciertos atributos en una estructura para esto. El colormap es necesario porque si no coincide dará error al crear la ventana. El tipo de pixel del borde no nos va a preocupar mucho ahora. Los tipos de eventos que vamos a querer controlar de la ventana si que son importantes (si se pulsa una tecla, se mueve el ratón, se redimensiona la ventana, ...).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
// Damos los valores que queremos a la estructura // Atri_ventana. // Indicamos el colormap, la estructura de colores. // Si no fallara al crear la ventana. // Lo hacemos en base a visual_info. Atri_ventana.colormap=XCreateColormap(Pantalla, RootWindow(Pantalla, visual_info->screen), visual_info->visual, AllocNone); // Indicamos que eventos hay que notificar a esta ventana. Atri_ventana.event_mask = KeyPressMask | ExposureMask | ButtonPressMask | StructureNotifyMask | KeyReleaseMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask; // El tipo de pixel del borde. Atri_ventana.border_pixel = 0;
|
Ahora sÃ, creamos la ventana, le damos tÃtulo, la asociamos al contexto de OpenGL, hacemos la ventana visible y la ponemos donde queremos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
// Creamos la ventana. IDventana = XCreateWindow( Pantalla, // La pantalla RootWindow(Pantalla, visual_info->screen), // La ventana padre 0, 0, // Posicion de la ventana 600, 400, // Tamaño de la ventana 0, // Ancho del borde visual_info->depth, // Profundidad respecto a otras ventanas InputOutput, // Tipo de ventana. Debe ser este para poder pintar. visual_info->visual, // Informacion visual CWBorderPixel | CWColormap | CWEventMask, // Valores que pasamos //en Atri_ventana. &Atri_ventana); // Estructura con atributos de la ventana. // Ponemos mas propiedades a la ventana como el titulo o el // icono(ahora ninguno). En nuestro caso solo el titulo. XSetStandardProperties(Pantalla, IDventana, "USW", "USW", None, argv, argc, NULL); // Asocio la ventana y el contexto GLX glXMakeCurrent(Pantalla, IDventana, ContextoGLX); // Hago visible la ventana XMapWindow(Pantalla, IDventana); // Podemos mover la ventana. XMoveWindow(Pantalla, IDventana, 100,100);
|
Ahora podemos hacemos la inicialización exclusiva de OpenGL. Esto, si hemos seguido el curso, nos lo sabemos todo. No cambia de un sistema operativo a otro.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// Inicializamos ahora OpenGL de la forma que queramos // Que haga comprobacion de profundidad glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glClearDepth(1.0); // Color con que limpia la pantalla cada frame glClearColor(0.0, 0.0, 0.4, 0.0); // Ponemos la perspectiva glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Pongo el viewport. glViewport(0, 0, 600, 400); // Pongo perspectiva. gluPerspective(50.0f, (float)600/(float)400, 0.5f, 50.0f); // Coloco la camara. gluLookAt(0,0,-4,0,0,0,0,1,0); glMatrixMode(GL_MODELVIEW);
|
Esta definición de puntero nos servirá para acceder a los aventos de teclado que llegen. Lo veremos en el bucle de programa.
1 2 3
|
// Puntero del tipo de los eventos de teclado // para cuando se necesite. XKeyEvent *kevento;
|
Lo que viene ahora es un poco más enrevesado de entender. A ver como me sale. Definiremos un Atom. Un Atom es un nombre o identificativo para una caracteristica o evento de una ventana. Primero lo creamos indicando a qué caracteristica o evento se va a referir. PodrÃa ser el titulo, color de fondo, ..., en nuestro caso nos interesa el evento de cierre o borrado de la ventana. Luego lo asociamos a la ventana. Apartir de ahÃ, la ventana sabrá que queremos controlar el cierre de la ventana y cuando álguien pinche con el ratón en el aspa o icono de cierre de esa ventana, lo que pasará es que la ventana enviará un evento en vez de cerrarse sin más. Podremos capturar ese evento, hacer lo que sea necesario y luego cerrar la ventana nosotros mismos, en el programa. Si no lo hicieramos, al pinchar con el ratón en el icono de cierre, la ventana se cerrarÃa y nuestro programa seguirÃa ejecutandose tan feliz, creyendo que tiene una ventana. Al usar la ventana o cualquier cosa que esté asociada a esa ventana, fallará, ¡porque ya no hay ventana y nadie ha avisado al programa!. Asà pues creamos el Atom y lo asociamos a la ventana.
1 2 3 4 5 6 7 8 9 10 11 12 13
|
// Definimos un "Atom", un nombre o indicador para una caracteristica // o evento de la ventana. Asociamos este Atom al evento de borrado de // la ventana "WM_DELETE_WINDOW". Atom CerradoVentana = XInternAtom(Pantalla, "WM_DELETE_WINDOW", true); // Indicamos a la ventana que queremos controlar el evento de borrado // pasandole el Atom. // Mas tarde, cuando comprobemos los eventos tendremos que // comprobar el evento "ClientMessage" para ver si se ha producido // el cierre de la ventana (u otro que hubieramos definido igual). // Por su parte, la ventana, en vez de cerrarse, lo que hara sera // notificar el evento, no se borrara. El borrado o lo que creamos // conveniente, lo haremos nosotros. XSetWMProtocols(Pantalla, IDventana, &CerradoVentana, 1);
|
Entramos ya en el bucle principal del programa. Se ejecutará hasta que "SALIR" sea true.
1 2 3 4 5
|
// Entramos en el bucle del programa. // Se repite continuamente mientras la // variable SALIR no sea true. while (!SALIR) {
|
Lo primero del bucle es revisar los eventos que llegan, teclado ratón, ... y lo haremos con un bucle que se repetirá hasta que no haya eventos pendientes. Dentro de este bucle leeremos el evento siguiente, lo trataremos según el evento que sea con un "switch-case" y al final saldremos del bucle si no hay más eventos que tratar. Los comentarios del código son bastante explicativos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
// Bucle que se repetira hasta haber // tratado todos los mensajes. do { // Obtengo el siguiente mensaje y lo // pongo en "evento". XNextEvent(Pantalla, &evento); // Trato el evento segun el tipo switch (evento.type) { // Si el menseje es producido desde un Atom que hayamos // indicado a la ventana. case ClientMessage: // Comprobamos que efectivamente el mensaje llega // por el cierre de la ventana. En realidad solo // hemos definido este y solo llegara este. if(evento.xclient.data.l[0] == CerradoVentana ) // Ponemos a true el indicador de fin // de programa. SALIR=true; break; // Si es la presion de una tecla. case KeyPress: // Uso el puntero kevento para // acceder a los datos especiales // del evento de teclado. kevento = (XKeyEvent *) &evento; // Uso el keycode como indice de la // matriz del teclado. TECLAS[kevento->keycode]=true; break; // Si se suelta una tecla. case KeyRelease: // Lo mismo que en keypress pero con false kevento = (XKeyEvent *) &evento; TECLAS[kevento->keycode]=false; break; // Si es la presion de un boton del raton. case ButtonPress: //... por ahora no lo usamos break; // Si se suelta un boton del raton. case ButtonRelease: //... por ahora no lo usamos break; // Si se ha reconfigurado la ventana, // cambio de tamaño por ejemplo. case ConfigureNotify: // Ajusto el viewport al nuevo tamaño. glViewport(0, 0, evento.xconfigure.width, evento.xconfigure.height); // Ajusto la perspectiva y la camara glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(50.0f, (float)evento.xconfigure.width/(float)evento.xconfigure.height, 0.5f, 50.0f); gluLookAt(0,0,-4,0,0,0,0,1,0); glMatrixMode(GL_MODELVIEW); break; // Se debe redibujar el contenido de la ventana, // antes estaba oculta por ejemplo. case Expose: //... por ahora no lo usamos break; // Si el raton entra en la ventana case EnterNotify: //... por ahora no lo usamos break; // Si el raton abandona la ventana case LeaveNotify: //... por ahora no lo usamos break; } // Mientras haya mensajes pendientes // sigo con el bucle. } while(XPending(Pantalla));
|
Despues toca pintar la escena y moverla, si es que se mueve. Primero borro. Luego compruebo la tecla ESCape, que es la única que usamos por ahora. Pinto lo que sea (una esfera con glu por ejemplo). Por último cambio los bufferes back y front para presentar lo dibujado en pantalla, lo habitual en OpenGL. Con esto terminamos el bucle principal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
// Aqui pintare la escena y la movere // Limpio el buffer (back buffer) para pintar cosas // nuevas glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Compruebo si se ha pulsado la tecla ESCape. // Si es que si pongo SALIR a true y el programa terminara. // Uso usa funcion para convertir el codigo en que pongo // la tecla a el keycode que uso de indice. if( TECLAS[XKeysymToKeycode(Pantalla,XK_Escape)] ) SALIR=true; // Pongo color glColor3f(1,0,0); // Cargo identidad glLoadIdentity(); // Pinto una esfera con GLU. GLUquadricObj *q; q=gluNewQuadric(); gluQuadricDrawStyle(q, GLU_FILL); gluQuadricNormals(q, GLU_SMOOTH); gluQuadricTexture(q, GL_FALSE); gluSphere(q,1,16,16); gluDeleteQuadric(q); // Cambio los back y front buffer's de forma // que lo que he pintado se vea. glXSwapBuffers(Pantalla, IDventana); } // Fin del bucle principal.
|
Sobre los códigos de tecla. De momento solo usamos "XK_Escape" pero en el futuro querremos saber que código (keysym) corresponde a cada tecla. Lo podremos mirar en el fichero en que se definen. Es este: /usr/include/X11/keysymdef.h.
Terminamos con el borrado de contextos, ventanas y cierres de Display. Aquà llegaremos solo si termina el bucle principal porque la variable "SALIR" es true. El programa termina retornando código 0, sin error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// Si se termina el bucle principal termina // el programa. // Desasocio el contexto glXMakeCurrent(Pantalla, None, NULL); // Destruyo el contexto. glXDestroyContext(Pantalla, ContextoGLX); // Destruyo la ventana XDestroyWindow(Pantalla, IDventana); // Cierro la pantalla, la conexion con // el servidor X. XCloseDisplay(Pantalla); // Termino el programa retornando 0, // que termina bien. return 0; } // Fin de main(..)
|
El resultado serÃa este:
Y el código completo este: usw23.cpp .
|