Modelformat

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Es gibt einige Arten von Modelformaten: Textbasierende, binäre, sogar grafische (siehe parametrisierte Geometry Maps). Die Formate sind auf bestimmte Ziele ausgerichtet und dementsprechend auch im Aufbau unterschiedlich.


In diesem Artikel möchte ich ein für OpenGL optimiertes Format vorstellen, welches als weiteres Kriterium eine sehr geringe CPU und Ladezeit in Anspruch nimmt. Also sollten wir uns erstmal die für OpenGL benötigten Daten angucken.

typedef Vertex   float        x,y,z;
typedef Color    float        r,g,b;
typedef Normal   float        x,y,z;
typedef Texcoord float        u,v;
typedef Index    unsigned int i;


OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen.

Die effektivsten Varianten sind Displaylisten mit Vertexarray und VBO. In Sachen Geschwindigkeit sind Displaylisten immer noch die beste Möglichkeit (siehe z.B. folgendes Dokument von ATI für die GDC2006, Seite 14), haben aber in Sachen Flexibilität Nachteile (entweder müssen sie neu erstellt werden, oder im Shader angepasst werden). VBO bietet uns zwei Möglichkeiten, die Daten in den VRam zu speichern. Entweder in Form mehrer Streams oder als ein einziger Stream. Hier sollte man alle Daten als einzelne Streams behandeln, also Vertexliste, Normalliste, Colorliste,... . So können wir später einzelne Streams abschalten und anschalten, wenn wir sie beim Zeichnen benötigen.


Wozu der Index? Der Index zeigt auf einen Wert in der Vertex-, Color-, Normal-, ...-Stream. Man kann also bei großen Models viel Platz sparen, wenn man Indices verwendet, da man jedes Datenelement nur einmal ablegt und durch den Index mehrfach verwenden kann.

struct Model
{
 Vertex   *Verticelist;
 Normal   *Normallist;
 Color    *Colorlist;
 Texcoord *Texcoordlist;
 Index    *Indices;
};

Nun brauchen wir auch Texturen und Shader für unsere Models. Um diese zu verwenden brauchen wir Faces, also müssen wir Flächen bilden. Jede Fläche bekommt den Startindex, die Vertexanzahl und ein Material zugewiesen. Das Material ist ein Name, über den wir auf eine Ressource zugreifen können, die die Texturen und Shader verwalten.

struct Face
{
  unsigned int Start;
  unsigned int Count;
  string       MaterialName;
};


Der Name ist allerdings ziemlich groß und würde in jeden Face wieder auftreten. Also speichern wir im Format eine Liste von Materialnamen und speichern im Face den Index in der Liste.

struct Face
{
  unsigned int Start;
  unsigned int Count;
  unsigned int MaterialIndex;
};

struct Model
{
 Vertex   *Verticelist;
 Normal   *Normallist;
 Color    *Colorlist;
 Texcoord *Texcoordlist;
 Index    *Indices;
 string   *MaterialList[32]; //feste Strings sind besser zu laden
 Face     *Facelist;
};


Für das Material File könnte z.B. ein Script oder eine XML Datei erstellt werden, die alle benötigten Texturen, Flags und Shader enthält. Wenn man Shader verwendet, dann könnte man die Tangenten mit dazu packen.

Okay, nun müssen wird die Daten auch noch speichern und das so, dass wir sie möglichst schnell wieder laden können. Hier kann man Serialisierung aufgreifen und die Daten und den Loader entsprechend anpassen. Bei diesem Ansatz wird jede dynamische Variable (arrays/pointer) überarbeitet. Alle Daten, die gleich bleiben, werden 1 zu 1 geschrieben, für die dynamischen Daten wird ein Bereich am Ende des Formates zur Verfügung gestellt und dorthin verschoben. Die Differenz zwischen Position des Pointers/Array und der Stelle der abgelegten Daten nennt man Offset. Dieser wird an der entsprechenden Stelle in die Liste geschrieben. Ein Beispiel kann das wohl besser beschreiben:

//eigentliche Struktur
struct Data
{
  unsigned int VerticeCount;
  Vertex* Vertice; //Array durch verticecount korrekt nutzbar (Delphi pVertex(Vertice+i)^ oder bei c++/freepascal Vertice[i])
};

//gut speicherbare und ladbare Version
struct SpeicherbareDaten
{
  unsigned int VerticeCount;
  #if defined(64BIT)//Pointer sind 64bit groß, der Offset liegt vor relocate an der Pointerstelle
    Vertex* Vertice;
  #else
    #if defined(32BIT)//Pointer sind 32bit groß, der offset=pointer und die anderen 4 Byte sind toter speicher
      Vertex* Vertice;
      unsigned int unused;
    #else
      Die Bitarchitektur ist nicht vorgesehen.
    #endif
  #endif
};

void relocate(SpeicherbareDaten* Data)
{
  Data->Vertice=(Vertex*)((char*)Data+(unsigned int)Data->Vertice);
}

Aktuell hat keine Console oder PC eine größere Speicheradressierung als 64Bit. Also werden die dynamischen Elemente mit offsets von 4byte und 4byte ungenutzt besetzt. Wir könnten die Daten nun mit einem read Aufruf laden und das relocate korrigiert den offset zu einem Pointer.


Die Probleme der meisten Formate liegen im Aufbau, wenn oft noch geparsed und in mehreren Etappen geladen werden muss (braucht CPU Zeit und verschwendet Zeit beim Festplattenzugriff) und in der unpassenden Datenbeschaffenheit (eventuell konvertieren oder noch schlimmer, Speicherreservierung und konvertieren). Das oben beschriebene Speicher- und Ladesystem braucht nur einen einzigen Read Aufruf und hat danach die Daten komplett im Speicher. Dann noch ein relocate und wir haben die fertigen Daten. Der Vorteil des relocate ist, dass man lediglich für dynamische Elemente einmal eine Addition ausführt (10 Arrays = 10 Additionen). Durch die Anpassung an das von OpenGL gewollte Datenformat kann man ganz einfach die Daten in den VRam laden lassen (VBO Streams lassen grüssen) und leitet das Laden des Materials an die entsprechende Stelle weiter.


Meine Umsetzung ist hier zu finden.

Ein weiterer Vorteil ist der Codeumfang, denn dieser ist in meiner Implementierung gerademal 205 Zeilen lang. Mein Testmodel hatte ~15k Vertice, Normals, Texcoords, ~46k Indices, 15k Faces, 1Material und keine Properties. Die Dateigröße betrug 1,5MB und wurde in 7ms ungecached (erster Aufruf der Datei) und 3ms gecached(die Datei wurde seit dem Start von Windows schonmal angefasst) geladen, Pointer angepasst, VBOs für alle Daten erstellt und der Ladeprozess für die Textur angestoßen (alles in stolzen 7ms). Das Laden des ganzen Models mit der einer Textur (512x512 dxt5) hat knapp 20ms gedauert. Ein nicht ganz aktuelles Screenshot.


Bei Fragen, Verbesserungsvorschlägen oder ähnlichem, mich(TAK2004) einfach per PM im Forum ansprechen.