domingo, 7 de febrero de 2010

El formato de archivo MD2 de Quake II

Cuando comencé a programar pequeños programas usando 3D me vi en la necesidad de encontrar una manera de guardar y de recuperar los modelos.

Investigando por Internet me encontré con varios tipos de formato y con el primero que experimente fue con el 3DS, un formato binario bastante viejo, de 1990 aproximadamente, que era utilizado por las primeras versiones de 3D Studio DOS (recién funcionando sobre Windows se convirtió en MAX) y que a lo largo del tiempo se convirtió en un formato defacto para el paso de escenas o modelos entre diferentes programas de 3D.

Evidentemente había en Internet librerías que permitían manejar este formato, pero cuando uno busca aprender las librerías de terceros no sirven para mucho (en el momento no tenia una buena cantidad de experiencia o las librerías eran muy avanzadas y su lectura del código se volvía bastante complicada y en otras, el código era terriblemente desprolijo) y como es un formato defacto y por ende no estandarizado terminaba sufriendo modificaciones de terceros que complicaban el trabajo de aprendizaje. Pero igualmente llegue a trabajar con el formato, ya que me había tomado el tiempo de leer toda la documentación, y al probarlo me di cuenta que el formato brindaba mas información de que la necesitaba y el manejo de las animaciones resultaba bastante engorroso.

A si que seguí con la búsqueda y me encontré con el formato MD2, el formato utilizado para guardar los modelos en el id Tech 2 engine, el cual fue el corazón de varios juegos en su época (Quake II, SiN, Soldier of Fortune). En esa misma búsqueda me encontré con un articulo, mas tutorial que articulo, en el cual explicaba el formato del archivo MD2, su funcionamiento y brinda código en C++ (prolijo y claro código). El articulo en cuestión fue escrito por David Henry en el 2002 y fue de gran ayuda para el desarrollo de AIRO, por esa razón lo traduje al español y lo comparto con ustedes.


Introducción
"Si, un nuevo tutorial de MD2..." Si, pero el mio te mostrara como renderearlos de una manera diferente ;-) Pero a todo esto ¿que demonios es un MD2?, el MD2 es un formato de archivo diseñado para guardar modelos 3D y es utilizado por el engine del Quake II de ID Software. Y aquí te mostraré cómo cargarlo y mostrarlo en la pantalla usando OpenGL.

Probablemente pensaste "Rayos, este tipo se quedo atascado en los viejos días de 1997", pero hay buenas razones para utilizarlo. Primero, porque el MD2 es un buen formato de archivo para el aprendizaje debido a su sencillez (si eres principiante, no podrás entenderlo, pero mira otros formatos y veras ;.)) y porque este formato es totalmente gratis (¡pero no los modelos, así que ten cuidado!)

Aquí un resumen de lo que veremos en este artículo:
  • El formato de archivo MD2
  • Desarrollar una clase CMD2MODEL
  • Leer y guardar un modelo MD2
  • Mostrarlo en la pantalla
  • Animación
Además, el código fuente es totalmente gratis y se pueden descargar al final de este documento.

Antes de empezar me gustaría decir que estoy asumiendo que estas familiarizado con C++ y la API de OpenGL. Ok, empecemos con algo de teoría sobre el formato MD2.

El formato de archivo MD2
Al igual que la mayoría de formatos de archivo, el MD2 se compone de dos cosas: un encabezado de archivo y los datos. La cabecera contiene algunas variables muy importantes que se utilizan cuando se carga el archivo como el tamaño de los datos a cargar, un número mágico o la versión del formato, por lo que su tamaño debe ser siempre el mismo. Es por eso que en general, la cabecera es una estructura. En oposición, el tamaño de los datos puede variar de un archivo a otro, y contiene múltiples estructuras para vértices, triángulos, las coordenadas de textura, etc. La figura 1 representa la arquitectura de archivo:


Definición de la estructura de cabecera del MD2 (llamado md2_t):

// md2 header
typedef struct
{
    int     ident;              // magic number. must be equal to "IDP2"
    int     version;            // md2 version. must be equal to 8

    int     skinwidth;          // width of the texture
    int     skinheight;         // height of the texture
    int     framesize;          // size of one frame in bytes

    int     num_skins;          // number of textures
    int     num_xyz;            // number of vertices
    int     num_st;             // number of texture coordinates
    int     num_tris;           // number of triangles
    int     num_glcmds;         // number of opengl commands
    int     num_frames;         // total number of frames

    int     ofs_skins;          // offset to skin names (64 bytes each)
    int     ofs_st;             // offset to s-t texture coordinates
    int     ofs_tris;           // offset to triangles
    int     ofs_frames;         // offset to frame data
    int     ofs_glcmds;         // offset to opengl commands
    int     ofs_end;            // offset to end of file

} md2_t;

Ok, voy a explicar brevemente todas estas variables.

En primer lugar tiene lo que se denomina un "número mágico". Al cargar el archivo en la memoria, comprueba este valor y asegúrate de que es igual a "IPD2". Si no es igual a "IPD2", entonces puedes cerrar el archivo y detener la carga ya que no es un archivo MD2. La siguiente variable indica la versión del archivo y debe ser igual a 8.

Después tenemos las dimensiones de la textura (el ancho y la altura respectivamente). No vamos a utilizar estas variables porque la textura de el modelo MD2 se almacena en otro archivo, la mayoría de las veces en un archivo PCX o TGA, y obtendremos las dimensiones de la textura de estos archivos.

framesize especifica el tamaño en bytes de cada fotograma. Sí, pero ¿qué diablos es un fotograma? Un fotograma es como una imagen en una película. Repitiendo fotogramas a una cierta velocidad se obtiene una animación. Así que un fotograma guarda los vértices y triángulos en una posición particular. Un archivo MD2 clásico esta compuesto por 199 fotogramas distribuidos en 21 animaciones1. Cada fotograma contiene una lista de vértices para todos los triángulos del fotograma (cada fotograma tiene la misma cantidad de triángulos). Por el momento, recuerda que vamos a necesitar esta variable para saber cuanta memoria hay que asignar para el almacenamiento de cada fotograma.

Las siguientes variables son muy similares.

num_skins indica el número de texturas disponibles para este modelo. Por ejemplo, usted puede tener una textura para el equipo rojo y otra para el equipo azul en un juego de equipo. El nombre de cada textura se almacena en una arreglo de 64 bytes en ofs_skins en el archivo. Sin embargo, no vamos a utilizar estos nombres porque son específicos para el directorio de Quake2, por ejemplo: “player/ogro/igdosh.pcx”.

num_xyz es la cantidad total de vértices del modelo. Que corresponden a la suma del número de vértices en cada fotograma.

num_st es la cantidad de coordenadas de textura que se almacenan en el archivo en el ofs_st. Ten en cuenta que este número no es necesariamente igual al número de vértices. En nuestro código se utilizará otra manera de obtener estas coordenadas de texturas en tiempo real, así que no tendrás que cargar el arreglo de coordenadas de textura desde el archivo.

num_tris nos da la cantidad total de triángulos en el modelo.

num_glcmds es el número de comandos de OpenGL. La lista de comandos GL es una matriz de enteros que nos permite hacer que el modelo use tiras de triángulo o un "ventilador" de triángulos  (GL_TRIANGLE_STRIP y GL_TRIANGLE_FAN), en lugar de los triángulos clásicos (GL_TRIANGLES).Los comandos GL son muy poderosos. Es fácil obtener una representación de 10 o 15 cuadros por segundo más rápido!2

Por último, esta num_frames. Especifica el número total de fotogramas que contiene el modelo. De hecho, cada uno de ellos es referido como fotogramas clave, que son cuadros tomados a intervalos de tiempo discreto, ya que sería imposible mantener unos 200 o 300 fotogramas por animación! Por consiguiente, sólo nos quedamos con algunos de estos para cada animación y vamos a calcular todos los cuadros intermedios que necesitaremos cuando la rendericemos, mediante interpolación lineal (lo explicaré más adelante). Mira la figura 2 para ver un ejemplo. Aquí está representado un modelo simplista con una animación que necesita unos 20 fotogramas para mostrarse completamente, pero sólo 5 de estos se mantienen.Los fotogramas del número 1 al 4, 6 al 9, 11 al 14 y del 16 al 19 deben ser calculados antes del render para conseguir una fluida animación.



El último bloque de variables en la cabecera contiene las compensaciones para el acceso a diferentes tipos de datos modelo. ofs_skins: para los nombres de la textura del modelo, ofs_st: para coordenadas de textura, ofs_tris: para los vértices, ofs_frames: el primer fotograma de la modelo, ofs_glcmds: la lista de comandos de OpenGL y por supuesto, ofs_end: que le indica el final del archivo (no lo vamos a necesitar).

¡Sí, hemos terminado con el encabezado! Ahora echemos un vistazo a las estructuras necesarias para almacenar los datos del modelo. Como en el encabezado, vamos a utilizar las estructuras para mantener los fotogramas,los vértices y los comandos de OpenGL.

El primer tipo de datos, muy útil en la mayoría de las aplicaciones 3D, es el vector. No necesitamos una clase Vector complicada así que voy a mantener las cosas simples: una simple matriz de 3 float representa un vector.

typedef float vec3_t[3];

Cada modelo está compuesto de (num_frame * num_xyz) vértices. Esta es la estructura que tienen un solo vértice:

// vertex
typedef struct
{
    unsigned char   v[3];                // compressed vertex (x, y, z) coordinates
    unsigned char   lightnormalindex;    // index to a normal vector for the lighting

} vertex_t;

Pueden haber notado que v[3] contiene el vértice (x, y, z) y por el tipo unsigned char, estas coordenadas sólo puede ir de 0 a 255. De hecho, estas coordenadas 3D están comprimidos (3 bytes en lugar de 12, si queremos usar float o vec3_t). Para descomprimirlo, vamos a utilizar otros datos propios de cada cuadro. lightnormalindex es un índice a una tabla pre calculada de normales. Vectores normalizados serán utilizados para la iluminación.

La última pieza de información necesaria para un vértice son las coordenadas de textura. También están empaquetadas en una estructura:

// texture coordinates
typedef struct
{
    short    s;
    short    t;

} texCoord_t;

Al igual que para los vértices, los datos están comprimidos. Aquí se utiliza un tipo short (2 bytes) en lugar de float (4 bytes) para almacenar las coordenadas de textura. Pero para utilizarlos, tenemos que convertirlos a float debido a que las coordenadas de textura tienen un rango de 0,0 a 1,0, y si seguimos utilizando los valores de short, solamente podríamos tener 0 a 1, y su valor intermedio! Entonces, ¿cómo descomprimirlos? Es muy sencillo. Divide el valor short por el tamaño de la textura:

RealST[i].s = (float)texCoord[i].s / header.skinwidth;
RealST[i].t = (float)texCoord[i].t / header.skinheight;

suponiendo que RealST es un objeto de una estructura similar a texCoord_t pero con float, en lugar del tipo short y texCoord es una matriz de texCoord_t cargada de un archivo MD2.


Cada fotograma (o fotograma clave) del modelo se almacena en una estructura definida así:

// frame
typedef struct
{
    float       scale[3];       // scale values
    float       translate[3];   // translation vector
    char        name[16];       // frame name
    vertex_t    verts[1];       // first vertex of this frame

} frame_t;

Cada fotograma se almacena como una estructura frame_t, manteniendo todos los datos específicos para ese fotograma. Así, un modelo clásico (es decir, un modelo de jugador) cuenta con 199 objetos frame_t. Dije hace un minuto que vamos a descomprimir vértices utilizando los datos del fotograma. Éstos son los datos. Para descomprimir cada vértice, tendremos que escalarlos multiplicando sus coordenadas por los valores de scale[3] y trasladándolos usando el vector translate[3] (también podríamos escribir vec3_t translate en vez de float translate[3]),

name[16] es simplemente el nombre del fotograma. Por último, verts[1] es el primer vértice del fotograma. Otros vértices de este fotograma se almacenan justo después del primer vértice, para que podamos acceder a ellos así:

frame.verts[ 2 ] // get the second vertex of the frame
frame.verts[ i ] // get the i th vertex of the frame
frame.verts[ num_xyz - 1 ] // get the last vertex of this frame

Así obtenemos las coordenadas reales del vértice :

vertex.x = (frame.verts[i].v[0] * frame.scale[0]) + frame.translate[0]
vertex.y = (frame.verts[i].v[1] * frame.scale[1]) + frame.translate[1]
vertex.z = (frame.verts[i].v[2] * frame.scale[2]) + frame.translate[2]

donde i varía de 0 a (num_xyz - 1).

Mira la figura 3 para ver una representación de las relaciones entre las animaciones, los fotogramas y los vértices:


 Así que cada animación contiene n fotogramas que a su vez contienen num_xyz vértices cada uno.

Ahora tenemos que asociar cada vértice con su pareja de coordenadas de textura. Pero en lugar de vincular una vertex_t con una texCoord_t, están vinculados por el triplete para formar un triángulo, o una malla:

// triangle
typedef struct
{
    short   index_xyz[3];    // indexes to triangle's vertices
    short   index_st[3];     // indexes to vertices' texture coorinates

} triangle_t;

Esta es la forma en que se almacenan en el archivo. Tenga en cuenta que index_xyz y index_st son los índices de los datos y no los propios datos. Los datos deben ser almacenados por separado en arreglos vertex_t y texCoord_t o si lo prefiere descomprimirlos durante la carga del modelo, en estructuras similares con tipo float. Suponiendo que Vertices[] es una matriz de vertex_t, TexCoord[] una serie de texCoord_t, Meshes[] una serie de triangle_t y anorms[] una serie de vec3_t que almacena todos los vectores normalizados pre calculados. Usted puede dibujar el modelo usando este método:

glBegin( GL_TRIANGLES );
  // draw each triangle
  for( int i = 0; i < header.num_tris; i++ )
  {
      // draw triangle #i
      for( int j = 0; j < 3; j++ )
      {
          // k is the frame to draw
          // i is the current triangle of the frame
          // j is the current vertex of the triangle

          glTexCoord2f( (float)TexCoord[ Meshes[i].index_st[j] ].s / header.skinwidth,
                        (float)TexCoord[ Meshes[i].index_st[j] ].t / header.skinheight );

          glNormal3fv( anorms[ Vertices[ Meshes[i].index_xyz[j] ].lightnormalindex ] );
        
          glVertex3f( (Vertices[ Meshes[i].index_xyz[j] ].v[0] * frame[k].scale[0]) + frame[k].translate[0],
                      (Vertices[ Meshes[i].index_xyz[j] ].v[1] * frame[k].scale[1]) + frame[k].translate[1],
                      (Vertices[ Meshes[i].index_xyz[j] ].v[2] * frame[k].scale[2]) + frame[k].translate[2] );
      }
  }
glEnd();


Muy bien, esto no es muy fácil de visualizar y este método utiliza GL_TRIANGLES. Podemos conseguir mejores resultados utilizando GL_TRIANGLE_SRTIP y GL_TRIANGLE_FAN. Pero ¿cómo?, usando los comandos OpenGL.

Esto es acerca de estructuras de datos. Ahora puedo mostrarte completamente la arquitectura del archivo:


Desarrollando una clase CMD2Model

Gracias a los comandos OpenGL, no tendrás que utilizar las estructuras triangle_t y texCoord_t, porque todo esto, también está incluido en la lista de comandos OpenGL que vamos a utilizar. He cubierto el caso en que no deseas utilizar los comandos OpenGL por si no deseas hacer uso de OpenGL.

Ahora estamos preparados para desarrollar una clase que representa un modelo de objetos MD2. Aquí está el prototipo:

// ============================================
// CMD2Model - MD2 model class object.
// ============================================

class CMD2Model
{
public:
    // constructor/destructor
    CMD2Model( void );
    ~CMD2Model( void );


    // functions
    bool    LoadModel( const char *filename );
    bool    LoadSkin( const char *filename );

    void    DrawModel( float time );
    void    DrawFrame( int frame );

    void    SetAnim( int type );
    void    ScaleModel( float s ) { m_scale = s; }


private:
    void    Animate( float time );
    void    ProcessLighting( void );
    void    Interpolate( vec3_t *vertlist );
    void    RenderFrame( void );


public:
    // member variables
    static vec3_t   anorms[ NUMVERTEXNORMALS ];
    static float    anorms_dots[ SHADEDOT_QUANT ][256];

    static anim_t   animlist[21];       // animation list


private:
    int             num_frames;         // number of frames
    int             num_xyz;            // number of vertices
    int             num_glcmds;         // number of opengl commands

    vec3_t          *m_vertices;        // vertex array
    int             *m_glcmds;          // opengl command array
    int             *m_lightnormals;    // normal index array

    unsigned int    m_texid;            // texture id
    animState_t     m_anim;             // animation
    float           m_scale;            // scale value

};

Cada modelo MD2 será un objeto CMD2Model. Mmm... esta clase parece bastante extraña sobre todo porque no hay ni objeto frame_t ni objeto vertex_t. ¿Y dónde están las coordenadas de textura almacenadas? algunas explicaciones son necesarias...

En primer lugar tenemos al clásico constructor y destructor que inicializa todas las variables miembro a 0 (con excepción de m_scale) y libera la memoria asignada durante la carga de datos.

¿Y acerca de las funciones? Creo que son auto-explicativas. LoadModel() carga el modelo de un archivo y lo inicializa y LoadSkin() carga la textura y inicializa m_texid.

DrawModel() es la función que usaremos para dibujar el modelo de animación con todas las transformaciónes necesarias. El parámetro de tiempo es necesario para calcular el fotograma a renderizar de la animación actual.

DrawFrame() es la función que usaremos para dibujar el modelo en un fotograma específico.

SetAnim() y ScaleModel() se utilizan para establecer la animación actual y el valor de la escala.

Animate(), ProcessLighting(), Interpolate() y RenderFrame() son funciones privadas, ya que sólo deben ser utilizada dentro de la función publica DrawModel(). Esta procesa todos los cálculos para dibujar el adecuado fotograma interpolado y aligerado.

Ahora las variables miembro. anorms es una arreglo de vectores normalizados pre calculados. Cada vértice tendrá un índice almacenado en el arreglo m_lightnormals para acceder a su propio vector normal. anorms_dots parece anorms, pero esta vez el guarda productos escalares pre calculados. Lo necesitaremos cuando procesemos la luz. animlis es un arreglo de animaciones. Aquí está el prototipo de la estructura de anim_t:

// animation
typedef struct
{
    int     first_frame;            // first frame of the animation
    int     last_frame;             // number of frames
    int     fps;                    // number of frames per second

} anim_t;

Puedes haber notado que estas tres últimas variables  miembro son estáticas. Esto es debido a que son las mismas para todos los modelos MD2, así que sólo se necesita una copia de ellos.

Entonces tenemos num_frames que almacena el número total de fotogramas, num_xyz el número de vértices por cuadro y num_glcmds el número de comandos de OpenGL.

m_vertices guarda coordenadas 3D para cada vértice en número de coma flotante. El  arreglo m_glcmds guarda la lista de comandos OpenGL. Por el momento, no tengas miedo de estos "comandos de OpenGL", piensa que es magia. Te lo explicaré cuando los necesitemos para dibujar la malla del modelo. Para estos tres últimos arreglos, vamos a asignar memoria dinámicamente.

m_texid almacenará el ID de la textura en OpenGL. m_anim almacena información acerca de la animación actual a reproducir. Es un objeto animState_t (consultar los comentarios para una breve descripción):

// animation state
typedef struct
{
    int     startframe;              // first frame
    int     endframe;                // last frame
    int     fps;                     // frame per second for this animation

    float   curr_time;               // current time
    float   old_time;                // old time
    float   interpol;                // percent of interpolation

    int     type;                    // animation type

    int     curr_frame;              // current frame
    int     next_frame;              // next frame

} animState_t;

Por último, m_scale guarda el valor de escala para todos los ejes. Es mejor escalar los vértices multiplicándolos por el valor de m_scale que utilizar glScalef(), porque esta función también escala los vectores normales y esto produce extraños efectos en luz.

He dicho que no vamos a utilizar las estructuras triangle_t ni texCoord_t, pero ¿qué pasa con las estructuras vertex_t y frame_t? Sólo vamos a utilizar estos al cargar el modelo en LoadModel() y transformar datos de fotograma para que sean almacenados en los arreglos m_vertices y m_lightnormals.

Antes de terminar esta sección, quiero darte las definiciones del constructor y destructor:

// ----------------------------------------------
// constructor - reset all data.
// ----------------------------------------------

CMD2Model::CMD2Model( void )
{
    m_vertices      = 0;
    m_glcmds        = 0;
    m_lightnormals  = 0;

    num_frames      = 0;
    num_xyz         = 0;
    num_glcmds      = 0;

    m_texid         = 0;
    m_scale         = 1.0;

    SetAnim( 0 );
}


// ----------------------------------------------
// destructor - free allocated memory.
// ----------------------------------------------

CMD2Model::~CMD2Model( void )
{
    delete [] m_vertices;
    delete [] m_glcmds;
    delete [] m_lightnormals;
}

Para el constructor, pusimos todas las variables miembro (se excluyen las variables estáticas y m_scale) a 0. Inicializamos m_scale a 1,0, porque si queremos ponerlo a 0, no habría nada para dibujar. Para el destructor, sólo limpiamos la memoria...

Ok, estamos listos para empezar realmente! Vamos a pasar a la siguiente sección: cargar un archivo de modelo MD2!

Lectura y almacenamiento de un modelo de MD2

Cargamos un modelo MD2 pasando su nombre de archivo en el parámetro de la funcion LoadModel(). La cual devuelve true si tuvo éxito y falso si algo falla durante la carga. Mira la primera parte de la función:

// ----------------------------------------------
// LoadModel() - load model from file.
// ----------------------------------------------

bool CMD2Model::LoadModel( const char *filename )
{
    std::ifstream   file;           // file stream
    md2_t           header;         // md2 header
    char            *buffer;        // buffer storing frame data
    frame_t         *frame;         // temporary variable
    vec3_t          *ptrverts;      // pointer on m_vertices
    int             *ptrnormals;    // pointer on m_lightnormals


    // try to open filename
    file.open( filename, std::ios::in | std::ios::binary );

    if( file.fail() )
        return false;

    // read header file
    file.read( (char *)&header, sizeof( md2_t ) );


    /////////////////////////////////////////////
    //      verify that this is a MD2 file

    // check for the ident and the version number

    if( (header.ident != MD2_IDENT) && (header.version != MD2_VERSION) )
    {
        // this is not a MD2 model
        file.close();
        return false;
    }

    /////////////////////////////////////////////

En primer lugar definimos algunas variables locales que necesitaremos durante la carga del modelo. file es una secuencia de archivo para extraer los datos del modelo que se encuentran en el archivo. header es un objeto md2_t que almacenará la cabecera del archivo que contiene al modelo. Después tenemos a buffer. Se trata de un buffer de gran tamaño para almacenar todos los datos del fotograma. Las tres últimas variables son diferentes punteros para acceder a los datos guardados en buffer.

Empezamos por tratar de abrir el archivo especificado en modo de sólo lectura y devolver false si falla. Con el archivo abierto, cargamos la cabecera del modelo con lo que podemos comprobar el número mágico (el ident) y la versión del modelo para estar seguros de que es un archivo MD2. La ident debe ser siempre igual a "IDP2" y la versión del modelo a 8. Así que podemos definir MD2_IDENT y MD2_VERSION de esta manera:

// magic number "IDP2" or 844121161
#define MD2_IDENT                (('2'<<24) + ('P'<<16) + ('D'<<8) + 'I')

// model version
#define MD2_VERSION              8

Tenga en cuenta que también puede verificar el número mágico comparando ident con 844121161 o usando la función strcmp() (ident debe entonces ser definido como  char[4]).

Ahora que estamos seguros de que es un archivo válido MD2, podemos continuar cargándolo:

// initialize member variables
    num_frames  = header.num_frames;
    num_xyz     = header.num_xyz;
    num_glcmds  = header.num_glcmds;


    // allocate memory
    m_vertices      = new vec3_t[ num_xyz * num_frames ];
    m_glcmds        = new int[ num_glcmds ];
    m_lightnormals  = new int[ num_xyz * num_frames ];
    buffer          = new char[ num_frames * header.framesize ];


    /////////////////////////////////////////////
    //          reading file data

    // read frame data...
    file.seekg( header.ofs_frames, std::ios::beg );
    file.read( (char *)buffer, num_frames * header.framesize );

    // read opengl commands...
    file.seekg( header.ofs_glcmds, std::ios::beg );
    file.read( (char *)m_glcmds, num_glcmds * sizeof( int ) );

    /////////////////////////////////////////////

Aquí primero debemos inicializar las variables numéricas de la cabecera del modelo. Entonces podremos asignar la memoria necesaria para nuestros m_vertices, m_glcmds, m_lightnormals y el arreglo buffer. Observa que hay un mismo número de elementos para m_vertices y m_lightnormals. Así que podremos tener un índice para un vértice que apunte a ambos, tanto a sus puntos de coordenadas 3D, como a sus normales. Conseguiremos ese puntero del arreglo m_glcmds.

La memoria es asignada para que podamos leer los datos del archivo. Antes de leer los datos, pasamos a la posición especificada por los offset en la cabecera. Sólo leemos los datos para los fotogramas y los comandos OpenGL. Vamos a inicializar m_vertices y m_lightnormals con buffer de esta manera:

 // vertex array initialization
    for( int j = 0; j < num_frames; j++ )
    {
        // adjust pointers
        frame       = (frame_t *)&buffer[ header.framesize * j ];
        ptrverts    = &m_vertices[ num_xyz * j ];
        ptrnormals  = &m_lightnormals[ num_xyz * j ];

        for( int i = 0; i < num_xyz; i++ )
        {
            ptrverts[i][0] = (frame->verts[i].v[0] * frame->scale[0]) + frame->translate[0];
            ptrverts[i][1] = (frame->verts[i].v[1] * frame->scale[1]) + frame->translate[1];
            ptrverts[i][2] = (frame->verts[i].v[2] * frame->scale[2]) + frame->translate[2];

            ptrnormals[i] = frame->verts[i].lightnormalindex;
        }
    }

Esto es lo más difícil de entender. Primero recorremos cada fotograma. Para cada fotograma, extraemos los datos de buffer usando nuestro puntero frame_t* definido al principio de la función. También ajustamos los punteros *m_vertices y *m_lightnormals para que apunten al comienzo de donde se almacenarán los datos del fotograma actual.

Entonces recorremos cada vértice del fotograma actual que estamos procesando. Inicializamos los vértices de coordenadas 3D con la fórmula que se explico antes, en la sección sobre el formato de archivo MD2. También inicializamos el índice de las normales que almacenamos en la estructura vertex_t.

Hemos inicializado nuestras tres variables numéricas y los tres arreglos de datos, así que hemos terminado con el archivo del modelo. ¿Era tan difícil? Ahora cerramos el archivo, liberamos buffer y devolvemos true:

    // free buffer's memory
    delete [] buffer;

    // close the file and return
    file.close();
    return true;
}

Ahora, ¿qué pasa con la textura? Para la textura, sólo tenemos el ID de la textura para almacenar en m_texid. Las texturas de los modelos MD2 se almacenan generalmente en archivos TGA o PCX. Como cargar una textura desde un archivo está fuera del alcance de este artículo, no explicare cómo funciona. Supongo que tienes una función que carga una textura desde un archivo y devuelve un número de ID válido. En el código fuente que puedes descargar, he escrito un simple manegador de textura que puede cargar he inicializar una textura desde un mapa de bits, Targa o PCX. Así es como se carga la textura con la función LoadSkin():

// ----------------------------------------------
// LoadSkin() - load model texture.
// ----------------------------------------------

bool CMD2Model::LoadSkin( const char *filename )
{
    m_texid = LoadTexture( filename );

    return (m_texid != LoadTexture( "default" ));
}

Sólo unas pocas palabras acerca de mi manejador de textura: en primer lugar he escrito una función LoadTexture() en linea para facilitar la lectura del código. Esa función accede a la función LoadTexture() que se encuentra dentro del manejador de texturas. El manejador de texturas en un singleton, cuando es inicializado, el crea una textura por default (la cual es un tablero de ajedres de color blanco y negro). Al cargar una textura desde un archivo, primero se comprueba si la textura ya se ha cargado. En caso afirmativo, devuelve el ID de la textura, si no es así, intentara abrir el archivo y cargarlo. Si la carga falla, o el archivo no existe, devuelve el ID de la textura predeterminada. Así que cuando se llama a texmgr.LoadTexture("default"), este no carga una textura, sino que devuelve el ID de la textura predeterminada. Cuando es devuelto, se comprueba el ID de la textura que esta función nos paso al cargar nuestra textura y devolvemos false si es igual al ID de la  textura predeterminada.

Esto es todo por esta sección. Hemos cargado todos los datos que necesitamos.

Dibujando el modelo

Es hora de renderear el modelo que hemos cargado!

La principal función del modelo para el dibujo es DrawModel(). Sin embargo, esta función no dibuja directamente el modelo, sino que procesa algunas transformaciones y cálculos antes de llamar a la función RenderFrame(). Echemos un vistazo a la definición de la función:

// ----------------------------------------------
// DrawModel() - draw the model.
// ----------------------------------------------

void CMD2Model::DrawModel( float time )
{
    glPushMatrix();
        // rotate the model
        glRotatef( -90.0, 1.0, 0.0, 0.0 );
        glRotatef( -90.0, 0.0, 0.0, 1.0 );

        // render it on the screen
        RenderFrame();
    glPopMatrix();
}

Ok, hay sólo dos simples rotaciones antes de dibujar y por el momento, el parámetro time no se usa ...pero vamos a actualizar esta función más tarde, cuando animemos. Tenemos que girar el modelo en los ejes X y Z, porque no se almacena usando los ejes de OpenGL. Puedes comentar las dos llamadas a glBegin() para ver por qué hacemos esto :-)

Recuerdas el valor m_scale y la función ScaleModel() de que hablé antes. Para evitar tener un modelo enorme en la pantalla una vez terminado el render, se escala cada uno de los vértices del fotograma actual que se esta rendereando. La operación es llevada a cabo por la funcion Interpolate() llamada por RenderFrame(). La interpolacion de los vertices normalizados no tienen nada que hacer con la escala, porque por el momento no se esta animando, la funcion Interpolate() sólo escala vértices. Más tarde vamos a reescribirla para interpolar los vértices de dos fotogramas. Aquí está el código:

// ----------------------------------------------
// Interpolate() - interpolate and scale vertices
// from the current and the next frame.
// ----------------------------------------------

void CMD2Model::Interpolate( vec3_t *vertlist )
{
    for( int i = 0; i < num_xyz ; i++ )
    {
        vertlist[i][0] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][0] * m_scale;
        vertlist[i][1] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][1] * m_scale;
        vertlist[i][2] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][2] * m_scale;
    }
}

Esta función inicializa un arreglo de vértices del fotograma actual con los vértices ya escalados. Así que la función RenderFrame() utilizará el arreglo pasado en el parámetro para dibujar y no utilizar así el arreglo original m_vertices directamente. También será más fácil de manipular vertlist que m_vertices.

Ahora me gustaría hablar un poco de iluminación. Hay dos caminos para iluminar el modelo. La primera manera es usando las funciones de iluminación de OpenGL. Para ello, sólo tenemos que establecer la normal de cada vértice que estamos dibujando. No hay ninguna dificultad, el índice se almacenado en m_lightnormals nos da una  normal precalculada de la tabla anorms.

La segunda forma de iluminar el modelo es utilizando glColor() para cada vértice para "falsear" la iluminación y el sombreado. Esta es la forma utilizada en el motor de Quake II. Para este método, hay cierto trabajo que hacer. Así que vamos a poner todo eso en la función ProcessLighting(), llamada por RenderFrame() igual que la función Interpolate(). Pero antes, tenemos que crear algunas variables globales e inicializar otras...

// number of precalculated normals
#define NUMVERTEXNORMALS        162

// number of precalculated dot product results (for lighting)
#define SHADEDOT_QUANT          16

// precalculated normal vectors
vec3_t   CMD2Model::anorms[ NUMVERTEXNORMALS ] = {
#include    "anorms.h"
};

// precalculated dot product results
float    CMD2Model::anorms_dots[ SHADEDOT_QUANT ][256] = {
#include    "anormtab.h"
};

static float    *shadedots = CMD2Model::anorms_dots[0];
static vec3_t   lcolor;

/////////////////////////////////////////////////

vec3_t          g_lightcolor    = { 1.0, 1.0, 1.0 };
int             g_ambientlight  = 32;
float           g_shadelight    = 128;
float           g_angle         = 0.0;

/////////////////////////////////////////////////

La lista de resultados de las normales precalculadas y del producto escalar son dos grandes y no muy interesantes cosas de ver, por eso se almacenan en archivos de cabezera que simplemente se incluyen para que inicializen los arreglos estáticos.

shadedots es un puntero que es ajustado en la función ProcessLighting(). El apuntara a un elemento del arreglo anorms_dots.

lcolor almacenará los valores RGB del color de la luz final.

Por último, las tres últimas variables globales para el valor de luz ambiental (que van desde 0 a 255), el valor del sombreado (de 0 a 255) y el ángulo desde donde la luz proviene (0,0 a 360,0).

Aquí está la definición de la función ProcessLighting():

// ----------------------------------------------
// ProcessLighting() - process all lighting calculus.
// ----------------------------------------------

void CMD2Model::ProcessLighting( void )
{
    float lightvar = (float)((g_shadelight + g_ambientlight)/256.0);

    lcolor[0] = g_lightcolor[0] * lightvar;
    lcolor[1] = g_lightcolor[1] * lightvar;
    lcolor[2] = g_lightcolor[2] * lightvar;

    shadedots = anorms_dots[ ((int)(g_angle * (SHADEDOT_QUANT / 360.0))) & (SHADEDOT_QUANT - 1) ];
}

Primero creamos una variable local que vamos a utilizar para inicializar el color de la luz final (lcolor) y luego ajustar el puntero shadedots. La fórmula es bastante bizarra, pero no te preocupes, funciona bien, y eso es todo lo que queremos ;-). Viene desde el código fuente del Quake II.

¡Ahora a dibujar cada triángulo! Recuerda que al principio de este documento mostre una parte del código para dibujar cada triángulo del fotograma actual. Lo malo es que estábamos utilizando GL_TRIANGLES para dibujar, y para eso necesitamos especificar tres vértices por triángulo. Por otra parte, es más lento que dibujarlos usando GL_TRIANGLE_STRIP o GL_TRIANGLE_FAN por que necesitan menos vértices para dibujar más triángulos. La Figura 5 muestra la idea:


Lo mejor sería que pudiéramos dibujar todo el modelo utilizando GL_TRIANGLE_STRIP y GL_TRIANGLE_FAN. Esto es para lo que están echos los comandos gl. La lista de comandos OpenGL es un conjunto particular de números enteros. Vamos a inicializar un apuntador que apunta al principio de la lista y leer cada comando hasta que el puntero devuelva 0. 0 es el último valor en la lista de comandos de OpenGL. Ahora, ¿cómo es que funciona?

  • Se leen el primer valor. Este valor indica dos cosas: el tipo de triángulo a dibujar (GL_TRIANGLE_STRIP si el número es positivo y GL_TRIANGLE_FAN si es negativo) y el número n de vértices a dibujar para este modo de dibujo.
  • Los n * 3 siguientes valores almacenan información sobre los vértices a dibujar.
  • Los dos primeros son (s, t) las coordenadas de textura y el tercero es el índice del vértice a dibujar.
  • Una vez que todos los vértices de este grupo son procesados, se lee un nuevo valor para obtener un nuevo grupo... Si el valor leído es 0, ya esta echo.
No es muy simple la primera vez, pero con un poco de práctica verás que en realidad es bastante simple. Mira la figura 6 para una representación de la lista de comandos OpenGL (cada rectángulo representa un comando que es un valor entero):


Ok, he terminado con la teoría. Ahora el código:

// ----------------------------------------------
// RenderFrame() - draw the current model frame
// using OpenGL commands.
// ----------------------------------------------

void CMD2Model::RenderFrame( void )
{
    static vec3_t    vertlist[ MAX_MD2_VERTS ];  // interpolated vertices
    int              *ptricmds = m_glcmds;       // pointer on gl commands


    // reverse the orientation of front-facing
    // polygons because gl command list's triangles
    // have clockwise winding
    glPushAttrib( GL_POLYGON_BIT );
    glFrontFace( GL_CW );

    // enable backface culling
    glEnable( GL_CULL_FACE );
    glCullFace( GL_BACK );


    // process lighting
    ProcessLighting();

    // interpolate
    Interpolate( vertlist );

    // bind model's texture
    glBindTexture( GL_TEXTURE_2D, m_texid );


    // draw each triangle!
    while( int i = *(ptricmds++) )
    {
        if( i < 0 )
        {
            glBegin( GL_TRIANGLE_FAN );
            i = -i;
        }
        else
        {
            glBegin( GL_TRIANGLE_STRIP );
        }


        for( /* nothing */; i > 0; i--, ptricmds += 3 )
        {
            // ptricmds[0] : texture coordinate s
            // ptricmds[1] : texture coordinate t
            // ptricmds[2] : vertex index to render

            float l = shadedots[ m_lightnormals[ ptricmds[2] ] ];

            // set the lighting color
            glColor3f( l * lcolor[0], l * lcolor[1], l * lcolor[2] );

            // parse texture coordinates
            glTexCoord2f( ((float *)ptricmds)[0], ((float *)ptricmds)[1] );

            // parse triangle's normal (for the lighting)
            // >>> only needed if using OpenGL lighting
            glNormal3fv( anorms[ m_lightnormals[ ptricmds[2] ] ] );

            // draw the vertex
            glVertex3fv( vertlist[ ptricmds[2] ] );
        }

        glEnd();
    }

    glDisable( GL_CULL_FACE );
    glPopAttrib();
}

Empezamos creando dos variables locales. vertlist[] es un arreglo de cordenadas 3D de punto flotante, que contiene los vértices interpolados y escalado del fotograma a dibujar. El arreglo es estático por lo que es declarado sólo una vez. Es mejor para el rendimiento que crear una nueva matriz en cada llamada de esta función. El tamaño del arreglo es constante y es el número máximo de vértices que un modelo puede tener.

La segunda variable es ptricmds. Es el puntero que leerá los comandos OpenGL.

Luego guardamos los atributos del polígono, en una orientación inversa de polígonos orientados hacia delante debido a los comandos GL y habilitamos el backface culling. Procesamos todos los cálculos necesarios para la iluminación,  interpolamos y escalamos los vértices, y asignamos la textura al modelo.

Todo el renderizado se hace en la sentencia while. En primer lugar tenemos el tipo de triángulo y el número de vértices para dibujar. En la declaración de for analizamos cada vértice. Debido a que cada vértice tiene 3 valores almacenados en la lista de comandos GL, por eso incrementamos el puntero por 3 cuando se procesan todos los vértices del grupo.

Para cada vértice, especificamos el color de iluminación, utilizando el puntero a la tabla que contiene los resultados del producto escalar para el ángulo de la luz y el color de la iluminación final calculado por la función ProcessLighting(). Las coordenadas de texturas son asignadas desde int a float. Obtenemos el vector normal de la tabla anorms y rendereamos el vértice del arreglo que inicializamos antes.

Observe que si usted no utiliza la iluminación de OpenGL, la llamada a glNormal3fv() no hará nada y si la usa, la llamada a glColor3f() no afectara nada.

Animar

Los modelos 3D se ven mejor cuando están animados. Así que vamos a animarlos.

Recuerda el arreglo estático animlist. Ha sido diseñado para almacenar todos los datos mínimos de una animación, es decir, el índice del primer y ultimo fotograma, y la cantidad de fps para el funcionamiento de la animación. Todo esto es reagrupado en una estructura anim_t que ya hemos visto antes. Aquí está la inicialización:

// ----------------------------------------------
// initialize the 21 MD2 model animations.
// ----------------------------------------------

anim_t CMD2Model::animlist[ 21 ] = 
{
    // first, last, fps

    {   0,  39,  9 },   // STAND
    {  40,  45, 10 },   // RUN
    {  46,  53, 10 },   // ATTACK
    {  54,  57,  7 },   // PAIN_A
    {  58,  61,  7 },   // PAIN_B
    {  62,  65,  7 },   // PAIN_C
    {  66,  71,  7 },   // JUMP
    {  72,  83,  7 },   // FLIP
    {  84,  94,  7 },   // SALUTE
    {  95, 111, 10 },   // FALLBACK
    { 112, 122,  7 },   // WAVE
    { 123, 134,  6 },   // POINT
    { 135, 153, 10 },   // CROUCH_STAND
    { 154, 159,  7 },   // CROUCH_WALK
    { 160, 168, 10 },   // CROUCH_ATTACK
    { 196, 172,  7 },   // CROUCH_PAIN
    { 173, 177,  5 },   // CROUCH_DEATH
    { 178, 183,  7 },   // DEATH_FALLBACK
    { 184, 189,  7 },   // DEATH_FALLFORWARD
    { 190, 197,  7 },   // DEATH_FALLBACKSLOW
    { 198, 198,  5 },   // BOOM
};

Vamos a utilizar un índice para acceder a los datos de animación, pero es mejor definir una macro para cada índice para facilitar la lectura del código fuente:

// animation list
typedef enum {
    STAND,
    RUN,
    ATTACK,
    PAIN_A,
    PAIN_B,
    PAIN_C,
    JUMP,
    FLIP,
    SALUTE,
    FALLBACK,
    WAVE,
    POINT,
    CROUCH_STAND,
    CROUCH_WALK,
    CROUCH_ATTACK,
    CROUCH_PAIN,
    CROUCH_DEATH, 
    DEATH_FALLBACK,
    DEATH_FALLFORWARD,
    DEATH_FALLBACKSLOW,
    BOOM,

    MAX_ANIMATIONS

} animType_t;


Los datos de la animación actual se almacena en la variable m_anim pero es un poco diferente de la estructura anim_t. Así que para establecer una animación debemos recuperar los datos de la animación e inicializar los datos de la animación actual con ellos. Este es el trabajo de la función SetAnim():

// ----------------------------------------------
// SetAnim() - initialize m_anim from the specified
// animation.
// ----------------------------------------------

void CMD2Model::SetAnim( int type )
{
    if( (type < 0) || (type > MAX_ANIMATIONS) )
        type = 0;

    m_anim.startframe   = animlist[ type ].first_frame;
    m_anim.endframe     = animlist[ type ].last_frame;
    m_anim.next_frame   = animlist[ type ].first_frame + 1;
    m_anim.fps          = animlist[ type ].fps;
    m_anim.type         = type;
}

Primero comprobamos si el tipo es valido y luego inicializamos las variables miembros de m_anim. Usted puede pasar a type cualquier macro definida anteriormente.

Ahora verás una nueva función: Animate(). Esta función se llama en la función DrawModel(), por lo que debemos reescribirla:

// ----------------------------------------------
// DrawModel() - draw the model.
// ----------------------------------------------

void CMD2Model::DrawModel( float time )
{
    // animate. calculate current frame and next frame
    if( time > 0.0 )
        Animate( time );

    glPushMatrix();
        // rotate the model
        glRotatef( -90.0, 1.0, 0.0, 0.0 );
        glRotatef( -90.0, 0.0, 0.0, 1.0 );

        // render it on the screen
        RenderFrame();
    glPopMatrix();
}

Aquí animaremos sólo si el tiempo es mayor que 0,0. De lo contrario no hay animación, el modelo es estático. Mira el código fuente de la función Animate():

// ----------------------------------------------
// Animate() - calculate the current frame, next
// frame and interpolation percent.
// ----------------------------------------------

void CMD2Model::Animate( float time )
{
    m_anim.curr_time = time;

    // calculate current and next frames
    if( m_anim.curr_time - m_anim.old_time > (1.0 / m_anim.fps) )
    {
        m_anim.curr_frame = m_anim.next_frame;
        m_anim.next_frame++;

        if( m_anim.next_frame > m_anim.endframe )
            m_anim.next_frame = m_anim.startframe;

        m_anim.old_time = m_anim.curr_time;
    }

    // prevent having a current/next frame greater
    // than the total number of frames...
    if( m_anim.curr_frame > (num_frames - 1) )
        m_anim.curr_frame = 0;

    if( m_anim.next_frame > (num_frames - 1) )
        m_anim.next_frame = 0;

    m_anim.interpol = m_anim.fps * (m_anim.curr_time - m_anim.old_time);
}

En la primera vez, la función calcula el primer y siguiente fotograma usando la cantidad de fps especificados en la animación actual. Para la segunda vez, comprueba esos valores y verifica que sean correctos (no pueden ser mayor al número total de fotogramas que contiene el modelo). Por último, la interpolación porcentual es calculada a partir de la cantidad de fps y del tiempo.

Ahora debemos revisar nuestra función Interpolate(), esta vez para realmente interpolar vértices. De lo contrario, tendríamos una animación muy pobre, debido al número de fotogramas que el modelo puede guardar. Con la interpolación, podemos crear un "infinidad" de fotogramas (que crearemos cuando necesitemos dibujar). La fórmula es muy simple:

Xinterpolated = Xinital+ InterpolationPercent * (Xfinal - Xinital)

Así que vamos a interpolar todos los vértices del actual y del próximo fotograma. La nueva función Interpolate() se vera así:

// ----------------------------------------------
// Interpolate() - interpolate and scale vertices
// from the current and the next frame.
// ----------------------------------------------

void CMD2Model::Interpolate( vec3_t *vertlist )
{
    vec3_t  *curr_v;    // pointeur to current frame vertices
    vec3_t  *next_v;    // pointeur to next frame vertices

    // create current frame and next frame's vertex list
    // from the whole vertex list
    curr_v = &m_vertices[ num_xyz * m_anim.curr_frame ];
    next_v = &m_vertices[ num_xyz * m_anim.next_frame ];

    // interpolate and scale vertices to avoid ugly animation
    for( int i = 0; i < num_xyz ; i++ )
    {
        vertlist[i][0] = (curr_v[i][0] + m_anim.interpol * (next_v[i][0] - curr_v[i][0])) * m_scale;
        vertlist[i][1] = (curr_v[i][1] + m_anim.interpol * (next_v[i][1] - curr_v[i][1])) * m_scale;
        vertlist[i][2] = (curr_v[i][2] + m_anim.interpol * (next_v[i][2] - curr_v[i][2])) * m_scale;
    }
}

Por cierto, estamos escalando vértices interpolados... Y eso es todo! Sólo tienes que llamar una vez a las funciones SetAnim() y ScaleModel() con el parámetro de tu elección, y a DrawModel() con el tiempo actual en segundos en el parámetro durante el bucle de dibujo. Eso no es tan malo.

Justo antes de terminar, quiero mostrarte cómo hacer para dibujar un simple fotograma, en caso de que lo necesites (por ejemplo: para dibujar una estatua):

// ----------------------------------------------
// RenderFrame() - draw one frame of the model
// using gl commands.
// ----------------------------------------------

void CMD2Model::DrawFrame( int frame )
{
    // set new animation parameters...
    m_anim.startframe   = frame;
    m_anim.endframe     = frame;
    m_anim.next_frame   = frame;
    m_anim.fps          = 1;
    m_anim.type         = -1;

    // draw the model
    DrawModel( 1.0 );
}


Esta función ajustara las variables de la animación antes de llamar a DrawModel() quien dibujara el fotograma específico.

Conclusión

Aquí estamos, finalmente se terminó! :-)

Este artículo está lejos de ser perfecto y puede ser ampliamente mejorado como por ejemplo: soportando múltiples texturas o separando los datos del modelo (lista de vértices, lista de normales...) de los parámetros del modelo (fotograma actual, animación actual...) para evitar el almacenamiento de los mismo datos del modelo varias veces cuando más de una entidad es dibujada por el mismo modelo... Es difícil crear una clase CMD2Model perfecta que pueda funcionar en cualquier programa con un simple cortar y pegar ...

Espero que este artículo te aya ayudado a aprender acerca del modelo de formato de los archivos MD2 y más en general acerca de los archivos de modelos 3D. También espero que no sea demasiado confuso. Por favor no spameen mi buzón de correo sobre mi Inglés, no es mi lengua materna. De lo contrario, puede ponerse en contacto conmigo en tfc_dukeNOSPAMclub-internet.fr para cualquier cosa que quieran decir sobre este artículo (sugerencias, errores, ...).

Usted puede descargar el código fuente (Visual C + + versión 6.0) y los binarios con un modelo y su arma. El código fuente de este artículo es gratuito y se proporciona sin garantía expresa o implícita. Utilicelo a su propio riesgo. Download: q2md2_us.zip.

NOTA: el código de mi cargador de MD2 ha sido completamente reescrito desde que publiqué este artículo (mejor código C++). Puede descargar la última versión: md2loader.zip.

Gracias a Squintik (squintikNOSPAMwanadoo.fr) de Game-Lab, quien me ayudo para la versión en Inglés de este documento.

screenshot 2
screenshot 3
screenshot 1

Recursos


1 - Estas restricciones son relativas ya que con el código fuente puedes modificarlas, pero ten en cuenta que la aplicación en donde crees el archivo MD2 puede tener estas limitaciones por compatibilidad.
2 - En las nuevas generaciones de aceleradoras y drivers este incremento del rendimiento casi no existe hasta el punto en que no hay casi diferencia entre los tres modo expuestos.

Ultima revisión: 25/03/2010

No hay comentarios.: