"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 12. Rompe ladrillos ahora en 3D
Domingo 22 de Octubre del 2017

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


12. Rompe ladrillos ahora en 3D Imprimir Correo electrónico
Videojuegos - Curso de Programación de juegos
Escrito por Vicengetorix   
Si, eso es lo que vamos a hacer en este capitulo. Con pocas modificaciones y manteniendo casi intacta la estructura del juego y los calculos principales, somos capaces, con lo que hemos aprendido en otros capitulos del curso, de hacer el "Rompe Ladrillos" en 3D.




Usaremos, pues, el codigo del capitulo 11 (Rompe ladrillos 2D paso a paso) y lo modificaremos para que haga lo mismo en 3D. Tras eso incluiremos algun pequeño cambio para que, cambiando el punto de vista, aprovechemos un poco mas la proyeccion 3D.

El cambio mas obvio es que antes pintabamos en pantalla un grafico cargado de disco para poner los ladrillos, la pelota y la paleta, y ahora necesitaremos los vertices de modelos representando los ladrillos, la paleta y la pelota. A tal efecto recuperaremos el codigo de la funcion GeneraLista() de los capitulos 9 (Listas, Texto y FPS) y 10 (Teclado, ratón y cámara), que al generar una lista que pinta un cubo nos sera util para dibujar los ladrillos y la paleta (escalando en el eje Y y el eje Z, ya lo veremos). Para dibujar la pelota usaremos la funcion auxSolidSphere(...) que ya hemos usado en los capitulos 9 y 10.
Lo que si usaremos para los ladrillos de colores y la paleta seran texturas cargadas de disco.

El resto de graficos (fondo y mensajes entre saques) seran en 2D como en la version anterior del programa.

El borde de rebote (el borde del campo de juego) ahora no coincidira con el de la ventana, asi que habra que dibujarlo en el espacio 3D, para lo que simplemente usaremos una reticula de lineas blancas.

El cambio mas importante en el codigo, pero menos visible mientras jugamos, es el cambio de coordenadas. Si, si, cambio de coordenadas:
El campo de juego en 2D era de unos 600x600, que era lo que ocupaba el area cliente de la ventana. En 3D eso no seria practico, ya que con ese rango de coordenadas obligariamos a OpenGL a ampliar el rango de la comprobacion de profundidad y podria dar errores (habria que probarlo).
En todo caso nosotros lo que haremos sera mantener las coordenadas antiguas (600x600) internamente, y a la hora de pintar en pantalla en 3D las convertiremos a las mismas coordenadas pero en un rango mas pequeño pero con numeros en coma flotante en vez de enteros. Para hacer esto nos bastara con una simple regla de tres (sencillo ¿no?):
Tomaremos como referencia en ancho del campo de juego. Antes era el ancho del area cliente de la ventana contenido en la variable "rect.right", y ahora decidiremos que es 20.
Asi pues para convertir cualquier coordenada del programa anterior al nuevo sistema en 3D bastara con multiplicarla por 20 y dividirla entre rect.right. La formula quedaria asi:

" Nueva_coordenada = (Vieja_coordenada*20.0f)/rect.right "

Para asegurarnos de que el resultado es en coma flotante y no se pierde precision se puede usar cast:

" Nueva_coordenada = (double)(Vieja_coordenada*20.0f)/(double)rect.right "

En el nuevo codigo incluiremos un timer (los vimos en el capitulo 9, en el apartado de FPS)
para que cada cierto tiempo de juego cambie el punto de vista y asi animaremos mas el juego.
Veremos alguna funcion nueva de OpenGL que no hemos visto (suavizado y culling) pero que se podrian no usar y el juego seria el mismo.

Al final el resultado sera sencillo pero resulton, demostrando asi lo facil que es hacer un juego tu solo en casa, y no digamos si se juntan 4 amigos y le echan horas y ganas.

Hechas las presentaciones pasamos al codigo, como es habitual, paso a paso. Recordad que partimos del juego en 2D del capitulo anterior (11).

Primero, como siempre vemos los cambios en las definiciones globales.

El primer cambio es incluir la biblioteca glAux para poder dibujar la bola sin complicarnos, para lo cual incluimos estas lineas.

1
#pragma comment( lib, "glaux.lib" )	 

y la cabecera desues de incluir opengl y glu.

1
#include <GL/glaux.h>

Incuimos tambien los datos que usaremos para la iluminacion y el material, tal y como vimos en el capitulo 8 Iluminacion.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Datos de las componentes ambiente, difusa y especular
// de nuestra fuente de luz.
GLfloat ambiente[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat difusa[] = { 0.8, 0.8, 0.8, 1.0 };
GLfloat especular[] = { 0.8, 0.8, 0.8, 1.0 };
// Datos de las componentes ambiente, difusa y especular
// del material. Todo 1.0 en difusa y especular sera blanco,
// la luz ambiente es menor para aumentar la diferencia
// de sombras.
GLfloat mat1_ambiente[] = { 0.7, 0.7, 0.7, 1.0 };
GLfloat mat1_difusa[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat1_especular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat1_emision[] = { 0.99, 0.99, 0.99, 1.0 };

Eliminamos el indicador de textura de la bola y el identificador de lista para la bola, ya que la pintaremos con glAux y sin textura. Los indicadores de textura se usaran ahora para las texturas que aplicaremos a los ladrillos en 3D (como en el capitulo 7 Texturas).

1
2
3
4
5
6
7
8
9
10
// Para los id's de texturas que genera OpenGL para nuestro programa. 
GLuint tex_usw, tex_paleta, tex_nivel;
GLuint tex_fondo, tex_felicidades, tex_bahh;
GLuint tex_ladrillo[5];
 
// Variables de tipo unsigned int para guardar
// el identificativo de las "display list"
// Solo del ladrillo. Ahora la bola la pintaremos
// diferente.
GLuint Ladrillo; // Bola;

Pondremos en coma flotante las variables bola_x y bola_y, y pal_x y pal_y, para que los calculos que haremos despues sean correctos y el compilador no se lie.

1
2
3
4
5
6
7
8
// Variables para las coordenadas de la bola
// Ponemos estos valores como punto flotante
// ya que viene mejor para los calculos.
double bola_x, bola_y;
// Variables para las coordenadas de la paleta.
// La X inicial la cambiaremos luego para centrarla.
// Estos tambien en punto flotante.
double pal_x = 200, pal_y = 500;

Dos nuevas variables para contener el alto y largo del ladrillo (y la paleta) pero en su version 3D para ser pintado (en las nuevas coordenadas).

1
2
3
// Estas variables son para en alto y el largo 
// del ladrillo en la representacion 3D.
double a_l, l_l;

Tres variables mas para la gestion de las vistas (desde donde se ve la escena) y la forma en que cambia la vista (ira cambiando segun se juega).
La primera contendra el indice correspondiente a la vista actual (tendremos 7).
La segunda es un indicador que un "timer" pondra a true para que la funcion Pinta() cambie la vista ( y luego ponga a false ).
La tercera es un indicador que cambiaremos con la tecla "R" para poder dejar la vista fija o para que vaya cambiando al ritmo que diga el "timer" de antes.

1
2
3
4
5
6
7
8
// Variable que indica el tipo de vista en curso
int Vista=0;
// Indicador de si ha cumplido el tiempo
// para cambiar la vista.
bool CambiaVista=false;
// Indicador de si la vista va rotando o
// esta fija.
bool RotacionVistas=true;

Con estos cambios terminamos con la definiciones globales. (por lo demas iguales que en el programa en 2D del capitulo 11).

Las funciones Genera Fuente(), Escribe(...) y CargaTextura(...) no se modifican. En todo caso, en esta ultima se puede cambiar el modo de aplicacion de la textura de DECAL a MODULATE. En mi PC se ve igual.

La funcion GeneraLista() es la misma que en el capitulo 10 (Teclado, ratón y cámara), salvo porque el identificador de la lista es ahora la variable Ladrillo.
Con la funcion glScalef achataremos el cubo en 1 o 2 de los ejes para obtener la forma deseada.

La funcion Pinta(), si me lo permitis, la dejaremos para lo ultimo. Despues de ver el resto del programa tendra mas sentido todo.

La funcion IniciaGL() tendra algun cambio.

El primero es habilitar el test de profundidad, que para el programa en 2D lo habia deshabilitado.

1
2
3
// Habilita test de profundidad. A partir de ahora, lo que 
// esta mas cerca se pinta encima.
glEnable(GL_DEPTH_TEST);

Ahora van una serie de lineas con cosas que no hemos visto hasta ahora. Se podrian quitar y el programa cambiaria poco.
Lo que hacen es decir a OpenGL que pinte las cosas en pantalla suavizadas, sin escalones (esto se llama "antialiasing" que traducido al cristiano es eliminar los escalones de las lineas al pintarlas en la pantalla).
El resultado de estas lineas no esta muy claro porque las implementaciones que se hacen de este tema segun la tarjeta grafica o el driver son por asi decirlo, "variopintas".
Se hace a nivel de punto, linea y poligono. En mi PC el de poligono no hace nada pero a lo mejor en el vuestro funciona. Las lineas si que se pintan mejor. Suerte.

1
2
3
4
5
6
7
8
9
10
11
// Este codigo sirve para que OpenGL pinte 
// las primitivas (punto, linea y poligono)
// sin escalones (con antialiasing).
// Su efectividad depende mucho del
// hardware y su diver.
glHint(GL_POINT_SMOOTH_HINT,GL_NICEST);
glHint(GL_LINE_SMOOTH_HINT,GL_NICEST);
glHint(GL_POLYGON_SMOOTH_HINT,GL_NICEST);
glEnable(GL_POINT_SMOOTH);
glEnable (GL_POLYGON_SMOOTH);
glEnable(GL_LINE_SMOOTH);

Las siguientes dos lineas indican a OpenGL que parte de un objeto debe pintar. Aqui si nos debemos detener.

El concepto de OpenGL que vamos a ver ahora es el Culling (del ingles desechar).
En un objeto 3D cerrado como por ejemplo un cubo, las caras traseras no se ven porque estan tapadas por las delanteras. Esto OpenGL lo resueleve con el test de profundidad y el buffer de profundidad como vimos. Por otra parte, vimos tambien el orden en que debemos indicar a OpenGL los vertices de un poligono; en orden inverso a las agujas del reloj.
¿Recordamos estas dos cosas? son importantes.
Pues bien si un poligono es pintado, desde el punto de vista, en pantalla en orden inverso a las agujas del reloj, OpenGL lo considera una cara delantera, lo vemos por delante. Si lo pinta en el orden de las agujas del reloj, considerara que lo estamos viendo por detras y es una cara trasera.
Dicho esto, con el culling en OpenGL podemos indicarle que pinte todas las caras (las traseras no se veran por el test de profundidad pero OpenGL hara el intento), las frontales, las traseras o ninguna.
Esto se hace con la funcion glCullFace(...) con los parametros GL_FRONT, GL_BACK o GL_FRONT_AND_BACK. Con uno no pintara las caras delanteras, con el otro las traseras y con el tercero ni traseras ni delanteras (no pintara nada).
Luego habilitas el culling con la funcion glEnable(GL_CULL_FACE), al estilo OpenGL.


En el programa vamos a deshabilitar el pintado de caras traseras para ahorrar tiempo de calculo. Si tuvieramos un simple cuadrado en el espacio 3D y quisieramos verlo dar vueltas, tendriamos que deshabilitar el culling antes de pintarlo.

1
2
3
4
5
6
7
// Estas dos funciones indican a OpenGL
// que no pinte las caras traseras de los
// objetos (vertices en orden de las manillas
// del reloj, desde donde miramos).
// Ahorran tiempo de computo.
glCullFace(GL_BACK) ;
glEnable(GL_CULL_FACE);

La poyeccion en este caso, ya no sera ortogonal, asi que cambio la proyeccion y, ya puestos, le indico la situacion de la camara (el punto de vista y a donde mira) al empezar el programa.

1
2
3
4
5
// En este programa pongo de principio
// proyeccion en perspectiva.
gluPerspective(50.0f, (float)rect.right/(float)rect.bottom, 0.5f, 50.0f);
// Y coloco el punto de vista inicial.
gluLookAt(10,10,-24, 10,9,0, 0,-1,0);

Ahora calculo el largo del ladrillo, para las coordenadas en 2D antiguas y para las nuevas que me van a servir para pintar en 3D.

1
2
3
4
5
6
7
8
9
10
// Pongo eltamaño que tendra el ladrillo en base a la medida 
// del area cliente de la ventana.
// El largo debe ser el necesario para que quepan los
// necesarios ladrillos por fila.
largo_ladrillo = (double)rect.right/(double)Por_fila;
 
// En esta variable guardo el largo del
// ladrillo, convertido a las dimensiones
// del nuevo campo de juego en 3D.
l_l = (largo_ladrillo*20.0f)/(double)rect.right;

Y hago lo mismo con el alto del ladrillo.

1
2
3
4
5
6
7
// El alto sera una tercera parte del largo.
alto_ladrillo = largo_ladrillo/3.0f;
 
// En esta variable guardo el alto del
// ladrillo, convertido a las dimensiones
// del nuevo campo de juego en 3D.
a_l = l_l/3.0f;

Y la posicion de la paleta en medio del campo de juego.

1
2
// Posiciono la paleta en medio de la ventana
pal_x = (double)rect.right/2 - largo_ladrillo/2.0f;

En la parte en que se cargan las texturas cambiaremos las texturas a cargar segun el programa. No nos ocuparemos de eso aqui, en el codigo queda claro.

El ultimo añadido a la funcion IniciaGL() es la parte en que habilito la iluminacion y la luz 0 de openGL. Lo mismo que hicimos en el capitulo 8, Iluminacion.
En este programa solo usaremos 1 luz, pero se pueden usar 8.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Modificamos la posicion de la luz 0 de OpenGL. 
// El cuarto parametro es un 1.0 para que sea una luz puntual.
GLfloat posicion[] = { 0.0, 10.0, -10.0, 1.0 };
glLightfv(GL_LIGHT0, GL_POSITION, posicion);
// Habilitamos el calculo de la iluminacion.
glEnable(GL_LIGHTING);
// Encendemos la luz 0.
glEnable(GL_LIGHT0);
// Definimos cada una de las componentes de la luz
// que emite nuestra bombilla (la luz 0).
glLightfv(GL_LIGHT0, GL_AMBIENT, ambiente);
glLightfv(GL_LIGHT0, GL_DIFFUSE, difusa);
glLightfv(GL_LIGHT0, GL_SPECULAR, especular);

En la funcion ProcesaMensajes(...) haremos cambios tambien.

Definiremos un "timer" como en el capitulo 9 (Listas, Texto y FPS)  en la parte de FPS, pero esta vez para cambiar la vista del campo de juego cada 15 segundos. Es una forma de añadir dificultad al jueguito. Solo cambiaremos el indicador CambiaVista a true. Luego, en la funcion Pinta() nos encargaremos de ver como esta y si esta a true cambiar la vista y ponerlo otra vez a false.

1
2
3
4
5
6
7
8
9
10
11
12
// 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)
{
// Pongo el indicador para que
// la funcion Pinta sepa que se ha
// cumplido el tiempo para cambiar
// la vista y actue en consecuencia.
CambiaVista=true;
}
break;

En esta funcion tengo que tener en cuenta las texturas que he creado ya que aqui las destruyo.
Normalmente habria una funcion para cuando termina el programa que haria estas cosas pero me parece mas didactico simplificar el codigo.

La funcion CreaVentana() no cambia.

En la funcion principal, WinMain(...) añadiremos la creacion del "timer" unicamente.

1
2
3
4
// que se ejecuta cada 15 segundos. 
// Sera el que nos indique si debemos cambiar el punto
// de vista del juego.
SetTimer(IdVentana, 1, 15000, NULL);

Vamos por fin con el autentico corazon de nuestro programa, la funcion Pinta().

El primer cambio es al pintar el fondo y el logo. antes solo debia pintarlos, ahora debo poner la proyeccion ortogonal antes por que ahora lo normal es que este en perspectiva.
Asi que guardo primero en la pila de matrices la proyeccion para restaurarla cuando termine de pintar en ortogonal. Tambien deshabilito el test de profundidad y la iluminacion. Luego cambio la proyeccion y despues pinto el fondo y el logo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cambio la proyeccion a ortogonal (la 
// normal en este juego es en perspectiva)
// y deshabilito el test de profundidad para
// pintar en 2D.
 
// Cambio a matriz proyeccion
glMatrixMode(GL_PROJECTION);
// Guardo la matriz proyeccion
// general del programa para recuperarla
// cuando termine de pintar en ortogonal
glPushMatrix();
// Cargo matriz identidad
glLoadIdentity();
// Pongo proyeccion ortogonal
glOrtho(0,rect.right,rect.bottom,0,-1,1);
// Cambio a matriz de modelo para pintar
glMatrixMode(GL_MODELVIEW);
// Deshabilito el test de profundidad
glDisable(GL_DEPTH_TEST);
// Deshabilito la iluminacion (queremos poner
// en pantalla los dibujos con su color).
glDisable(GL_LIGHTING);

Despues pinto como hacia antes y luego dejo todo como estaba:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Restablezco los valores anteriores
// a pintar en 2D.
 
// Cambio a matriz de proyeccion.
glMatrixMode(GL_PROJECTION);
// Recupero la que guarde con PushMatrix
// antes de cambiar.
glPopMatrix();
// Cambio a la matriz de modelo para pintar
glMatrixMode(GL_MODELVIEW);
// Habilito el test de profundidad (3D)
glEnable(GL_DEPTH_TEST);
// Habilito la iluminacion.
glEnable(GL_LIGHTING);

Tras esto toca pintar los bordes del campo, que como comentamos al principio ya no son los bordes de la ventana. Lo haremos con simples lineas sin textura (las dehabilitaremos para pintarlas). Algunas de ellas las pintaremos en un bucle for para ahorrar lineas de codigo.
Despues de pintarlas habilitamos de nuevo las texturas.

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
// Pintare ahora el borde 3D del campo
// de juego (ahora no coincidira con el
// borde de la ventana) simplemente con
// lineas blancas.
 
// Variable para usar en los bucles for
GLfloat v;
// Deshabilita texturas para pintar las lineas
// simplemente blancas.
glDisable(GL_TEXTURE_2D);
// Cargo matriz identidad.
glLoadIdentity();
// Grosor de las lineas a dibujar a 2.
glLineWidth(2);
// Pinto con lineas.
glBegin(GL_LINES);
// A la izquierda.
// Primera linea vertical.
glVertex3f( -0.1, 0,1);
glVertex3f( -0.1, 16,1);
// Segunda linea vertical.
glVertex3f( -0.1, 0,0);
glVertex3f( -0.1, 16,0);
// Tercera linea vertical.
glVertex3f( -0.1, 0,-1);
glVertex3f( -0.1, 16,-1);
// Lineas horizontales del borde izquierdo
// con un buble for.
for(v=0;v<=16;v+=2) { glVertex3f( -0.1, v,1);
glVertex3f( -0.1, v,-1); }
 
// Arriba.
// Primera linea horizontal
glVertex3f( 0, 0,1);
glVertex3f( 20, 0,1);
// Segunda linea horizontal
glVertex3f( 0, 0,0);
glVertex3f( 20, 0,0);
// Tercera linea horizontal
glVertex3f( 0, 0,-1);
glVertex3f( 20, 0,-1);
// Lineas transversales con un bucle for.
for(v=0;v<=20;v+=2) { glVertex3f( v, 0,1);
glVertex3f( v, 0,-1); }
// A la derecha.
// Primera linea vertical
glVertex3f( 20.1, 0,1);
glVertex3f( 20.1, 16,1);
// Segunda linea vertical
glVertex3f( 20.1, 0,0);
glVertex3f( 20.1, 16,0);
// Tercera linea vertical
glVertex3f( 20.1, 0,-1);
glVertex3f( 20.1, 16,-1);
// Lineas horizontales del borde derecho
// con un buble for.
for(v=0;v<=16;v+=2) { glVertex3f( 20.1, v,1);
glVertex3f( 20.1, v,-1); }
 
// Termino de pintar lineas.
glEnd();
// Habilito texturas para el resto del
// programa.
glEnable(GL_TEXTURE_2D);

Luego viene la parte en la que pintamos el muro. El metodo sera el mismo que en 2D, salvo que pintamos el ladrillo en 3D, y con las nuevas coordenadas para pintar en pantalla.
Ya vimos como vamos a cambiar las coordenadas para pintar, al principio del capitulo.
Pues eso mismo hacemos con la colocacion inicial de las filas del muro.

Conviene aclarar que en 2D las coordenadas 0,0 de cada dibujo (ladrillos y bola), eran la esquina superior izquierda del dibujo. Si ponias un dibujo en las coordenadas de pantalla 20,30; aparecia en dibujo con la esquina superior izquierda en las coordenadas 20,30.
Ahora, en 3D, las coordenadas 0,0 del modelo estan en el centro del modelo.
Por esta razon la primera coordenada de la siguiente funcion no es 0 (el borde izquierdo), si no la mitad de un ladrillo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Convierto las coordenadas 2D a nuestras coordenadas del 
// nuevo tamaño 3D.
// La coordenada X no es 0 porque las coordenadas del ladrillo
// 3D empiezan en el centro en vez de la esquina superior
// izquierda, asi que lo desplazo el largo del ladrillo
// en 3D (l_l) dividido entre 2 (l_l/2.0f).
// La coordenada Y la convierto. El nuevo ancho es del campo de
// juego es "20" y el anterior "rect.right" (sea lo que sea).
// Aplicando una simple regla de 3 la formula sera:
// "Nueva coordenada = (vieja coordenada*20)/rect.right".
// En este caso incluimos el tema de separar 100 del borde
// superior y el ancho de las filas que hemos pintado (i1*a_l).
glTranslatef( l_l/2.0f,
(double)(i1*a_l)+((100*20)/(double)rect.right),
0);
Como veis, la conversion de coordenadas no es mas que aplicar la formula.
La tercera coordenada seguira siendo 0; vamos a dibujar en 3D pero todo en un plano (Z=0).

Luego, a la hora de pintar el ladrillo, tambien habra cambios. Pinto con la "display list" del ladrillo que en realidad es un cubo, por eso antes reduzco las coordenadas del eje Y con la funcion glScale(..), que si mal no recuerdo, no hemos usado hasta ahora. Esta funcion simplemente escala, multiplica las coordenadas por el factor que indicamos en la funcion. Si indicas 1,1,1 no hara nada. 0.5,0.5,0.5 reducira el modelo pintado despues a la mitad. 2,2,2 multiplicaras por 2 las subsiguientes coordenadas de vertices y aumentaras el objeto.
En nuestro caso solo queremos achatar el cubo para hacer nuestro ladrillo, asi que haremos un tercio la altura del ladrillo, el eje Y (1, 0.3333, 1).
Segun los manuales de OpenGL esta funcion penaliza el rendimiento. Lo correcto seria que el modelo tenga su tamaño definitivo pero esto me sirve de pretexto para explicar esta funcion y no me apetecia cambiar las coordenadas del cubo.

1
2
3
4
5
6
7
8
9
10
// Pinto el ladrillo ahora en 3D.
// Guardo la matriz de modelo
// porque ahora la voy a cambiar.
glPushMatrix();
// Reduzco el tamaño de el alto
// del ladrillo a la tercera
// parte.
glScalef(1,0.3333,1);
// Y pinto el ladrillo.
glCallList(Ladrillo);

Tras esto hay codigo con una funcion nueva de OpenGL.
Lo que hacemos, despues de pintar el ladrillo con la textura, es pintar un borde alrededor del mismo para que cuando se junten 2 ladrillos, se vea la separacion. Ademas esto suavizara los bordes de los ladrillos.
Para esta operacion usaremos la funcion glPolygonMode(...) que lo que hace es decir a OpenGL la forma que debe pintar los poligonos, rellenos, las lineas del borde o los puntos de los vertices.
Los parametros son 2. El primero indica a que caras se aplicara el efecto.
Puede ser: GL_FRONT_AND_BACK, GL_FRONT o GL_BACK, segun sea a las caras frontales, las caras traseras o las dos.
El segundo parametro indica que efecto se aplica: GL_POINT, GL_LINE o GL_FILL, segun se pinten solo los puntos de los vertices, las lineas de las aristas o el poligono con relleno (lo normal).
En el seguiente codigo ponemos esto en practica, tras pintar el ladrillo normal.

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
// Una vez pintado el ladrillo 
// vamos a pintar el borde para que
// se vea mejor. Lo haremos pintando
// otra vez el ladrillo pero en lineas
// solo.
// Deshabilito texturas. Solo
// queremos color normal.
glDisable(GL_TEXTURE_2D);
// Indico a OpenGL que las caras
// frontales, pinte los poligonos
// sin relleno, solo con lineas.
// Las traseras ya le dijimos que
// no las pintase.
glPolygonMode(GL_FRONT, GL_LINE);
// Ancho de las lineas a 1 pixel.
glLineWidth(1);
// Color gris.
glColor3f(0.5,0.5,0.5);
// Deshabilito iluminacion.
glDisable(GL_LIGHTING);
// Pinto el ladrillo otra vez,
// pero solo los bordes ahora.
glCallList(Ladrillo);
// Vuelvo a pintar las caras
// frontales con relleno.
glPolygonMode(GL_FRONT, GL_FILL);
// Habilito texturas.
glEnable(GL_TEXTURE_2D);
// Habilito luces.
glEnable(GL_LIGHTING);

Tenemos que restalecer la matriz de modelo para que no achate mas de lo que debe.

1
2
3
// Recupero la matriz de modelo
// como estaba antes de escalar.
glPopMatrix();

El ultimo cambio en la zona de pintar el muro es a la hora de desplazarse a la derecha para pintar el siguiente ladrillo de la fila. Como hablamos de pintar el desplazamiento sera el largo del ladrillo en las nuevas coordenadas de pantalla.

1
2
// El largo de ladrillo, en 3D claro (l_l).
glTranslatef(l_l, 0, 0);

Despues del muro, coloco y pinto la paleta, tambien acomodandolo a las nuevas coordenadas de la escena 3D y con el borde alrededor de las caras como hicimos con los ladrillos.

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
// Tal como hicimos con los ladrillos convertimos las 
// coordenadas a las nuevas, con la misma formula.
// El eje X lo movemos, ademas, la mitad de la paleta
// hacia la derecha (l_l/2.0f).
glTranslatef( (l_l/2.0f)+(pal_x*20.0f)/(double)rect.right,
(pal_y*20.0f)/(double)rect.right,
0);
// Indico a OpenGL la textura a usar.
glBindTexture(GL_TEXTURE_2D, tex_paleta);
// y pinto la paleta en 3D.
// Guardo la actual matriz de modelo.
glPushMatrix();
// Escalo la paleta en los ejes Y y Z.
glScalef(1,0.3333,0.5);
// Pinto la paleta (la misma lista
// que el ladrillo).
glCallList(Ladrillo);
 
// Una vez pintada la paleta
// vamos a pintar el borde para que
// se vea mejor. Lo haremos pintando
// otra vez la paleta pero en lineas
// solo.
// Deshabilito texturas. Solo
// queremos color normal.
glDisable(GL_TEXTURE_2D);
// Indico a OpenGL que las caras
// frontales, pinte los poligonos
// sin relleno, solo con lineas.
glPolygonMode(GL_FRONT, GL_LINE);
// Ancho de las lineas a 1 pixel.
glLineWidth(1);
// Color gris.
glColor3f(0.5,0.5,0.5);
// Deshabilito iluminacion.
glDisable(GL_LIGHTING);
// Pinto la paleta otra vez,
// pero solo los bordes ahora.
glCallList(Ladrillo);
// Vuelvo a pintar las caras
// frontales con relleno.
glPolygonMode(GL_FRONT, GL_FILL);
// Habilito texturas.
glEnable(GL_TEXTURE_2D);
// Habilito luces.
glEnable(GL_LIGHTING);
 
// Restablezco la matriz de modelo.
glPopMatrix();

Mas tarde, cuando pintamos la rayita blanca que indica la direccion de saque de la bola, tambien debemos transformar las coordenadas.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// La pintare de color blanco.
glColor3f(1,1,1);
// Pinto la linea.
glBegin(GL_LINES);
// Empezando en la posicion de la bola.
// De nuevo convierto las coordenadas al nuevo
// sistema de coordenadas ("*20.0f)/(double)rect.right").
glVertex2f( ((bola_x)*20.0f)/(double)rect.right,
((bola_y-8)*20.0f)/(double)rect.right );
// Hasta una coordenada que calculare en base a la
// variable angulo usando nuestras pseudofunciones
// trigonometricas y poniendo 2.0f como longitud de la linea.
// De nuevo convierto las coordenadas al nuevo
// sistema de coordenadas ("*20.0f)/(double)rect.right").
glVertex2f(
(((bola_x)*20.0f)/(double)rect.right)+(seno[angulo]*2.0f),
(((bola_y-8)*20.0f)/(double)rect.right)-(coseno[angulo]*2.0f)
);
glEnd();

Tras esto, los siguientes cambios son a la hora de pintar la bola.
Aparte de convertir las coordenadas de la bola a las nuevas coordenadas 3D, teniendo encuenta que el la coordenada 0,0 es en el centro del modelo, deshabilito texturas y defino el material con el que pintar. En realidad este material sera con el que se pinte todo lo que no tenga textura, ya que no cambio mas de material en todo el programa (la bola y la reticula del borde del campo).
La bola sera pintada con la funcion auxSolidSphere(...) que ya vimos en anteriores capitulos, con un radio de 8 (coorenadas antiguas en 2D) pero convertido a nuevas coordenadas 3D.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Convierto las coordenadas de la bola al nuevo sistema de 
// coordenadas.
glTranslatef( ((double)(bola_x)*20.0f)/(double)rect.right,
((double)(bola_y-8.0f)*20.0f)/(double)rect.right,
0);
// Desabilitamos las texturas ya que
// pintaremos la bola solo con material.
glDisable(GL_TEXTURE_2D);
// Definimos el material del la bola.
// Definimos el material del objeto modificando la forma en
// como refleja cada componente de la luz que le llega.
glMaterialfv(GL_FRONT, GL_AMBIENT, mat1_ambiente);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat1_difusa);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat1_especular);
// El brillo especular pequeño (de 0 a 255).
glMaterialf(GL_FRONT, GL_SHININESS, 20);
 
// Y pintamos la bola de 8 (antiguas coordenadas)
// de radio. Convertimos las coordenadas a
// las nuevas.
auxSolidSphere((8.0f*20.0f)/(double)rect.right);
// Y habilitamos texturas.
glEnable(GL_TEXTURE_2D);

Ahora, en la situacion de presionar las flechas izquierda o derecha y que se mueva la paleta, incluimos el movimiento del punto de vista, en el caso de ser la vista 6 del camo (la ultima).
Esta vista sera del tipo "primera persona", la camara estara situada al lado de la paleta y se movera con ella de forma solidaria. Mirara siempre hacia arriba. Nos dara la impresion mientras jugamos de estar sentados encima de la paleta.
Por eso debemos mover la camara (o punto de vista) a la vez que la paleta en caso se estar usando esta vista (Vista==6).

Primero creamos unas variables para guardar el calculo de coordenadas a 3D. Sera asi mas facil de entender el codigo.

1
2
3
// Variables para almacenar las 
// coordenadas del punto de vista.
double cx,cy;

Y luego, dentro de cada if de presionar tecla, ademas de el codigo de mover la paleta, incluimos el codigo para cambiar el punto de vista (si, si, es un poco chapuza repetir dos veces el mismo codigo pero hay veces que uno se cansa y lo deja como sea).

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
// Si la vista es la numero 6 (vista desde
// la paleta) cambiamos tambien el punto de
// vista.
if(Vista==6)
{
// Calculamos el punto medio de la paleta en
// coordenadas 3D (eje X).
cx = (l_l/2.0f)+(pal_x*20.0f)/(double)rect.right;
// Calculamos coordenada Y de la paleta en el
// campo de juego 3D.
cy = (pal_y*20.0f)/(double)rect.right;
// Ponemos matriz de proyeccion
glMatrixMode(GL_PROJECTION);
// Cargamos la matriz identidad para
// empezar de 0 con la matriz.
glLoadIdentity();
// Ponemos la perspectiva.
gluPerspective( 50.0f,
(float)rect.right/(float)rect.bottom,
0.5f,
50.0f);
// Colocamos el punto de vista y hacia donde mira, que
// sera hacia arriba (4.0f mas arriba, por ejemplo).
gluLookAt(cx,cy+2,-1.5f, cx, cy-4.0f, -1.5f, 0,0,-1);
// Ponemos la matriz de modelo para seguir
// pintando.
glMatrixMode(GL_MODELVIEW);
}
 

Despues de esto solo quedan cambios al final de la funcion pinta, tendremos que poner proyeccion ortogonal a los carteles de fin de partida y de nuevo muro, y el tema de ir cambiando de vista cada cierto tiempo o con la tecla "V".
Toda la parte del programa para el choque de la bola con los bordes, la paleta y los ladrillos, y el rebote en cada caso, se queda como estaba, con las mismas coordenadas del programa en 2D.

Antes de pintar los carteles (si fuera el caso) cambio la proyeccion, deshabilito la iluminacion y el test de profundidad.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A partir de ahora, en la funcion
// Pinta, solo dibujaremos en ortogonal
// asi que lo dejamos preparado.
 
// Ponemos matriz de proyeccion
glMatrixMode(GL_PROJECTION);
// Guardamos la matriz de proyeccion
// para restaurarla despues de pintar
// en ortogonal.
glPushMatrix();
// Cargo matriz identidad.
glLoadIdentity();
// Pongo proyeccion ortogonal.
glOrtho(0,rect.right,rect.bottom,0,-1,1);
// Cambio a matriz de modelo.
glMatrixMode(GL_MODELVIEW);
// y deshabilito iluminacion.
glDisable(GL_LIGHTING);
// y tambien test de profundidad.
glDisable(GL_DEPTH_TEST);

Y tras pintarlos dejo todo como estaba.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Restablezco ahora la 
// proyeccion normal para
// este juego.
// Cambio a matriz de proyeccion.
glMatrixMode(GL_PROJECTION);
// Restauro la matriz que guarde.
glPopMatrix();
// Cambio a matriz de modelo.
glMatrixMode(GL_MODELVIEW);
// Habilito iluminacion.
glEnable(GL_LIGHTING);
// Habilito test de profundidad.
glEnable(GL_DEPTH_TEST);

Gestiono la tecla "R" del mismo modo que hacia con la tecla "F" (para ver o no los FPS).
Servira para cambiar el indicador "RotacionVistas" que sera el que indique si se cambia la vista del juego cada 15 segundos o la vista estara fija.

1
2
3
4
5
6
7
8
// Usamos el mismo metodo que con la tecla F, 
// pero para la tecla R.
// La usaremos para cambiar el identificador
// que deja o no la vista fija.
if(LevantaTeclaR && Teclas['R'])
{ RotacionVistas=!RotacionVistas; LevantaTeclaR=false; }
else if( !Teclas['R']) LevantaTeclaR=true;
 

Uso tambien el indicador para escribir en pantalla un mensaje cuando la vista esta fija.

1
2
3
// Si las vistas no rotan, estan fijas, lo indico 
// en pantalla.
if(!RotacionVistas) Escribe(rect.right-110,20,"Vista fija");

Por fin ya solo nos queda el codigo que cambia de vista. Sin este codigo el programa funcionaria pero solo con la vista inicial, sin cambiarla.

La vista cambiara en dos situaciones. Si presiono la tecla "V" (se gestionara de forma analoga a las teclas "F" y "R"), y si el "timer" que definimos en la funcion WinMain(...) y gestonamos en la funcion ProcesaMensajes(...) pone el indicador "CambiaVista" a true y el indicador "RotacionVistas" es true tambien.

1
2
3
4
5
6
7
8
9
// El siguiente codigo se ejecutara en caso de 
// presionar la tecla "V" (de forma analoga a como
// lo hicimos con la "F" y la "R") o en el caso de
// Estemos en juego, no tengamos la rotacion de
// vistas bloqueada y se cumpla el tiempo del Timer
// que hemos creado (indicador "CambiaVista").
if( (LevantaTeclaV && Teclas['V']) ||
(CambiaVista && EnJuego && RotacionVistas) )
{

Dentro de este if , actualizamos la variable "Vista" con la siguiente vista (en el rango de 0 a 6)...

1
2
3
4
5
6
7
// Aumentamos la variable que contiene el 
// numero de vista actual.
Vista++;
// Si esta variable pasa de 6 (solo
// hemos definido 7 vistas), la
// pongo a 0 y empiezo la rotacion.
if(Vista>6) Vista=0 ;

... preparamos la matriz de proyeccion para el cambio de vista ...

1
2
3
4
5
6
7
8
9
10
// Pongo la matriz de proyeccion.
glMatrixMode(GL_PROJECTION);
// Cargo matriz identidad.
glLoadIdentity();
// Pongo perspectiva.
gluPerspective( 50.0f,
(float)rect.right/(float)rect.bottom,
0.5f,
50.0f );
 

... pongo la camara adecuada en base a la variable "Vista" ...

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
// Y selecciono la vista a poner en 
// base a la variable "Vista".
switch(Vista)
{
case 0:
gluLookAt(10,10,-24, 10,9,0, 0,-1,0);
break;
case 1:
gluLookAt(10,0,-24, 10,9,0, 0,-1,0);
break;
case 2:
gluLookAt(10,34,-7, 10,9,0, 0,-1,0);
break;
case 3:
gluLookAt(27,15,-20, 13,9,0, 0,-1,0);
break;
case 4:
gluLookAt(10,-16,-7, 10,9,0, 0,-1,0);
break;
case 5:
gluLookAt(20,28,-7, 8,9,0, 0,-1,0);
break;
case 6:
// En este caso tengo que poner el
// punto de vista de acuerdo con
// la posicion de la paleta, tal y como
// haciamos al presionar izquierda o
// derecha con vista == 6.
cx = (l_l/2.0f)+(pal_x*20.0f)/(double)rect.right;
cy = (pal_y*20.0f)/(double)rect.right;
gluLookAt(cx,cy+2,-1.5f, cx, cy-4.0f, -1.5f, 0,0,-1);
break;
}

... y termino poniendo la matriz de modelo, pongo "CambiaVista" a  false (hasta que el timer la vuelva a poner a true) y me aseguro que la tecla "V" solo hace su funcion una vez por pulsacion. Termino el if que empezamos antes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	// Una vez cambida la vista
// regreso a la matriz de modelo.
glMatrixMode(GL_MODELVIEW);
// Actualizo el indicador para hacer las
// cosas una vez por pulsacion de tecla.
LevantaTeclaV=false;
// Pongo el indicador del Timer a false.
// Cuado se cumpla el tiempo se pondra
// a true otra vez.
CambiaVista=false;
}
// Me aseguro de que no se repite la accion
// mas que una vez por pulsacion actualizando
// el indicador LevantaTecla.
else if( !Teclas['V']) LevantaTeclaV=true;

Terminamos la funcion Pinta() "suicheando" back buffer y front buffer como siempre.

Terminado el programa, el aspecto sera asi:



El codigo completo es este:  usw12.cpp 

El juego se puede descargar compilado y listo para jugarlo en un ZIP que incluye el codigo fuente: Muro3D.zip


Bien, si habeis seguido el curso paso a paso y habeis llegado hasta aqui, quiere decir que habeis aprendido a programar lo basico de OpenGL para hacer juegos simples en 2D y 3D, y habeis (espero) entendido, linea a linea, el codigo de un juego en 2D y otro en 3D.
Se puede afirmar que sabeis ya algo de programacion de juegos. Espero que esto os de animo para seguir aprendiendo e investigando.
En todo caso el curso de programacion de juegos de UnSitioWeb continua con mas temas interesantes y avanzados.




¡Sólo los usuarios registrados pueden escribir comentarios!
+/- Comentarios
Buscar

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