Modelformat: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
K (Korrekturgelesen, kleine Grammatikfehler korrigiert)
Zeile 1: Zeile 1:
Es gibt einige Arten von Modelformate: Textbasierende, binäre, sogar grafische (siehe parametrisierte Geometry Maps).
+
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.
  
Zeile 13: Zeile 13:
  
 
OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen.
 
OpenGL bietet mehrere Möglichkeiten diese Daten darzustellen.
*[[VBO]] (virtual buffer object)
+
*[[VBO]] (vertex buffer object)
 
*Bruteforce([[glBegin]]/glEnd)
 
*Bruteforce([[glBegin]]/glEnd)
 
*[[Vertexarray]]
 
*[[Vertexarray]]
 
*[[Displayliste]]
 
*[[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.
+
Die effektivsten 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 zwei 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 Vertexliste, 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.
+
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.
 
<cpp>struct Model
 
<cpp>struct Model
 
{
 
{
Zeile 30: Zeile 30:
 
};</cpp>
 
};</cpp>
  
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.
+
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 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 57: Zeile 57:
 
};</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. Wenn man Shader verwendet, dann könnte man die Tangenten mit reinpacken.  
+
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 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.  
+
Okay, nun müssen wir die Daten auch noch speichern und das möglichst in einer schnell ladbaren Form. 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:
+
Ein Beispiel kann das wohl besser beschreiben:
  
 
<cpp>//eigentliche Struktur
 
<cpp>//eigentliche Struktur
Zeile 89: 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 Speicheradressierung als 64Bit.
+
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.Laden könnten wir die Daten nun mit einem read aufruf und das relocate korrigiert den offset zu einem Pointer.
+
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 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.  
+
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).
+
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.
+
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.
  
  

Version vom 14. Mai 2008, 09:37 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.

Die effektivsten 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 zwei 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 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 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 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 wir die Daten auch noch speichern und das möglichst in einer schnell ladbaren Form. 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 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([[Benutzer:TAK2004|TAK2004]) einfach per PM im Forum ansprechen.