Modelformat: Unterschied zwischen den Versionen
DGLBot (Diskussion | Beiträge) K (Der Ausdruck ''<cpp>(.*?)</cpp>'' wurde ersetzt mit ''<source lang="cpp">$1</source>''.) |
|||
(7 dazwischenliegende Versionen von 5 Benutzern werden nicht angezeigt) | |||
Zeile 1: | Zeile 1: | ||
− | Es gibt einige Arten von Modelformaten, | + | 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. | Die Formate sind auf bestimmte Ziele ausgerichtet und dementsprechend auch im Aufbau unterschiedlich. | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | OpenGL | + | 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. |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | <source lang="cpp">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;</source> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen. | |
− | + | *[[VBO]] (vertex buffer object) | |
− | + | *Immediate (direkter Modus)([[glBegin]]/glEnd) | |
− | + | *[[Vertexarray]] | |
− | + | *[[Displayliste]] | |
− | + | Die effektivsten Varianten sind Displaylisten mit Vertexarray und VBO. In Sachen Geschwindigkeit sind Displaylisten immer noch die beste Möglichkeit (siehe z.B. [http://ati.amd.com/developer/gdc/2006/GDC06-OpenGL_Tutorial_Day-Hart-OpenGL_03_Performance.pdf 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. | |
− | + | <source lang="cpp">struct Model | |
− | + | { | |
− | + | Vertex *Verticelist; | |
+ | Normal *Normallist; | ||
+ | Color *Colorlist; | ||
+ | Texcoord *Texcoordlist; | ||
+ | Index *Indices; | ||
+ | };</source> | ||
− | + | 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. | |
− | + | <source lang="cpp">struct Face | |
− | + | { | |
− | + | unsigned int Start; | |
− | + | unsigned int Count; | |
− | + | string MaterialName; | |
− | + | };</source> | |
− | + | <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. | ||
+ | <source lang="cpp">struct Face | ||
+ | { | ||
+ | unsigned int Start; | ||
+ | unsigned int Count; | ||
+ | unsigned int MaterialIndex; | ||
+ | }; | ||
− | Für | + | struct Model |
− | Wenn man Shader verwendet, dann könnte man die | + | { |
+ | Vertex *Verticelist; | ||
+ | Normal *Normallist; | ||
+ | Color *Colorlist; | ||
+ | Texcoord *Texcoordlist; | ||
+ | Index *Indices; | ||
+ | string *MaterialList[32]; //feste Strings sind besser zu laden | ||
+ | Face *Facelist; | ||
+ | };</source> | ||
+ | <br> | ||
+ | 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. | |
− | Hier kann man | + | Ein Beispiel kann das wohl besser beschreiben: |
− | Bei diesem Ansatz wird jede dynamische | + | |
− | Alle Daten, die gleich bleiben, werden | + | <source lang="cpp">//eigentliche Struktur |
− | Die Differenz zwischen Position des Pointers/Array und der Stelle der abgelegten Daten | ||
− | Ein Beispiel kann | ||
− | < | ||
− | //eigentliche Struktur | ||
struct Data | struct Data | ||
{ | { | ||
unsigned int VerticeCount; | unsigned int VerticeCount; | ||
− | Vertex* Vertice;// | + | Vertex* Vertice; //Array durch verticecount korrekt nutzbar (Delphi pVertex(Vertice+i)^ oder bei c++/freepascal Vertice[i]) |
}; | }; | ||
− | //gut speicherbare und ladbare | + | //gut speicherbare und ladbare Version |
struct SpeicherbareDaten | struct SpeicherbareDaten | ||
{ | { | ||
unsigned int VerticeCount; | unsigned int VerticeCount; | ||
− | #if defined(64BIT)// | + | #if defined(64BIT)//Pointer sind 64bit groß, der Offset liegt vor relocate an der Pointerstelle |
Vertex* Vertice; | Vertex* Vertice; | ||
#else | #else | ||
− | #if defined(32BIT)// | + | #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 93: | Zeile 88: | ||
{ | { | ||
Data->Vertice=(Vertex*)((char*)Data+(unsigned int)Data->Vertice); | Data->Vertice=(Vertex*)((char*)Data+(unsigned int)Data->Vertice); | ||
− | } | + | }</source> |
− | </ | + | Aktuell hat keine Console oder PC eine größere Speicheradressierung als 64Bit. |
− | Aktuell hat keine Console oder PC | + | 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. |
− | Also werden die dynamischen Elemente mit offsets von 4byte und 4byte ungenutzt besetzt. | + | |
− | |||
− | Die Probleme der meisten Formate liegen | + | 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. | Meine Umsetzung ist hier zu finden. | ||
− | [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] |
− | Ein weiterer Vorteil ist der Codeumfang, denn dieser ist in meiner Implementierung gerademal | + | *[https://svn.linuxprofessionals.org/filedetails.php?repname=karmarama&path=%2Ftrunk%2Fdemos%2Fdata%2Fexporter.py exporter.py(Blender exporter Script)] |
− | Mein Testmodel hatte ~15k Vertice,Normals,Texcoords,~46k Indices,15k Faces,1Material und keine Properties. | + | Ein weiterer Vorteil ist der Codeumfang, denn dieser ist in meiner Implementierung gerademal 205 Zeilen lang. |
− | Die Dateigröße betrug 1,5MB und wurde in 7ms ungecached(erster | + | Mein Testmodel hatte ~15k Vertice, Normals, Texcoords, ~46k Indices, 15k Faces, 1Material und keine Properties. |
− | Das Laden des ganzen Models mit der einer Textur(512x512 dxt5) hat knapp 20ms gedauert. | + | 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. | ||
[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.] | ||
+ | |||
+ | |||
+ | Bei Fragen, Verbesserungsvorschlägen oder ähnlichem, mich([[Benutzer:TAK2004|TAK2004]]) einfach per PM im Forum ansprechen. |
Aktuelle Version vom 10. März 2009, 19:44 Uhr
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.
- VBO (vertex buffer object)
- Immediate (direkter Modus)(glBegin/glEnd)
- Vertexarray
- Displayliste
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.