Modelformat: Unterschied zwischen den Versionen
I0n0s (Diskussion | Beiträge) |
|||
Zeile 1: | Zeile 1: | ||
− | Es gibt einige Arten von | + | 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. | + | 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 | + | |
− | Also sollten wir uns erstmal die für OpenGL benötigten Daten angucken. | + | 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> | ||
− | + | ||
− | OpenGL bietet | + | |
− | *VBO(virtual buffer object) | + | OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen. |
− | *Bruteforce(glBegin/glEnd) | + | *[[VBO]] (virtual buffer object) |
− | * | + | *Bruteforce([[glBegin]]/glEnd) |
− | * | + | *[[Vertexarray]] |
− | Die performantesten Varianten sind | + | *[[Displayliste]] |
− | Displaylisten haben aber den | + | 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 | + | |
− | Hier sollte man alle Daten als einzelne Streams behandeln, also | + | |
− | 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. |
− | |||
− | Wozu das Index ? Der Index zeigt auf ein Wert in der Vertex-, Color-, Normal-,... | ||
− | 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> | ||
− | + | ||
− | Nun brauchen wir auch Texturen und Shader für unsere Models. | + | 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. | ||
− | 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. | ||
<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. | + | 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. | ||
<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 | + | 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 | + | |
− | + | 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: | |
− | Hier kann man Serializing aufgreifen und die Daten und den | + | |
− | Bei diesem Ansatz wird jede dynamische | ||
− | Alle Daten, die gleich bleiben, werden | ||
− | Die Differenz zwischen Position des Pointers/Array und der Stelle der abgelegten Daten ist der Offset und wird an der | ||
− | Ein Beispiel kann dies wohl besser beschreiben | ||
<cpp>//eigentliche Struktur | <cpp>//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 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 | + | 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. | + | 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 | + | |
− | + | ||
− | Die Probleme der meisten Formate liegen in dem Aufbau, welches oft noch geparsed und in | + | 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). | |
− | Das | + | 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. | + | |
− | Der Vorteil des relocate ist, dass man | + | |
− | Durch die Anpassung, an das von OpenGL gewollten Datenformat, kann man ganz einfach die Daten in den VRam laden lassen(VBO | + | 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] | ||
*[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 | + | 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. | + | 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 | + | 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. | + | 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, | + | |
+ | 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.
- VBO (virtual buffer object)
- Bruteforce(glBegin/glEnd)
- Vertexarray
- Displayliste
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.