ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4396111
Jetzt (Chat):
23 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - KurzartikelDruckversion

Characteranimation mit DirectX 8.0
Der Kurzartikel von Johannes Leimbach erklärt den Import und die Animation von MD2-Models.

© Copyright 2001 [whole tutorial] by Johannes Leimbach
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


oder Ein Schritt näher an Quake
"There is no spoon" -Neo, The Matrix


Kurz:

Inhalt : Importieren und animieren von Quake2 MD2 Models
Zielgruppe : Fortgeschrittene Programmierer in Direct3D8
Download :
Demoprojekt


Inhalt:

  1. Einleitung

  2. Das MD2-Format

  3. Die Durchführung

  4. Die Wrapper Klasse

  5. Wie klappt das ?

  6. Wie gehts jetzt weiter ?

  7. Schlusswort

  8. Links

  9. Download Demoprojekt (ca. 300 KB)

 

Einleitung

Wem ist nicht auch das gleiche passiert -kaum hat man Stefan Zerbst's Tutorial gelesen (und verstanden), schon plant man den nächsten großen Spielehit mit riesigen Außenwelten, genialen Texturen, einer tollen KI und furchteinflößenden Monstern.

Und Monstern!

Wenn es dir so ging wie mir, wirst du an diesem Punkt wohl das erste große Problem gehabt haben. Es ist gar nicht so leicht animierte Objekte in ein Spiel einzubinden. Wer sich in der DirectX Doku das entsprechende Beispiel angeguckt hat (Skinned Mesh)
wird wohl von der Komplexität erstaunt sich kleinere Ziele gesetzt haben....

Aber es geht auch einfacher als in dem DirectX Beispiel.

Viel einfacher.

Sehr viel einfacher:

Die Lösung liefert Quake, Quake II um genauer zu sein. Quake's Spielermodels liegen im MD2 Fomat vor, und diese können ohne größere Probleme in das eigene Spiel importiert werden ! (Juchu)

 

Das MD2 Format

Eine MD2-Datei besteht -vereinfacht gesagt -aus einer großen Liste von Dreiecken, die zusammengesetzt dann die Animationen ergeben. Das MD2-Format ist schon ein bisschen älter, die größte Schwachstelle ist wohl das es nicht "skelettbasiert" ist, wie es bei z.B. den Half-Life Modellen der Fall ist.

Der Nachteil liegt auf der Hand -ein nicht skelettbasiertes Objekt hat weniger Bewegungsgrade. Als Beispiel sei da nur die Möglichkeit erwähnt den Kopf in alle Richtungen zu drehen - kein Problem mit Half-Life -bei Quake II nur mit _sehr_ viel Nacharbeit möglich.

Der Vorteil des MD2 Formates ist jedoch die Einfachkeit, es kann ohne größere Probleme in jedes Spiel integriert und benutzt werden. Außerdem gibt es sehr viele Internetpages die MD2 Models aller Art zum Download anbieten -mehr dazu in der Linksection am Schluss.

Genug gelabert, jetzt werden wir uns einen eigenen MD2 Konverter programmieren !

 

Die Durchführung

Man braucht ziemlich viele Structs um ein Quake Model zu importieren :

//HINWEIS :
//Ich werde auf eine großteil der Structs nicht näher eingehen ->
//sie werden nur _einmal_ zum Laden verwendet.
//Gemäß Stefan's Motto : Man muss nicht alles Wissen, man muss nur Wissen wo es steht :-)

struct make_index_list
{
        int a, b, c;
        float a_s, a_t, b_s, b_t, c_s, c_t;
};

struct make_vertex_list
{
        float x, y, z;
};

struct make_frame_list
{
        make_vertex_list *vertex;
};

struct vec3_t
{
         float v[3];
};

struct dstvert_t
{
         short s, t;
};

struct dtriangle_t
{
         short index_xyz[3];
         short index_st[3];
};

struct dtrivertx_t
{
         BYTE v[3];
         BYTE lightnormalindex;
};

struct daliasframe_t
{
         float scale[3];
         float translate[3];
         char name[16];
         dtrivertx_t verts[1];
};

struct SMD2Header
{
         int ident;
         int version;
         int skinwidth;
         int skinheight;
         int framesize;
         int num_skins;
         int num_xyz;                   //Anzahl der Vertice
         int num_st;
         int num_tris;                   //Anzahl der Dreiecke
         int num_glcmds;
         int num_frames;             //Anzahl der Frames(=>Animationen) in der Datei)
         int ofs_skins;
         int ofs_st;
         int ofs_tris;
         int ofs_frames;
         int ofs_glcmds;
         int ofs_end;
} ;

 

struct trivert_t
{
         vec3_t v;
         int lightnormalindex;
};

struct frame_t
{
         vec3_t mins, maxs;
         char name[16];
         trivert_t v[MAX_VERTS];
} ;

// Der Modelvertex, wie wir ihn für D3D verwenden
struct MODELVERTEX
{
         D3DXVECTOR3 m_vecPos; //Position
         D3DCOLOR m_dwDiffuse; //Farbe
         D3DXVECTOR2 m_vecTex; //texturkoordinaten
} ;

//Den Vertex Direct3D bekannt machen
#define D3DFVF_MODELVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 | D3DFVF_TEXCOORDSIZE2(0) )

struct SMesh
{
         //std::vector ist in der Datei "vector" deklariert, und ist ein komfortabler Wrapper
         //für dynamische Arrays

         std::vector< MODELVERTEX> vertex;
};

 

Ganz schön viel, was??

Ich werde die Funktion der wichtigsten Structs kurz erläutern :

 

daliasframe_t :

Diese Struct dient zum identifizieren einer Animation Die einzige für uns wichtige Variable ist "name", sie wird für jeden Frame
mit dem Namen der Animation gefüllt, also z.B. "run1", oder "attack2". Mehr dazu unten

 

SMD2Header:

Hier stehen _alle_ Daten für des Models, viele sind unwichtig, die wichtigsten habe ich schon oben erklärt :-)
Diese Struktur wird zuerst aus der Datei gelesen

 

MODELVERTEX:

Mit diesem Vertex werden wir das Model zeichnen.

 

SMesh :

Das ist das "Herz" unseres Quake II Models. Hier werden nacher _alle_ Vertexdaten gespeichert.

std::vector ist ein dynamisches Array aus der Standard Template Library.

 

Die Wrapper Klasse

Noch kann man natürlich noch kein MD2 Model laden. Das werden wir jetzt nachholen! Ran an den Code...

//Dies sind die Beschränkungen von MD2-Models
//Sie sollten für normale Zwecke ausreichen

const int MAX_TRIANGLES = 4096;
const int MAX_VERTS = 2048;
const int MAX_FRAMES = 512;
const int MAX_MD2SKINS = 32;
const int MAX_SKINNAME = 64;


//So heißt unsere Wrapperklasse
class CMD2Model
{
//Private heißt dass _nur innnerhalb_ der Klasse auf
//diese Elemente zugegriffen werden kann

private :
         //Wird nur einmal beim Laden verwendet
         
make_index_list*                 m_index_list;
         make_frame_list*                m_frame_list;

         //Anzahl der Frames, Vertice und Triangles
         long                                     m_frames, m_vertices, m_triangles;
         //Hier sind die Vertexdaten gespeichert
         SMesh                                m_data [MAX_FRAMES];
         //Wird intern von Load aufgerufen und füllt m_data
         int                                        Init();

//Public bedeutet dass jeder auf diese Elemente zugreifen kann
public:
         CMD2Model();
         ~CMD2Model();

         //Lädt die Datei und macht alle nötigen Initialisierungen
         BOOL                            Load (char* );
         //Löscht das Model und macht Speicher frei
         void                                Destroy ();

         //Zeichnet Frame Number frame , mehr dazu unten
         BOOL                            Render (int frame);
         
         //Allgemeine Funktionen
         inline int                            GetFrameCount() { return m_frames; }
         inline int                            GetVertexCount() { return m_vertices; }
         inline int                            GetTriangleCount() { return m_triangles;}
};

 

Jetzt gehts ans Eingemachte, wir schreiben die Funktionen die wir eben deklariert (erstellt) haben

// Konstruktor
CMD2Model::CMD2Model ()
{
         //Alle Werte schön brav auf NULL setzen, damit auch nichts schiefgeht ;-)
          m_index_list = NULL;
          m_frame_list = NULL;
          m_frames = m_vertices = m_triangles = 0;
}

// Destruktor
CMD2Model::~CMD2Model ()
{
          //Wir schreiben schönen Code und geben alles wieder frei damit keine
          //Memoryleaks entstehen

          if( m_frame_list != NULL )
          {
                   for( int i = 0; i < m_frames; i++ )
                  {
                            delete [] m_frame_list[i].vertex;
                            delete [] m_frame_list;
                   }
         }


          if( m_index_list)
                   delete [] m_index_list;
}

//Destroy () macht weitestgehend genau das gleiche wie der Destruktor
//sie kann halt nur jederzeit aufgerufen werden, das ist der Vorteil
void
CMD2Model::Destroy ()
{
          //Wir löschen keine NULL -Pointer
          if( m_frame_list != NULL )
         {
                   for( int i = 0; i < m_frames; i++ )
                   {
                               delete [] m_frame_list[i].vertex;
                               delete [] m_frame_list;
                               m_frame_list = NULL;
                   }
                   if( m_index_list != NULL )
                   {
                             delete [] m_index_list;
                             m_index_list = NULL;
                   }
         }
}

 

//Jetzt kommt die wohl mit Abstand hässlichste Funktion in dem ganzen Tutorial.: Load
//Sie lädt eine MD2 Datei und führt weitere Initialisierungen durch
//OK - Augen zu und durch, machen wir es kurz und schmerzlos !
//--Und hoffen das man _NIE_ gefragt wird was sie jetzt im einzelnen genau macht *g*

int CMD2Model::Load( char *filename )
{
         FILE *modelfile = NULL;
         char g_skins[MAX_MD2SKINS][64];
         dstvert_t base_st[MAX_VERTS];
         BYTE buffer[MAX_VERTS*4+128];
         SMD2Header modelheader;
         dtriangle_t tri;
         daliasframe_t *out;

         //Hier wird die Datei im Binärmodus zum lesen geöffnet,
         //Wenn sie nicht existiert, oder das Laden aus sonst einem
         //Grund fehlschlägt, wird 0 zurückgegeben

         if( (modelfile = fopen (filename, "rb")) == NULL )
                  return 0;

         // Den Header der Datei lesen
         fread( &modelheader, 1, sizeof(SMD2Header), modelfile );
         modelheader.framesize = (int)&((daliasframe_t *)0)->verts[modelheader.num_xyz];

         // Daten aus dem Header
          m_frames = modelheader.num_frames;
          m_vertices = modelheader.num_xyz;
          m_triangles = modelheader.num_tris;
          m_index_list = new make_index_list [modelheader.num_tris];
          m_frame_list = new make_frame_list [modelheader.num_frames];

         for( int i = 0; i < modelheader.num_frames; i++)
         {
                   m_frame_list[i].vertex = new make_vertex_list [modelheader.num_xyz];
                   // Die Skin Informationen lesen
                   fread( g_skins, 1, modelheader.num_skins * MAX_SKINNAME, modelfile );
                   // Die Indizes des Polygonmeshes lesen
                   fread( base_st, 1, modelheader.num_st * sizeof(base_st[0]), modelfile );

                   int max_tex_u = 0, max_tex_v = 0;

                  for( i = 0; i < modelheader.num_tris; i++ )
                  {
                            // Die Vertices lesen
                            fread( &tri, 1, sizeof(dtriangle_t), modelfile);
                            (m_index_list)[i].a = tri.index_xyz[2];
                            (m_index_list)[i].b = tri.index_xyz[1];
                            (m_index_list)[i].c = tri.index_xyz[0];

                            // Texturemapping Koordinaten
                            (m_index_list)[i].a_s = base_st[tri.index_st[2]].s;
                            (m_index_list)[i].a_t = base_st[tri.index_st[2]].t;
                            (m_index_list)[i].b_s = base_st[tri.index_st[1]].s;

                             (m_index_list)[i].b_t = base_st[tri.index_st[1]].t;
                             (m_index_list)[i].c_s = base_st[tri.index_st[0]].s;
                            (m_index_list)[i].c_t = base_st[tri.index_st[0]].t;

                            max_tex_u = max( max_tex_u, base_st[tri.index_st[0]].s );
                            max_tex_u = max( max_tex_u, base_st[tri.index_st[1]].s );
                            max_tex_u = max( max_tex_u, base_st[tri.index_st[2]].s );
                            max_tex_v = max( max_tex_v, base_st[tri.index_st[0]].t );
                            max_tex_v = max( max_tex_v, base_st[tri.index_st[1]].t );
                            max_tex_v = max( max_tex_v, base_st[tri.index_st[2]].t );
                   }

                   // Da in den MD2 Files die Texturekoordinaten auf Pixelbasis (also abhängig von der Originalgröße der Texture)
                  //angegeben werden, werden sie hier umgerechnet

                   for ( i = 0; i < modelheader.num_tris; i++ )
                  {
                            m_index_list[ i ].a_s /= max_tex_u;
                            m_index_list[ i ].b_s /= max_tex_u;
                            m_index_list[ i ].c_s /= max_tex_u;
                            m_index_list[ i ].a_t /= max_tex_v;
                            m_index_list[ i ].b_t /= max_tex_v;
                            m_index_list[ i ].c_t /= max_tex_v;
                   }


                   //g_D3D.m_toolz.FTrace ist eine meiner Hilfsfunktionen.
                   //Sie macht nix anderes als den Text in die Datei "log.log" zu schreiben

                   g_D3D.m_toolz.FTrace ("Animationsnamen für : ");
                   g_D3D.m_toolz.FTrace (filename);
                   g_D3D.m_toolz.FTrace ("\n\n");

                   // Die Vertexdaten aller Animationsframes lesen

                   for( i = 0; i < modelheader.num_frames; i++ )
                   {
                            out = (daliasframe_t *)buffer;
                            fread( out, 1, modelheader.framesize, modelfile );

                            //Wenn dieser Frame einen Namen hat, speichern wir ihn in der Logfile
                            if (out->name)
                           {
                                    
g_D3D.m_toolz.FTrace (out->name);
                                     g_D3D.m_toolz.FTrace ("\n");
                           }

                           for( int j = 0; j < modelheader.num_xyz; j++ )
                           {
                                     (m_frame_list)[i].vertex[j].x = out->verts[j].v[0] * out->scale[0] + out->translate[0];
                                     (m_frame_list)[i].vertex[j].y = out->verts[j].v[1] * out->scale[1] + out->translate[1];
                                     (m_frame_list)[i].vertex[j].z = out->verts[j].v[2] * out->scale[2] + out->translate[2];
                            }
                   }


          fclose (modelfile);
          return Init();
}


//Puh, geschafft. Alle die noch hier sind bitte mal kurz die Hand heben ---Danke, doch noch so viele !
//Dies war wohl der Schwierigste Teil den ganzen Konverters.
//ABER HALT --was ist das :

//Letze Zeile von Load () :
//return Init ();

//Nein -Noch eine Funktion -die ist zwar auch recht happig, aber wer die Load-Funktion
//überstanden hat der wird auch mit Init keine Probleme haben !

//Auf gehts !

int CMD2Model::Init()
{
          // Für jede Animationsphase einen eigenen SMesh benutzen
          for ( int i = 0; i < GetFrameCount(); i++ )
          {
                    MODELVERTEX pVertex;
                    D3DXCOLOR LightColor(1.0f, 1.0f, 1.0f, 1.0f );

                    //Jetzt werden die Vertexdaten in m_data kopiert.
                    //Der geneigte Leser wird vielleicht merken, dass bei den
                    //Zuweisungen y und z vertauscht sind.
                    //Dies ist Absicht, da in der MD2-File diese
                    //Koordinaten vertauscht sind (um es zu präzisieren, Quake
                    //hat ein anderes Koordinatensystem als Direct3D)

                    for( int j = 0; j < GetTriangleCount(); j++)
                    {
                              pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].a].x;
                              pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].a].z;
                              pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].a].y;
                              pVertex.m_vecTex.x = m_index_list[j].a_s;
                              pVertex.m_vecTex.y = m_index_list[j].a_t;
                              pVertex.m_dwDiffuse = LightColor;
                              m_data[i].vertex.push_back (pVertex);

                              pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].b].x;
                              pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].b].z;
                              pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].b].y;
                              pVertex.m_vecTex.x = m_index_list[j].b_s;
                              pVertex.m_vecTex.y = m_index_list[j].b_t;
                              pVertex.m_dwDiffuse = LightColor;
                              m_data[i].vertex.push_back (pVertex);

                              pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].c].x;
                              pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].c].z;
                              pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].c].y;
                              pVertex.m_vecTex.x = m_index_list[j].c_s;
                              pVertex.m_vecTex.y = m_index_list[j].c_t;
                              pVertex.m_dwDiffuse = LightColor;
                              m_data[i].vertex.push_back (pVertex);
                    }
         
}
           //FERTIG !!!
          return 1;
}

 

//Jetzt sind die Initialisierungen endlich vorbei !!!
//Nun kommt nichts mehr schwieriges ! Ehrlich .
//Alles was wir jetzt noch tun müssen, ist das Model zu rendern

BOOL CMD2Model::Render( int frame )
{
          //Wird versucht ein Frame abzuspielen, den es gar nicht gibt ???
          if( frame >= GetFrameCount()-1 )
                    return 0;

          //Den richtigen Vertex Shader anmelden
          //g_D3D.m_lpD3DDevice ist global deklariert und ist ein
          //Pointer auf eine LPDIRECT3DDEVICE8

          g_D3D.m_lpD3DDevice->SetVertexShader (D3DFVF_MODELVERTEX);

           //Zeichnen
          HRESULT h = g_D3D.m_lpD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,               //Typ
                                                                                                      GetTriangleCount (),                           //Anzahl
                                                                                                      (BYTE**)&m_data[frame].vertex[0],//Pointer auf Daten
                                                                                                      sizeof(MODELVERTEX));                //Größe Vertex
          return (SUCCEEDED(h));
}

 

Wie klappt das?

Das ist ganz einfach : So lädt, zeichnet und zerstört man ein MD2 Modell

CMD2Model model;
model.Load ("player.md2");
model.Render (0);
model.Destroy ();

Einfach oder ?

Beim genauen betrachten der Funktion Render sieht man dass man eine Framenummer angeben muss. Welche Funktion hat diese Nummer???

Das ist ganz einfach. Um es besser erläutern zu können, zeige ich hier mal einen Ausschnitt der Animationen der Datei "player.md2" :


Frame 1 : stand01
Frame 2: stand02
Frame 3: stand03
Frame 4: stand04
Frame 5: stand05
Frame 6: run1
Frame 7: run2
Frame 8: run3
Frame 9: run4
Frame 10 : run5
Frame 11 : attack1
Frame 12 : attack2
Frame 13 : attack3
Frame 14: attack4
[SCHNIPP, hier fehlt was]

Und, verstanden?? Wenn man einen laufenden Spieler darstellen will, spielt man Frame 6 bis 9 ab. Greift er an, benutzt man Frame 11-14

Ganz einfach,oder ??!!!

 

Wie gehts jetzt weiter?

Jetzt hat man fast alles was man für den nächsten potentiellen Megahit braucht - Ein cooles Level, viel Motivation *gg* und animierte Charactere !

Jetzt fehlen nur nur noch Kleinigkeiten. Es ist ziemlich umständlich zum Laufen Frame6-9 abzuspielen, das in einer
bestimmten Zeit und dann auf Frame1 zu wechseln, weil der Spieler auf einmal stehen bleibt...

Im Demoprojekt ist dieses Problem gelöst. Wenn man die Laufanimation abspielen will benutzt mal folgende Funktion:

Render ("run");

Wow ! Man muss sich nicht mehr mit Frame6-9 rumärgern, man gibt nur einen Namen an, und der Rest wird automatisch gemacht. Praktisch !

Was weiterhin noch fehlt ist eine Kollisionsabfrage -Da sollte uns eine einfach Bounding Box Abfrage genügen.

Außerdem wird das Objekt noch ohne Textur gerendert -kein Problem :

g_D3D.m_lpD3DDevice->SetTexture (0, D3DTexture);
model.Render (0);

Man muss die Textur also extern setzen. Dies hat den Vorteil dass man 1 Objekt lädt, und z.B. 5 verschiedene Texturen dafür hat -man spart Speicher und Ladezeit.


Mehr dazu im Demoprojekt

 

Schlusswort

So, jetzt sind wir auch schon am Ende meines Tuturoials. Ich hoffe ihr hattet beim Lesen genausoviel Spass wie ich beim schreiben, auch wenn es teilweise wegen der komplizierten Ladefunktion etwas trocken wurde. Wenn euch dieses Tutorial gefallen hat (oder auch nicht), schreibt mir einfach:

Lob, Jobangebote, Ermutigungen, Verbesserungen, Kritik :

johannes.leimbach@gmx.de

 

Links

Milkshape :

Milkshape ist ein einfach zu bedienendes 3D Programm.
Das tolle daran sind aber die Import und Exportfilter.
Hier ein kleiner Auszug :

Import

  • Halflife SMD
  • Quake2 MD2
  • Quake3 MD3
  • Unreal 3D
  • PlayStation TMD
  • Serious Sam MDL
  • Autodesc ASC und 3DS
  • ....
  • ...

Export

  • Alle o.g. Formate
  • DirectX X-Format Ascii (!!!!!)
  • Noch ca. 10 weiter

Flipcode :

Dies ist eine der Besten Englischsprachigen Pages zum Thema
Spieleprogrammierung mit einer hervorragenden Suchfunktion

c-plusplus.de :

Die Beste Deutschsprachige Seite, mit sehr guten Foren zu allen
Themen. Ich bin dort übrigens unter dem Namen Headhunter aktiv

VB Games Interactive

Ein cooles Hobbyprogrammiererteam :-) --ratet mal wer da mit macht ???

http://home.planet.nl/~monstrous :

Einige sehr schöne Artikel zu Landschaftsrendering, Physik ...

http://www.planetquake.com/polycount/ :

Hier gibt es verschiedene Models zum Download

www.3dcafe.com

Die wohl größte Sammlung aller möglichen Grafiksachen im Internet

www.wotsit.org

Eine Beschreibung aller gängigen Fileformate -von a wie ASC
bis z wie ZIP

www.idsoftware.com :

Die Macher von Doom, Quake, Wolfenstein und all den anderen
schönen Spielen die in Deutschland indiziert sind :-)



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick