MD2 (acrónimo inglés de Message-Digest Algorithm 2, Algoritmo de Resumen del Mensaje 2) es una función de hash criptográfica desarrollada por Ronald Rivest en 1989. Elalgoritmo está optimizado para computadoras de 8 bits. El valor hash de cualquier mensaje se forma haciendo que el mensaje sea múltiplo de la longitud de bloque en el ordenador (128 bits o 16 bytes) y añadiéndole un checksum. Para el cálculo real, se utiliza un bloque auxiliar 48 bytes y una tabla de 256 bytes que contiene dígitos al azar del número pi.
Una vez que todos los bloques del mensaje alargado se han procesado, el primer bloque parcial del bloque auxiliar se convierte en el valor de hash del mensaje.
Usaremos MD2, el formato de archivo Quake 2. Quake 2 puede ser viejo, pero vamos a utilizar MD2 debido a que el formato de archivo es abierto y sencillo y hay un montón de archivos MD2 en línea que otras personas han hecho.
Otra razón por la que estamos usando MD2 es porque Blender , un programa de modelado 3D de código abierto, es capaz de exportar a MD2. Los profesionales suelen utilizar 3ds Max o Maya para el modelado 3D. Pero esos programas cuestan dinero, por lo que prefieren seguir con Blender.
Por desgracia, en la actualidad, el exportador Blender MD2 es bastante temperamental y con errores. No pude conseguirlo exportar animaciones en la versión 2.44, que es actualmente la versión más reciente; Tuve que usar la versión 2.42. Con suerte, el exportador será mejorado en versiones posteriores de Blender.
El uso de Blender, hice el chico 3D para nuestro programa, incluyendo una textura para él. Nuestro programa tiene el hombre a pie, como se muestra a continuación:
Esto es lo que la edición del tipo en Blender se ve así:
Cargando y Animación MD2 Archivos
Así que ahora que hemos hecho una animación MD2 de nuestro hombre, vamos a tener que cargarlo y animarlo. Miré en línea para el formato de archivo MD2, para que yo pudiera encontrar la manera de hacer eso. En el resto de esta lección, vamos a ver cómo funciona exactamente el formato de archivo MD2.
Vamos a poner todo el código específico para archivos MD2 en los archivos md2model.h y md2model.cpp. Tendremos un MD2Model clase que almacena toda la información acerca de una animación y se encarga de la elaboración de la animación. Veamos el archivo md2model.h para ver lo que la clase se ve así:
struct MD2Vertex { Pos Vec3f; Vec3f normal; }; struct MD2Frame { Char nombre [16]; MD2Vertex * vértices; }; struct MD2TexCoord { float texCoordX; flotar texCoordY; }; struct MD2Triangle { int vértices [3]; // Los índices de los vértices de este triángulo int texCoords [3]; // Los índices de las coordenadas de la textura del triángulo };
En primer lugar, tenemos algunas estructuras que nuestro MD2Model clase utilizará. Tenemos vértices, los marcos, las coordenadas de texturas, y triángulos. Cada cuadro tiene un nombre, que suele indicar el tipo de animación en el que es (por ejemplo, "correr", "de pie"). Los marcos sólo almacenan las posiciones y las normales de cada uno de los vértices utilizando una vértices matriz. Cada trama tiene el mismo número de vértices, de manera que el vértice en el índice 5, marco 1, por ejemplo, representa la misma parte del modelo como el vértice en el índice 5, el bastidor 2, pero está en una posición diferente. Un triángulo se define por los índices de los vértices en los marcos ' vértices matrices, y los índices de las coordenadas de textura en una matriz que aparecerá en la MD2Model clase.
clase MD2Model { privado : MD2Frame * marcos; int numFrames; MD2TexCoord * texCoords; MD2Triangle * triángulos; int numTriangles;
Estas son las principales áreas que vamos a necesitar para dibujar el modelo. Tenemos una gran variedad de marcos, coordenadas de texturas, y triángulos.
GLuint textureId;
Aquí, tenemos el id de la textura de la figura que vamos a animar.
int startFrame; // El primer cuadro de la animación actual int endFrame; // El último cuadro de la animación actual
Estos son los marcos de inicio y finalización de utilizar para la animación.
/ * La posición en la animación actual. 0 indica el comienzo de * La animación, que es en el marco de partida, y 1 indica el * Final de la animación, que está justo cuando el marco de partida es * Alcanzado de nuevo. Siempre se encuentra entre 0 y 1. * / flotar tiempo;
Er, acabo de leer los comentarios.
MD2Model ();
público :
~ MD2Model ();
He aquí nuestro constructor y destructor. El constructor es privado, porque sólo una especial MD2Model método será capaz de construir una MD2Model objeto.
// Cambia a la animación dada vacío (setAnimation const char * nombre);
Este método le permitirá a establecer la animación actual, ya que el archivo MD2 puede almacenar en realidad varias animaciones en ciertos rangos de marcos. Nuestra animación, por ejemplo, va a ocupar marcos 40 a 45. Cada cuadro tiene un nombre, que nos permitirá identificar los marcos apropiados una la cadena de animación dado, como veremos más adelante.
// Avanza la posición en la animación actual. Toda la animación // dura una unidad de tiempo. void adelantado ( float dt);
Este método se utiliza para avanzar la animación estado. Al llamar repetidamente avance , vamos a animamos a través de las diferentes posiciones de la figura 3D.
// Llama el estado actual del modelo de animación. void draw ();
Este método se encarga de la elaboración de hecho el modelo 3D.
// Carga un MD2Model desde el archivo especificado. Devuelve NULL si no era // un error al cargar la misma. estática MD2Model * Carga ( const char * filename); };
La carga método va a cargar un archivo MD2 dado. Es un método estático, indicado por la palabra clave " estática ". Esto significa que podemos decir que es el uso de MD2Model :: load ("somefile.md2") , y que no tenga que llamar en un determinado MD2Model objeto. Básicamente, la carga es como una función normal, excepto que puede acceder a los campos privados de MD2Modelobjetos.
Ese es el archivo md2model.h. Ahora echemos un vistazo a md2model.cpp.
espacio de nombres { // ... }
Un pequeño matiz C ++: tenemos que poner todas las constantes y funciones no-clase en este espacio de nombres {} bloque para que podamos tener otras constantes y funciones con el mismo nombre en diferentes archivos. Podríamos, por ejemplo, tienen una función llamada " foo ", tanto en este bloque de espacio de nombres y en main.cpp.
// Normales utilizadas en el formato de archivo MD2 flotador NORMALS [486] = {-0.525731f, 0.000000f, 0.850651f, -0.442863f, 0.238856f, 0.864188f, // ... -0.688191f, -0.587785f, -0.425325f};
En lugar de almacenar directamente normales, MD2 tiene 162 normales y especiales sólo da los índices de las normales. Esta matriz contiene todas las normales que utiliza MD2.
Cuando cargamos en el archivo, vamos a tener que preocuparse por una pequeña cosa llamada "endianness". En el diseño de las CPUs, los diseñadores tuvieron que decidir si desea almacenar los números con su byte más significativo primero o el último. Por ejemplo, el entero corto 258 = 1 (256) + 2 podría ser almacenado con los bytes (1, 2), con el byte más significativo primero, o con los bytes (2, 1), con el byte menos significativo primero. El primer medio de almacenamiento se llama "big-endian"; el segundo se llama "little-endian". Así, la gente que diseñó CPUs, en su infinita sabiduría, decidieron ambos.Algunas CPUs, incluyendo el Pentium, números de tiendas en forma ascendente hacia la izquierda, y otros números CPUs almacenar en forma de big-endian. Estúpido como parece, que endianness es "mejor", ha sido la fuente de discusiones sin sentido. Por lo tanto, estamos atascados con los dos, un problema que ha sido programadores molestos para las edades pasado.
¿Qué tiene esto que ver con nada? El problema surge cuando un entero que requiere múltiples bytes se almacena en el archivo MD2. Se almacena en forma ascendente hacia la izquierda. Pero el equipo en el que cargamos el archivo podría no utilizar la forma little-endian. Así que cuando cargamos el archivo, tenemos que escribir nuestro código cuidadosamente para asegurarse de que el orden de bytes de ordenador en el que se ejecuta el programa, no importa.
// Devuelve si el sistema es poco-endian bool littleEndian () { // El valor a corto 1 tiene bytes (1, 0) en little-endian y (0, 1) en // big-endian cortos s = 1; retorno ((( Char *) y s) [0]) == 1; }
Esta función será comprobar si estamos en un sistema little endian o big-endian. Si el primer byte del corto número entero 1 es un 1, entonces estamos en una máquina little-endian; de lo contrario, estamos en una máquina big-endian.
// Convierte una matriz de cuatro caracteres en un número entero, utilizando little-endian forma int Toint ( const char * bytes) { retorno ( int ) ((( unsigned char ) bytes [3] << 24) | (( unsigned char ) bytes [2] << 16) | (( unsigned char ) bytes [1] << 8) | ( unsigned char ) bytes [0]); } // Convierte una matriz de dos caracteres para un corto, utilizando el formulario little-endian corto toShort ( const char * bytes) { retorno ( corto ) ((( unsigned char ) bytes [1] << 8) | ( unsigned char ) bytes [0]); } // Convierte una matriz de dos caracteres a un corto sin signo, utilizando little-endian // formar sin firmar corto toUShort ( const char * bytes) { vuelta ( sin firmar corto ) ((( unsigned char ) bytes [1] << 8) | ( unsigned char ) bytes [0]); }
Aquí, tenemos funciones que convertirán una secuencia de bytes en un int , un corto , o un corto sin signo . Ellos usan el << operador BitShift, que, básicamente, sólo empuja un número de 0 bits en el final del número. Por ejemplo, el número binario 1001101 bit desplazado por 5 es 100110100000. Todos los bits "extra" en la parte delantera se acaban de extraer. Tenga en cuenta que las funciones funcionan independientemente del orden de bits de la máquina en la que se ejecuta el programa.
// Convierte una matriz de cuatro caracteres a un flotador, utilizando little-endian forma flotador estar flotando ( const char * bytes) { float f; si (littleEndian ()) { (( Char *) y f) [0] = bytes [0]; (( Char *) y f) [1] = bytes [1]; (( Char *) y f) [2] = bytes [2]; (( Char *) y f) [3] = bytes [3]; } otra cosa { (( Char *) y f) [0] = bytes [3]; (( Char *) y f) [1] = bytes [2]; (( Char *) y f) [2] = bytes [1]; (( Char *) y f) [3] = bytes [0]; } volver f; }
Ni siquiera flotar s son inmunes a la cuestión endianness. Para convertir cuatro bytes en un flotador , comprobamos si estamos en una máquina little-endian y luego establecemos cada byte del flotador fsegún corresponda.
// Lee los siguientes cuatro bytes como un entero, utilizando little-endian forma int readInt (ifstream y entrada) { char buffer [4]; input.read (buffer, 4); volver Toint (buffer); } // Lee los siguientes dos bytes como un corto, utilizando el formulario little-endian corto readShort (ifstream y entrada) { char buffer [2]; input.read (buffer, 2); volver toShort (buffer); } // Lee los siguientes dos bytes como un corto sin signo, mediante el formulario little-endian sin firmar corto readUShort (ifstream y entrada) { char buffer [2]; input.read (buffer, 2); volver toUShort (buffer); } // Lee los siguientes cuatro bytes como un flotador, mediante el formulario little-endian flotador readFloat (ifstream y entrada) { char buffer [4]; input.read (buffer, 4); volver estar flotando (buffer); } // Las llamadas readFloat tres veces y devuelve los resultados como un objeto Vec3f Vec3f readVec3f (ifstream y entrada) { float x = readFloat (entrada); flotar y = readFloat (entrada); float z = readFloat (entrada); volver Vec3f (x, y, z); }
Estas funciones hacen que sea conveniente para leer los próximos bytes de un archivo como int , corto , corto sin signo , flotador , o Vec3f .
// Hace que la imagen en una textura, y devuelve el id de la textura GLuint LoadTexture (Imagen * Imagen) { // ... } }
Aquí está nuestra LoadTexture función de la lección en texturas.
MD2Model :: ~ MD2Model () { si (! fotogramas = NULL) { para ( int i = 0; iborro
Así es destructor de la clase, lo que libera la memoria utilizada por todos los vértices, los marcos, las coordenadas de texturas, y triángulos.
MD2Model :: MD2Model () { marcos = NULL; texCoords = NULL; triángulos = NULL; tiempo = 0; }
El constructor inicializa algunos de los campos. El constructor no hace mucho; la acción es en la carga de método.
// Carga el modelo MD2 MD2Model * MD2Model :: load ( const char * filename) { ifstream de entrada; input.open (filename, istream :: binario); Char buffer [64]; input.read (buffer, 4); // debe ser "IPD2", si se trata de un archivo MD2 si (tampón [0] = 'I' || amortiguar [1] = 'D' ||! buffer [2]! = || buffer 'P' [3]! = '2') { devolver NULL; } si (readInt (entrada) = 8!) { // El número de versión de retorno NULL; }
Aquí está el método que se carga en un archivo MD2. En primer lugar, comprobamos que los primeros cuatro bytes del archivo son "IPD2", que deben ser los primeros cuatro bytes de cada archivo MD2.Luego, comprobamos que los próximos cuatro bytes, interpretados como un entero, son el número 8, que deben ser para los archivos MD2 que estamos cargando.
int textureWidth = readInt (entrada); // La anchura de las texturas int textureHeight = readInt (entrada); // La altura del texturas readInt (entrada); // El número de bytes por trama int numTextures = readInt (entrada) ; // El número de texturas si (numTextures = 1!) { retorno NULL; } int numVertices = readInt (entrada); // El número de vértices int numTexCoords = readInt (entrada); // El número de coordenadas de textura int numTriangles = readInt (entrada); // El número de triángulos readInt (entrada); // El número de comandos de OpenGL int numFrames = readInt (entrada); // El número de fotogramas
El formato de archivo MD2 dicta que el siguiente en el archivo, debe haber cierta información acerca de la animación en un cierto orden. Leemos en esta información y la almacenamos en variables. Parte de la información que no necesitamos, por lo que no almacenamos en cualquier lugar.
// Compensaciones (número de bytes a partir del principio del archivo al principio // de donde aparecen ciertos datos) int textureOffset = readInt (entrada); // El desplazamiento a las texturas int texCoordOffset = readInt (entrada); // El desplazamiento a las coordenadas de textura int triangleOffset = readInt (entrada); // El desplazamiento a los triángulos int frameOffset = readInt (entrada); // El desplazamiento a la marcos readInt (entrada); // El desplazamiento a la OpenGL comandos readInt ( de entrada); // El desplazamiento hasta el final del archivo
Siguiente en el archivo MD2 debe haber ciertos valores que indican el número de bytes desde el comienzo del archivo donde aparecen ciertos datos.
// Cargar la textura input.seekg (textureOffset, ios_base :: beg); input.read (buffer, 64); si (strlen (buffer) <5 -="" 0="" 4="" bmp="" buffer="" class="codekeyword" n="" span="" strcmp="" strlen="" style="color: #0000d0;" tamp="">devolver5>
Nosotros vamos a donde está indicada la textura, y la carga en los próximos 64 bytes como una cadena. La cadena es un nombre de archivo donde la textura para el modelo es. Nos aseguramos de que la textura es un mapa de bits y lo cargamos en.
// Cargar las coordenadas de textura input.seekg (texCoordOffset, ios_base :: beg); modelo-> texCoords = nuevo MD2TexCoord [numTexCoords]; para ( int i = 0; itexCoords + i; texCoord-> texCoordX = ( float ) readShort (entrada) / textureWidth; texCoord-> texCoordY = 1 - ( float ) readShort (entrada) / textureHeight; }
A continuación, cargamos en las coordenadas de textura. Cada coordenada de textura se representa como dos cortos s. Para llegar desde cada uno corto a la apropiada flotador , tenemos que dividir por la anchura o la altura de la textura que encontramos en el principio del archivo. Para la coordenada y, tenemos que tener 1 menos la coordenada porque el archivo MD2 mide la coordenada y de la parte superior de la textura, mientras que OpenGL mide desde el fondo de la textura.
// Cargue los triángulos input.seekg (triangleOffset, ios_base :: beg); modelo-> triángulos = nuevo MD2Triangle [numTriangles]; modelo-> numTriangles = numTriangles; para ( int i = 0; itriángulos + i; para ( int j = 0; j <3 j="" ngulo="" tri=""> vértices [j] = readUShort (entrada); } para ( int j = 0; j <3 j="" ngulo="" tri=""> texCoords [j] = readUShort (entrada); } }3>3>
Ahora, cargamos en los triángulos, que son sólo un montón de índices de vértices y coordenadas de textura.
// Cargue los marcos input.seekg (frameOffset, ios_base :: beg); Modelo-> marcos = nuevo MD2Frame [numFrames]; modelo-> numFrames = numFrames; para ( int i = 0; imarcos + i; marco> vértices = nuevo MD2Vertex [numVertices]; Escala Vec3f = readVec3f (entrada); Traducción Vec3f = readVec3f (entrada); input.read (marco> nombre, 16); para ( int j = 0; j vértices + j; input.read (buffer, 3); Vec3f v (( unsigned char ) buffer [0], ( unsigned char ) buffer [1], ( unsigned char ) buffer [2]); vertex-> pos = traducción + Vec3f (escala [0] * v [0], escalar [1] * v [1], escala [2] * v [2]); input.read (buffer, 1); int normalIndex = ( int ) (( unsigned char ) buffer [0]); vertex-> normales = Vec3f (NORMALS [3 * normalIndex], NORMALS [3 * normalIndex + 1], NORMALS [3 * normalIndex + 2]); } }
Ahora, cargamos en los marcos. Cada trama comienza con seis carrozas, indicando vectores por los que pueda escalar y traducir los vértices. Entonces, hay 16 bytes que indican el nombre del marco. Luego vienen los vértices. Para cada vértice, tenemos dos caracteres sin signo que indica la posición, que podemos convertir en balsas por escalamiento y traducirlos. Entonces, tenemos un carácter sin signo que da la vertor normal, como un índice en la NORMALS matriz que vimos anteriormente.
modelo-> startFrame = 0;
modelo-> endFrame = numFrames - 1;
volver modelo;
}
Por último se exponen los fotogramas inicial y final y volvemos al modelo.
vacío MD2Model :: setAnimation ( const char * nombre) { / * Los nombres de marcos normalmente comienzan con el nombre de la animación en * Que se encuentran, por ejemplo, "correr", y son seguidos por un no-alfabético * Carácter. Normalmente, indican su número de fotograma de la animación, * Por ejemplo, "run_1", "run_2", etc. * / bool encontrado = false; para ( int i = 0; isi
Esta función se da cuenta de los marcos de inicio y fin de la animación indicado usando los nombres de los diferentes marcos, que siguen el modelo sugerido por el comentario.
vacío MD2Model :: adelantado ( float dt) { si (dt <0 class="codekeyword" span="" style="color: #0000d0;">volver0>
Ahora, tenemos un método para avanzar en la animación, lo que hacemos al aumentar el tiempo de campo. Para mantenerlo entre 0 y 1, utilizamos el tiempo - = (int) tiempo (a menos que el tiempo es muy grande, en cuyo caso podríamos tener problemas convertirlo en un entero).
vacío MD2Model :: draw () {
glEnable (GL_TEXTURE_2D);
glBindTexture (GL_TEXTURE_2D, textureId);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Aquí es donde se dibuja el modelo 3D. Comenzamos diciendo OpenGL la textura y el tipo de mapeado de texturas que queremos utilizar.
// Averiguar los dos cuadros entre los que estamos interpolación int frameIndex1 = ( int ) (hora * (endFrame - startFrame + 1)) + startFrame; si (frameIndex1> endFrame) { frameIndex1 = startFrame; } int frameIndex2; si (frameIndex1otra cosa
Ahora, utilizando el tiempo de campo, que sepamos los dos cuadros entre los que queremos interpolar.
// Calcula la fracción que estamos entre los dos marcos flotan frac = (Tiempo - ( float ) (frameIndex1 - startFrame) / ( float ) (endFrame - startFrame + 1)) * (endFrame - startFrame + 1);
Ahora, podemos averiguar qué fracción estamos entre los dos marcos. 0 significa que estamos en el primer cuadro, 1 significa que estamos en el segundo, y 0,5 significa que estamos a mitad de camino en el medio.
// Dibuja el modelo como una interpolación entre los dos marcos glBegin (GL_TRIANGLES); para ( int i = 0; ipara
Ahora, vamos a través de los triángulos, y para cada vértice, tomamos la posición de ser una interpolación entre sus posiciones en los dos marcos.
Vec3f normales = v1> * normal (1 - frac) + V2-> normales * frac;
si (normal [0] == 0 && normal [1] == 0 && normal [2] == 0) {
Vec3f normal = (0, 0, 1);
}
glNormal3f (normal [0], normal [1], normal [2]);
Hacemos lo mismo para los vectores normales. Si la media pasa a ser el vector cero, podemos cambiar a un vector arbitrario, ya que el vector cero no tiene ninguna dirección y no se puede utilizar como un vector normal. En realidad hay una mejor manera de promediar dos direcciones, pero vamos a seguir con un promedio lineal porque es más fácil.
MD2TexCoord * TexCoord = texCoords + Triángulo> texCoords [j]; glTexCoord2f (texCoord-> texCoordX, texCoord-> texCoordY); glVertex3f (pos [0], POS [1], pos [2]); } } glEnd ();
Ahora, sólo encontramos la textura apropiada coordinar y llamar glTexCoord2f y glVertex3f .
No hay comentarios:
Publicar un comentario