"Ningún juego dura tanto hasta que te lo terminas como el que te programas tú mismo,
ninguno te absorbe tanto y ninguno te produce tanta satisfacción cuando lo has acabado"
Programacion de videojuegos
Inicio Curso de programación de juegos 23. Linux. Programa básico XWindow y OpenGL
Lunes 05 de Junio del 2023

Menu principal
Colaborar (con PayPal)

Para continuar con el trabajo de esta Web y poder pagar el hosting, viene bien la ayuda que sea. Gracias a todos.

Importe: 

Ultimas descargas
19.Jan

Clase que permite dibujar texto en OpenGL con mucha facilidad.Usa FreeType2.Para ver que hace y c...


23. Linux. Programa básico XWindow y OpenGL Imprimir Correo electrónico
Videojuegos - Curso de Programación de juegos
Escrito por Vicengetorix   

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 .





¡Sólo los usuarios registrados pueden escribir comentarios!
+/- Comentarios
Buscar
javier   |201.157.13.xxx |10-11-2013 21:45:57
Estupendo tutorial. Sólo me gustaría aclarar que no es "XWindows" sino
XWindow cuando se refiere a linux.

Saludos y felicidades.
Vicengetorix   |89.130.162.xxx |11-11-2013 00:13:53
Gracias a todos por los ánimos, y gracias, Javier, por la aclaración, ahora lo
cambio.
LoboOscuro  - Perfecto!     |190.221.24.xxx |21-09-2011 02:37:35
Ojala continues asi! Esto me ayudo bastante a comprender mejor sobre los eventos
y algunas propiedades de las ventanas! Esto es lo mejor de todo tu blog!
Anónimo   |190.121.238.xxx |09-02-2011 06:18:13
esta finuuuuu :D
in0x   |79.156.15.xxx |05-09-2010 15:18:23
He intentado compilar desde code::blocks para probarlo, pero me da varios
errores como éste:
usw23.cpp:(.text+0x43)||undefined reference to
`XOpenDisplay'|

Tengo todas las librerías necesarias instaladas (que yo sepa),
no sé si hay que configurar algo en code::blocks. También he probado de
compilar desde la terminal indicandole -lX11 y nada, sabéis por dónde puede
estar el problema?

Trabajo bajo Ubuntu 10.4 LST
in0x   |81.36.136.xxx |05-09-2010 20:40:29
Ya he solucionado el problema que tenía, me fataba linkar las otras
librerías.

Pero sólo lo he conseguido compilando con g++, desde code::blocks
no hay manera, cómo se linkan las librerías en code::blocks?

Gracias
Vicengetorix   |85.53.222.xxx |07-09-2010 18:34:44
Botón derecho sobre el proyecto en el explorador de proyectos, opción Project
settings, botón project's build options(abajo), pestaña linker settings,
cuadro link libraries. Aquí yo tengo puesto GL, X11 y GLU.

A mi me funciona.
in0x   |83.32.204.xxx |10-09-2010 12:57:26
Gracias Vicengetorix, al final lo encontré, no estoy familiarizado con
code::blocks. Funciona de lujo :)
kokorico  - Cual es la razon de Atom?   |80.169.246.xxx |27-07-2010 18:55:19
Hola,

Muchas gracias por el articulo. Lo que no entiendo es por que usas atom.
Es decir, si ya procesas los eventos de teclado y raton y los pasas a un array o
a la estructura que quieras, para que necesitas subscribirte a eventos? No es
hacer doble trabajo? O es que el evento de cerrado no es puede procesar como
event.WM_CLOSE_WINDOW?

Muchas gracias
Vicengetorix   |85.53.192.xxx |27-07-2010 21:45:10
Eso crei yo al principio.
Si ejecutaba desde el entrono grafico no habia
problema (aparentemente), pero al ejecutar desde la shell, al cerrar la ventana
el programa termina con error y no procesa el evento WM_CLOSE_WINDOW.
El atom
indica a la ventana que al dar al aspa, en vez de cerrarse de inmediato, lo que
provoca el error al quedarse el programa sin ventana de repente, solo envie un
mensaje que podamos capturar como "ClientMessage". Al cerrar via comando
nosotros, se ejecutara el evento WM_CLOSE_WINDOW.
Pechel   |148.202.29.xxx |05-07-2010 19:31:17
Muy chido pero no veo donde los eventos en tiempos de ejucucion dice que se
movera o se repintara o no se donde.
Vicengetorix   |85.53.215.xxx |05-07-2010 22:51:30
El evento "ConfigureNotify" gestiona el cambio de tamaño de la
ventana.
El evento "Expose" es el que indica que necesita ser repintada
(en nuestro caso se repinta siempre y no hace falta).

3.26 Copyright (C) 2008 Compojoom.com / Copyright (C) 2007 Alain Georgette / Copyright (C) 2006 Frantisek Hliva. All rights reserved."

 


Banner
Spanish Chinese (Simplified) English French German Japanese Portuguese Russian