EINLEITUNG

Original Author: Damiano Vitulli

Translation by: Click here

Zeit unserem schönen Würfel auf Wiedersehen zu sagen! In diesem Abschnitt werden wir eine Routine entwerfen, die 3DS-Dateien laden kann (ein verbreitetes Dateiformat das von vielen 3D-Modelern unterstützt wird). Ein 3D-Modeler erlaubt es jede beliebige Art von Objekt auf eine eher intuitive und anschauliche Weise zu kreieren anstatt einfach nur die Koordinaten per Hand einzugeben (was zu einer unmöglichen Aufgabe werden kann, wenn man nur wenig kompliziertere Objekte als den Würfel hat…)

Eigentlich trenne ich mich ungern einfach so von dem Würfel (solch eine simple und perfekte Figur). Wie auch immer.. Solange es keiner widerlegt sind Raumschiffe, Planeten, Raketen und alles andere was mit Flugsimulatoren zu tun hat total verschieden vom Würfel.

Bevor wir anfangen den code zu schreiben werden wir uns mit der 3DS-Dateistruktur beschäftigen. Mach’s dir gemütlich und schnapp dir dein Lieblings-Programmier-Getränk ;)  

DIE 3DS DATEI STRUKTUR

Eine 3DS-Datei enthält jede Menge Information, um jedes Detail einer aus einem oder mehr Objekten bestehenden 3D-Szene zu beschreiben. Die 3DS-Datei enthält eine Reihe von Blöcken die chunks genannt werden. Was befindet sich darin? Nun ja, alles Notwendige, um die Szene zu beschreiben: Den Namen jedes Objektes, Die Koordinaten der Ecken (Vertices), die Mapping-Koordinaten, die Liste der Polygone, die Farben der Flächen, die Animations-Schlüsselbilder etc.

Diese chunks haben keine feste Struktur. Das heißt, dass manche chunks von anderen abhängig sind und nur gelesen werden können, wenn ihr Elternelement vorher gelesen wurde. Es ist nicht nötig alle chunks auszulesen und wir werden uns hier nur auf die wichtigsten konzentrieren. Meinen Erklärungen über das 3DS-Format liegt die 3dsinfo.txt Datei (von Jochen Wilhelmy) zu Grunde, die die Struktur aller chunks ausführlich beschreibt.

Ein chunk besteht aus 4 Feldern:

  • Der ID: Eine zwei Byte lange hexadezimale Zahl die den chunk identifiziert. Diese Information sagt uns sofort ob dieser chunk für unsere Zwecke wichtig ist, oder nicht. Wenn wir den chunk brauchen können wir die Szenen-Informationen auslesen und, falls nötig, die Unterelemente gleich mit. Wenn wir den chunk nicht brauchen können wir ihn überspringen indem wir die folgenden Informationen zu Rate ziehen:
  • Die Länge des chunks: Eine 4 Byte lange Zahl die die Summe der Länge des chunks und alle sub-chunks (Unterelemente) darstellt.
  • Die eigentlichen Daten: Diese Feld hat eine veränderliche Länge und enthält die ganzen Daten für die Szene.

Diese Tabelle zeigt uns die offsets (Positionen in der Datei in Bytes) und die Länge (auch in Bytes) für jedes Feld in einem typischen chunk:

OffsetLengthDescription
02Chunk identifier
24Chunk length: chunk data + sub-chunks(6+n+m)
6nData
6+nmSub-chunks

In der letzten Zeile können wir sehen, auf welche Weise chunks voneinander abhängig sein können: Jedes Unterelement ist praktisch im Feld “Sub-chunks” des Elternelements enthalten.

Hier sind die wichtigsten chunks aus einer 3DS-Datei aufgelistet. Achte auf die Hierarchie die durch die Einrückung dargestellt ist:

MAIN CHUNK 0x4D4D
   3D EDITOR CHUNK 0x3D3D
      OBJECT BLOCK 0x4000
         TRIANGULAR MESH 0x4100
            VERTICES LIST 0x4110
            FACES DESCRIPTION 0x4120
               FACES MATERIAL 0x4130
            MAPPING COORDINATES LIST 0x4140
               SMOOTHING GROUP LIST 0x4150
            LOCAL COORDINATES SYSTEM 0x4160
         LIGHT 0x4600
            SPOTLIGHT 0x4610
         CAMERA 0x4700
      MATERIAL BLOCK 0xAFFF
         MATERIAL NAME 0xA000
         AMBIENT COLOR 0xA010
         DIFFUSE COLOR 0xA020
         SPECULAR COLOR 0xA030
         TEXTURE MAP 1 0xA200
         BUMP MAP 0xA230
         REFLECTION MAP 0xA220
         [SUB CHUNKS FOR EACH MAP]
            MAPPING FILENAME 0xA300
            MAPPING PARAMETERS 0xA351
      KEYFRAMER CHUNK 0xB000
         MESH INFORMATION BLOCK 0xB002
         SPOT LIGHT INFORMATION BLOCK 0xB007
         FRAMES (START AND END) 0xB008
            OBJECT NAME 0xB010
            OBJECT PIVOT POINT 0xB013
            POSITION TRACK 0xB020
            ROTATION TRACK 0xB021
            SCALE TRACK 0xB022
            HIERARCHY POSITION 0xB030

Wie schon vorher erwähnt müssen wir stets das Elternelement auslesen, bevor wir die Unterelemente lesen können. Stell dir vor die 3DS-Datei sei ein Baum und der chunk den wir brauchen ist ein Blatt (Und wir sind ganz klein und auf der Erde ^^). Um ans Blatt ranzukommen müssen wir beim Stamm anfangen und an allen Ästen entlang klettern die zu dem Blatt führen. Wenn wir zum Beispiel den chunk VERTICES LIST lesen wollen müssen wir erst den MAIN CHUNK lesen, dann den 3D EDITOR CHUNK dann den OBJECT BLOCK und zu guter Letzt den TRIANGULAR MESH chunk. Die anderen chunks können einfach übersprungen werden. Lasst uns unser Bäumchen säubern, sodass wir uns auf die Äste konzentrieren können die für dieses Tutorial wichtig sind: „vertices“, „faces“, „mapping coordinates“ und ihre Elternelemente:

MAIN CHUNK 0x4D4D
   3D EDITOR CHUNK 0x3D3D
      OBJECT BLOCK 0x4000
         TRIANGULAR MESH 0x4100
            VERTICES LIST 0x4110
            FACES DESCRIPTION 0x4120
            MAPPING COORDINATES LIST 0x4140

Hier sind die chunks im Detail beschrieben:

MAIN CHUNK
Identifier0x4d4d
Length0 + sub-chunks length
Chunk fatherNone
Sub chunks3D EDITOR CHUNK
DataNone
3D EDITOR CHUNK
Identifier0x3D3D
Length0 + sub-chunks length
Chunk fatherMAIN CHUNK
Sub chunksOBJECT BLOCK, MATERIAL BLOCK, KEYFRAMER CHUNK
DataNone
OBJECT BLOCK
Identifier0x4000
LengthObject name length + sub-chunks length
Chunk father3D EDITOR CHUNK
Sub chunksTRIANGULAR MESH, LIGHT, CAMERA
DataObject name
TRIANGULAR MESH
Identifier0x4100
Length0 + sub-chunks length
Chunk fatherOBJECT BLOCK
Sub chunksVERTICES LIST, FACES DESCRIPTION, MAPPING COORDINATES LIST
DataNone
VERTICES LIST
Identifier0x4110
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksNone
DataVertices number (unsigned short)
Vertices list: x1,y1,z1,x2,y2,z2 etc. (for each vertex: 3*float)
FACES DESCRIPTION
Identifier0x4120
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksFACES MATERIAL
DataPolygons number (unsigned short)
Polygons list: a1,b1,c1,a2,b2,c2 etc. (for each point: 3*unsigned short)
Face flag: face options, sides visibility etc. (unsigned short)
MAPPING COORDINATES LIST
Identifier0x4140
Lengthvarying + sub-chunks length
Chunk fatherTRIANGULAR MESH
Sub chunksSMOOTHING GROUP LIST
DataVertices number (unsigned short)
Mapping coordinates list: u1,v1,u2,v2 etc. (for each vertex: 2*float)

Da jetzt das 3DS-Format klar genug sein sollte, können wir jetzt zum code dieses Tutorials übergehen. Wie? Du hast kein Plan? =D Lass uns einfach weitermachen. Der Aufbau dieser “chunk”-Struktur wird dir beim Weiterlesen klarer werden. Immerhin sind wir doch Programmierer und verstehen C besser als unser eigenes Geschwätz ;)

EINE KURZE EINWEISUNG

Die Schritte die nötig sind ein 3DS-Objekt zu laden und in dem Format zu speichern, das wir für unsere 3D-Engine definiert haben sind:

  1. eine „while“-Schleife implementieren (genau wie für den Textur-Lader), die alles bis zum Dateiende durchgeht
  2. Die chunk-ID und die chunk-Länge bei jedem Durchgang der Schleife lesen
  3. Den Inhalt jedes wichtigen chunks analysieren (Hierzu nehmen wir einen „switch“)
  4. Wenn der chunk einer von denen ist, die wir nicht brauchen, springen wir einfach um die Länge des chunks weiter in der Datei indem wir den Lesezeiger (Pointer) zur neuen Position bewegen die sich aus der Länge des aktuellen chunks zuzüglich der aktuellen Dateiposition berechnet. So können wir jeden chunk überspringen den wir (einschließlich seiner Unterelemente) nicht brauchen. Oder anders ausgedrückt: Springen wir einfach zum nächsten Ast! Fühlst’ dich schon ein bisschen wie ein Affe? =)
  5. Wenn uns der aktuelle chunk zu einem chunk führt den wir brauchen oder sogar schon die Daten enthält die wichtig für uns sind, dann lesen wir einfach die Daten aus uns gehen zum nächsten chunk.

ENDLICH... CODE!

Als erstes erstellen wir die neuen Dateien die unsere Routinen enthalten. Bisher haben wir die Datei tutorial(n).cpp benutzt, um die Hauptdatentypen der engine zu definieren, aber da unsere Datenstrukturen immer größer werden, werden wir die Deklarationen der Datentypen in eine Header-Datei verlagern, die wir tutorial4.h nennen.

Zunächst erhöhen wir die Anzahl der Ecken und Polygone die unsere engine fähig ist zu verwalten:

#define MAX_VERTICES 8000
#define MAX_POLYGONS 8000 

Dann fügen wir das Feld char name[20] der Struktur obj_type hinzu. Dieses Feld wird den Namen des geladenen Objektes speichern.

Als letztes ändern wir noch den Namen unserer Objekt-Variable von obj_type cube zu obj_type object. Nur, um die Allgemeinheit unseres Objektes zu verdeutlichen.

Die nächste Datei ist die 3dsloader.cpp. In dieser Datei fügen wir folgende Routine ein:

char Load3DS (obj_type_ptr p_object, char *p_filename)
{
   int i;
   FILE *l_file;
   unsigned short l_chunk_id;
   unsigned int l_chunk_length;
   unsigned char l_char;
   unsigned short l_qty;
   unsigned short l_face_flags; 

Die Load3DS Routine erwartet zwei Parameter: Einen Pointer zur Objektdatenstruktur und den Namen der Datei die zu laden ist. Wenn die Datei nicht gefunden wurde wird „0“ zurückgegeben. Wenn die Datei gefunden und gelesen wurde „1“.

Es gibt nicht so viele Variablen zu initialisieren: Wir haben unseren Standard-Laufvariable i, einen Pointer zur Datei *l_file und eine Zusatzvariable, um unsere Datenbytes zu extrahieren: l_char. Die anderen Variablen sind:

  • unsigned short l_chunk_id; Eine 2 Byte Hexadezimalzahl die uns die ID des chunks verrät
  • unsigned int l_chunk_length; Eine 4 Byte Zahl die die Länge des chunks angibt.
  • unsigned short l_qty; Eine Zusatzvariable die uns die Menge der zu lesenden Daten verrät. 
  • unsigned short l_face_flags; Diese Variable enthält verschiedene Informationen die unser aktuelles Polygon betreffen. (sichtbar, nicht sichtbar, etc.) die der 3D-Editor benutzt, um die Szene zu rendern (darzustellen). Diesen Wert werden wir lediglich dazu benutzen den Pointer/Zeiger in der Datei zur Position des nächsten chunks zu bewegen.

Also lasst uns endlich die Datei öffnen

   if ((l_file=fopen (p_filename, "rb"))== NULL) return 0; //Open the file
   while (ftell (l_file) < filelength (fileno (l_file))) //Loop to scan the whole file 
   { 

Die while-Schleife wird die ganze Datei durchlaufen. Die ftell Funktion gibt uns die aktuelle Position in der Datei zurück während filelength die Länge der Datei zurückgibt.

      fread (&l_chunk_id, 2, 1, l_file); //Read the chunk header
      fread (&l_chunk_length, 4, 1, l_file); //Read the length of the chunk 

Hier haben wir die ID und die Länge des chunks ausgelesen und jeweils in l_chunk_id und l_chunk_length gespeichert. Als erstes analysieren wir den Inhalt von l_chunk_id.

      switch (l_chunk_id)
      {
         case 0x4d4d: 
         break; 

Yippieh! wir haben den MAIN CHUNK gefunden! Und was machen wir jetzt damit? Ganz einfach… Nix! Der MAIN CHUNK hat nämlich keine Daten. Nun ja, wir brauchen aber die Daten der Unterelemente. Für diesen Fall haben wir ein extra „case“ statement sodass nicht einfach der ganze MAIN CHUNK übersprungen wird, weil sonst wären wir schon am Dateiende (Durch das default-statement am Ende des switches). Auf diesen default-case komme ich aber später noch mal zurück.

Der Ansatz für den 3D EDITOR CUNK ist eigentlich derselbe. Das ist der nächste Knotenpunkt durch den wir uns durchnavigieren müssen, um an die nötigen Informationen zu kommen. Und wieder einmal hat dieser chunk keine Daten… Also tun wir einfach so als würden wir’s lesen =) das führt uns zum Unterelement OBJECT BLOCK.

         case 0x3d3d:
         break; 

Der OBJECT BLOCK chunk hat endlich ein paar interessante Informationen: Den Name des Objektes. Diesen speichern wird im Feld „name“ der „object“ Struktur. Die while-Schleife wird beendet wenn das Nullbyte ’\0’ gelesen wird oder die Zahl der Zeichen 20 übersteigt. Pass auf! Wir haben gerade die gesamten Daten des chunks gelesen und somit den Pointer zum nächsten chunk bewegt.

         case 0x4000: 
            i=0;
            do
            {
               fread (&l_char, 1, 1, l_file);
               p_object->name[i]=l_char;
               i++;
            }while(l_char != '\0' && i<20);
         break; 

Der nächste chunk ist wieder nur ein leerer Knotenpunkt als Elternteil der nächsten wichtigen chunks.

         case 0x4100:
         break; 

Und endlich sind wir bei den Eckpunkten (Vertices) angekommen! Der chunk VERTICES LIST enthält alle Ecken des Models. Zuerst lesen wir die Anzahl der Ecken aus und dann mit Hilfe einer For-Schleife die einzelnen Ecken. Dann speichern wir jede Ecke in das entsprechende Feld der „object“-Struktur.

         case 0x4110: 
            fread (&l_qty, sizeof (unsigned short), 1, l_file);
            p_object->vertices_qty = l_qty;
            printf("Number of vertices: %d\n",l_qty);
            for (i=0; i<l_qty; i++)
            {
               fread (&p_object->vertex[i].x, sizeof(float), 1, l_file);
               fread (&p_object->vertex[i].y, sizeof(float), 1, l_file);
               fread (&p_object->vertex[i].z, sizeof(float), 1, l_file);
            }
         break; 

Der chunk FACES DESCRIPTION enthält eine Liste der Polygone des Objekts. Wie schon im ersten Tutorial erklärt, enthält die Polygonstruktur nicht die Koordinaten sondern nur die Nummern, die den Indizes in der Eckenliste entsprechen. Um diesen chunk auszulesen, machen wir genau dasselbe wie für den Ecken-chunk: Wir lesen die Anzahl der Flächen und dann mit einem For-Loop die einzelnen Flächen. Jede Fläche hat ein weiters 2-Byte-Feld, die Flächen-Schalter (flags), die nur für die 3D-Editoren wichtig sind. Wir lesen es nur, um zum nächsten chunk zu kommen.

         case 0x4120:
            fread (&l_qty, sizeof (unsigned short), 1, l_file);
            p_object->polygons_qty = l_qty;
            printf("Number of polygons: %d\n",l_qty); 
            for (i=0; i<l_qty; i++)
            {
               fread (&p_object->polygon[i].a, sizeof (unsigned short), 1, l_file);
               fread (&p_object->polygon[i].b, sizeof (unsigned short), 1, l_file);
               fread (&p_object->polygon[i].c, sizeof (unsigned short), 1, l_file);
               fread (&l_face_flags, sizeof (unsigned short), 1, l_file);
            }
         break;

Jetzt lesen wir die MAPPING COORDINATES LIST. Wieder einmal lesen wir die Anzahl und benutzen diese für die For-Schleife. Jeder Punkt hat zwei Texturkoordinaten u und v erinnerst du dich? Nein?? Was machst du dann hier? ;)

         case 0x4140:
            fread (&l_qty, sizeof (unsigned short), 1, l_file);
            for (i=0; i<l_qty; i++)
            {
               fread (&p_object->mapcoord[i].u, sizeof (float), 1, l_file);
               fread (&p_object->mapcoord[i].v, sizeof (float), 1, l_file);
            }
         break; 

Die default case Anweisung! Das heißt, dass wir am Ende der Routine sind. Ganz einfach: Wenn wir chunks finden, die wir nicht kennen oder nicht lesen wollen, gehen wir mit der fseek Funktion (mit Hilfe der Information über die Länge des chunks: chunk_length) zum Anfang des nächsten chunks.

         default:
            fseek(l_file, l_chunk_length-6, SEEK_CUR);
      } 
   } 

Das war’s! Nur ein eins fehlt noch. Wir schließen die Datei und geben „1“ zurück.

   fclose (l_file); // Closes the file stream
   return (1); // Returns ok
} 

 

ZUSAMMENFASSUNG

Der 3DS-Reader den wir entwickelt haben ist ein Anfang für weitere komplexere Reader. Du musst dir auch im Klaren darüber sein, dass unsere Routine nur 3DS-Dateien lesen kann, die nur genau ein Objekt beinhalten welches mittig platziert ist. Das nächste Tutorial (das Matrizen-Tutorial), wird Funktionalitäten hinzufügen, die nötig sind weitere Objekte zu laden. Das wird der spannende Part. Wir müssen ja schließlich unsere Raumschiffe noch dazubekommen, richtig? Sonst haben wir nix zum Zerstören =)

Dieses Tutorial war nicht so schwer, oder? Außerdem haben wir die schwere Arbeit aus dem vorigen Tutorial ja schon hinter uns. Wir können den ganzen code aus diesem Tutorial für das nächste verwenden, in welchem wir lernen werden wie man mit den OpenGL-Funktionen Licht hinzufügt. Also ciao erstmal ihr Programmierer ;)

SOURCE CODE

The Source Code of this lesson can be downloaded from the Tutorials Main Page