"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
Martes 19 de Noviembre del 2019

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...


9. Listas, Texto y FPS Imprimir Correo electrónico
Videojuegos - Curso de Programación de juegos
Escrito por Vicengetorix   
Índice de Artículos
9. Listas, Texto y FPS
Texto
Velocidad y FPS
Todas las páginas
En este capitulo veremos una serie de cosas tan necesarias como cualquier otra cosa pero menos vistosas, aunque el texto en pantalla nos hace falta ya como el comer. Cuando seamos capaces de hacer una especie de "printf" en OpenGL, lo usaremos para saber los Frames Por Segundo (FPS), la velocidad a la que nuestro programa se mueve y controlarla.





Lo primero para cumplir los objetivos de este capitulo es aprender lo que son las "listas de pintado" en OpenGL (Display List).

El concepto es facil. Tengo un numero de vertices (por ejemplo los de nuestro cubo) que voy a pintar varias veces porque quiero poner varios cubos iguales en mi escena. En vez de pintar varias veces el cubo en cada frame (un frame es como un fotograma, cada vez que pinto la escena), le mando a OpenGL los datos de mi cubo con un identificativo y cada vez que lo quiero pintar le digo a OpenGL que pinte el cubo (con los datos que le pase antes), mediante este identificativo.
Resumiendo. Primero le paso a OpenGL los datos del cubo, fuera de la funcion Pinta(), y cuando quiero pintar el cubo solo tengo que decirle que lo pinte, 1, 2, 3,.. o 300 veces.

Ahora veremos como hacemos esto de forma practica. Empezaremos con el codigo del capitulo anterior, en la version de luz puntual.

Definimos una variable de tipo GLuint global para almacenar el identificativo de la lista que vamos a crear.

1
2
3
4
// Variable de tipo unsigned int para guardar 
// el identificativo de la "display list"
GLuint Cubo;
 

Lo primero, por ordenar un poco el programa vamos a sacar de la funcion Pinta() el codigo con el que pintabamos el cubo y lo pasaremos a otra funcion en la que generaremos la lista. Esta funcion la llamaremos GeneraLista().

Lo primero sera generar el identificativo para la lista con la funcion glGenLists. Esta funcion tiene como parametro en numero de listas a generar (en nuestro caso 1) y retorna el identificativo.

Luego con la funcion glNewList indicamos a OpenGL que empezamos a indicarle las instrucciones que debe guardar en la lista.
Los parametros son el identificativo de la lista y GL_COMPILE.

1
2
3
4
5
6
7
8
// Genero una (1) lista y guardo el 
// identificativo en "Cubo"
Cubo = glGenLists(1);
// Esta funcion es la que marca el inicio
// de la definicion de la lista.
// Todas las funciones OpenGL entre esta
// funcion y glEndList() se guardaran en la lista.
glNewList(Cubo, GL_COMPILE);

Tras este codigo van todas las funciones con las que pintabamos el cubo, desde glBegin a glEnd, y que hemos borrado de la funcion Pinta() y pasado aqui.
Al final terminamos con la creacion de la lista poniendo la funcion glEndList().

1
2
// Fin de la creacion de la lista.
glEndList();

Con esto terminamos la funcion GeneraLista().

Conviene aclarar que entre las funciones de inicio y fin de la generacion de la lista se puede incluir, por ejemplo, las funciones glVertex dentro de un bucle for o while y pasar las coordenadas en variables. En la lista se almacenara solo los datos que finalmente, como resultado del algoritmo que hagas, se envian a la tajeta de video (glVertex, glNormal, glTexCoord, ..., solo almacena lo relativo a OpenGL y con datos concretos), asi que no se puede guardar en una lista  un algoritmo que genere diferentes formas en base a variables. Solo guarda datos de vertices concretos y exclusivamente datos OpenGL.

Luego nos queda usar la lista generada en la funcion Pinta().

Aqui ya no estaba el codigo para pintar nuestro cubo, lo habiamos pasado a la funcion GeneraLista(), y ahora para pintar nuestro cubo deberemos invocar esta lista que hemos crado con la funcion glCallList(...) pasandole como parametro la variable que contiene el identificativo de la lista que contiene nuestro cubo: glCallList(Cubo).

Asi pues, incluiremos nuevo codigo donde estaba antes el codigo delimitado por glBegin(...) y glEnd(), estas dos funciones incluidas (quiero decir que ya no hacen falta, que las hemos puesto al crear la lista):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// En vez de pintar el cubo con todas las funciones 
// glVertex, glNormal y glTexCoord, solo tengo
// que ejecutar la lista "Cubo".
glCallList(Cubo);
 
// Pinto otro cubo un poco mas lejos
glTranslatef(0,0,-2.0f);
glCallList(Cubo);
 
// Desabilito pintar con texturas para pintar
// otros dos cubos blancos
glDisable(GL_TEXTURE_2D);
 
// Pinto otro cubo, ya sin textura a un lado.
glTranslatef(2,0,2.0f);
glCallList(Cubo);
 
// Pinto otro cubo mas lejano.
glTranslatef(2,0,-4.0f);
glCallList(Cubo);
 

En este codigo hemos pintado 4 cubos, en vez de el unico cubo que pintabamos antes.
Cada funcion glCallList(cubo) del codigo pinta un cubo. El primero se pintara donde se pintaba el cubo original, y los otros los hemos puesto en otros lugares de nuestra escena, moviendolos con glTranslate (observa que no hemos cargado la matriz identidad entre una funcion glTranslate y otra, por lo que las traslaciones se sumaran, si antes he movido un cubo 2 y ahora muevo otro cubo otros 2, el segundo cubo se habra movido 4 en total, 2+2).

Aqui aprovecho para hacer una apreciacion sobre las transformaciones y rotaciones que puede que os ahorre bastante tiempo, que de otro modo tendriais que perder hasta dar con el motivo por el que los objetos no se ponen donde quereis.
En OpenGL no es lo mismo mover y rotar que rotar y mover.
Para entenderlo mejor. Cuando movemos o rotamos no estamos haciendolo sobre el objeto si no sobre el eje de coordenadas.
Si giramos el eje de coordenadas 90 grados respecto al eje Y, si luego movemos hacia el eje Z, no mandaremos al objeto hacia el fondo de la pantalla si no hacia un lado. No veremos el objeto que pretendemos colocar el pantalla, porque habremos puesto el eje Z donde antes estaba el eje X.
En cambio si movemos hacia el eje Z primero veremos el objeto tan lejos como hayamos movido. Si luego giramos, estaremos girando el objeto sobre su propio eje pero todavia en el mismo lugar del espacio.
Graficamente:

Gira y mueve:


Mueve y gira:


Esta claro que aqui el orden si altera el producto (de matrices).

Continuamos.
Tambien he desabilitado las texturas - glDisable(GL_TEXTURE_2D) - tras el segundo cubo, para dibujar los 2 primeros con textura y los 2 ultimos solo con el material que habiamos definido para el cubo (mas o menos blanco).

Tras esto ya no tengo que desabilitarlas antes de pintar la esfera.

Solo nos queda un detalle para que el resultado de nuestro programa, modificado para usar "display list" sea el deseado. Incluimos una llamada a la funcion GeneraLista() dentro de la funcion IniciaGL(), al final. Si no lo hicieramos, el programa nunca pasaria por el codigo que genera la lista y no podria luego pintarla.

1
2
3
// LLamo a la funcion que creamos para generar la lista. 
GeneraLista();
 

Tambien tenemos la opcion de borrar las listas creadas si queremos reusar los indices con la funcion glDeleteLists(...), que permite borrar un indice o un rango de ellos.

El resultado en pantalla seria asi:


El codigo completo es este: usw9a.cpp




Con lo que hemos aprendido, ya podemos empezar con el texto en pantalla que, como habreis adivinado, vamos a implementar con "display lists".
En este apartado veremos una programacion muy de Windows, aunque el texto se puede implementar de otras formas. La nuestra es la mas rapida y comoda por ahora.

Un pequeño anticipo de lo que vamos a hacer para poder poner texto en pantalla.
Vamos a definir una nueva funcion para crear las "display lists" para las letras, una por simbolo del codigo ASCII.

Como ya deberiais saber, el codigo ASCII es simplemente la asociacion de un numero a cada letra, y tambien a cada simbolo que se usa en escritura. Hasta el codigo 32 los numeros corresponden a codigos no imprimibles que nosotros no vamos a usar. El codigo 32 es el primero que vamos a crear, es el espacio en blanco. Si fueramos ingleses nos bastaria con generar del 32 al 128, el codigo ASCII original con mayusculas, minusculas y poco mas como parentesis, corchetes, mayorque, ... . Como somos castellano hablantes (de otro idioma tambien habria que hacerlo) tendremos que generar del 32 al 255, el codigo ASCII completo, en nuestro caso incluyendo ñ, Ñ, ü, Ü,  á, Á, é, É, ... .


Dicho esto, lo que haremos sera generar un "display  list" por cada caracter de codigo ASCII del 32 al 255, de forma secuencial y en el mismo orden. Asi, al pintar  las listas con el indice correspondiente al codigo ASCII menos 32, de cada letra, de izquierda a derecha en pantalla, aparecera la palabra que queramos. Los codigos de cada letra en una frase que vamos a escribir en pantalla, los almacenaremos en una matriz (no una de OpenGL, si no un "array" de C) de tipo char, lo mismo que usamos en C la funcion "printf".

Empezamos con el codigo.

Definimos de forma global una variable de tipo GLuint para almacenar el identificativo base, el primero de nuestra lista de 223 (255 menos 32), a partir del que se crearan todos de manera consecutiva.

1
2
3
4
5
6
// Variable de tipo unsigned int para guardar 
// el identificativo base de las "display list"
// que generara para almacenar las letras.
// A partir de este, los siguientes, consecutivamente,
// seran usados en estricto orden.
GLuint ListaFuente=0;

Creamos la funcion GeneraFuente() donde crearemos las "display list" a partir de una fuente del sistema.
Creamos dos variables de tipo HFONT, propio de windows para almacenar la fuente que vamos a crear y la fuente antigua y restaurarla despues.

1
2
3
4
5
6
// Variable para guardar el identificativo
// de una fuente de Windows
HFONT fuente=NULL;
// Otro para guardar el que esta activo ahora
// y al terminar volver a dejar el que estaba.
HFONT fuente2=NULL;

Creamos la fuente windows como nos apetezca. En este ejemplo uso los parametro que me da la gana pero se pueden cambiar al gusto.
Uso la funcion CreateFont. Los comentarios del codigo son bastante explicitos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Creo la fuente que voy a convertir en listas de OpenGL.
fuente = CreateFont(
25, // Altura del caracter
0, // Ancho del caracter. 0 es proporcionado a la altura.
0, // Angulo de escape
0, // Angulo respecto a X
FW_BOLD, // Grosor del caracter. 0 es por defecto.
// Valor de 0 a 1000.
FALSE, // Italica
FALSE, // Subrallada
FALSE, // Tachada
ANSI_CHARSET, // Conjunto de caracteres
OUT_TT_PRECIS, // Tipo de fuente preferida
CLIP_DEFAULT_PRECIS, // Precision de recorte
ANTIALIASED_QUALITY, // Calidad de salida
FF_DONTCARE|DEFAULT_PITCH, // Familia y ¿pitch?
L"Arial"); // Fuente del sistema a usar

Despues asigno esta nueva fuente al "device context" o dispositivo de contexto de nuestra ventana OpenGL.

1
2
3
// Selecciono la fuente que acabo de crear como la actual
// en el contexto de dispositivo de nuestra ventana.
fuente2 = (HFONT)SelectObject(DevContex, fuente);

Ahora genero los identificativos de lista de OpenGL para los 223 caracteres y guardo el primero en la variable que habiamos definido para ello.

1
2
3
4
// Creo 223 "display lists" para guardar las letras.
// Todos los caracteres ASCII desde el espacio en
// blanco (codigo 32).
ListaFuente = glGenLists(255-32);

LLegamos al momento critico, usamos la funcion wglUseFontBitmaps, propia de la implementacion de OpenGL para windows y creamos todas las listas de un tiron. Esta funcion esta hecha para eso.

1
2
3
4
// Usamos esta funcion de OpenGL especifica de Windows
// para crear 223 listas a partir del identificativo de ListaFuente
// y a partir del 32 con la fuente del contexto de dispositivo.
wglUseFontBitmaps(DevContex, 32, 255-32, ListaFuente);

Una vez creadas las listas, solo queda restablecer la fuente antigua y borrar la que hemos creamos y ya no necesitamos porque ya estan las listas creadas.

1
2
3
4
// Una vez creadas las listas restablezco la fuente anterior.
SelectObject(DevContex, fuente2);
// Borro la fuente que cree.
DeleteObject(fuente);

Ya estaria la funcion GeneraFuente() pero voy a poner otra version mas simple, en la que no se puede modificar la fuente, solo se usa la que te da el sistema por defecto.

1
2
3
4
5
6
7
8
9
10
11
12
// Selecciono la fuente que me da el sistema.
SelectObject (DevContex, GetStockObject (SYSTEM_FONT));
 
// Creo 223 "display lists" para guardar las letras.
// Todos los caracteres ASCII desde el espacio en
// blanco (codigo 32).
ListaFuente = glGenLists(255-32);
 
// Usamos esta funcion de OpenGL especifica de Windows
// para crear 223 listas a partir del identificativo de ListaFuente
// y a partir del 32 con la fuente del contexto de dispositivo.
wglUseFontBitmaps(DevContex, 32, 255-32, ListaFuente);

Como veis, mas corto y menos versatil.Cada uno elige.

Pequeña aclaracion de que hay en las "display lists" que se crean.
Para hacer los caracteres de texto internamente, OpenGL crea un bitmap por cada letra y para pintar cada uno, usa la funcion glBitmap(...), que pone el bitmap en pantalla independientemente de la proyeccion. Es una funcion 2D. El bitmap lo pone en la posicion de pantalla que toca, a partir del ultimo pixel que pinto. Esta posicion se puede modificar con la funcion glRasterPos, asi se puede poner el bitmap donde haga falta.

Creamos ahora la funcion Escribe(...) que sera de uso parecido a "printf" pero con la opcion de posicionar lo que escribimos en pantalla. La funcion sera con 3 parametros fijos y otros de numero variable.

La definicion sera asi:

1
2
3
4
// Los dos primeros parametros son la posicion
// y el resto es igual que la funcion prinf.
void Escribe(int x, int y, char *cadena,...)
{

Definimos un "array" de letras.

1
2
// Array para guardar texto.
char texto[256];

Defino una variable especifica para tratar los argumentos variables.

1
2
// Tipo de datos para argumentos variables.
va_list ap;

Compruebo que le paso una cadena a escribir, si no lo dejo.

1
2
// Si no mando nada a escribir me voy.
if (cadena == NULL) return

De estas tres lineas, la primera y la ultima son macros especificas para el tratamiento de argumentos variables.
Al final lo que hace este bloque de codigo el tratar la cadena que pasamos a la funcion y los argumentos variables posteriores exactamente igual que la funcion "printf" y dejar el resultado en el array "texto" que habiamos creado al principio de la funcion.

1
2
3
4
5
6
7
// Con las siguientes 3 lineas proceso la
// entrada tipo "printf" de mi funcion,
// los argumentos variables y su encaje en
// el texto general en base a las "%" que encuentre.
va_start(ap, cadena);
vsprintf(texto, cadena, ap);
va_end(ap);

Despues compruebo si la iluminacion de OpenGL esta habilitada y me quedo en una variable de tipo bool con el estado. Hago lo mismo con las Texturas 2D y la profundidad.
Luego deshabilito la iluminacion, las texturas y el test de profundidad para que se vean las letras correctamente (en el caso de la profundidad, si no la deshabilito puede no verse el texto segun el hardware de video).

1
2
3
4
5
6
7
8
9
10
11
// Compruebo si esta habilitada la iluminacion,
// texturas y profundidad para dejarlas luego igual.
bool luz = glIsEnabled(GL_LIGHTING);
bool tex = glIsEnabled(GL_TEXTURE_2D);
bool pro = glIsEnabled(GL_DEPTH_TEST);
// Ahora deshabilito la iluminacion.
glDisable(GL_LIGHTING);
// Deshabilito Texturas.
glDisable(GL_TEXTURE_2D);
// Deshabilito profundidad.
glDisable(GL_DEPTH_TEST);

Salvo los estados de las matrices de proyeccion y modelo, y pongo la proyeccion ortogonal con las medidas de nuestra ventana.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Este bloque es para guardar las matrices de 
// OpenGL y dejarlas igual al final.
// Selecciono matriz de projeccion.
glMatrixMode(GL_PROJECTION);
// La guardo en la pila.
glPushMatrix();
// Cargo la matriz identidad.
glLoadIdentity();
// Pongo proyeccion ortogonal.
glOrtho(0,rect.right,rect.bottom,0,-1,1);
// Selecciono matriz de modelo.
glMatrixMode(GL_MODELVIEW);
// La guardo en la pila.
glPushMatrix();
// Cargo la matriz identidad.
glLoadIdentity();

Pongo el color que quiero para lo que voy a escribir. Si quisieramos hacerlo variable, podriamos no ponerlo y usar glColor antes de usar la funcion Escribe.

1
2
// El color con el que voy a pintar.
glColor3f(1,1,1);

Le paso a OpenGL la posicion donde voy a poner el texto a escribir de pantalla con la funcion glRasterPos que ya vimos de pasada antes. Gracias a que hemos puesto la proyeccion ortogonal, la "posicion raster" coincidira con los pixels de nuestra ventana empezando en la esquina superior izquierda y la posicion que le pasemos sera donde pinte la esquina inferior izquierda del primer caracter del texto.

1
2
3
4
// Le indico donde va a empezar a pintar de la
// pantalla en base a los parametros de nuestra
// funcion.
glRasterPos2i(x,y);

Ahora va una funcion que sirve para guardar las variables de estado de OpenGL relativas a las "display lists" que se pueden alterar al pintar luego el texto.

1
2
// Esta funcion guarda los valores de lista. 
glPushAttrib(GL_LIST_BIT);

Establecemos el indicador base a partir del cual habiamos creado las "display lists" y le restamos 32 (el codigo del espacio en blanco, la primera "display list" que habiamos creado.

1
2
3
4
5
// Establecemos el identificativo base de las listas
// a usar (-32 porque el caracter de la fuente con valor
// ASCII mas bajo es el espacio cuyo valor es 32, y de ahi
// para arriba.
glListBase(ListaFuente-32);

Esta es la funcion clave, la que pinta el texto en pantalla.
La funcion glCallLists el la que pinta varias listas consecutivas a partir del identificativo que se le paso en la funcion anterior, glListBase (por ejemplo, si glListBase lo ponemos a 100 y pintamos 3 listas, pintara las listas cuyos identificativos sean 100, 101 y 102).                glCallLists recibe tres parametros, el numero de listas a pintar (numero de letras incluidos espacios), el tipo de los indices (GL_UNSIGNED_BYTE) y el ultimo sera el puntero al array con los indices de las listas a pintar (un array de caracteres) ; se calcula sumando el indice de cada caracter (codigo ASCII de cada caracter) mas el glListBase (por eso le hemos restado 32, porque no hemos creado listas para los primeros 32 indices).

1
2
3
4
5
6
7
// Ejecuto tantas listas como la longitud del texto a 
// escribir, los indices son del tipo unsigned byte y
// los indices de las listas estan en el puntero que se
// pasa como tercer parametro(los codigos ASCII del texto).
// A estos indices se les suma la base establecida en la
// funcion anterior.
glCallLists(strlen(texto), GL_UNSIGNED_BYTE, texto);

Nota: con la funcion strlen calculamos la longitud en caracteres de la cadena.

Restablezco los valores para las "display lists".

1
2
// Restablezco los valores de lista.
glPopAttrib();

Dejo color blanco, que es el que voy a usar como base para ver bien todo.

1
2
// Restablezco como color el blanco, nuestro color base.
glColor3f(1,1,1);

Y restablezco tambien la proyeccion y matriz de modelo anterior.

1
2
3
4
5
6
7
8
// Restablezco la matrix de modelo anterior.
glPopMatrix();
// Cambio a la matriz de projeccion.
glMatrixMode(GL_PROJECTION);
// Restablezco la matrix de proyeccion anterior.
glPopMatrix();
// Dejo como activa la matriz de modelo.
glMatrixMode(GL_MODELVIEW);

Dejo tambien la iluminacion, las texturas y el test de profundidad como estaban, habilitados o no.

1
2
3
4
5
// Si antes estaba habilidata la iluminacion,
// Texturas o profundidad las dejo igual.
if(luz) glEnable(GL_LIGHTING);
if(tex) glEnable(GL_TEXTURE_2D);
if(pro) glEnable(GL_DEPTH_TEST);

Con esto tenemos ya nuestra propia funcion "printf" para OpenGL, que podemos poner en cualquier parte de nuestra funcion Pinta() sin problemas porque deja todo como estaba tras escribir el texto.

Para terminar y ver algo escrito hay que incluir la funcion GeneraFuente() en la funcion IniciaGL().

1
2
// Genero la fuente con la que voy a escribir.
GeneraFuente();

pero siempre antes de crear las texturas. Si usas la funcion wglUseFontBitmaps despues de una funcion glTexParameter, por alguna razon que desconozco, la primera falla y no se generan las listas para el texto (con esto intento ahorraros un par de dias de desesperacion al ver que no sale y no sabes porque).

Para finalizar solo queda escribir lo que queramos en la funcion Pinta() con nuestra flamante funcion Escribe(...), en cualquier lugar, despues de borrar la pantalla anterior con glClear, claro.

1
2
// Escribo texto en pantalla.
Escribe(20,360,"Hola Mundo OpenGL" );

Separamos un poco del borde izquierdo (20) y lo ponemos abajo de nuestra ventana (360), o donde nos de la gana.

Al fin podemos escribir en OpenGL:


El codigo completo es este: usw9b.cpp





Por ultimo en este capitulo vamos a profundizar un poco mas en el control del tiempo del programa, la velocidad a la que se ejecuta. Veremos como calcular y controlar los FPS (Frames Por Segundo).

Este apartado no es especifico de OpenGL, 3D o 2D.
¿Poque controlar la velocidad? Imaginemos que somos unos programadores de juegos de un estudio Americano donde trabajan 300 personas entre programadores, grafistas, betatesters, directivos, ... y trabajamos en nuestra estacion de trabajo de 2 millones de dolares usa, donde ya tenemos nuestro juego ajustado al milimetro y a la velocidad adecuada para jugar.
Entonces, tras la comercializacion el juego llega a nuestro ordenador de casa, normalito, e intentamos ejecutarlo. ¿que pasaria? es probable que no se mueva, si el programador no lo ha tenido en cuenta.
¿Habeis ejecutado un juego antiguo que os gustara hace años en una maquina moderna? A mi me ha pasado que un juego que en su momento me hizo pasar horas de entretenimiento, ahora es inmanejable porque los muñequitos se mueven a la velocidad del rayo en mi computadora actual.

Nuestro juego debe estar preparado para poder ejecutarse en una maquina mejor que la nuestra a la misma velocidad y, hasta donde sea posible, sea jugable en una maquina peor.

¿Que son los Frames Por Segundo? La frase es una mezcla de ingles (frames) y español (por segundo). El original ingles es Frames Per Second (FPS).
No es mas que el numero de fotogramas que se visualizan en un segundo. Si fotogramas. La animacion de nuestro juego no es mas que una sucesion muy rapida de imagenes que modificamos poco a poco (o no tan poco). Es como en una pelicula.
Para que el ojo vea el movimiento suave debe ser de mas de 24 imagenes por segundo, de ahi para arriba.

En el punto en que estamos de nuestro programa, en un PC normal de hoy en dia, los FPS puede ser una cifra astronomica, es probable que pinte toda la imagen en menos de 1 milisegundo. Los FPS de nuestro programa seran mas de 1000, asi que si queremos que nuestro juego sea jugable y con una velocidad estable que no dependa del hardware ni de lo que esta corriendo en el PC, tendremos que controlar los FPS.
Para eso lo primero es calcularlos y luego retener de alguna manera nuestro bucle de juego.
Eso haremos hasta el final del capitulo.

El calculo de FPS es tirando a facil. Se miran los milisegundos desde que la maquina esta arrancada con la funcion GetTickCount() de windows al principio de nuestro bucle y al final tambien se mira. Se resta el primer valor del segundo con lo que tendremos los milisegundos que tarda nuestro bucle.  Este valor de divide entre 1000 (milisegundos) y ya tenemos los FPS de nuestro programa (FPS = veces que pintamos la escena en 1 segundo o 1000 milisegundos). ¿Facil?.

Tambien se pueden usar, para mas precision, el "high performance timer" de windows que tiene mayor precision que milisegundos y nos permitira fiarnos mas de nuestros calculos. Esta segunda opcion la veremos despues

Como siempre en estas cosas, los metodos pueden ser infinitos. No sotros optaremos por dos posibilidades para controlar la velocidad del programa: Timers o "high performance timer".

La primera es la mas facil pero menos util por ser menos controlable, es usar un Timer de windows.
Un Timer en windows se define para que haga algo cada X tiempo, el que le digamos, en milisegundos.

Pasamos al codigo.

Definimos de forma global una variable i, el tipico indice, que usaremos para mover el texto que pusimos en pantalla y asi poder ver la velocidad.

1
2
3
4
// El tipico indice de C.
// Lo usamos para mover el texto en
// la pantalla.
int i=0;

De la funcion WinMain(...) quitaremos la funcion Pinta() para pasarla a la funcion ProcesaMensajes(...) y definiremos el Timer.

1
2
3
// Definimos un timer de windows con identificativo 1 y 
// que se ejecuta cada 20 milisegundos.
SetTimer(IdVentana, 1, 20, NULL);

Quitamos de aqui la funcion Pinta().

1
2
// Comentamos esta linea porque la pasamos a la funcion ProcesaMensajes.
//Pinta(); // Funcion que pinta algo y se repite continuamente.

El la funcion ProcesaMensajes(...), dentro del switch, interceptamos un nuevo mensaje de windows, el que viene de los Timer que tengamos definidos, si viene del Timer con ID 1 (el que hemos definido) es que ya toca hacer lo que queramos para ese Timer, que en nuestro caso sera ejecutar la funcion Pinta(), que hemos pasado aqui. Es una forma de decir a windows que ejecute la funcion Pinta() cada X milisegundos (20 en nuestro codigo). De esta manera la velocidad sera estable.

1
2
3
4
5
6
7
8
9
10
11
// Incluimos la gestion de los mensages que vienen de los timer.
case WM_TIMER:
// Si el ID del timer es 1, podriamos tener mas de un timer.
if (1 == wParam)
{
// Funcion que hace nuestro bucle de juego principal.
// Ahora esta aqui.
Pinta(); // Funcion que pinta algo y se repite continuamente.
}
break;
 

Para terminar, en la funcion Pinta() uso el indice i que cree para mover el texto.

1
2
3
4
5
// Escribo texto en pantalla.		
// Pero esta vez lo muevo de abajo a arriba.
if(i>340) i=0;
Escribe(20,360-i,"Hola Mundo OpenGL" );
i++;

El codigo completo es este: usw9c.cpp


El siguiente metodo es el que vamos a usar. Partimos del codigo anterior a definir Timers (usw9b.cpp). Esta vez no usaremos Timers (se pueden usar pero para otra cosa).
Modificaremos el bucle general de la aplicacion windows, lo primero quitando la funcion WaitMessage(), que como su nombre indica, espera a que haya mensajes. En nuestro caso no queremos que espere a que ocurra nada, si no que tiene que estar dando vueltas el bucle de windows continuamente.

Lo que haremos sera esto: Conseguiremos, antes de pintar, una toma de tiempo, pintaremos la escena y tomaremos otra toma de tiempo, comprobaremos cuanto tiempo pasa y si es poco exploraremos mensajes y tomaremos la segunda toma de tiempo de nuevo, comprobaremos de nuevo si es poco tiempo y asi hasta que haya pasado el tiempo que queremos, en ese momento tomaremos de nuevo la primera toma y pintaremos la escena, y a explorar mensajes y tomar el segundo tiempo hasta que toque pintar de nuevo.
De esta forma controlamos la velocidad minima, en una maquina mas lenta podria ir mas lento o igual, en una mas rapida ira igual que en la nuestra.

Esto se puede hacer con la funcion GetTickCount() de windows, que nosotros no vamos a usar porque no tiene la precision suficiente en velocidades altas (menores de 15 milisegundos).

Nosotros vamos a usar el "high performance timer" de windows que en PCs medianamente modernos es soportada, y da resoluciones mas pequeñas que milisegundos.
Usaremos la funcion QueryPerformanceCounter(...) para obtener las tomas de tiempo (no tiene nada que ver con la hora actual) de alta resolucion, y la funcion QueryPerformanceFrequency(...) para obtener el numero de "ticks", de los que devuelve la anterior funcion, que hacen un segundo.
De esta forma hallaremos el tiempo, en segundos, que transcurre entre dos tomas de tiempo con este calculo:
( Tiempo2 - Tiempo1 ) / Frecuencia = Tiempo en segundos.

Para estos calculos usaremos el tipo LARGE_INTEGER (en realidad no es un tipo si no una estructura) de 64 bits, que es el que devuelven las dos funciones que vamos a usar.

Ahora toca cambiar el codigo.

Como siempre comenzamos por las nuevas definiciones globales.

Un indice que usaremos para mover el texto y asi comprobar la velocidad de nuestro programa.

1
2
3
// Indice para mover el texto en pantalla y asi
// apreciar la velocidad.
int i=0;

Dos variables, una para guardar los FPS que calculemos y luego ponerlos en pantalla, y otra a la que inicializamos con el valor de los FPS a los que queremos que vaya el programa.

1
2
3
4
5
6
// Variable para guardar nuestro calculo
// de FPS reales
double FPS_reales;
// Variable donde ponemos los FPS a los que
// queremos que vaya nuestro programa.
int FPS_que_queremos=25;

Aprovecho para reducir el tamaño de la letra y el tipo a una que me gusta mas, en la funcion CreateFont(...).

En la funcion Pinta() incluyo el codigo para pintar texto que se mueva y los PFS que vamos a calcular.

1
2
3
4
5
6
7
8
// Escribo texto en pantalla.
// Se movera un pixel hacia arriba
// cada vez que pinte.
if(i>=360) i=0;
Escribe(20,360-i,"Hola Mundo OpenGL" );
i++;
// Escribo los FPS reales con 4 digitos decimales.
Escribe(500,20,"FPS: %.4f",FPS_reales );

En la funcion WinMain(...), esta todo el meollo del asunto que tratamos.

Definimos unas variables de tipo LARGE_INTEGER para guardar los datos del "high performance timer" y una de tipo double para guardar el tiempo en segundos que vamos a calcular.

1
2
3
4
5
6
7
// Definimos tres variables de 64 bits que usaremos para 
// obtener el dato de tiempo en cada pasada del bucle
LARGE_INTEGER t1,t2,frecuencia;
// Esta variable de tipo coma flotante de doble precision
// para guardar el tiempo en segundos que tarda cada pasada
// del programa.
double segundos=0;

Luego tomamos una toma de tiempo previa y obtenemos la frecuencia que usaremos para calcular los segundos (la frecuencia de procesador es fija, por eso se toma fuera del bucle. Podria variar, solo en un portatil que se ponga en ahorro de energia).

1
2
3
4
5
6
7
// Obtenemos la primera toma de tiempo en t1.
QueryPerformanceCounter(&t1);
// Obtenemos la frecuencia del procesador para
// calcular, despues, en segundos lo que tarda
// nuestro bucle en base a dos tomas de tiempo
// con "QueryPerformanceCounter".
QueryPerformanceFrequency(&frecuencia);

Solo queda ver el bucle en si.

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
// Entramos en el bucle del programa
while(TRUE) // Se ejecuta continuamente.
{
// Tratamos los mensajes que haya de la forma habitual
// Exploramos la cola de mensajes.
if(PeekMessage(&Mensaje, NULL, 0, 0, PM_NOREMOVE))
{ // procesándolos adecuadamente
// En este caso terminamos.
if(!GetMessage(&Mensaje, NULL, 0, 0)) return (int)Mensaje.wParam;
TranslateMessage(&Mensaje);
DispatchMessage(&Mensaje);
}
// Eliminamos la espera. Ahora queremos ejecutar continuamente
// aunque no haya mensajes.
//else WaitMessage(); // en caso contrario esperamos un mensaje
 
// Tomo el segundo tiempo en t2
QueryPerformanceCounter(&t2);
// Calculo los segundos en base a los dos tiempos, t1 y t2.
// t2, excepto en la primera pasada del bucle, sera siempre
// mayor que t1 (segundos es un decir, sera muuucho menos
// de un segundo).
segundos=(double)(t2.QuadPart-t1.QuadPart)/(double)frecuencia.QuadPart;
 
// Si, en base a los segundos calculados y el frame rate que queremos,
// toca ya pintar otra vez la escena, tomamos de nuevo tiempo en t1 y
// pintamos de nuevo. Si no toca volvemos al principio del bucle, vemos
// si hay mensajes y tomamos de nuevo el segundo tiempo y comprobamos
// otra vez.
// Aprovechamos para calcular el frame rate real (si la maquina fuera
// muy lenta o tuvieramos algun problema en el codigo, podria ser menor
// de lo que hemos fijado)
if( (segundos) >= 1.0f/(double)FPS_que_queremos )
{
// Tomamos el primer tiempo en t1.
QueryPerformanceCounter(&t1);
// Calculamos frame rate real.
// Sera el numero de veces que el trocito de segundo que tarda
// nuestro bucle, cabe en 1 segundo entero.
FPS_reales = 1.0f/segundos;
// Pintamos nuestra escena.
Pinta();
}
} // Fin while.

Como se ve, lo primero es tratar los mensajes de windows como siempre hemos hecho excepto por el detalle de que ya no esperamos a que llegue un mensaje. Asi ejecutamos el bucle continuamente sin paradas.
Luego tomamos el segundo tiempo.
Calculamos, en segundos, el tiempo transcurrido entre el tiempo 1 y el tiempo 2, en base a la frecuencia.
Con este dato comprobamos si hay que pintar la escena o no (observad que los FPS son la inversa del tiempo en segundos y el tiempo en segundos la inversa de los FPS).
Si no hay que pintar porque no ha pasado el tiempo necesario, volvemos al principio del bucle, y si ha pasado el suficiente tiempo tomo el primer tiempo de nuevo, calculo el Frame rate (los FPS) y pinto. Regreso al principio del bucle.


El codigo completo es este: usw9d.cpp


En el proximo capitulo toca ya que el teclado y el raton nos sirvan de algo en nuestro juego, y puede que por fin movamos la camara.
















¡Sólo los usuarios registrados pueden escribir comentarios!
+/- Comentarios
Buscar
Mario  - Preguntilla   |186.104.20.xxx |11-07-2010 21:06:24
Hola. Me gustaría saber que habría que agregarle a la función Escribe() para
poder cambiar la fuente de esta cada vez que aparezca.

Gracias de antemano
Vicengetorix   |194.179.126.xxx |12-07-2010 11:55:23
Habría que generar los display list de la otra fuente, tal y como hemos hecho
con la primera, y a la hora de estribir, antes, habría que cambiar la base de
las listas (glListBase) para que apunte a la segunda fuente.
Para elegir entre
fuentes se podría hacer añadiendo un parámetro a la función.
jromerorasero  - Duda al inicializar la lista de fuentes   |87.220.201.xxx |05-03-2010 22:25:18
Buenas tardes.
Tengo una dudilla, y es que al llamar a GeneraFuente() desde
IniciaGL() yo la puse al final de la función, junto a GeneraLista(). Si hacía
esto no obtenía ningún resultado en la pantalla, ¿puedes explicarme porqué
hay que llamarla justo ahí?
Un saludo y gracias.
Vicengetorix   |194.179.126.xxx |12-03-2010 02:17:29
Ver el foro (es por no responder 2 veces)
FabianSN  - Que mala suerte!!!   |85.53.197.xxx |23-11-2011 19:18:38
Venía bien con el curso, hasta que empecé a probar los ejemplos y ahora me
encuentro con otro problema, los fps no me aumentan a más de 5.
Si, como se
lee, no más de 5 fps.
La unica forma de que aumenten es arrastrando la ventana
desde su barra de título bien abajo, que solo se vea el mensaje de FPS para que
aumente a 30.
Probé tambien el rompe ladrillos en 2D y no me anda a más de 5
fps.
Tengo una placa NVIDIA GeForce FX 5200 de 128 Mb y en mi Pc tengo 1Gb de
RAM, será que es una plataforma muy lenta? Con muchos juegos no tengo
problemas.
He probado cambiando algunas cositas y toquetee un poco el código
para que me marque el máximo FPS en algún momento de la ejecucion y he notado
gracias a eso que aumentan los FPS mientras la ventana esta parcialmente fuera
del área visible, es más, si minimizo y máximizo la ventana me muestra un
pico de FPS máximo de 999 y si oculto casi toda la ventana, dejando solo
v...
Vicengetorix   |85.53.218.xxx |21-07-2009 12:02:07
Pienso que tienes un problema con los drivers de la tarjeta. La tarjeta es mas
que suficiente para mover los programas.
Seguramente los juegos que has probado
estan hechos con DirectX.
Yo he testeado los programas en dos ATI (X550 y
mobility X600) y una Intel (no de las modernas) y funcionan con 200 FPS o mas.

Prueba el "OpenGL Extensions Viewer" para ver la capacidad de tu
tarjeta y bajate el ultimo driver.
Comentame como va.

Suerte.
FabianSN  - Son Contento     |85.53.197.xxx |23-11-2011 19:18:06
Bueno, te hice caso y me fui a buscar algún driver más actualizado.

Cambió
muchísimo, ahora estoy a 75/80 fps, creo que voy a probar cuando pueda, el
programa que me mencionas para ver la capacidad de mi tarjeta/sistema.

En
realidad los juegos comerciales no me daban problemas pero otros desarrollos en
C++ hecho por usuarios particulares como vos, esos si me daban problemas de
velocidad, aún cuando usaban DirectX.

En fin, creo que todo pasa por la
placa video/perfomance PC, pero por lo menos me haz solucionado un problema de
años en mi PC.

Ya antes había intentado incursionar en la creación de
juegos de video y los fps siempre fueron un obstaculo, ya 80 fps para mi es
suficiente para hacer muchas cosas, no obstante voy a seguir investigando si
puede aumentar aún más.

Muchas gracias maestro, me ha sido de gran ayuda.
FabianSN  - FPS   |85.53.197.xxx |23-11-2011 19:15:44
Continúo este problemita que mencione antes aquí, para no armar otro hilo en
otro lado, ya que es un problema de FPS, de lo cual habla este
articulo.

Probé el soft \"OpenGL Extensions Viewer 3.0\" e
hice un test de velocidades y me informa que hasta OpenGL 1.5 la velocidad ronda
entre 180/200 FPS,pero cuando hace el test para OpenGL 2.0 y 2.1 la velocidad
cae entre 50/70 FPS.

¿Tendrá que ver con la libreria de OpenGL que estoy
usando la cual es la última version?
¿Hay forma de que los programas los
compile para que usen opengl 1.5?
O bien, ¿hay forma de decirle que se ejecute
con opengl 1.5?

Saludos y gracias, pero no termino de entender bien entre la
libreria opengl con sus versiones, el driver nuevo que baje de NVIDIA y que
testea este soft que mencione antes.
Vicengetorix   |85.53.215.xxx |24-07-2009 11:35:18
Nos estamos adelantando al curso.
El tema de las versiones tiene que ver con las
extensiones.
De momento todo lo que estamos haciendo es con la version 1.1 de
OpenGL, aunque nuestra tarjeta grafica acepte mas.
En dos capitulos veremos
estas cosas.
La libreria que usas es la que el driver de tu tarjeta instala mas
la que viene con Windows (que es 1.1). Para usar versiones superiores o
extensiones, debes preparar tu programa a mano o con alguna libreria como
GLEE.
Por ahora no te preocupes del tema si el PC ya te va suficientemente
rapido (no solo de tarjeta grafica vive OpenGL, tambien procesador central,
memoria, ...).
Pedro  - Hola   |189.145.138.xxx |16-06-2009 22:51:51
Hola, exelentes tutoriales espero pronto subas los demas gracias.

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