Modelformat: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
Zeile 1: Zeile 1:
Es gibt einige Arten von Modelformaten, Textbasierte, Binäre sogar grafische(siehe parametrisierte geometry Maps).<br>
+
Es gibt einige Arten von Modelformate: Textbasierende, binäre, sogar grafische (siehe parametrisierte Geometry Maps).
Die Formate sind auf bestimmte Ziele ausgerichtet und dementsprechend auch im Aufbau unterschiedlich.<br>
+
Die Formate sind auf bestimmte Ziele ausgerichtet und dementsprechend auch im Aufbau unterschiedlich.
<br>
+
 
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.<br>
+
 
Also sollten wir uns erstmal die für OpenGL benötigten Daten angucken.<br>
+
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.
 +
 
 
<cpp>typedef Vertex  float        x,y,z;
 
<cpp>typedef Vertex  float        x,y,z;
 
typedef Color    float        r,g,b;
 
typedef Color    float        r,g,b;
Zeile 9: Zeile 10:
 
typedef Texcoord float        u,v;
 
typedef Texcoord float        u,v;
 
typedef Index    unsigned int i;</cpp>
 
typedef Index    unsigned int i;</cpp>
<br>
+
 
OpenGL bietet mehere möglichkeiten diese Daten darzustellen.<br>
+
 
*VBO(virtual buffer object)
+
OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen.
*Bruteforce(glBegin/glEnd)
+
*[[VBO]] (virtual buffer object)
*vertexarray
+
*Bruteforce([[glBegin]]/glEnd)
*displaylist(vertexarray oder bruteforce)
+
*[[Vertexarray]]
Die performantesten Varianten sind Displaylist mit vertexarray und VBO.<br>
+
*[[Displayliste]]
Displaylisten haben aber den nachteil, dass sie gering langsamer sind und nicht so flexibel wie VBO sind.<br>
+
Die performantesten Varianten sind Displaylisten mit Vertexarray und VBO. Displaylisten haben aber den Nachteil, dass sie geringfügig langsamer und nicht so flexibel wie VBO sind. VBO bietet uns 2 Möglichkeiten, die Daten in den VRam zu speichern, als einzelne Streams oder als ein einziger Stream. Hier sollte man alle Daten als einzelne Streams behandeln, also Verticeliste, Normalliste, Colorliste,... . So können wir später einzelne Streams abschalten und anschalten, wenn wir sie beim Zeichnen benötigen.
VBO bietet uns 2 möglichkeiten, die Daten in den VRam zu speichern, als einzelne Streams und als ein einziger Stream.<br>
+
 
Hier sollte man alle Daten als einzelne Streams behandeln, also Verticelist,Normallist,Colorlist,... .<br>
+
 
So können wir später einzelne Streams abschalten und anschalten, wenn wir sie beim Zeichnen benötigen.<br>
+
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.
<br>
 
Wozu das Index ? Der Index zeigt auf ein Wert in der Vertex-, Color-, Normal-,... stream.<br>
 
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.
 
 
<cpp>struct Model
 
<cpp>struct Model
 
{
 
{
Zeile 31: Zeile 29:
 
  Index    *Indices;
 
  Index    *Indices;
 
};</cpp>
 
};</cpp>
<br>
+
 
Nun brauchen wir auch Texturen und Shader für unsere Models.<br>
+
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.
Um diese zu verwenden brauchen wir Faces, also müssen Flächen bilden.<br>
 
Jede Fläche bekommt den Startindex, die Vertexcount und nun noch das Material.<br>
 
Das Material ist ein Name, über den wir auf eine Ressource zugreifen können, die die Texturen und Shader verwalten.
 
 
<cpp>struct Face
 
<cpp>struct Face
 
{
 
{
Zeile 43: Zeile 38:
 
};</cpp>
 
};</cpp>
 
<br>
 
<br>
Der Name ist allerdings ziemlich groß und würde in jeden Face wieder auftreten.<br>
+
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.
Also speichern wir im Format eine Liste von Materialnamen und speichern im Face den Index in der Liste.<br>
 
 
<cpp>struct Face
 
<cpp>struct Face
 
{
 
{
Zeile 59: Zeile 53:
 
  Texcoord *Texcoordlist;
 
  Texcoord *Texcoordlist;
 
  Index    *Indices;
 
  Index    *Indices;
  string  *MaterialList[32];//feste Strings sind besser zu laden
+
  string  *MaterialList[32]; //feste Strings sind besser zu laden
 
  Face    *Facelist;
 
  Face    *Facelist;
 
};</cpp>
 
};</cpp>
 
<br>
 
<br>
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.<br>
+
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.  
Wenn man Shader verwendet, dann könnte man die tangenten mit reinpacken.<br>
+
 
<br>
+
Okay, 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 Lader 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 ist der Offset und wird an der Stelle der Liste geschrieben.  
Okey Nun müssen wird die Daten auch noch speichern und das möglichst schnell Ladbar.<br>
+
Ein Beispiel kann dies wohl besser beschreiben:
Hier kann man Serializing aufgreifen und die Daten und den Loader entsprechend anpassen.<br>
+
 
Bei diesem Ansatz wird jede dynamische variable(arrays/pointer) überarbeitet.<br>
 
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.<br>
 
Die Differenz zwischen Position des Pointers/Array und der Stelle der abgelegten Daten ist der Offset und wird an der stelle der Liste geschrieben.<br>
 
Ein Beispiel kann dies wohl besser beschreiben.<br>
 
 
<cpp>//eigentliche Struktur
 
<cpp>//eigentliche Struktur
 
struct Data
 
struct Data
 
{
 
{
 
   unsigned int VerticeCount;
 
   unsigned int VerticeCount;
   Vertex* Vertice;//array durch verticecount korrekt nutzbar(Delphi pVertex(Vertice+i)^ oder bei c++/freepascal Vertice[i])
+
   Vertex* Vertice; //Array durch verticecount korrekt nutzbar (Delphi pVertex(Vertice+i)^ oder bei c++/freepascal Vertice[i])
 
};
 
};
  
//gut speicherbare und ladbare version
+
//gut speicherbare und ladbare Version
 
struct SpeicherbareDaten
 
struct SpeicherbareDaten
 
{
 
{
 
   unsigned int VerticeCount;
 
   unsigned int VerticeCount;
   #if defined(64BIT)//pointer sind 64bit groß, der offset liegt vor relocate an der pointerstelle
+
   #if defined(64BIT)//Pointer sind 64bit groß, der Offset liegt vor relocate an der Pointerstelle
 
     Vertex* Vertice;
 
     Vertex* Vertice;
 
   #else
 
   #else
     #if defined(32BIT)//pointer sind 32bit groß, der offset=pointer und die anderen 4 byte sind toter speicher
+
     #if defined(32BIT)//Pointer sind 32bit groß, der offset=pointer und die anderen 4 Byte sind toter speicher
 
       Vertex* Vertice;
 
       Vertex* Vertice;
 
       unsigned int unused;
 
       unsigned int unused;
Zeile 99: Zeile 89:
 
   Data->Vertice=(Vertex*)((char*)Data+(unsigned int)Data->Vertice);
 
   Data->Vertice=(Vertex*)((char*)Data+(unsigned int)Data->Vertice);
 
}</cpp>
 
}</cpp>
Aktuell hat keine Console oder PC ein größeren, als 64Bit, Speicheradressierung.<br>
+
Aktuell hat keine Console oder PC ein größeren Speicheradressierung als 64Bit.
Also werden die dynamischen Elemente mit offsets von 4byte und 4byte ungenutzt besetzt.<br>
+
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.
Laden könnten wir die Daten nun mit einem read aufruf und das relocate korrigiert den offset zu einem pointer.<br>
+
 
<br>
+
 
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).<br>
+
Die Probleme der meisten Formate liegen in dem Aufbau, welches oft noch geparsed und in mehreren 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.  
<br>
+
Der Vorteil des relocate ist, dass man lediglich für dynamische Elemente einmal eine Addition ausführt(10 Arrays = 10 Additionen).
Das Oben beschriebene Speicher und Ladesystem braucht nur einen einzigen Read aufruf und hat danach die Daten komplett im Speicher.<br>
+
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.
Dann noch ein relocate und wir haben die fertigen Daten.<br>
+
 
Der Vorteil des relocate ist, dass man ledeglich für dynamische elemente einmal eine addition ausfürt(10 arrays=10 additionen).<br>
+
 
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.<br>
+
Meine Umsetzung ist hier zu finden.
<br>
 
Meine Umsetzung ist hier zu finden.<br>
 
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Finclude%2Fkar_model.hpp kar_model.hpp]
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Finclude%2Fkar_model.hpp kar_model.hpp]
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Fsrc%2Fkar_model.cpp kar_model.cpp]
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Fsrc%2Fkar_model.cpp kar_model.cpp]
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Fdemos%2Fdata%2Fexporter.py exporter.py(Blender exporter Script)]
 
*[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Fdemos%2Fdata%2Fexporter.py exporter.py(Blender exporter Script)]
Ein weiterer Vorteil ist der Codeumfang, denn dieser ist in meiner Implementierung gerademal 205Zeilen lang.<br>
+
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.<br>
+
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).<br>
+
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.<br>
+
Das Laden des ganzen Models mit der einer Textur (512x512 dxt5) hat knapp 20ms gedauert.
 
[http://karmarama.developer-alliance.org/upload/model.PNG Ein nicht ganz aktuelles Screenshot.]
 
[http://karmarama.developer-alliance.org/upload/model.PNG Ein nicht ganz aktuelles Screenshot.]
<br>
+
 
Bei Fragen, verbesserungsvorschlägen oder ähnlichem, mich(TAK2004) einfach per PM im Forum ansprechen.
+
 
 +
Bei Fragen, Verbesserungsvorschlägen oder ähnlichem, mich([[Benutzer:TAK2004|TAK2004]) einfach per PM im Forum ansprechen.

Version vom 13. Mai 2008, 23:30 Uhr

Es gibt einige Arten von Modelformate: 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 performantesten Varianten sind Displaylisten mit Vertexarray und VBO. Displaylisten haben aber den Nachteil, dass sie geringfügig langsamer und nicht so flexibel wie VBO sind. VBO bietet uns 2 Möglichkeiten, die Daten in den VRam zu speichern, als einzelne Streams oder als ein einziger Stream. Hier sollte man alle Daten als einzelne Streams behandeln, also Verticeliste, Normalliste, Colorliste,... . 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.

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 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.

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 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.

Okay, 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 Lader 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 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 Speicheradressierung als 64Bit. 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 mehreren 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 lediglich für dynamische Elemente einmal eine Addition ausführt(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.

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([[Benutzer:TAK2004|TAK2004]) einfach per PM im Forum ansprechen.