Observad como el nombre simbolico de los numeros y letras en realidad no existe, es en realidad el mismo codigo el
Virtual Key Code y el ASCII en estos casos (las letras el de la mayuscula). Por eso lo expreso con
'A',
'3',
'H', ... que en C es una forma de poner el codigo ASCII.
Para controlar todo el teclado nos creaemos un array de 255 valores booleanos y cada una de las casillas representara una tecla.
El valor del
Virtual Key Code (la casilla de en medio de la tabla) sera el indice que usaremos para aceder a las teclas en nuestro array y el valor de cada casilla (
true o
false) sera si esta presionada o no. Este valor lo pondremos cada vez que windows envie un mensaje
WM_KEYDOWN (tecla presionada) o
WM_KEYUP (tecla soltada).
Durante el programa podre consultar el estado de cualquier tecla con solo comprobar si la casilla correspondiente es
true (presionada) o
false (no presionada), y acedere a dicha casilla a traves del nombre simbolico (la primera columna) asignado al valor del
Virtual Key Code. El codigo seria parecido a esto:
1 2 3 4 5 6 7 8 9 10 11
|
bool Teclas[256]; . . . case WM_KEYDOWN: Teclas[(int)wParam]=true; . . . if(Teclas[VK_DOWN]) { ... if(Teclas['F']) { ...
|
El ejemplo no es codigo C completo pero ilustra el uso que haremos del metodo. Mas abajo estara mas claro.
Con el raton simplemente nos vamos a crear una estructura con los datos que necesitaremos y la actualizaremos atendiendo a los eventos que sobre el raton llegan del sistema. Algunos eventos del raton no los vamos a usar pero dejaremos el codigo preparado para el futuro.
Los eventos del raton son:
- WM_LBUTTONDOWN Boton izquierdo presionado
- WM_RBUTTONDOWNBoton derecho presionado
- WM_LBUTTONUPBoton izquierdo se levanta
- WM_RBUTTONUP Boton derecho se levanta
- WM_LBUTTONDBLCLK Doble click de boton izquierdo
- WM_RBUTTONDBLCLK Doble click de boton derecho
- WM_MOUSEMOVE El raton se mueve
- WM_MOUSEWHEEL Se gira la rueda del raton
- WM_MOUSELEAVE. El raton sale de la ventana
Este ultimo (cuando el raton sale de la ventana) necesita usar la funcion
TrackMouseEvent cada vez que se mueve el raton para generar el evento (no se porque, preguntad a Microsoft).
En el programa solo tendremos que consultar el estado de esta estructura para saber como esta nuestro raton.
Hasta ahora solo hemos visto conceptos de Windows; es el momento de volver a OpenGL y ver el concepto de la camara o el punto de vista desde el que se ve la escena. Lo que se diga de la proyeccion en perspectiva tambien se puede aplicar a proyeccion ortogonal (isometrica por ejemplo) pero no tendra mucho sentido si pretendemos pintar en 2D.
Hasta ahora, cuando poniamos la proyeccion, la camara o punto de vista, se quedaba siempre en las coordenadas 0,0,0 y mirando hacia la parte negativa del eje Z (por eso lo considerabamos la profundidad de la escena).
Para ver los objetos simplemente los poniamos frente a nosotros, alrededor, mas o menos, del punto 0,0,-4 (por ejemplo) que es un lugar de la escena que sabiamos que estaba delante de la camara.
Para mover estos objetos modificamos la
matriz de modelo con
glTranslate y
glRotate, pues bien, para hacer lo mismo con la camara basta con modificar la
matriz de proyeccion, tras definir la perspectiva, con
glTranslate y
glRotate, igual que haciamos con la de modelo para los objetos.
Con
glTranslate moveremos la camara y con
glRotate la giraremos para mirar en otra direccion, asi de facil.
El proceso sera asi de sencillo: Cambiamos a matriz de proyeccion, definimos la perspectiva, giramos y movemos la camara, y volvemos a la matriz de modelo y el resto del programa.
Ademas, para mas facilidades, podemos usar la funcion
gluLookAt de la libreria glu, que nos hace las traslaciones y giros necesarios para colocar la camara en un punto mirando a otro (nos lo dan mascado).
La funcion
gluLookAt usa 9 parametros, los 3 primeros es la posicion de la camara, los 3 segundos es el punto hacia donde mira la camara y los 3 terceros el vector que indica hacia arriba segun esa camara. Si este ultimo vector no es 0,1,0 quiere decir que la camara esta torcida, y se vera el mundo torcido, es el vector que indica la
inclinacion de la camara:
En la imagen vemos una camara torcida enfocando a un tiesto.
Ahora pasaremos al codigo. En nuestro programa giraremos la camara con el raton presionando el boton izquierdo, podremos alejar y acercar la camara con la rueda del raton o las flechas de arriba y abajo, giraremos los cubos con las teclas 'Q' y 'W', aumentaremos y disminuiremos los FPS del programa con 'A' y 'Z', y veremos o no los FPS en pantalla con la tecla 'F'.
Para todo esto empezamos como siempre por las nuevas
definicionas globales.
Y lo primero sera una definicion nueva para el compilador, sin la cual no reconoce, y por tanto no compila,
WM_MOUSEWHEEL y
TrackMouseEvent.
1 2 3 4 5 6
|
// Tengo que definir esta variable a mas de 0x0400 // para que pueda usar WM_MOUSEWHEEL y TrackMouseEvent, // si no, no la reconoce (ver Winuser.h) #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0500 #endif
|
Luego definimos variables que vamos a usar despues:
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
|
// Defino un contador para girar los cubos. int ang=0; // Contator para alejarnos de la escena. GLfloat dist=0; // Dos contadores para girar la camara, una // para cada eje a rotar. GLfloat ang_camaraX = 0; GLfloat ang_camaraY = 0; // Matriz que almacena el estado del teclado. bool Teclas[256]; // Variable para almacenar el ultimo caracter pulsado. unsigned char Letra; // Indicador de si son visibles los FPS en pantalla. bool pintaFPS=false; // Indicador de si hemos levantado la tecla. bool LevantaTecla=true; // Estructura para los datos del raton que vamos a usar. // Observad como guardamos la coordenada del raton actual // y la del frame anterior. struct raton { int prev_x; int prev_y; int rueda; int x; int y; bool izquierdo; } Raton; // Estructura necesaria para controlar si el raton sale de // nuestra ventana con la funcion TrackMouseEvent. TRACKMOUSEEVENT tme;
|
En la funcion
IniciaGL(), inmadiatamente despues de la definicion de la proyeccion en perspectiva, coloco la camara con la funcion
gluLookAt en 0,0,3 mirando a 0,0,0 y recta (0,1,0).
1 2
|
// Coloco la camara y pongo hacia donde mira con la funcion gluLookAt gluLookAt(0,0,3, 0,0,0, 0,1,0);
|
En la funcion
CreaVentana() vamos solo a introducir el codigo para dar valores a la estructura
TRACKMOUSEEVENT con objeto de controlar cuando sale el raton de la ventana con la funcion
TrackMouseEvent y generar el evento correspondiente (con otros eventos no hay que hacer esta movida, preguntad a Microsoft).
1 2 3 4 5 6
|
// Relleno los datos de la estructura TRACKMOUSEEVENT // para poder controlar cuando sale el raton de la ventana tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = IdVentana; tme.dwHoverTime = 1;
|
Vamos ahora a la funcion
ProcesaMensajes(...) donde esta la parte importante del control de teclado y raton, la captura de mensajes procedentes del sistema. Recapitulamos sobre esta funcion.
Esta funcion se asigna, cuando creamos la ventana, como la funcion que gestiona los mensajes que llegan a la ventana. El segundo parametro es el tipo de mensaje que le llega y los dos ultimos parametros son los datos asociados al mensaje que llegue (diferente significado para cada mensaje). Asi, con un "
switch -
case" de C comprobamos que mensaje llega y actuamos en base a ello.
En nuestro programa no usaremos todos los eventos de raton, pero en el codigo los dejaremos todos ya puestos aunque si llegan algunos no haremos nada. En el futuro no tendremos que buscar el evento que era, solo tendremos que teclear en el codigo adecuado en la clausula
case correspondiente.
En el
case del evento
WM_KEYDOWN ya vimos hace tiempo un ejemplo de uso del teclado al detectar la tecla ESC para poder salir del programa (y del simbolo de su
Virtual Key Code:
VK_ESCAPE ).
Este es el codigo de los "
case" que hemos incluido o cambiado.
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 84 85 86 87
|
// En caso de presionar el boton izquierdo del raton case WM_LBUTTONDOWN: // Pongo true en la variable para el boton // izquierdo de nuestra estructura. Raton.izquierdo = true; break; // En caso de presionar el boton derecho del raton case WM_RBUTTONDOWN: break; // En caso de soltar el boton izquierdo del raton case WM_LBUTTONUP: // Pongo false en la variable para el boton // izquierdo de nuestra estructura. Raton.izquierdo = false; break; // En caso de soltar el boton derecho del raton case WM_RBUTTONUP: break; // En caso de hacer doble click con el boton izquierdo del raton case WM_LBUTTONDBLCLK: break; // En caso de hacer doble click con el boton derecho del raton case WM_RBUTTONDBLCLK: break; // En caso de que el raton salga de nuestra ventana case WM_MOUSELEAVE: // Pongo false en la variable para el boton // izquierdo de nuestra estructura. // Si el raton sale de la ventana es como // si soltara es boton. Raton.izquierdo = false; break; // En caso de que se mueva el raton case WM_MOUSEMOVE: // Si se mueve fuera de la ventana genero el mensaje // WM_MOUSELEAVE con esta funcion TrackMouseEvent(&tme); // Guardo las nuevas coordenadas del raton que me // llegan en el parametro lParam en las variables // x e y de nuestra estructura. Raton.x=LOWORD(lParam); Raton.y=HIWORD(lParam); break; // En caso de mover la rueda del raton. case WM_MOUSEWHEEL: // Pongo el valor en la variable para la rueda // de nuestra estructura. // Hacia delante sera positivo y hacia atras // sera negativo. Raton.rueda=(int)wParam; break; // Caso de mensaje de pulsacion de una tecla case WM_KEYDOWN: // Pongo a true el estado de la tecla en el array que // representa el teclado. wParam contiene el Virtual // Key Code de la tecla. Teclas[(int)wParam]=true; // El caso de ESC para salir lo trato especialmente. switch(wParam) // y segun el contenido de wParam { case VK_ESCAPE: // ESC causa la salida del programa // Funcion para enviar mensaje de cerrado a la // ventana y despues de la aplicacion PostMessage(Ventana, WM_CLOSE, 0, 0); break; } break; // En caso de soltar una tecla case WM_KEYUP: // Actualizo el estado de la tecla en el array a false. Teclas[(int)wParam]=false; // Pongo a 0 la variable para guardar el codigo ASCII // de la tecla pulsada. Letra=0; break; // En caso de que se pulse una tecla con codigo ASCII. // Este evento nos servira para hacernos una especie de // getchar o gets e introducir texto. case WM_CHAR: // Guardo el codigo ASCII generado Letra=(unsigned char)wParam; break;
|
En la funcion
WinMain(...) añadiremos algo.
Dentro del
if en el que pintamos la escena si le ha llegado el momento y despues de pintar la escena, actualizo algunos valores de la estructura de los datos del raton y la dejo preparada para la proxima pasada. Otros datos se actualizan solo en base a los eventos que vimos antes.
1 2 3 4 5 6 7 8 9 10
|
// Pintamos nuestra escena. Pinta(); // Al terminar el bucle pongo a 0 nuestro // valor para la rueda del raton Raton.rueda=0; // Guardo las coordenadas del raton como las anteriores // ahora que las he usado como actuales. Raton.prev_x = Raton.x; Raton.prev_y = Raton.y;
|
Despues de esto, ya tenemos la infraestructura preparada para usarla en la funcion
Pinta(). Aqui definiremos, en base al teclado y en raton, como se comportara el programa.
El primer bloque de codigo sera el que defina el movimiento de la camara en base al raton, asi que lo primero sera poner la matriz de proyeccion, la que modifico con este bloque.
Basicamente lo que hace es modificar los valores de las variables globales que habiamos definido, en caso de que el boton izquierdo del raton este presionado, segun el movimiento del raton. Luego aplica los giros y traslaciones adecuadas con los datos de esas variables.
Para saber como esta el raton basta con preguntar por el valor de las variables de la estructura "
Raton".
El codigo tiene suficientes comentarios, creo.
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
|
// Seleccionamos la matriz de proyeccion. glMatrixMode(GL_PROJECTION); // Cargo la identidad. glLoadIdentity(); // Pongo la perspectiva. gluPerspective(50.0f,(float)rect.right/(float)rect.bottom,0.5f,50.0f); // Podria hallar los datos y usar la funcion gluLookAt pero no lo voy // a hacer. //gluLookAt(0,0,0+dist, 0,0,-1, 0,1,0); // Si el boton izquierdo del raton esta presionado if(Raton.izquierdo) { // Si he movido el raton a la derecha if(Raton.prev_x > Raton.x) ang_camaraX--; // Si he movido el raton a la izquierda if(Raton.prev_x < Raton.x) ang_camaraX++; // Con estas dos lineas controlo que el angulo x este entre 0 y // 360. No es necesario pero si me dedico a dar vueltas, el // programa podria hacer cosas raras. if(ang_camaraX < 0) ang_camaraX+=360; if(ang_camaraX > 360) ang_camaraX-=360; // Si he movido el raton hacia abajo if(Raton.prev_y > Raton.y) ang_camaraY--; // Si he movido el raton hacia arriba if(Raton.prev_y < Raton.y) ang_camaraY++; // Con estas dos lineas controlo que el angulo y este entre 0 y // 360. No es necesario pero si me dedico a dar vueltas, el // programa podria hacer cosas raras. if(ang_camaraY < 0) ang_camaraY+=360; if(ang_camaraY > 360) ang_camaraY-=360; // Si giro la rueda del raton hacia delante if(Raton.rueda>0) { // Aumento la distancia en 0.5 dist+=0.5; // Controlo que no sea mayor de 20, // no sea que lo pierda de vista if(dist>20) dist=20; } // Si giro la rueda del raton hacia atras if(Raton.rueda<0) { // Reduzco la distancia en 0.5 dist-=0.5; // Controlo que no sea menor de 0, asi // mantengo la escena a la vista. if(dist<0) dist=0; } } // fin if(Raton.izquierdo) // Aplico los cambios en las variables a la matriz de // proyeccion y asi modifico la camara. Cuidado con // el orden de las transformaciones. // Giro a izquierda o derecha glRotatef(ang_camaraX/4,0,1,0); // Giro arriba o abajo glRotatef(ang_camaraY/4,1,0,0); // Me alejo o acerco de la escena. glTranslatef(0,0,0-dist);
|
Tras esto regreso a la matriz de modelo.
Este bloque de codigo sera de control de teclado. Para preguntar por una tecla solo tengo que mirar el valor que hay en su posicion del array "Teclas", para lo que usare el nombre simbolico de la primera columna de la tabla que vimos antes con los
Virtual Key Codes.
Los comentarios indican lo que hara cada tecla.
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
|
// Controlo ahora lo que hago con teclado // Si presiono la tecla de la flecha abajo me alejo, // igual que hice con la rueda del raton. El // cambio se aplicara en la roxima pasada de la // funcion Pinta(). if(Teclas[VK_DOWN]) { dist+=0.2; if(dist>20) dist=20; } // Controlo tambien la tecla de la flecha arriba para // acercarme igual que hice con la rueda del raton if(Teclas[VK_UP]) { dist-=0.2; if(dist<0) dist=0; } // Si presiono la tecla "a" o "A" if(Teclas['A']) { // Aumento los FPS FPS_que_queremos+=5; // Controlo que no pase de 500 (por que me ha dado la gana) if(FPS_que_queremos>500) FPS_que_queremos=500; } // Si presiono la tecla "z" o "Z" if(Teclas['Z']) { // Reduzco los FPS FPS_que_queremos-=5; // Controlo que no baje de 25 if(FPS_que_queremos<25) FPS_que_queremos=25; } // Si presiono la tecla "q" o "Q" if(Teclas['Q']) { // Aumento el angulo de giro de los cubos ang+=2; // Si pasa de 360 lo hago 0 if(ang>360) ang=0; } // Si presiono la tecla "w" o "W" if(Teclas['W']) { // Reduzco el angulo de giro de los cubos ang-=2; // Si baja de 0 lo hago 360 if(ang<0) ang=360; } // Aplico el giro a la matriz de modelo para que modifique // los cubos que dibujare ahora. glRotatef(30+ang,0,1,0);
|
Las siguientes 3 lineas de codigo merecen un comentario aparte.
Imaginemos que nuestro programa tiene un FPS de 200, se pinta 200 veces en un segundo. Si en cada pasada del bucle miramos como esta una tecla y hacemos algo ¿cuantas veces se hara eso, si tardamos medio segundo entre presionar la tecla con el dedo y soltarla? La respuesta, despues de un pequeño calculo, es 100. Y el tiempo con la tecla presionada no es fijo porque depende de nuestro dedo y la mecanica de la tecla, asi que el numero de veces en realizar la tarea asignada a la tecla estaria, por ejemplo, entre 70 y 130 cada vez que pulsamos. Si la tarea de la tecla es cambiar un switch de un estado de nuestro programa, el estado de ese indicador tras pulsar seria aleatorio, ya que habria cambiado un numero entre 70 y 130 veces de estado en una sola pulsacion.Las tres lineas de codigo siguientes implementan un switch que cambia solo una vez de estado con cada pulsacion de tecla, usando un indicador auxiliar para controlar si tras la pulsacion, se ha levantado la tecla.
1 2 3 4 5 6 7 8 9
|
// Este trozo de codigo es para visualizar o no en // pantalla los FPS presionando la tecla "f" o "F". // Usamos un indicador para ver si se ha levantado // la tecla. De otra forma cambiaria el estado de // pintaFPS muchas veces en un solo pulsado de tecla // y el resultado seria imprevisible. if(LevantaTecla && Teclas['F']) { pintaFPS=!pintaFPS; LevantaTecla=false; } else if( !Teclas['F']) LevantaTecla=true;
|
El ultimo bloque de codigo escribe en pantalla los FPS o no, en base al indicador anterior, escribe las coordenadas del raton al lado del puntero del raton y escribe la leyenda de teclas a usar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
// Escribo los FPS reales con 4 digitos decimales si // pintaFPS es true. if(pintaFPS) Escribe(480,40,"FPS: %.4f",FPS_reales ); // Escribo las coordenadas del raton haya donde este el raton. Escribe(Raton.x,Raton.y,"Raton: %i - %i",Raton.x,Raton.y); // Escribo la leyenda de lo que se puede hacer en nuestro // programa. Escribe(370,248,"Click izquierdo presionado para mover"); Escribe(370,268,"con el raton (rueda para distancia)"); Escribe(370,288,"F - Ver FPS"); Escribe(370,308,"A Z - Modificar FPS"); Escribe(370,328,"Q W - Rotar cubos"); Escribe(370,348,"Arriba Abajo - Acercar y alejar");
|
Con esto hemos terminado el capitulo actual. Nuestro programa, apartir de ahora sera "interactivo".
Una ultima apreciacion. En el codigo habiamos definido una variable para almacenar el codigo ASCII proveniente del evento
WM_CHAR que, al final, no hemos usado en nuestro bucle de pintado. He creido conveniente mostrar como obtener el codigo ASCII de la tecla pulsada para que quien quiera se entretenga en implementar un
gets o
getchar. En todo caso se deberia implementar de forma similar a cuando pulsamos la "F" en nuestro programa para evitar que una sola pulsacion en la tecla "a" (por ejemplo) produzca 100 letras "a".
Al final el programa se veria asi:

El codigo completo es este:
usw10.cpp