Modelformat

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Es gibt einige Arten von Modelformaten, Textbasierte, 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. Vertex=float x,float y,float z Color=float r,float g,float b Normal=float x,float y,float z Texcoord=float u,float v Index=int i

OpenGL bietet mehere möglichkeiten diese Daten darzustellen. -VBO(virtual buffer object) -Bruteforce(glBegin/glEnd) -vertexarray -displaylist(vertexarray oder bruteforce) Die performantesten Varianten sind Displaylist mit vertexarray und VBO. Displaylisten haben aber den nachteil, dass sie gering langsamer sind und nicht so flexibel wie VBO sind. VBO bietet uns 2 möglichkeiten, die Daten in den VRam zu speichern, als einzelne Streams und als ein einziger Stream. Hier sollte man alle Daten als einzelne Streams behandeln, also Verticelist,Normallist,Colorlist,... . So können wir später einzelne Streams abschalten und anschalten, wenn wir sie beim Zeichnen benötigen.

Wozu das Index ? Der Index zeigt auf ein 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 diesen mehrfach verwenden kann.

Model -Verticelist -Normallist -Colorlist -Texcoordlist -Indices

Nun brauchen wir auch Texturen und Shader für unsere Models. Um diese zu verwenden brauchen wir Faces, also müssen Flächen bilden. Jede Fläche bekommt den Startindex, die Vertexcount und nun noch das Material. Das Material ist ein Name, über den wir auf eine Ressource zugreifen können, die die Texturen und Shader verwalten. Face -Start -Count -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.

Face -Start -Count -MaterialIndex

Model -Verticelist -Normallist -Colorlist -Texcoordlist -Indices -Materiallist -Facelist

Für Die Material File könnte z.B. ein Script oder 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 reinpacken.

Okey Nun müssen wird die Daten auch noch speichern und das möglichst schnell Ladbar. Hier kann man Serializing 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 1zu1 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 ist der Offset und wird an der stelle der Liste geschrieben. Ein Beispiel kann dies 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 ein größeren, als 64Bit, Speicheradressierung. Also werden die dynamischen Elemente mit offsets von 4byte und 4byte ungenutzt besetzt. Laden könnten wir die Daten nun mit einem read aufruf und das relocate korrigiert den offset zu einem pointer.

Die Probleme der meisten Formate liegen in dem Aufbau, welches oft noch geparsed und in meheren etappen geladen werden muss(braucht CPU Zeit und verschwendet Zeit beim Festplattenzugriff) und in den 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 ledeglich für dynamische elemente einmal eine addition ausfürt(10 arrays=10 additionen). Durch die Anpassung, an das von OpenGL gewollten Datenformat, kann man ganz einfach die Daten in den VRam laden lassen(VBO streams lassen grüssen) und leiten das Laden des Materials an die entsprechende Stelle weiter.

Meine Umsetzung ist hier zu finden. kar_model.hpp kar_model.cpp Ein weiterer Vorteil ist der Codeumfang, denn dieser ist in meiner Implementierung gerademal 205Zeilen 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 angestossen(alles in stolzen 7ms). Das Laden des ganzen Models mit der einer Textur(512x512 dxt5) hat knapp 20ms gedauert. Ein nicht ganz aktuelles Screenshot.