"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 22. Bounding volumes y reestructuración
Lunes 05 de Junio del 2023

Menu principal
Colaborar (con PayPal)

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

Importe: 

Ultimas descargas
19.Jan

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


22. Bounding volumes y reestructuración Imprimir Correo electrónico
Videojuegos - Curso de Programación de juegos
Escrito por Vicengetorix   
Este capítulo será un poco "variopinto". Veremos el cálculo de los "bounding volumes", volúmenes dentro de los cuales se circunscribe un modelo.  Aprovecharemos también para reestructurar el código, que ya se está haciendo muy largo nuestro único fichero de código. También encapsularemos los modelos en una clase para cargar varios facilmente y lo mismo haremos con los shaders. Solucionaremos, también, dos problemitas con el código previo. 

La pregunta inmediata es ¿para qué calculamos los "bounding volumes"?. Pues para, en un futuro, poder hacer "frustum culling" (descartar lo que no se ve) o calcular algún tipo de choque entre modelos. Son dos ejemplos.
Calcularemos el "bounding box" (cubo dentro del cual está el modelo) y la "bounding sphere" (lo mismo pero una esfera). Calcularemos los dos en tiempo de carga del modelo y dibujaremos, más tarde, el cubo y la esfera contenedora (o delimitadora con traducción exacta) alrededor del modelo.

La reestruccturación del código es la parte más necesaria. Hasta ahora ha sido cómodo mantener todo en un solo fichero de código pero ya empieza a ser complicado encontrar las cosas y además va siendo hora de normalizar la forma de trabajo (no mucho).
Sacaremos del fichero principal todo menos la función Pinta() y añadiremos a este fichero principal una función Inicia() para inicializaciones (será llamada desde IniciaGL() ) y una función Cierra() para liberar memorias y esas cosas (será llamada desde ProcesaMensajes(...) ).
Crearemos un fichero de cabecera (.h) llamado USW.h donde irá casi todo el resto del código, incluida WinMain, las funciones IniciaGL, ProcesaMensajes, CreaVentana, las funciones matemáticas, etc.
Obviamente habrá que incluir este fichero al principio del programa principal.
Creamos también un fichero llamado USWmodelo.h en el que crearemos una clase para manejar la carga y pintado de modelos y así poder cargar los que queramos.
Hacemos lo mismo con un fichero llamado USWshader.h pero con una clase para gestionar los shaders.
Las funciones, estructuras y variables que teníamos para la carga de modelos serán incluidas en el fichero USWmodelo.h y las referentes a shaders en USWshaders.h.

Veamos USWmodelo.h.
Contiene la clase modeloCOB, además de las estructuras y el código para carga y pintado de modelos que vimos en anteriores capítulos.

La definicion de la clase es esta.

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
// Clase modeloCOB que encapsula la carga y 
// manejo de modelos truSpace de texto.
class modeloCOB
{
private:
// Variables auxiliares para
// guardar vectores, puntos, vertices o
// lo que se nos antoje de tres coordenadas
float v[3],v2[3];
// Array para guardar la matriz que leeremos
// del fichero y deberemos aplicar a los vertices
// de cada malla de las que se componga el modelo.
float trans[4][4];
// Numero de vertices
GLuint NVertices;
// Puntero para guardar vertices
GLfloat (*Vertices)[3];
// Puntero para guardar los vertices
// definitivos.
GLfloat (*Puntos)[3];
// Numero de coordenadas de textura.
GLuint NCorTex;
// Puntero para guardar coordenadas
// de textura
GLfloat (*CorTex)[2];
// Puntero para guardar coordenadas
// de textura definitivas.
GLfloat (*uvTex)[2];
// Numero de normales.
GLuint NNormales;
// Puntero para normales por vertice.
GLfloat (*Normales2)[3];
// Puntero para normales definitivas
// por vertice.
GLfloat (*Normales)[3];
// Numero de normales por cara.
GLuint NFNormales;
// Puntero para las normales
// por cara.
GLfloat (*FNormales)[3];
// Numero de caras (triangulos)
// del modelo.
GLuint NCaras;
// Puntero para guardar las caras.
Triangulo *Caras;
// Numero de mateiales diferentes
// en este objeto
GLuint NMateriales;
// Puntero para guardar los materiales.
Material *Materiales;
 
// Definimos tres variables para guardar el identificativo
// de los VBOs que vamos a usar.
GLuint vboVertices;
GLuint vboNormales;
// Funcion para separar datos de tipo "<5,45>"
// de un fichero .cob. p=1 retorna el primer parametro,
// p=2 el segundo.
int IndiceCara(char *cad,int p=1);
// Funcion para borrar el modelo
void BorraModelo();
 
public:
// Funcion para cargar modelo de fichero .COB (trueSpace) en
// ASCII y triangulado.
bool CargaModelo(char *fich);
// Dibuja el modelo.
void PintaModelo();
// Constructor
modeloCOB();
// Constrructor con carga
modeloCOB(char *fich);
// Destructor
~modeloCOB();
// He dejado este VBO en la parte publica
// para usarlo desde fuera de la clase en caso
// de querer aplicar otra textura mas al modelo
// con estas coordenadas.
GLuint vboTextura;

// Este estructura almacenara los datos
// de los "bounding volumes" del modelo
_bound bound;
 
};

Si os fijais, vereis que hay muy pocas cosas nuevas, en realidad solo una, aparte de que está todo incluido en una clase.
Lo único nuevo es  la última definición. Es para almacenar los datos de los "bounding volumes" que vamos a calcular. La estructura se define en la zona de definiciones globales del fichero.

1
2
3
4
5
6
7
8
9
10
11
12
// Estructura para los datos de los 
// bounding volumes.
struct _bound {
GLfloat maxX;
GLfloat minX;
GLfloat maxY;
GLfloat minY;
GLfloat maxZ;
GLfloat minZ;
GLfloat radio;
 
};

Los 6 primeros miembros de la estructura son para los datos que definen la "bounding box".
La "caja delimitadora" será definida por dos vértices opuestos del cubo. Con estos dos vértices podremos componer toda la caja (está alineada con los ejes). Los vértices serán maxX,maxY,maxZ y minX,minY,minZ.
El último miembro es el radio de la "bounding sphere", la esfera delimitadora del modelo.
Solo necesitamos el radio porque vamos a asumir que los modelos que cargamos están todos centrados en el centro de coordenadas con lo que asumiremos como centro de la esfera contenedora la posición del modelo. Acordaos de centrar los modelos antes de salvarlos.

Los datos los conseguiremos al cargar el modelo en la función CargaModelo(...), que ahora es parte de la clase modeloCOB.
Incluiremos una variable para almacenar datos temporales para el cálculo del radio.

1
2
3
// Variable temporal para el calculo
// del radio.
GLfloat r_temp;

Luego, cada vez que cargamos un vértice, miraremos si tiene las coordenadas más pequeñas o más grandes para cada eje y, si es así, las guardaremos. Al final tendremos las coordenadas más grandes y más pequeñas de todo el modelo, la "bounding box".
También iremos comprobando la distancia de cada vértice al centro de coordenadas (0,0,0) para, al final, quedarnos con la mayor, el radio de la "bounding sphere".
Con el primer vértice inicializamos las variables.

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
// Calculamos la bounding box y bounding
// sphere.
// Primero, si es el primer vertice que
// cargamos (i==0), cargamos el valor
// como punto inicial.
if(i==0) {
// Para la bounding box.
bound.maxX = v2[1];
bound.maxY = v2[2];
bound.maxZ = v2[0];
bound.minX = v2[1];
bound.minY = v2[2];
bound.minZ = v2[0];
// Para la bounding sphere
// la distancia del vertice al
// centro de coordenadas.
bound.radio=(GLfloat)sqrt(v2[1]*v2[1] + v2[2]*v2[2] + v2[0]*v2[0]);
}
// En los siguientes vertices
// nos quedamos con las coordenadas
// maximas y minimas para cada eje
// para la bounding box.
else {
if( v2[1] > bound.maxX ) bound.maxX=v2[1];
if( v2[2] > bound.maxY ) bound.maxY=v2[2];
if( v2[0] > bound.maxZ ) bound.maxZ=v2[0];
if( v2[1] < bound.minX ) bound.minX=v2[1];
if( v2[2] < bound.minY ) bound.minY=v2[2];
if( v2[0] < bound.minZ ) bound.minZ=v2[0]; }
 
// Para la bounding sphere nos quedamos con
// la mayor distancia de un vertice a la coordenada
// 0,0,0.
r_temp=(GLfloat)sqrt(v2[1]*v2[1] + v2[2]*v2[2] + v2[0]*v2[0]);
if(bound.radio < r_temp) bound.radio = r_temp;

Recuerdo que este código va introducido en el bucle en el que cargamos los vértices y se repite con cada vértice.
Al final de la carga tendremos los datos de los "bounding volumes" a nuestra disposición para lo que queramos usarlos.

Haremos una última modificación de esta función para resolver un problema.
En los modelos sin textura no se cargaban bien las coordenadas de textura aunque las tuviera definidas y era por que no se inicializaban las variables tOffset y tRepite. Si el modelo no tenía textura se quedaban sin definir (típico fallo). Luego al multiplicar y sumar estas variables a las coordenadas de textura daban resultados inpredecibles.
La solución es simplemente inicializarlas siempre, haya o no textura.

1
2
3
4
5
// Pongo valores por defecto de offset y repite
Materiales[M].tOffset[0]=0;
Materiales[M].tOffset[1]=0;
Materiales[M].tRepite[0]=1;
Materiales[M].tRepite[1]=1;

Con esto hemos terminado con el fichero USWmodelo.h.

Vamos ahora con el fichero USWshader.h.
Básicamente contiene la clase shader que gestiona la carga de los shaders. Cada objeto de tipo shader que creemos nos permitirá tener un fragment y un vertex shader que podremos usar facilmente. El código es el de el anterior capítulo pero incluido en una clase.
La definición de la clase es esta.

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
// Clase que encapsula la carga y uso de shaders.
class shader
{
private:
// Definimos identificadores para el programa,
// el vertex shader y el fragment shader.
// El programa se compone de un vertex shader
// y un fragment shader.
GLuint po, vs, fs;
public:
// Constructor
shader() {po=0; vs=0; fs=0;} ;
// Constructor que, ademas, carga los shaders.
shader(char *v, char *f);
// Destructor
~shader();
// Funcion para cargar los shaders. Los
// parametros son los ficheros a cargar.
// Carga vertex y fragment shaders, los compila
// y los deja listos para el uso.
void CargaShader(char *v, char *f);
// Funcion que indica que use los shaders cargados.
void Usa();
// Funcion que deja la "fixed functionality"
void Quita();
};

El único cambio es en el destructor de la clase. En el anterior capítulo se me olvido la parte en que se eliminan los shaders y el programa (mis disculpas). En todo caso el código es auto explicativo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Destructor
shader::~shader()
{
// Si no habia cargado shaders no sigo.
if(po==0) return;
// Desvinculo los shaders del programa
// (si no no deja borrarlos)
glDetachShader(po,vs);
glDetachShader(po,fs);
// Borro los shaders.
glDeleteShader(vs);
glDeleteShader(fs);
// Borro el programa.
glDeleteProgram(po);
}

No me extiendo más en este fichero; no creo que sea necesario. Echadle un vistazo al fichero.

El fichero más importante, y el primero que hay que incluir, es USW.h. Sobre este fichero no hay gran cosa que decir porque el código que tiene no ha cambiado, solo se ha movido aquí para hacer más paqueño el ".CPP".

Solo queda el fichero usw22.cpp.
Aquí está la función Inicia() en la que no hay código nuevo pero hemos incluido en ella la ultima parte de lo que estaba en la función IniciaGL(). En ella cargaremos texturas, modelos, shaders, ..., definiremos luces, lo que sea antes del bucle principal. Esta función se ejecuta una vez al iniciar el programa (de hay su nombre).
El único cambio reseñable es la forma de cargar el modelo. Ahora lo haremos como una función miembro de un objeto en vez de como una función independiente.

1
2
// Cargamos el modelo.
modelo.CargaModelo("dibus//cosa.cob");

Como habréis notado, cargamos un modelo nuevo. Al final está el link

La función Cierra() se ejecutará una vez antes de salir del programa. Aquí eliminaremos objetos y liberaremos memoria. De momento solo las texturas.

En la parte de las definiciones globales, arriba, primero incluiremos los anteriores ficheros que hemos creado.

1
2
3
4
5
6
7
int AnchoVentana = 600;	  // Lo que dice el nombre de la variable
int AltoVentana = 400; // Lo que dice el nombre de la variable
 
#include "USW.h"
#include "USWmodelo.h"
#include "USWshader.h"
 

Las dos variables del tamaño de la ventana las dejo las primeras porque se van a usar en USW.h. Las dejo aquí para cambiarlas más fácil si se quiere.

Creo un objeto de la clase modeloCOB para nuestro modelo.

1
modeloCOB modelo;

A partir de ahora podremos cargar facilmente muchos modelos, simplemente creando un objeto de esta clase por cada modelo que se quiera cargar.

Añado una variable para usar la tecla "B" (la usaremos para cambiar entre pintar bounding box o bounding sphere).

1
2
// Tecla B
bool LevantaTeclaB=true;

Y otra como indicador de lo que se está pintando.

1
2
3
// Indicador de si se pinta 
// esfera o caja (bounding)
bool bound=true;

Llegamos por fin a la función Pinta(), la de toda la vida.

El primer cambio es que la función PintaModelo() es miembro de un objeto.

1
2
// Pinto el objeto que sea.
modelo.PintaModelo();

Tras pintar el modelo pinto el "bounding box" o la "bounding sphere" en base a la variable "bound", definida antes, alrededor del modelo.
Para la esfera usaremos las "Quadrics" de la librería GLU. Para la caja usaremos líneas, aunque no pintaremos todas, no es necesario para verla.

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
// Pongo linea fina
glLineWidth(1);
// Segun el indicador bound ...
if(bound)
{
// Pintamos la bounding sphere con
// quadrics o ...
GLUquadricObj *q;
q=gluNewQuadric();
gluQuadricDrawStyle(q, GLU_SILHOUETTE);
gluQuadricNormals(q, GLU_SMOOTH);
gluQuadricTexture(q, GL_FALSE);
gluSphere(q,modelo.bound.radio,16,16);
gluDeleteQuadric(q);
}
else
{
// Pintamos la bounding box.
glBegin(GL_LINE_STRIP);
glVertex3f( modelo.bound.maxX,modelo.bound.maxY,modelo.bound.maxZ);
glVertex3f( modelo.bound.minX,modelo.bound.maxY,modelo.bound.maxZ);
glVertex3f( modelo.bound.minX,modelo.bound.minY,modelo.bound.maxZ);
glVertex3f( modelo.bound.maxX,modelo.bound.minY,modelo.bound.maxZ);
glVertex3f( modelo.bound.maxX,modelo.bound.maxY,modelo.bound.maxZ);
glVertex3f( modelo.bound.maxX,modelo.bound.maxY,modelo.bound.minZ);
glVertex3f( modelo.bound.minX,modelo.bound.maxY,modelo.bound.minZ);
glVertex3f( modelo.bound.minX,modelo.bound.minY,modelo.bound.minZ);
glVertex3f( modelo.bound.maxX,modelo.bound.minY,modelo.bound.minZ);
glVertex3f( modelo.bound.maxX,modelo.bound.maxY,modelo.bound.minZ);
glEnd();
}

Por fin, solo queda el código para cambiar entre esfera y caja con la tecla "B" (cambiando la variable "bound"), y escribir en pantalla un mensaje del uso de esta tecla.

1
2
3
4
5
6
7
// Si se presiona B cambio el indicador bound
// evitando la repeticion de tecla.
if(LevantaTeclaB && Teclas['B'])
{ bound=!bound; LevantaTeclaB=false; }
else if( !Teclas['B']) LevantaTeclaB=true;
// Escribimos lo que hace la tecla B en pantalla
Fuente.escribe(370,60,"B cambia entre bounding box y sphere" );

Al final, el programa ejecutándose debe verse así:



Los ficheros de código serían estos:
usw22.cpp .
USW.h .
USWmodelo.h .
USWshader.h .

El modelo es este: cosa.cob .


Con los billboard se podría haber hecho lo mismo que con los modelos y los shaders, encapsularlos en una clase para usarlos fácilmente. Tal vez más adelante.







¡Sólo los usuarios registrados pueden escribir comentarios!
+/- Comentarios
Buscar
Black Master  - Ayuda porfavor     |88.17.187.xxx |03-04-2013 15:26:20
He intentado hacerlo funcionar pero me da errores en cada linea de
glBindBufferARB(
Excepción no controlada en 0x00000000 en Shaders y
Reestructurado.exe: 0xC0000005: Access violation. Ayuda porfavor
Black Master  - Te dejo el source     |88.17.187.xxx |03-04-2013 15:55:55
https://www.dropbox.com/s/9u9apq2dvzr6l44/Shaders% 20y%20Reestructurado.rar
Vicengetorix   |87.221.172.xxx |06-04-2013 15:46:51
El error debe ser por que no inicializas la extension
ARB_vertex_buffer_object.
Si la tarjeta es moderna, usala sin ARB ya que estara
implementada en la version de opengl pero hay que indicar a tu programa la
version con alguna libreria como GLEE o GLEW. Esto se hace en el capitulo 18.

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