Grafikprogrammierung
OpenGL mit SDL

version 0.1 - 31.12.03

by nixon

http://www.excluded.org
nixon@excluded.org
euirc #excluded

 

 

0. Über dieses Tutorial
1. Kurzer Einblick ins Projekmanagement
2. SDL (Simple Direct Media Layer)
    2.1 Was ist SDL
    2.2 SDL - Der Anfang
    2.3 Bitmaps blitten
    2.4 Die Eventloop
    2.5 Bilder bewegen
3. OpenGL (Open Graphics Library)
    3.1 OpenGL - Der Anfang
    3.2 Die Primitives & zeichnen
    3.4 3D-Objekte
    3.5 Rotation
    3.6 Texture Mapping
    3.7 Bewegung im dreidimensionalen Raum
    3.8 Blending
    3.9 Partikel Systeme
4. Links & Literatur
5. Anmerkung des Autors

 

0. Über dieses Tutorial

Dieses Tutorial enstand im Rahmen eines Programmierprojektes mit einigen Kollegen. Es soll einen Einblick in OpenGL und SDL bieten, und ist eher für Anfänger geeignet, als für solche, die ihre eigene 3D-Engine versuchen zu optimieren.
Ich habe eine kleine Einführung in das Projektmanagement vorangestellt, da ich denke, daß es dem einen oder anderen von Nutzen kann. Ich möchte auch darauf hinweisen, dass ich mich selbst zu den Amateuren in der Grafikprogrammierung zähle, und meine, in diesem Tutorial veröffentlichten, Sourcecodes nicht perfekt sein müssen. Gleiches gilt, für die kleine Exkursion in das Projektmanagement.
Das Tutorial soll lediglich einen Einblick in die Materie bieten.
Ich entschuldige mich hier auch gleich für mögliche Rechtschreibfehler oder Unklarheiten im Geschriebenen. Ich hoffe, jeder kommt damit klar. Ansonsten wünsche ich viel Spaß mit diesem Werk, und würde mich über ein wenig Feedback freuen.

1. Kurzer Einblick ins Projektmanagement

Sein eigenes Spiel programmieren. Wenn das nicht der Traum eines jeden Menschen ist, dann aber bestimmt der einiger wenigen. Das Programmieren eines Spieles ist ein Projekt. Etwas nicht alltägliches, in einem bestimmten Zeitraum abzuschliessendes. Ein solches Projekt muss gemanaged werden. Zumindest, in bestimmten Größenordnungen. Nehmen wir mal an, man plant ein großes Spiel zu programmieren. Es gibt viele organisatorische Dinge neben dem Programmieren, die genauso wenig zu missachten sind. Ist ein Projekt schlecht geplant, so kann man davon ausgehen, daß es früher oder später zusammenbrechen wird, weil alles aus dem Ruder läuft.
Was also ist im Vorhinein bei Projekten zu beachten? Noch bevor man etwas anderes macht sollte man eine Stichwortliste aufstellen, mit allen offensichtlichen und wichtigen Fragen. Bei Einem Spieleprojekt kann das so aussehen:

  1. Zu welchem Genre ist das Spiel zu zählen?
  2. Auf welchen Systemen soll es laufen?
  3. In welcher Sprache soll es geschrieben sein?
  4. Welche Bibliotheken werden verwendet?
  5. Um was geht es in dem Spiel? (Story?)
  6. Charaktere, Waffen, ... - falls vorhanden, beschreiben
  7. Einen zeitlichen Rahmen. Wann plant man fertig zu werden?
  8. Welche Ressourcen (Kapital, Zeit, Material) sind notwenig zur Durchführung?
  9. Müssen rechtliche Dinge geklärt werden? (z.B. Lizenzen/Copyrights)
Besonders die beiden letzten Punkte können die Gründe für ein Scheitern eines Projektes sein. Meist vergessen die Projektarbeiter Dinge in die Wege zu leiten, die für die Vollendung des Produktes oder die Publizierung relevant sind. Das ist äußerst ärgerlich, aber zu vermeiden, wenn man sich vorher Gedanken macht.

Ein weiterer wichtiger Punkt ist das Planen, Zusammenstellen und Koordinieren eines Teams. Bei kleineren Projekten ist man als "Lone Wolf", als Einzelgänger, wohl besser beraten. Werden die Projektarbeiten aber überdimensional, dann ist zu einem Team zu raten. Dabei sollten Mitarbeiter für bestimmte Arbeitsbereiche gewählt werden. Um auf die Spieleprogrammierung zurück zukommen: Man wird Programmierer brauchen, genauso wie Grafiker und Leveldesigner.

Wenn man sich die Arbeit entsprechend aufteilt, und alle Projektmitglieder zuverlässig und kompetent sind, wird das ganze "einigermaßen" glatt über die Bühne gehen. Doch in so ziemlich in jedem Projekt gib es die eine oder andere Zeit, in dem alles zu zerbrechen droht. Etwa zur Hälfte der Zeit, die für das Projekt geplant war, wird man feststellen, dass sich das Team in einem Motivationstief befindet. Völlig normal. Man sollte sich davon nicht abschrecken lassen, und beugt vor. Regelmäßige Treffen mit den Mitarbeitern, um bereits Erledigtes revue passieren zu lassen und das weitere Vorgehen zu besprechen helfen, die Motivationskurve wieder auf ein annehmbares Level zu bringen.

Spätestens zu diesem Zeitpunkt sollte man einen Projektleiter bestimmt haben, der sich entweder selbst um die organisatorischen Dinge kümmert und die Fäden in der Hand hat, oder einen Mitarbeiter dazu ernennt.

Als Projektleiter sollte man auch nicht davor zurückschrecken den einen oder anderen Mitarbeiter "in den Hintern zu treten", wenn dieser nicht ordentlich arbeitet. Es kann sogar soweit kommen, daß man sich von diesem in Bezug auf das Projekt verabschieden muß.

Arbeitet man für einen Auftraggeber, ist es zwingend erforderlich regelmäßige Treffen mit diesem zu vereinbaren, und den aktuellen Stand zu offenbaren. Ich habe es schon erlebt, daß ein Auftraggeber in einem Endprodukt manche Features wünscht, aber schon zwei Wochen später seine Pläne komplett über den Haufen wirft, ohne es dem Projektteam mitzuteilen. Oder sehr oft kommt es vor, daß Auftragnehmer das vom Auftraggeber Gesagte falsch interpretieren. Es ist äußerst unschön, nach zwei Monaten Arbeit, wieder von vorne zu beginnen, weil der Auftraggeber sich das "anders gedacht hat".
Dagegen hilft wie gesagt, dem Auftraggeber regelmäßig den aktuellen Stand zu demonstrieren und seine Meinung dazu anhören, oder ihn - wenn etwas seinen Wünschen nicht entspricht - von den eigenen Vorstellungen zu überzeugen. Es ist tatsächlich so, daß die meisten Auftraggeber, garnicht genau wissen was sie haben wollen, bzw. wie das Produkt aussehen soll. Das bietet natürlich die gröste Freiheit für den Entwickler, bringt aber auch solche Gefahren, wie erwähnt, mit sich.

Der eine oder andere fragt sich nun bestimmt, weshalb ich das in ein Tutorial über Grafikprogrammierung gepackt habe. Ich bin der Meinung, daß man sich besonders in diesem Themengebiet frührer oder später mit Projektmanagement außeinander setzen wird, und es nicht falsch sein kann, schon früh etwas davon gehört zu haben. Begriffe wie Projekt-Struktur-Plan und Lasten- bzw. Pflichtenheft erspare ich mir. Es gibt genügend gute Bücher und Tutorials im Netz, die Projektmanagement ausgiebig behandeln.

2. SDL (Simple Direct Media Layer)

SDL (Simple Direct Media Layer) ist eine Bibliothek zum Anzeigen von Bitmaps in einem Fenster.

2.1 Was ist SDL

Wer will schon ein Tutorial lesen, und nicht gleich nebenbei alles ausprobieren?! Das geht mir so, und euch bestimmt auch. Daher verrate ich schon jetzt, wo man alles nötige bekommt.

Die neuste SDL Version findet man auf deren Website:   http://www.libsdl.org

Ansonsten wird nur ein gewöhnlicher Texteditor (ich verwende in der Regel mcedit) und der GNU-C Compiler (http://www.gnu.org) benötigt. Außerdem gehe ich von fundierten C-Kenntnissen aus. Ich werde hier keine Einführung in C bieten. Wer merkt, daß er diese Sprache noch nicht ausreichend beherrscht, sollte sich vorher entsprechenden Tutorials widmen.

2.2 SDL - Der Anfang

Ist es nicht schön, wenn man zu Beginn gleich etwas tolles zu sehen bekommt? Klar ist es das. Und dazu muss ein Fenster her.
Hier einmal der Source, um dieses zu bewerkstelligen. Ich werde folgend auf die einzelnen Anweisungen eingehen.

Kompiliert und ausgeführt wird das dann einfach so:

$ gcc -lSDL -o output source.c
$ ./output

Vorallem Anfänger vergessen gerne mal das -lSDL. Dann kommt es zu solch unschönen Linker-Fehlermeldungen in der Form undefined reference to ...

Aber nun endlich der Sourcecode:

#include <SDL/SDL.h>

int main(int argc, char** argv)
{
  SDL_Surface* screen;

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE);
  if (!screen) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  SDL_Delay(2000);
  return 0;
}
Hier wird nun ein schönes 640x480 Fenster ganz in Schwarz erzeugt.

Gehen wir Zeile für Zeile durch:

In der ersten, wie zu erwarten, das einfügen des benötigten Headers: #include <SDL/SDL.h>
Dieser befindet sich in der Regel im Include-Verzeichnis (etwa /usr/include/ ) im Unterverzeichnis SDL/.

In unserer main() Funktion wird zu allererst ein Pointer screen auf ein SDL_Surface - eine Oberfäche - deklariert.
Nachdem wir nun eine Oberfläche deklariert haben, initialisieren wir SDL:
int SDL_Init(Uint32 flags);
Als Parameter flags werden Subsysteme angegeben, die initialisiert werden sollen.
Dazu zählt SDL_INIT_VIDEO genauso wie SDL_INIT_AUDIO und noch weitere, die uns allerdings vorerst nicht interessieren werden.
Bleiben wir einmal bei SDL_INIT_VIDEO.
Nachdem wir nun SDL und das Video-System hoffentlich erfolgreich initialisiert haben, registrieren wir durch die Funktion int atexit(void (*function) (void)); eine Funktion, die aufgerufen wird, wenn das Programm normal, also z.B. via return; oder exit(); terminiert wird.
In diesem Fall handelt es sich um SDL_Quit() .
Und nun zum interessantesten Teil des Quelltextes:

  screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE);
  if (!screen) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

Durch SDL_Surface* SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags); setzt man ein Fenster mit Breite width und Höhe height .
Da wäre dann noch die Farbtiefe und weitere Flags.
Durch enstprechendes Flag, wäre auch ein Fullscreen denkbar, was dann so aussähe: SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE | SDL_FULLSCREEN);

Mit SDL_HWSURFACE geben wir die Anweisung, die Surfaces in den Grafikspeicher abzulegen. Würde man hier ein solches Flag nicht angeben, so werden Surfaces immer im Hauptspeicher abgelegt. Das Flag dazu lautet SDL_SWSURFACE .
Es gibt noch eine ganze Zahl weiterer Eigenschaften, auf die ich nicht weiter eingehen möchte. Lediglich eine lezte möchte ich kurz erwähnen. Die SDL_OPENGL Eigenschaft werden wir später noch verwenden, wenn wir in OpenGL einsteigen. Letztendlich gibt die Funktion einen Pointer auf ein Surface zurück.
In der folgenden Kontrollanweisung wird überprüft, ob unser Pointer auf eine tatsächliche Oberfläche zeigt, oder es sich um einen NULL-Pointer handelt.

Schliesslich setzen wir durch SDL_WM_SetCaption("MeinFenster", "MeinFenster"); die Window-Caption, und warten dann 2000 ms (= 2 Sekunden), bis das Programm beendet wird. SDL_Delay(Uint32 ms); baut ein entsprechendes Delay, angegeben in Millisekunden, ein.

2.3 Bitmaps blitten

Nachdem wir nun ein Fenster erzeugt haben, und der Anblick dieses Prachtwerkes aber schon gelangweilt hat, bringen wir ein bisschen Bilder ins Spiel.
"Blitten" heißt das Zauberwort. Oder verständlicher: Bilder auf den Bildschirm bringen. Ein wichtiger Hinweis bring Erleuchtung: Bilder sind nichts anderes als Oberflächen. Solche Obeflächen werden dann einfach auf unsere Bildschirmoberfläche, in dem Sourcecode weiter oben deklariert als screen geblittet. Soweit die Theorie. Schreiten wir also zur Praxis hinüber.
#include <SDL/SDL.h>

int main(int argc, char** argv)
{
  SDL_Surface* screen;
  SDL_Surface* img;  /* hier unser Image Surface */

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE);
  if (!screen) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  img = SDL_LoadBMP("exclued.bmp");
  if (!img) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }
  SDL_BlitSurface(img, 0, screen, 0);
  SDL_FreeSurface(img);
  SDL_UpdateRect(screen, 0, 0, 0, 0);

  SDL_Delay(2000);
  return 0;
}

So könnte das aussehen:

Wer das so übernommen hat, und ausprobieren wollte, ist wohl gescheitert, und hat die Meldung: "Could not open excluded.bmp". Es ist ja wohl kaum zu erwähnen, daß das garnicht funktionieren kann, wenn man kein Bild hat zum Anzeigen. Wenn ihr das also testen wollt, dann gebt den relativen oder absoluten Pfad eines Bildes an, das auch existiert.

Also zum Sourcecode:
Zu Beginn, gleich unter der Deklarierung unserer Screen-Oberfläche, haben wir ein weiteres Surface deklariert. Vielleicht erinnert sich noch jemand, an das, was ich einige Zeilen vorher geschrieben habe. "Bilder sind nichts anderes als Oberflächen". Damit wäre die Deklaration des Zeigers img auf ein Surface erklärt. Wir benötigen ihn, um das Bild, das geblittet werden soll zu laden. Und genau das geschieht in der nächsten neuen Zeile:
img = SDL_LoadBMP("excluded.bmp");
Die Funktion SDL_Surface* SDL_LoadBMP(const char* file); lädt ein Bitmap und gibt einen Surface Pointer zurück. Diese Rückgabe übergeben wir natürlich an unseren Pointer img.
Sollte das Laden der Bitmap fehlgeschlagen, etwa weil es nicht existiert, dann gibt die Funktion NULL zurück.
In der folgenden Zeile müssen wir also erst einmal überprüfen, ob wir tatsächlich ein Bild geladen haben. Ließe wir diesen Schritt aus, und das Bild dennoch versuchen zu blitten, dann würde unser tolles Programm mit einem "Segmentation Fault" abbrechen.
Also überprüfen wir kurz, und fahren dann mit dem Blitten fort.
SDL_BlitSurface(img, 0, screen, 0);
Hiermit blitten wir unser Surface img auf unser Screen-Surface screen .
Der erste Parameter der Funktion ist, wie unschwer zu erkennen das Source-Surface, dann folgt dass Source-Rect, das Destination-Surface und zu guter letzt, das Destination-Rect.
Vielleicht ist es einfacher, wenn wir uns den Prototyp genauer anschauen:
int SDL_BlitSurface(SDL_Surface *src, SDL_Rect srcrect, SDL_Surface *dst, SDL_Rect dsrect);
"rect" kommt von rectangle und bedeutet Rechteck. Doch das Quell-Rechteck, und das Ziel-Rechteck brauchen wir an dieser Stelle noch nicht. Nachher, bei dem Bewegen von Bildern werde spielt es eine Rolle, und ich werde näher darauf eingehen. Wichtig sind hier nur die Quell-Oberfläche und die Ziel-Oberfläche. Wir blitten also unsere Quell-Oberfläche, hier img , auf unsere Ziel-Oberfläche, unseren screen . Allein mit dieser Funktion ist allerdings noch nicht zu sehen, auf unserem Bildschirm.

Um etwas sehen zu können müssen wir unseren screen updaten. Dazu der Funktionsaufruf SDL_UpdateRect(screen, 0, 0, 0, 0);. Und der Prototyp zu dieser: void SD_UpdateRect(SDL_Surface* , Sint32 x, Sint32 y, Sint32 w, Sint32 h); . Der Erste Parameter verlang einen Pointer auf ein SDL_Surface . Die anderen vier sind Koordinaten und Größenangaben. Nämlich die x und y Position, sowie die Breite und die Höhe. Übergibt man an diese 0, so wird die komplette Oberfläche geupdatet. Jetzt gibt alles Sinn, nicht wahr? Wir haben screen übergeben, und verlangen auch gleich, dass das komplette Surface refresht wird. Also unser kompletter Screen.

Es bleibt noch ein Funktionsaufruf, der zwischen den beiden bereits erklärten steckt, zu betrachten. SDL_Freesurface(img); Nachdem wir unser Bild geblittet haben, existiert unser Bild noch im Speicher. Würden wir das Bild ein weiteres Mal verwenden wollen, dann würden wir diesen Funktionsaufruf unterlassen, oder zu einem späteren Zeitpunkt verwenden. In diesem Beispiel wollen wir nur ein Bild einmal anzeigen, also löschen wir das Bild aus dem Speicher. Das Bild als Visuelles Element geht damit nicht verloren. Schliesslich haben wir es bereits auf unseren Screen geblittet, den wir nur noch updaten müssen.
Super, jetzt können wir schon Bilder anzeigen. Wie man diese dann bewegen kann werde ich ihm übernächsten Kapitel angehen, denn dazu benötigen wir ersteinmal die Eventloop.

2.4 Die Eventloop

Was wäre das beste Spiel ohne eine Interaktion des Spielers?! Bedeutungslos!
In der Eventloop kann auf bestimmte Events (Ereignisse) wie etwa ein Tastendruck eingegangen werden. Das Blitten eines Bildes habe ich hier herausgenommen, um die Übersichtlichkeit zu wahren und Verwirrungen zu vermeiden.

#include <SDL/SDL.h>

int main(int argc, char** argv)
{
  SDL_Surface* screen;
  SDL_Event event;   // unser Event Element

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE);
  if (!screen) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* Die Eventloop */
  while(1) {
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
          printf("%s\n", SDL_GetKeyName(event.key.keysym.sym));
          break;
        case SDL_MOUSEBUTTONDOWN:
          printf("Mousebutton: %i\n",event.button.button);
          break;
      }
    }
  }
  SDL_Delay(2000);
  return 0;
}

Unter der Deklaration unseres Screen-Surfaces deklarieren wir unser Event-Handler Element vom Typ SDL_Event. In diesem Element werden unsere ganzen Ereignisse gespeichert. Und das bringt uns auch schon zur eigenlichen Ereignisschleife, die in einer Endlosschleife eingebettet wurde. Darüber mag man streiten, ob nun das Verwenden von Endlosschleifen von einem guten Programmierstil zeugen. Sicherlich hätte man an dieser Stelle auch eine Variable auf einen bestimmten Zustand überprüfen können, die beim Event SDL_QUIT auf entsprechenden Wert gesetzt wird. Doch das lässt uns jetzt mal völlig kalt. Den ersten Event unserer Schleife habe ich schon erwähnt: SDL_QUIT. Doch bevor ich diesen Schritt mache, und die einzelnen Events erkläre sollte ich wohl ein paar Wörtchen zu der Funktion darüber loswerden.
Die Funktion int SDL_PollEvent(SDL_Event *event) gibt true, also 1, zurück, wenn ein Ereignis vorliegt. Die Schleife wird solange durchlaufen, wie ein Ereignis passiert. Übergeben an diese Funktion wird unsere Event Variable event. Es handelt sich dabei nicht um einen Zeiger. Dies erklärt, weshalb wir sie mit dem Referenzoberator & übergeben.

Jetzt komme ich doch noch dazu, ein wenig in die Basics von C einzusteigen. Wer schonmal was von "Call By Reference" gehört hat, kann diesen Absatz getrost überspringen.

Der Referenzoperator dient dazu, die Adresse einer Variablen zu ermitteln. Ein Pointer zeigt auf eine bestimmte Adresse, und da unsere Funktion einen Pointer auf ein SDL_Event erwartet, also auf eine Adresse zeigen möchte, geben wir ihr auch eine. Nämlich die Adresse unserer SDL_Event Variablen. Und dies geschieht eben mit dem Referenzoperator. Ändere ich den Wert, auf den unser Pointer zeigt, so ändere ich den Wert der Variablen, weil der Pointer eben auf die Adresse unser Variablen im Speicher zeigt. Alles verstanden? Wunderbar.

Weiter im Text. Nun haben wir geklärt wie und woher wir die Events bekommen. Jetzt gilt es, diese Events zu erkennen und darauf zu reagieren. Die Event-Typen liegen in event.type. Mit einer Switch-Anweisung können wir dann die einzelnen Events "herauspicken". Es gibt eine ganze Reihe von Events, und ich stelle hier nur drei von ihnen vor.
Da wäre zuerst SDL_QUIT. Klickt der User, auf das kleine X, in der Titelleiste des Fensters, um dieses zu schliessen, so bekommt unsere Anwendung das Signal SDL_QUIT. Daraufhin wird nach Anweisung unser Programm via exit(0); beendet.

Die anderen beiden Events sind ein wenig interessanter. SDL_KEYDOWN wird dann ausgelöst, wenn der Benutzer eine Taste drückt. Um welche Taste es sich dabei handelt lässt sich durch die Funktion char* SDL_GetKeyName(SDLKey key); aus der SDL_keyboard.h SDL_event.h und SDL_keyboard.h werden übrigens durch das einbinden von SDL.h miteingebunden. Zurück zu der Funktion. Sie bekommt einen SDLKey und gibt dann einen Char-Pointer zur"uck, durch den wir unseren Key-Namen bekommen. Und dieser wird dann ganz einfach ausgegeben.
Um welchen Key es sich handelt ist in unserer Event Struktur zu finden: event.key.keysym.sym
An dieser Stelle schlage ich vor, einen Blick in die bettrefende Manpage ($ man SDL_Event) zu werfen, um sich im Klaren darüber zu sein, wie diese Struktur aufgebaut ist.
Dort wird man diese Typendefinition antreffen:

       typedef union{
         Uint8 type;
         SDL_ActiveEvent active;
         SDL_KeyboardEvent key;
         SDL_MouseMotionEvent motion;
         SDL_MouseButtonEvent button;
         SDL_JoyAxisEvent jaxis;
         SDL_JoyBallEvent jball;
         SDL_JoyHatEvent jhat;
         SDL_JoyButtonEvent jbutton;
         SDL_ResizeEvent resize;
         SDL_ExposeEvent expose;
         SDL_QuitEvent quit;
         SDL_UserEvent user;
         SDL_SywWMEvent syswm;
       } SDL_Event;
Auf die Einzelheiten werde ich nicht eingehen. Ich wollte sie nur kurz vorstellen. Für weiteres ist die Manpage hilfreich.

Genauso wie die Keyboard-Events arbeiten auch die Mousevents. Ich habe hier einmal den MOUSEBUTTONDOWN-Event ausgewählt. Wir eine Maustaste gedrückt, so wird angezeigt, welcher der Buttons gedrückt wurde. Und genau das verrät uns event.button.button. Für alles weitere wie gesagt die Manpage. Erfolgreich haben wir die Eventloop behandelt, und jetzt können wir uns dranmachen, Bilder zu bewegen.

2.5 Bilder bewegen

Mit dem Wissen der Eventloop ist das Bewegen eines Bildes sehr einfach. Das einzig neue, daß nun noch hinzukommen wird, ist das löschen eines Bildes vom Screen und es an einer anderen Stelle neu hinzuzufügen. Wie immer geht der Source voran:
#include <SDL/SDL.h>

int MoveBmp(SDL_Surface* screen, SDL_Surface* img, int dir_x, int dir_y)
{
  SDL_Rect rect, clrrect;
  static int x = 0, y = 0;

  x+=dir_x;
  y+=dir_y;

  rect.w = img->w;
  rect.h = img->w;
  rect.x = x;
  rect.y = y;

  clrrect.w = img->w;
  clrrect.h = img->h;
  clrrect.x = img->x;
  clrrect.y = img->y;

  SDL_FillRect(screen, &clrrect, SDL_MapRGB(screen->format, 0, 0, 0));
  SDL_BlitSurface(img, 0, screen, &rect);
  SDL_Flip(screen);

  return 0;
}

int main(int argc, char** argv)
{
  SDL_Surface* screen, *img;
  SDL_Event event;
  Uint8* key;

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF);
  if (!screen) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

/* Bild laden */
  img = SDL_LoadBMP("exclued.bmp");
  if (!img) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

/* Ereignisschleife */
  while(1) {
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	  if (event.key.keysym.sym == SDLK_ESCAPE)
	    exit(0);
      }
    }
    key = SDL_GetKeyState(NULL);
    if (key[SDLK_UP])
      MoveBmp(screen, img, 0, -1);
    if (key[SDLK_DOWN])
      MoveBmp(screen, img, 0, 1);
    if (key[SDLK_RIGHT])
      MoveBmp(screen, img, -1, 0);
    if (key[SDLK_LEFT])
      MoveBmp(screen, img, 1, 0);
  }
  SDL_Delay(2000);
  return 0;
}
Oje, das sieht auf den ersten Blick nach viel neuem aus. Ist auch so. Aber es ist nicht viel schweres dabei. Alles klar, fassen wir mal kurz zusammen. Wie wir SDL initiieren wissen wir, wie wir ein Bitmap laden wissen wir, wie eine Eventloop funktioniert wissen wir. Bleiben doch nur noch die unterschiedlichen Tastenanschläge zu erkennen und das Bild löschen und neu zu setzen. Beginnen wir bei den Tasten. Dazu der Codeschnipsel:
/* ... */
  Uint8* key;
/* ... */
  key = SDL_GetKeyState(NULL);
    if (key[SDLK_UP])
      MoveBmp(screen, img, 0, -1);
    if (key[SDLK_DOWN])
      MoveBmp(screen, img, 0, 1);
    if (key[SDLK_RIGHT])
      MoveBmp(screen, img, -1, 0);
    if (key[SDLK_LEFT])
      MoveBmp(screen, img, 1, 0);
/* ... */
Wir haben einen Zeiger key angelegt, die den Zustand der Tastatur aufnehmen soll. Dies geschieht durch die Funktion Uint8* SDL_GetKeyState(int* numkeys);. Sie gibt den Zustand der Tastatur als Zeiger auf ein Array zurück. Danach lässt sich einfach überprfüfen, ob zum Beispiel die Pfeiltaste nach oben gedrückt ist.

Nehmen wir mal an, der Benutzer betätigt die Pfeiltaste nach oben, also SLDK_UP, dann rufen wir unsere Funktion MoveBmp(...) auf. Die ersten beiden Parameter dieser Funktion sind unsere Screen-Oberfläche und das Surface des Bildes, das zu bewegen ist. Die anderen beiden Parameter geben einmal die Schrittweite in X-Richtung und zum anderen in Y-Richtung an. Wenn der Benutzer also "nach oben" drückt, so soll sich das Bild einen Schritt nach oben bewegen. Die X-Position ändert sich dabei nicht, nur die Y-Position verringert sich in diesem Fall um 1. Lassen wir die anderen Möglichkeiten von Tastendrucke mal Beiseite liegen, und widmen uns nun der Bewegung des Bildes, wobei wir im Hinterkopf behalten, dass wir die Pfeiltaste nach oben gedrückt haben, und sich das Bild einen Schritt nach oben bewegen soll.

int MoveBmp(SDL_Surface* screen, SDL_Surface* img, int dir_x, int dir_y)
{
  SDL_Rect rect, clrrect;
  static int x = 0, y = 0;

  x+=dir_x;
  y+=dir_y;

  rect.w = img->w;
  rect.h = img->w;
  rect.x = x;
  rect.y = y;

  clrrect.w = img->w;
  clrrect.h = img->h;
  clrrect.x = x - dir_x;
  clrrect.y = y - dir_y;

  SDL_FillRect(screen, &clrrect, SDL_MapRGB(screen->format, 0, 0, 0));
  SDL_BlitSurface(img, 0, screen, &rect);
  SDL_Flip(screen);

  return 0;
}
Beim Funktionsaufruf haben wir vier Parameter übergeben: Das Screen- und das Bild-Surface, und die Schritte, die wir in X- bzw. Y-Richtung gehen wollen. Lokal deklariert sind zwei statische Variablen x und y vom Typ Integer. Die Werte dieser bleiben nach der return 0; Anweisung und dem Beenden der Funktion erhalten. Das ist hier notwendig, da wir bei folgendenden Funktionsaufruf, das Bild immer weiter bewegen wollen.

Wir addieren einfach die Schrittweiten dir_x und dir_y zu x und y, und erhalten so die neuen Zielkoordinaten des Bildes.

Soweit so gut. Zurück in die erste Zeile in der Funktion. Wir deklarieren zwei Variablen des Typs SDL_Rect (Nochmal für die vergesslichen: rect von rectangle = Rechteck). Wir haben also zwei Rechtecke deklariert. Bei SDL_Rect handelt es sich um eine Struktur, die zum einen X- und Y-Koordinaten, wie auch Breite und Hoehe eines Rechteckes speichert. Wir definieren unser erstes Rechteck rect durch festlegen der neuen Koordinaten x und y sowie der Breite (img->w) und der Hoehe (img->h) des Bildes, das zu bewegen ist. Das ist unser Ziel-Rechteck. An die Position dessen soll nachher das neue Bild geblittet werden. Das zweite Rect, dass wir festlegen dient dazu das Bild vom Bildschirm verschwinden zu lassen. Treffend habe ich es clrrect wie "clear rect" genannt. Anders könnte man auch den kompletten Screen schwarz überzeichnen, um das alte Bild zu "löschen". Das würde alles unnötig langsam machen. Daher setzt man ein Rechteck auf die Postition des Bildes und zeichnet es einfach schwarz. Wozu schließlich den ganzen Screen überzeichnen, wenn sich die Bewegung nur in einem Ecke abspielt?!

Die X- und Y-Postition dieses Rechtecks ist natürlich die des Bildes, an der es sich zur Zeit befindet. Die Breite und die Höhe die gleiche des Bildes. Und jetzt gehts erst richtig los. Mit int SDL_FillRect(SDL_Surface* dst, SDL_Rect* dstrect, Uint32 color); füllt man ein Rechteck mit einer Farbe. Wir wollen das Bild vom Bildschirm entfernen, und daher müssen wir das clrrect färben. Unser Hintergrund ist schwarz und es macht Sinn, das Rechteck in der gleichen Farbe zu färben. Das Destination-Surface ist klar unser screen, das Zielrechteck clrrect. Bleibt der letzte Parameter zu erklären.

"RGB" ist im Funktionsnamen. Das gibt Aufschluß darüber, daß sie irgendetwas mit RGB-Farben zu tun haben muß (RGB = Red Green Blue). Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b); ist der Prototyp dazu. Die RGB Farbe, die in den letzten drei Parametern "zusammengemischt" wird, wird auf das Pixelformat unserer Oberfläche angewendet. Ein Pixelformat (SDL_PixelFormat) ist eine Struktur, die Informationen über das Surface Format bereithält. Dazu gehören z.B. die Bits/Pixel, ein Pixelwert für transparente Pixel und u.a. der Alphawert. Unsere Farbe wird also auf dieses Format angewendet, und wir bekommen einen 32 bit unsigned int Wert zurück, der auch gleich der letzte Parameter der SDL_FillRect(...) ist. Keine große Sache.

SDL_BlitSurface(...) haben wir bereits kennengelernt. Hinzu kommt jetzt nur noch das Zielrechteck als vierter Parameter, in das wir blitten. Oder vielleicht einfacher ausgedrückt: An dessen Position wir blitten.

Und da ist wieder etwas völlig neues: int SDL_Flip(SDL_Surface* screen); schaltet den Screen Buffer um. Vielleicht hat der eine oder andere bemerkt, dass wir unseren Video Mode mit einem zusätzlichen Flag initiiert haben:
screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF); Wir haben das Double Buffering aktiviert.

Double Buffering ist eine Technologie, die es uns erlaubt, Bilder relativ flimmerfrei zu bewegen. Die Theorie ist einfach. Neben unserem Screen Surface gibt es noch ein weiteres Surface, auf das erst gezeichnet wird. Danach wird das Surface umgeschaltet. Und so wechseln sich die Surfaces dann immerwieder ab. Ohne Double Buffering wäre das Bewegen des Bildes eine Qual für unsere Augen.

Jetzt habe ich die Funktion zum Bilder bewegen ausgiebig erklärt. Springen wir kurz zurück zu unseren Tastendrucken. Je nachdem welche Taste man gedrückt hat, bewegt sich das Bild in eine Richtung. Wer mehr über die Tasten erfahren m"ochte, dem lege ich man SDLKey ans Herzen.

Ich möchte garnicht weiter in SDL einsteigen. Lieber möchte ich mit OpenGL beginnen. Es sei noch gesagt, daß es zu SDL weitere hilfreiche, gut dokumentierte Bibliotheken gibt. Mit der SDL_ttf zum Beispiel lässt sich Schrift darstellen, und mit SDL_mixer Musik ausgeben. Wenn euch das interessiert, dann schaut mal bei   http://www.libsdl.org   vorbei.

3. OpenGL (Open Graphics Library)

Wir haben nun gelernt, wie man ein Fenster erzeugt, Bilder blitten und bewegen kann. Das sind doch geniale Vorraussetzngen für ein Spiel, aber ein wohl eher langweiliges. Mit OpenGL lässt sich da schon einiges mehr machen. Wie wäre es mit einem Spiel im Dreidimensionalen? Aber soweit sind wir noch garnicht. Also erstmal die Basics.

OpenGL ist die Abkürzung für "Open Graphics Library". Dabei handelt es sich um eine leistungsfähige, low-level Software Bibliothek für die meisten Plattformen. Mit Hilfe dieser lassen sich relativ einfach 3D Objekte darstellen.

Mesa ist eine freie Grafik Bibliothek, deren API der von OpenGL sehr gleicht. Zu haben ist diese unter http://www.mesa3d.org

Alle folgenden Programme lassen sich durch gcc -lSDL -lGL -lGLU -o output source.c kompilieren.

3.1 OpenGL - Der Anfang

Ein Fenster zu erzeugen, oder Events zu behandeln gehört nicht zu den Aufgaben von OpenGL. Daher gibt es dutzende Möglichkeiten Applikationen, die OpenGL verwenden zu schreiben. Ich habe mich für SDL entschieden, weil ich es für die einfachste Möglichkeit halte. Eine weitere weitverbreitete ist GLUT (GL Utility Tool).
Doch GLUT bietet anders als SDL einige Features nicht, und ist in meinen Augen in manchen Bereichen viel zu umständlich. Außerdem, soviel ich gehört habe, wird GLUT nicht weiterentwickelt.

Als wir unser erstes SDL Fenster erzeugt haben, erwähnte ich, daß man beim Setzten des Video Modes das man mit dem Flag SDL_OPENGL OpenGL verwenden kann. Und genau das werden wir jetzt tun. Das Screen-Surface wird nicht mehr benötigt:

  if (!SDL_SetVideoMode(640, 480, 16, SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }
Außerdem müssen wir noch den GL- und den GLU-Header einbinden:
#include <GL/gl.h>
#include <GL/glu.h>

Das war jetzt ersteinmal der Anfang. Als nächstes müssen wir OpenGL initiieren:

  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1, 100.0);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
"Viewport" heißt übersetzt ins Deutsche "Darstellungsfeld". Und genau das setzt man mit dieser Funktion. Ein Darstellungsfeld.
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height); x und y geben die Linkere untere Ecke, des Darstellungsfeldes an, und width und height die Breite bzw. die Höhe dieses Feldes. Da wir zuvor ein 640x480 Fenster erzeugt haben gibt es Sinn, das ganze Fenster als Darstellungsfeld zu nehmen, also die gleiche Breite und Höhe zu verwenden.

Bei der zweiten Funktion hat man die Wahl zwischen smooth shading (GL_SMOOTH) und flat shading (GL_FLAT). Gemeint ist die "Nuancierung von Farben" oder "Schattierung". Smooth Shading benötigt natürlich mehr Rechenleistung.

void gl_Enable(GLenum cap) dient dazu bestimmte GL "Fähigkeiten" zu aktivieren. Dazu gehören zum Beispiel Nebel, Licht und auch GL_DEPTH_TEST. Wir wollen nachher noch dreidimensionale Objekte erstellen. Dazu brauchen wir den Depthbuffer (Depth = Tiefe). Und diesen aktivieren wir hier schon einmal.

OpenGL ist ziemlich Mathelastig. Dies wird spätestens jedem klar, der den Begriff Matrizen jetzt zum ersten Mal liest. Das ist Mathe. Aber halb so wild. OpenGL nimmt uns da ziemlich viel Arbeit ab. Mit glMatrixMode(GL_PROJECTION); setzen wir die Projektionsmatrix. Sie dient für die Projektion der 3D-Koordinaten auf 2D-Bildschirmkoordinaten.
Zwei Funktionen später wird die Modelviewmatrix gesetzt. Das ist unsere Matrix zum Zeichnen und für die Ansicht.

gluPerspective(...); ist ein bisschen knifflig. Erstmal der Prototyp:
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
Fangen wir mal hinten an. zFar und zNear sind ziemlich einfach zu erklären. Mit zFar wird die Tiefe des Bildschirmes angesehen. Wenn zFar zum Beispiel 100.0 ist, dann geht der Bildschirm 100 Einheiten in die Tiefe. Ein Objekt, das also bei z = 50.0 würde gesehen werden. Eines bei z = 101.0 dann nicht mehr. Bei einer höheren Tiefe werden natürlich auch mehr Rechnerleistungen benötigt.
Das andere ist zNear. Diese gibt die Z-Koordinate an, ab der Objekte sichtbar sind. Ich denke das muß nicht weiter erläutert werden.
Vielleicht wird es hier mal Zeit, das Koordinatensystem vorzustellen.

Es besteht, wer hätte es gedacht, aus drei Achsen. Der X-, der Y- und der Z-Achse. Die X-Achse verläuft von links nach rechts, also von -x nach +x. Die y-Achse von unten nach oben und die Z-Achse von hinten nach vorne.
Das ist wohl die einzige Sache, die einem Probleme bereiten könnte. Nehmen wir an wir haben zwei Objekte, ein A und ein B und sei z(X) die Z-Koordinate und z(A) > z(B). Also liegt A vor B in Richtung des Betrachters. Sei V der Betrachter, dann läge A, wenn z(A) < z(V) vor dem Betrachter. Es wäre also sichtbar. z(A) > z(V) befände sich allerdings hinter dem Betrachter. Es wäre nicht sichtbar. Die Vorstellung mag ein bisschen komisch sein zu Beginn, man gewöhnt sich aber schnell daran. Achja, je kleiner die Position auf der Z-Achse wird, umso kleiner wird das Objekt. Verständlich.

Nach dieser kleinen Exkursion zurück zur eigentlichen Funktion. Der Parameter fovy ist der Blickwinkel in die Y-Richtung, daher "field of view in the y-direction".
Das Verhältnis des "fovy" in X-Richtung wird an zweiter Stelle, aspect, angegeben. Dieses Verhältnis und das Verhältnis von X zu Y, oder einfacher, von Breite zu Höhe. Unsere Breite ist 640 und unsere Höhe 480. Also folgt daraus das Verhältnis 640/480. Sei das Verhältnis aspect = 2.0, dann wäre der Blickwinkel des Betrachters doppelt so weit in X-Richtung als in Y-Richtung.

void glClear(GLbitfield mask); löscht die angegebenen Buffer. Hier sind der Color-Buffer (GL_COLOR_BUFFER_BIT) und der Depth-Buffer (GL_DEPTH_BUFFER_BIT) zum löschen angegeben.

Und dann ist da noch void glLoadIdentity(void);. Durch diese Funktion wird die aktuelle Matrix durch die Einheitsmatrix ersetzt. Wem das jetzt zuviel Mathe wird, der sollte sich einfach merken, daß damit die Betrachter Position auf "Anfang" gesetzt wird. Vielleicht ist das nicht ganz treffend, aber eine einfachere Vorstellung.

Bisher nichts erkennbares geschehen. Also machen wir uns mal daran, ein erstes Primitive zu zeichnen.

3.2 Die Primitives & zeichnen

Wir werden nun fünf neue Funktionen kennenlernen, die ich erstmal einzelnd vorstelle.
Da wären zu allererst: void glBegin(GLenum mode); und void glEnd(void); Mit Hilfe dieser beiden Funktionen werden einzelne Primitives oder eine Gruppe von Primitives abgegrenzt.
Diese Primitives werden durch die Eckpunkte, deren Koordinaten mit glVertexf(); angegeben werden. Nochmal zurück zur glBegin(). Der Parameter der Funktion gibt an, wie die Eckpunke zu interpretieren sind.
GL_TRIANGLES behandelt somit immer 3 Eckpunkte als Dreieck, GL_QUADS als Viereck und GL_POLIGONS als Vielecke.

GL_QUADS
GL_TRIANGLES
GL_POLYGON

Es gibt da noch weitere, die ich vielleicht im Laufe des Tutorials vorstellen werde.
Für glVertex(); gibt es mehrere Möglichkeiten, je nachdem wie die Eckpunkte angegeben werden. Also zum Beispiel als Integer oder Double. Ich habe mir angewöhnt ausschließlich Float zu verwenden, und auch nur mit diesen werde ich in diesem Tutorial arbeiten.
Der Prototyp zum Angeben der Eckpunkte lautet also: void glVertex3f(GLfloat x, GLfloat y, GLfloat z); Die vierte Funktion ist die void SDL_GL_SwapBuffers(void); Als wir nur SDL verwendet haben, gab es auch eine Funktion, um den Framebuffer umzuschalten. So auch hier. Diese Funktion updatet aber auch gleich noch das Display dazu.

glTranslate(); ist ähnlich wie glVertex();. Doch nur unter der Betrachtung, daß es mehrere Möglichkeiten dieser Funktion gibt. Nämlich Float oder Double Koordinaten. Ich werde wieder nur Float verwenden.
So sieht das dann aus: void glTranslatef(GLfloat x, GLfloat y, GLfloat z);
Mit dieser Funktion bewegt man den Koordinatenursprung an einen bestimmten Punkt (x,y,z). Diese nützliche Funktion wird uns noch oft begegnen, daher ist es wichtig sie jetzt schon zu verstehen. Nehmen wir mal an, der Aktuelle Koordinatenursprung sitz bei (0.0, 0.0, 0.0). Das tut er natürlich immer, betrachtet in diesem System. Doch nehmen wir an, es existiert ein Koordinatensystem A in einem Koordinatensystem B. Dann ist die Ursprungsposition von A in B (0.0, 0.0, 0.0). Jetzt "translaten" wir diesen Ursprung weiter in den Bildschirm rein, sprich: Z wird kleiner.
glTranslatef(0.0f, 0.0f, -1.0f); setzen wir nun den Ursprung von A nach (0.0, 0.0, -1.0). Würden wir die gleiche Funktion nocheinmal aufrufen, dann fände man den Ursprung bei (0.0, 0.0, -2.0) wieder. Will heißen, glTranslatef(); versetzt den Ursprung relativ.

glLoadIdentity();
glTranslatef(0.0f, 0.0f, -1.0f);
Die Betrachterposition bleib davon unbeeinflusst.
Dann zeichnen wir mal eine Triangle.

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
int main(int argc, char** argv)
{
  /* SDL initiieren */
  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(640, 480, 16, SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* OpenGL initiieren */
  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1, 100.0);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();

  glTranslatef(0.0f, 0.0f, -10.0f);

  /* Triangle zeichnen */
  glBegin(GL_TRIANGLES);
    glVertex3f( 0.0f,  1.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 0.0f);
    glVertex3f( 1.0f, -1.0f, 0.0f);
  glEnd();

  /* Framebuffer umschalten */ 
  SDL_GL_SwapBuffers();

  SDL_Delay(2000);
  return 0;
}
Und das Ergebnis sieht wie folgt aus:

Das sieht doch mal toll aus. Doch wie funktioniert das mit den Eckpunkten.
Um die Triangle zu zeichnen haben wir das hier verwendet:

  glBegin(GL_TRIANGLES);
    glVertex3f( 0.0f,  1.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 0.0f);
    glVertex3f( 1.0f, -1.0f, 0.0f);
  glEnd();
Es werden drei Eckpunkte bestimmt, wobei der erste, die Spitze der Triangle ist, der zweite die linke untere und der dritte die rechte untere Ecke ist.

Man bedenkte, daß der Ursprung vorher auf z = -10.0 gesetzt, also in den Bildschirm, wurde, um überhaupt etwas sehen zu können

Für ein Viereck (GL_QUADS) müsste man noch einen vierten Eckpunkt angeben. Man sollte sich schon zu Beginn die Reihenfolge der Eckpunkte angewöhnen. Also nicht, daß man bei einem Viereck links unten anfängt, und bei dem nächsten rechts oben. Ich habe mir angewöhnt ganz links oben zu beginnen, und dann gegen den Urzeigersinn zu laufen.

3.3 Farben

Alles was wir zeichnen werden, wäre weiß, wenn man Shapes nicht auch colorieren könnte. Viel ist dazu nicht nötig, lediglich die Funktion void glColor3f(GLfloat red, GLfloat green, GLfloat blue);. Auch hier gibt es weitere Varianten neben Float. Es besteht die Möglichkeit ein komplettes Shape in einer Farbe zu colorieren oder den Eckpunkten unterschiedliche Farbwerte zu geben, um somit einen Farbverlauf zu erzeugen. Die drei Parameter sind die RGB-Kanäle. 1.0 ist die höchste Farbintensität, und 0.0, die tiefste:
  glBegin(GL_TRIANGLES);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f( 0.0f,  1.0f, 0.0f);
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 0.0f);
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f( 1.0f, -1.0f, 0.0f);
  glEnd();
Hier sind die drei Ecken unserere Triangle in Rot, Grün und Blau gefärbt. Schauen wir uns das Ergebnis an.

mit smooth shading
(GL_SMOOTH)
mit flat shading
(GL_FLAT)

Hier erkennt man nun deutlich den Unterschied zwischen "Smooth Shading" und "Flat Shading".

Das wars erstmal mit Farben. Spannender wird es, wenn wir jetzt unser erstes 3D-Objekt erzeugen.

3.4 3D-Objekte

Als ich mit OpenGL angefangen hatte, musste ich feststellen, daß ich ungeheure Probleme hatte mir Objekte im dreidimensionalen vorzustellen. Anfangs habe ich mir immer Skizzen gemacht. Das machen wir auch bei unserem Würfel, denn wir jetzt basteln. Ein Würfel hat sechs Flächen und acht Eckpunkte, die ich hier mal nummeriert habe.

Wir fangen vorne an, und zeichnen die Fläche (1, 2, 3, 4), außerdem weisen wir gleich eine Farbe für das ganze Viereck zu.

  glBegin(GL_QUADS);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f(-1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f, 1.0f, 1.0f);
    /*...*/
Das ist das vorderste Viereck. Breite und Höhe jeweils 2 Einheiten (von -1 bis 1), und 1 Einheit in Richtung Betrachter verschoben. Das Shape wurde hier nicht mit glEnd(); abgeschlossen, da noch weitere folgen werden, und man auch ganze Gruppen einschließen kann., Die zweite Fläche, die wir zeichnen wollen hat die gleichen X- und Y-Koordinaten bis auf die Z-Koordinate, die wir auf -1 rücken werden:
   /*...*/
   glColor3f(0.0f, 1.0f, 0.0f);
   glVertex3f(-1.0f, 1.0f,-1.0f);
   glVertex3f(-1.0f,-1.0f,-1.0f);
   glVertex3f( 1.0f,-1.0f,-1.0f);
   glVertex3f( 1.0f, 1.0f,-1.0f);
   /*...*/
Das ist Fläche (5, 6, 7, 8). Das Prinzip ist bei jeder Fläche das gleiche, sodaß ich nur noch die linke Seite vorstellen werde, und man den Rest, je nach Belieben selbst machen kann.
   /*...*/
   glColor3f(0.0f, 0.0f, 1.0f);
   glVertex3f(-1.0f, 1.0f, 1.0f);
   glVertex3f(-1.0f, 1.0f, 1.0f);
   glVertex3f(-1.0f,-1.0f,-1.0f);
   glVertex3f(-1.0f,-1.0f,-1.0f);
   /*...*/
Soweit so gut, hier der vollständige Code:
#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>

/* Funktion zum Zeichnen des Würfels
int DrawCube()
{
 glBegin(GL_QUADS);

  /* Vorderseite (1, 2, 3, 4) */
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f(-1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f, 1.0f, 1.0f);

  /* Rückseite (5, 6, 7, 8) */
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f,-1.0f);
    glVertex3f(-1.0f,-1.0f,-1.0f);
    glVertex3f( 1.0f,-1.0f,-1.0f);
    glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Linke Seite (1, 2, 6, 5) */
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f(-1.0f,-1.0f, 1.0f);
    glVertex3f(-1.0f,-1.0f,-1.0f);
    glVertex3f(-1.0f, 1.0f,-1.0f);

  /* Rechte Seite (4, 3, 7, 8) */
    glColor3f(0.0f, 1.0f, 1.0f);
    glVertex3f( 1.0f, 1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f,-1.0f);
    glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Decke (5, 1, 4, 8) */
    glColor3f(1.0f, 1.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f,-1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f( 1.0f, 1.0f, 1.0f);
    glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Boden (6, 2, 3, 7) */
    glColor3f(1.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f,-1.0f,-1.0f);
    glVertex3f(-1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f, 1.0f);
    glVertex3f( 1.0f,-1.0f,-1.0f);

  glEnd();
  return 0;
}

int main(int argc, char** argv)
{
  SDL_Event event;
  /* SDL initiieren */
  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(640, 480, 16, SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* OpenGL initiieren */
  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1, 100.0);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();

  glTranslatef(0.0f, 0.0f, -10.0f);

  while(1) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    DrawCube();
    SDL_GL_SwapBuffers();
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	  if (event.key.keysym.sym == SDLK_ESCAPE)
	    exit(0);
      }
    }
  }

  SDL_Delay(2000);
  return 0;
}
Das sollte eigentlich alles bekannt sein. Ich habe noch die Eventloop hinzugefügt, da wir diese gleich brauchen werden. Ich erkläre noch kurz einen Codeteil, bevor wir zum nächsten Kapitel überschreiten:
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    DrawCube();
    SDL_GL_SwapBuffers();
In unserer Eventloop, wird bei jedem Schleifendurchlauf der Color- und der Depth-Buffer gelöscht, anschließend der Cube gezeichnet und der Framebuffer umgeschaltet. Der Cube wird also immer wieder "erneuert", was eine Rolle spielt, im nächsten Kapitel. Nämlich Rotation.

3.5 Rotation

Wir haben zwar einen Würfel erzeugt, doch sehen wir nicht viel von ihm, wenn wir als auf die Vorderseite starren. Um ihn richtig geniessen zu können müsste man alle Seiten betrachten können. Aus diesem Grund lassen wir ihn rotieren. Dazu brauchen wir nicht mehr als void glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);.
Zu den Parametern:
angle ist der Winkel um den wir in eine Richtung drehen. x, y und z gibt einen Punkt an. Um den Vektor von Ursprung zu diesem Punkt wird gedreht. Lassen wir den Würfel mal mit 0.1° um die X- und Y-Achse drehen.
  while(1) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    DrawCube();
    SDL_GL_SwapBuffers();
    glRotatef(0.1f, 1.0f, 1.0f, 0.0f);
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	  if (event.key.keysym.sym == SDLK_ESCAPE)
	    exit(0);
      }
    }
  }
Das wars auch schon. Kurz und schmerzlos.

Ohne größere Umstände könnte man den Würfel auf bestimmten Tastendruck rotieren lassen. Doch Vorsicht. Wenn man rotiert, dann wird die aktuelle Matrix mit der Rotationsmatrix multipliziert. Will heißen, daß man das komplette System rotiert. Erzeugt man einen weiteren Würfel neben dem bereits existierenden, dann würde er auch mit rotiert werden.

3.6 Texture Mapping

Langsam beginnt es interessanter zu werden. Wir wollen jetzt unseren Würfel texturieren. Dazu müssen wir eine Textur laden und diese auf die Seiten des Würfels mappen. Wieder benutzen wir eine SDL-Funktionen. Nämlich die, ein Bild zu laden. Dazu schreiben wir uns eine neue Funktion zusammen:

  int LoadTexture(char* filename)
  {
    /* Eine Bitmap laden */
    SDL_Surface* img;
    img = SDL_LoadBMP("texture.bmp");
    if (!img) {
      printf("Error: %s\n", SDL_GetError());
      exit(1);
    }
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, img->w, img->h, 0, GL_BGR, GL_UNSIGNED_BYTE, img->pixels);

    return 0;
  }
Das Array texture wurde global deklariert. Mit dieser Funktion laden wir unsere Textur. Neu hier ist nur der Fettgedruckte Teil:

    /*...*/
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, img->w, img->h, 0, GL_BGR, GL_UNSIGNED_BYTE, img->pixels);
    /*...*/
void glGenTextures(GLsizei n, GLuint *textureNames); vergibt an alle Texturen automatisch eine eindeutige Bezeichnung. n ist dabei die Anzahl der Texturen, und textureNames Das Array in dem die Bezeichnungen liegen. Darauf folgt void glBindTexture(GLenum target, GLuint textureName);. Diese Funktion dient zu zwei Zwecken. Wenn wir eine Texture verwenden, die noch nicht generiert wurde, dann wird diese generiert und die Dimension wird festgelegt. Hier eine 2D-Textur (GL_TEXTURE_2D), also eine mit Beite und Höhe. Sollte die Textur bereits generiert worden sein, dann wird sie aktiviert. Und zu gute Letzt setzen wir noch einige Eigentschaften unserer Textur:
void glTexParameteri(GLenum target, GLenum pname, const GLint params); Wie bei glBindTexture() geben wir mit dem ersten Parameter target die Ziel-Textur, entweder GL_TEXTURE_1D oder GL_TEXTURE_2D, an. pname erwartet den symbolischen Namen eines Texturparameters. Möglich ist einer der folgenden:
GL_TEXTURE_MIN_FILTER und GL_TEXTURE_MAG_FILTER für Filtermethode bei Verkleinerung bzw. Vergrößerung
und GL_TEXTURE_WRAP_S und GL_TEXTURE_WRAP_T als Verhalten der Textur in s bzw. t Richtung.
Für jeden dieser Texturparameter gibt es bestimmte Werte params. Hier eine kleine Übersicht:

TexturparameterWert
GL_TEXTURE_MIN_FILTER GL_NEAREST
GL_LINEAR
GL_NEAREST_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_LINEAR
GL_TEXTURE_MAG_FILTER GL_NEAREST
GL_LINEAR
GL_TEXTURE_WRAP_S
GL_TEXTURE_WRAP_T
GL_REPEAT
GL_CLAMP

Zu den Werten:

GL_NEARESTDas Texel, welches am nächsten an der Mitte des Pixels ist wird verwendet
GL_LINEARweist den Mittelwert des nächstliegenden 2x2-Texelquadrats zu
GL_REPEATTextur wird in s bzw. t Richtung zyklisch wiederholt
GL_CLAMPnur die äußersten Spalten werden in s bzw. t Richtung fortgesetzt

Diese Textur wurde für die folgende Übersicht verwendet.

GL_NEAREST
GL_LINEAR
Es ist deutlich zu erkennen, daß die Qualität mit GL_LINEAR besser ist. Allerdings geht es auf die Kosten der Geschwindigkeit.
GL_TEXTURE_WRAP_S : GL_CLAMP
GL_TEXTURE_WRAP_T : GL_CLAMP
GL_TEXTURE_WRAP_S : GL_CLAMP
GL_TEXTURE_WRAP_T : GL_REPEAT
GL_TEXTURE_WRAP_S : GL_REPEAT
GL_TEXTURE_WRAP_T : GL_CLAMP
GL_TEXTURE_WRAP_S : GL_REPEAT
GL_TEXTURE_WRAP_T : GL_REPEAT

Die letzte benötigte Funktion ist diese hier: void glTexImage2D(GLenum target, GLint level, GLint components, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);. Nachdem wir unsere Daten eingelesen haben speichern wir diese in unserem Textur Objekt. Zu den Parametern:

targetdas Texturtarget
level Das Detaillevel. 0 ist das Bilddetaillevel. Level n ist das nte Mipmap reduzierte Bild.
componentsDie Zahl der Farbbestandteile (1, 2, 3 oder 4)
width Die Breite der Textur (eine 2er Potenz)
height Die Höhe der Textur (eine 2er Potenz)
border Die Breite des Rahmens (entweder 0 oder 1)
format Das Format der Pixel Daten (GL_COLOR_INDEX, GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_RGB, GL_RGBA, GL_LUMINANCE, oder GL_LUMINANCE_ALPHA)
type Der Datentyp der Pixel Daten (GL_UNSIGNED_BYTE, GL_BYTE, GL_BITMAP, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT oder GL_FLOAT);
pixels Ein Zeiger auf die Bilddaten.
Analysieren wir die Funktion also mit den gegebenen Argumenten. target ist wie zuvor eine 2D Textur, also GL_TEXTURE_2D. Dann verwenden wir das Bilddetaillevel mit 0. Die Zahl der Farbbestandteile (components) ist bei uns 3, die Breite(width) und Höhe (height) soll natürlich die unseres geladenen Bildes sein genauso wie die Bilddaten als letzten Parameter (pixels). Einen Rahmen haben wir nicht, daher ist border 0. Da die Pixeldaten einer Bitmap immer BGR abgespeichert werden, also blauer Kanal, grüner Kanal und roter Kanal müssen wir unsere Pixelformat formatauch entsprechend als GL_BGR angeben. Und type ist ganz unspektakulär der Datentp der Daten.

Ein kleiner Hinweis noch: glTexImage2D(); kann nur solche Bilder verarbeiten, deren Breite und Höhe eine 2er Potenz ist. Möglich wären somit zum Beispiel 2x2, 4x4, 32x64, 128x512.

Jetzt haben wir die Funktion, eine Bitmap zu laden und eine Textur zu erstellen. Das werden wir in unseren Rotierenden Würfel einbauen:

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>

int texture;

/* Funktion zum Laden der Texturen */
int LoadTexture(char* filename)
{
    /* Eine Bitmap laden */
    SDL_Surface* img;
    img = SDL_LoadBMP("texture.bmp");
    if (!img) {
      printf("Error: %s\n", SDL_GetError());
      exit(1);
    }
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, img->w, img->h, 0, GL_BGR, GL_UNSIGNED_BYTE, img->pixels);

    return 0;
}

/* Funktion zum Zeichnen des Würfels */
int DrawCube()
{
  glBindTexture(GL_TEXTURE_2D, texture);

  glBegin(GL_QUADS);

  /* Vorderseite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);

  /* Rückseite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Linke Seite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);

  /* Rechte Seite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Decke */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Boden */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f,-1.0f,-1.0f);

  glEnd();
  return 0;
}

int main(int argc, char** argv)
{
  SDL_Event event;
  /* SDL initiieren */
  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(640, 480, 16, SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* OpenGL initiieren */
  LoadTexture("texture.bmp");

  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1, 100.0);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();

  glTranslatef(0.0f, 0.0f, -10.0f);

  while(1) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    DrawCube();
    SDL_GL_SwapBuffers();
    glRotatef(0.1f, 1.0f, 1.0f, 0.0f);
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	  if (event.key.keysym.sym == SDLK_ESCAPE)
	    exit(0);
      }
    }
  }
  return 0;
}
Hier begegnen uns weitere neue Funktionen. Nicht sehr neu sollte glEnable(GL_TEXTURE_2D); sein. Wir müssen das 2D-Texturing aktivieren.
Neu hingegen ist void glTexCoord2f(GLfloat s, GLfloat t);. Endlich kommt auch das ominöse s und t ins Spiel, wovon vorhin bei den Texturparametern die Rede war.
Mit dieser Funktion geben wir die Textureckpunkte an jedem Eckpunkt unseres Würfels an.

Greifen wir uns mal nur die Vorderseite heraus:

    /*...*/
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);
    /*...*/
Der Texturpunkt (0,0) liegt links unten, und wird auf den linken unteren Eckpunkt der Vorderseite gelegt. (1,0) auf den rechten unteren. So wird die Textur einmal auf die Vorderseite gelegt. Nun kann es vorkommenm, daß eine Shape unheimlich groß ist, und die Textur im Vergleich winzig. Würde man in diesem Fall die Textur einmal auf das Shape legen, dann wäre es ziemlich verzogen. Dann kann es gewünscht sein, diese Textur mehrmals, zum Beispiel zweimal auf das Shape zu legen. also nicht den Texturpunkt (0,1) auf die rechte untere Seite des Shapes, sondern (0,2). Logisch, oder nicht. Gleiches gilt für Texturen die größer als das Shape sind: z.B. (0,0.5) auf den Eckpunkt. Ich empfehle selbst ein wenig damit herumzuspielen.

Vorhin hatte ich bereits erwähnt, daß glBindTexture(); zwei Funktionen hat. Zum einen das Generieren einer Textur und zum anderen das Aktivieren, wenn bereits generiert. Und letzteres ist der Fall. Die Textur wird hier aktiviert.

Das Resultat von allem sieht so aus:

3.7 Bewegung im dreidimensionalen Raum

Jetzt wollen wir es wagen, uns in einem dreidimensionalen Raum, wie es in jedem 3D-Shooter der Fall ist, mal abgesehen von der Kollisionsabfrage, zu bewegen. Wer aufmerksam gelesen hat, dem sollte es ein Leichtes sein, sich das selbst zu erarbeiten. Für alle andere stelle ich den Weg hier vor.
Was brauchen wir dazu, und was haben wir schon? Das und ein wenig Mathematik bescheren uns eine tolle Bewegung. Ein Punkt ist nochmal genauer hervorzuheben. Die Rotation. Wenn wir rotieren, dann rotieren wir das gesamte System. Das heißt, wenn man nach links rotiert, dann rotieren die Achsen mit, und eine Bewegung in den Bildschirm hinein, also entlang der Z-Achse würde uns eine völlig andere, als die gewünsche, Bewegungsrichtung bescheren. Das soll aber nicht sein, wenn man sich gescheit bewegen will. In jedem Shooter ist das gleich: dreht man sich nach Links, dann wird in Wirklichkeit das komplette System nach rechts gedreht, und zwar um die Y-Achse, bewegt man sich nach Vorne, dann auf der neu errechneten Achse, bedingt durch den Rotationswinkel.
Kompliziert, aber Bilder helfen:

Hier ist das System nicht rotiert.
Eine Bewegung von Q in Richtung P verliefe somit Problemlos, da z = QP.
Das System ist um den Winkel α gedreht. Hier allerdings - eigentlich falsch, aber von dem gleichen System aus betrachtet irrelevant - dargestellt, als Drehung des Betrachters.
Um sich von Q in Richtung P zu bewegen, bedienen wir uns sin und cos. Auf diesem Wege bekommen wir ein Steigungsdreieck auf QP. In Abhängigkeit eines Faktors (einer Bewegungsgeschwindigkeit) bewegen wir uns von Q nach P auf QP.
Für uns wäre das eine Bewegung nach x + sin(α) und z + cos(α), aber wie gesagt: Das System bewegt sich, nicht der Betrachter und dasentgegengesetzt der scheinbaren Bewegung des Betrachter. Sind wir zum Beispiel um 10° nach rechts gedreht (eigentlich das System um 10° nach links), dann scheinen wir uns, beim Vorwärtsgehen um sin(10) * FAKTOR nach rechts und cos(10) * FAKTOR nach vorne in den Bildschirm hinein zu bewegen.
Doch das System bewegt sich um sin(10) * FAKTOR nach links und cos(10) * FAKTOR nach aus dem Bildschirm heraus. Oder um das ganz genau auszudrücken:
Wir setzen den neuen Koordinatenursprung auf x = x - sin(10) * FAKTOR und z = z + cos(10) * FAKTOR.

Wir werden die Sinus- und Cosinus-Funktion aus der math.h verwenden. Problematisch ist dabei, daß beide Funktionen das Bogenmaß erwarten, wir aber erst einmal nur die Gradzahl besitzen.
Also müssen wir diese in das Bogenmaß umrechen. Die Formel dazu lautet x = α * π / 180

Die Bewegungstasten werden w, a, s und d sein. Außerdem werden wir eine Maussteuerung einbauen.

Womit fängt man nun am besten an? Ich denke einen Raum kann jeder selbst erzeugen, und daher beginnen wir mit der Maussteuerung. Dazu kann es hilfreich sein, einen Blick in die Eventloop, speziell die SDL_Event Struktur zu werfen. Ein Element in dieser Struktur nenn sich motion. Dieses Element beinhaltet die relative Bewegung der Maus in X- und Y-Richtung.
Dazu die Eventloop:

while(1) {

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  Draw();
  SDL_GL_SwapBuffers();

  while(SDL_PollEvent(&event)) {
    switch(event.type) {
      case SDL_QUIT: exit(0);
      case SDL_MOUSEMOTION:
        roty += (float) event.motion.xrel / 5;
	rotx += (float) event.motion.yrel / 5;
	glLoadIdentity();
        glRotatef(rotx, 1.0f, 0.0f, 0.0f);
        glRotatef(roty, 0.0f, 1.0f, 0.0f);
        glTranslatef(x, y, z);
    }
  }
  /*...*/
Wir errechnen unseren Rotationswinkel um X- und Y-Achse indem wir die neue relative Bewegung zu dem alten Winkel addieren.
Als nächstes setzen wir unseren Koordinatenursprung zurück, drehen um die Achsen und setzen den neuen Ursprung:

Den Ursprung neu zu setzen vereinfacht einiges. So brauchen wir bedingt durch die Bewegung die neue Ursprungsposition ausrechnen und alles wie gewohnt Zeichnen, und nicht die Position jedes Shapes neu zu bestimmen. Die Koordinaten des neuen Ursprungs (x, y, z) werden bei jeder Bewegung errechnet.
Zu diesen Bewegungen kommen wir jetzt:

  /*...*/
  key = SDL_GetKeyState(NULL);

  /* Vorwärts */
  if (key[SDLK_w]) {
    glLoadIdentity();
    x -= (float) sin(roty*PI/180) * MOVESPEED;
    z += (float) cos(roty*PI/180) * MOVESPEED;
    glRotatef(rotx, 1.0, 0.0, 0.0);
    glRotatef(roty, 0.0, 1.0, 0.0);
    glTranslatef(x, y, z);
  }
  /* Rückwärts */
  if (key[SDLK_a]) {
    glLoadIdentity();
    x -= (float) sin((roty-90)*PI/180) * MOVESPEED/2;
    z += (float) cos((roty-90)*PI/180) * MOVESPEED/2;
    glRotatef(rotx, 1.0, 0.0, 0.0);
    glRotatef(roty, 0.0, 1.0, 0.0);
    glTranslatef(x, y, z);
  }
  /* Seitwärts rechts */
  if (key[SDLK_s]) {
    glLoadIdentity();
    x += (float) sin(roty*PI/180) * MOVESPEED;
    z -= (float) cos(roty*PI/180) * MOVESPEED;
    glRotatef(rotx, 1.0, 0.0, 0.0);
    glRotatef(roty, 0.0, 1.0, 0.0);
    glTranslatef(x, y, z);
  }
  /* Seitwärts links */
  if (key[SDLK_d]) {
    glLoadIdentity();
    x += (float) sin((roty-90)*PI/180) * MOVESPEED/2;
    z -= (float) cos((roty-90)*PI/180) * MOVESPEED/2;
    glRotatef(rotx, 1.0, 0.0, 0.0);
    glRotatef(roty, 0.0, 1.0, 0.0);
    glTranslatef(x, y, z);
  }

  if (key[SDLK_ESCAPE])
    exit(0);
}
PI wurde zu Beginn definiert (#define PI 3.141592654) genauso wie MOVESPEED (#define MOVESPEED 0.02).
Hier berechnen wir die Bewegung in X- und Z- Richtung. Je nach Bewegung wird subtrahiert oder addiert. Anschließend rotieren wir wieder und setzen den Ursprung neu.
Bei den Seitwärtsbewegungen mit a uns d ziehen wir von unserem Rotationswinkel um Y (roty) 90° ab, da wir uns bei Seitwärtsbewegungen um 90° gedreht nach vorne Bewegen würden.

Hier der komplette Source, mit dem Raum, den ich mir entworfen habe:

#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

/* Filter Methode */
#define FILTER GL_LINEAR
//#define FILTER GL_NEAREST

#define PI 3.141592654
#define MOVESPEED 0.02

/* Das Texturen Array für drei Texturen*/
GLuint texture[3];

/* Der Raum wird gezeichnet */
int Draw()
{
  glBindTexture(GL_TEXTURE_2D, texture[0]);
  glBegin(GL_QUADS);
  /* Vorderseite (1, 2, 3, 4) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-10.0f, 3.0f, 10.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 1.0f); glVertex3f( 10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f( 10.0f, 3.0f, 10.0f);

  /* Rückseite (5, 6, 7, 8) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-10.0f, 3.0f,-10.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-10.0f,-1.0f,-10.0f);
    glTexCoord2f(7.0f, 1.0f); glVertex3f( 10.0f,-1.0f,-10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f( 10.0f, 3.0f,-10.0f);

  /* Linke Seite (1, 2, 6, 5) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-10.0f, 3.0f, 10.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 1.0f); glVertex3f(-10.0f,-1.0f,-10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f(-10.0f, 3.0f,-10.0f);

  /* Rechte Seite (4, 3, 7, 8) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 10.0f, 3.0f, 10.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 1.0f); glVertex3f( 10.0f,-1.0f,-10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f( 10.0f, 3.0f,-10.0f);
  glEnd();

  glBindTexture(GL_TEXTURE_2D, texture[1]);
  glBegin(GL_QUADS);
  /* Decke (5, 1, 4, 8) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-10.0f, 3.0f,-10.0f);
    glTexCoord2f(0.0f, 7.0f); glVertex3f(-10.0f, 3.0f, 10.0f);
    glTexCoord2f(7.0f, 7.0f); glVertex3f( 10.0f, 3.0f, 10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f( 10.0f, 3.0f,-10.0f);
  glEnd();

  glBindTexture(GL_TEXTURE_2D, texture[2]);
  glBegin(GL_QUADS);
  /* Boden (6, 2, 3, 7) */
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-10.0f,-1.0f,-10.0f);
    glTexCoord2f(0.0f, 7.0f); glVertex3f(-10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 7.0f); glVertex3f( 10.0f,-1.0f, 10.0f);
    glTexCoord2f(7.0f, 0.0f); glVertex3f( 10.0f,-1.0f,-10.0f);
  glEnd();
  return 0;
}

/* Texturen laden & generieren */
int LoadTexture(char* filename)
{
  /* mit dieser Variablen zählen wir die Textur-ID's hoch */
  static int i = 0;
  SDL_Surface* img;

  img = SDL_LoadBMP(filename);
  if (!img) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }
  
  glGenTextures(1, &texture[i]);
  glBindTexture(GL_TEXTURE_2D, texture[i]);
  
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, FILTER);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, FILTER);
  
  glTexImage2D(GL_TEXTURE_2D, 0, 3, img->w, img->h, 0, GL_BGR, GL_UNSIGNED_BYTE, img->pixels);

  printf("loaded file '%s' as texture %i\n", filename, i);
  i++;
  return 0;  
}

int main(int argc, char** argv)
{
  SDL_Event event;
  float x=0.0f, y=0.0f, z=0.0f, rotx=0.0f, roty=0.0f;
  Uint8* key;

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(1024, 768, 16, SDL_HWSURFACE | SDL_OPENGL | SDL_FULLSCREEN)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* Texturen laden */
  LoadTexture("pics/wand.bmp");
  LoadTexture("pics/decke.bmp");
  LoadTexture("pics/boden.bmp");

  /* OpenGL initiieren */
  glViewport(0, 0, 1024, 768);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 1024/768, 0.1f, 100.0f);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  /* Cursor ausblenden */
  SDL_ShowCursor(0);

  glLoadIdentity();
  glTranslatef(x, y, z);
  
  while(1) {

    /* Raum zeichnen */
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    Draw();
    SDL_GL_SwapBuffers();

    /* Eventloop */
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	      if (event.key.keysym.sym == SDLK_ESCAPE)
	        exit(0);
	      break;
       /* Maussteuerung */
        case SDL_MOUSEMOTION:
          roty += (float) event.motion.xrel / 5;
	  rotx +=  (float) event.motion.yrel / 5;
	  glLoadIdentity();
          glRotatef(rotx, 1.0f, 0.0f, 0.0f);
          glRotatef(roty, 0.0f, 1.0f, 0.0f);
          glTranslatef(x, y, z);
	  break;
      }
    }

    /* Bewegungstasten abfragen */
    key = SDL_GetKeyState(NULL);
    if (key[SDLK_w]) {
      glLoadIdentity();
      x -= (float) sin(roty*PI/180) * MOVESPEED;
      z += (float) cos(roty*PI/180) * MOVESPEED;
      glRotatef(rotx, 1.0, 0.0, 0.0);
      glRotatef(roty, 0.0, 1.0, 0.0);
      glTranslatef(x, y, z);
    }
    if (key[SDLK_a]) {
      glLoadIdentity();
      x -= (float) sin((roty-90)*PI/180) * MOVESPEED/2;
      z += (float) cos((roty-90)*PI/180) * MOVESPEED/2;
      glRotatef(rotx, 1.0, 0.0, 0.0);
      glRotatef(roty, 0.0, 1.0, 0.0);
      glTranslatef(x, y, z);
    }
    if (key[SDLK_s]) {
      glLoadIdentity();
      x += (float) sin(roty*PI/180) * MOVESPEED;
      z -= (float) cos(roty*PI/180) * MOVESPEED;
      glRotatef(rotx, 1.0, 0.0, 0.0);
      glRotatef(roty, 0.0, 1.0, 0.0);
      glTranslatef(x, y, z);     
    }      
    if (key[SDLK_d]) {
      glLoadIdentity();
      x += (float) sin((roty-90)*PI/180) * MOVESPEED/2;
      z -= (float) cos((roty-90)*PI/180) * MOVESPEED/2;
      glRotatef(rotx, 1.0, 0.0, 0.0);      
      glRotatef(roty, 0.0, 1.0, 0.0);
      glTranslatef(x, y, z);
    }
  }
  return 0;
}

Et voilà:

Wer sich für die verwendeten Texturen interessiert, kann sie hier bekommen: download textures.tar.gz - 339 KB

3.8 Blending

Beim Blending werden die Farbwerte neuer Objekte (S) mit den bereits vorhandenen Farbwert im Framebuffer (D) addiert, um Objekte transparent erscheinen zu lassen. Dies geschieht auf diese Weise:
Sei (Sr, Sg, Sb, Sa) der Blending Faktor des Primitives S und (Dr, Dg, Db, Da) der Blending Faktor des Framebuffers. Weiter sei RGBA der Wert von S bzw. D mit dem Index s bzw. d, dann folgt daraus der neue Farbwert:
(Rs * Sr + Rd * Dr, Gs * Sg + Gd * Dg, Bs * Sb + Bd * Db, As * Sa + Ad * Da)

Für ein erfolgreiches Blending muss der Depth-Buffer deaktiviert werden und das Blending aktiviert weden. Die Art und Weise, wie die Farben kombiniert werden, legt man mit void glBlendFunc(GLenum sfactor, GLenum dfactor); fest. sfactor gibt an, wie der Source Blending Faktor errechnet wird, und dfactor für den Destination Blending Faktor. Dazu gibt es mehrere Konstanten, sowohl für Source (S), als auch für Destination (D):

Für die Source (S) und Destination(D):
KonstanteBlending Faktor
GL_ZERO(0, 0, 0, 0);
GL_ONE(1, 1, 1, 1);
GL_SRC_ALPHA(As, As, As, As);
GL_DST_ALPHA(Ad, Ad, Ad, Ad)

Für Source (S):
KonstanteBlending Faktor
GL_DST_COLOR(Rd, Gd, Bd, Ad);
GL_ONE_MINUS_DST_ALPHA(1, 1, 1, 1) - (Ds, Ds, Ds, Ds)
GL_SRC_ALPHA_SATURATE(f, f, f, 1)
wobei f = min(As, 1-Ad)

Für Destination (D):
KonstanteBlending Faktor
GL_ONE_MINUS_SRC_ALPHA(1, 1, 1, 1) - (As, As, As, As)

Als Beispielsource verwende ich den texturierten Würfel aus 3.6 mit einigen kleinen Veränderungen, u.a. einer geeigneteren Textur:

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>

int texture;

/* Funktion zum Laden der Texturen */
int LoadTexture(char* filename)
{
    /* Eine Bitmap laden */
    SDL_Surface* img;
    img = SDL_LoadBMP("texture.bmp");
    if (!img) {
      printf("Error: %s\n", SDL_GetError());
      exit(1);
    }
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, img->w, img->h, 0, GL_BGR, GL_UNSIGNED_BYTE, img->pixels);

    return 0;
}

/* Funktion zum Zeichnen des Würfels */
int DrawCube() 
{
  glBindTexture(GL_TEXTURE_2D, texture);

  glBegin(GL_QUADS);

  /* Vorderseite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);

  /* Rückseite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Linke Seite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);

  /* Rechte Seite */
    glTexCoord2f(0.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Decke */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f,-1.0f);

  /* Boden */
    glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f,-1.0f,-1.0f);

  glEnd();
  return 0;
}

int main(int argc, char** argv)
{
  SDL_Event event;
  /* SDL initiieren */
  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(640, 480, 16, SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* OpenGL initiieren */
  LoadTexture("texture.bmp");

  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_TEXTURE_2D);
//  glEnable(GL_DEPTH_TEST);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1, 100.0);
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();

  glTranslatef(0.0f, 0.0f, -10.0f);

  while(1) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    DrawCube();
    SDL_GL_SwapBuffers();
    glRotatef(0.1f, 1.0f, 1.0f, 0.0f);
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	  if (event.key.keysym.sym == SDLK_ESCAPE)
	    exit(0);
      }
    }
  }
  return 0;
}
Ich räume ein, daß das Ergebnis bauf diesem Screenshot etwas undeutlich zu erkennen ist. Daher sollte es jeder besser selbst ausprobieren.

Der relevante Codeteil, ist dieser:

//  glEnable(GL_DEPTH_TEST);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE);
Um erfolgreich "Blenden" zu können müssen wir den Depth-Buffer deaktivieren, daher wurde der entsprechende Teil hier auskommentiert. Warum man das tun muss ist am deutlichsten, wenn man es mal mit Depth-Buffer ausprobiert. Wir aktivieren Blending und definieren die Kombinationsweise der Farben. Und mehr ist dazu nicht nötig. Doch es gibt einen Haken an der ganzen Sache. Möchte man mit transparenten Objekten und nicht transparenten Objekten arbeiten, dann ist eine bestimmte Reihenfolge des Zeichnens relevant. Zu allererst sin die undurchsichtigen Objekte zu zeichnen, gefolgt von den transparenten, beginnend bei denen mit der größten Entfernung. Berücksichtigt man diese Reihenfolge nicht, dann kann es passieren, daß zum Beispiel undurchsichtige Objekte von transparenten verdeckt werden.

3.9 Partikel Systeme

Mit Partikel Systemen lassen sich tolle Dinge, wie Feuer, Regen, Wasserfontänen, etc. darstellen. In diesem Abschnitt wollen wir uns ein Feuer basteln.
Dabei ist zu bedenken, daß Ein Feuer einen Ursprung hat, an dem ein jeder Partikel in eine Richtung wandert. Nennen wir diesen Partike Funkten. Auch in der Realität ist zu sehen, daß ein solcher Funken, wenn man ihn mit den Augen verfolgt mit der Zeit verschwindet. Er hat praktisch ein Leben. Jeder Partikel hat ein Leben, das mit der Zeit abläuft. Ist ein Partikel "gestorben", so wird er wieder am Usrprung, mit voller Lebensenergie geboren.
Um das zu realisieren muss eine enstsprechende Partikel-Struktur her:

 struct Particle {
    float life;
    float growfac;
    float x;
    float y;
    float mx;
    float my;
    float r;
    float g;
    float b;
  };
Die Struktur besteht aus der Lebensenergie life des Partikels, einem Faktor, um den das Partikel altert, einem X- und Y-Startwert x und y, einer Steigung in X- und Y-Richtung mx und my, und den RGB-Farbwerten r, g, b.
Jeder dieser Werte, bis auf die Lebensenergie, wird zufällig ausgewählt werden. Auch die Farben. Daher benötigen wir ein Array mit Farbwerten, aus dem gewählt werden kann:


float color[5][3] = {
		{1.0, 0.0, 0.0},
		{0.5, 0.0, 0.0},
		{1.0, 0.1, 0.0},
        	{1.0, 0.5, 0.0},
        	{1.0, 1.0, 1.0},
	 };


Hier fünf Farben, aus denen wir wählen werden. Als nächstes benötigen wir eine Funktion, die unsere Parikel zum Leben erweckt:

struct Particle GiveLife(struct Particle p)
{
  int z;
  p.life = 1.0f,
  p.growfac = (float) ((rand() % 10)+2 / 100.0f);
 
  p.x = (float)(rand() % 10) / 100.0f;
  p.y = (float)(rand() % 10) / 100.0f;

  if (rand() % 2 == 1)
    p.mx = (float)((rand() % 10) / 1000.0f) * -1;
  else
    p.mx = (float)((rand() % 10) / 1000.0f);

  p.my = (float)(((rand() % 10) + 1) / 1000.0f);

  z = (float)(rand() % 5);
  p.r = color[z][0];
  p.g = color[z][1];
  p.b = color[z][2];
  return p;
}
Die Funktion erwartet ein einzelner Partikel und gibt ein eines zurück. In der Funktion werden jeder Eigenschaft des Partikels Zufallswerte gegeben.
Als nächstes brauchen wir eine Funktion, die jeden Partikel altern, bewegen und zeichnen lässt.

struct Particle* DrawParticles(struct Particle* p)
{
  int i;
  for (i=0; i < PARTIKELNUM ; i++) {
    p[i].x += p[i].mx;
    p[i].y += p[i].my;

    glColor4f(p[i].r, p[i].g, p[i].b, p[i].life);
    glBindTexture(GL_TEXTURE_2D, texture);
    glBegin(GL_QUADS);
      glTexCoord2f(0.0f, 1.0f); glVertex3f(p[i].x - 0.01f, p[i].y + 0.01f, 0.0f);
      glTexCoord2f(0.0f, 0.0f); glVertex3f(p[i].x - 0.01f, p[i].y - 0.01f, 0.0f);
      glTexCoord2f(1.0f, 0.0f); glVertex3f(p[i].x + 0.01f, p[i].y - 0.01f, 0.0f);
      glTexCoord2f(1.0f, 1.0f); glVertex3f(p[i].x + 0.01f, p[i].y + 0.01f, 0.0f);
    glEnd();

    p[i].life -= p[i].growfac;
    if (p[i].life <= 0.0f)
      p[i] = GiveLife(p[i]);

  }
  return p;
}

In der FOR-Schleife, bewegen wir jeden Partikel um mx und my, zeichnen ihn und dekrementieren anschließend die Lebensenergie um den Alterungsfaktor. Sollte die Lebensenergie unter 0 sein, dann wird der Partikel wiederbelebt:

  p[i].life -= p[i].growfac;
  if (p[i].life <= 0.0f)
    p[i] = GiveLife(p[i]);
Soweit zu den Partikel-Funktionen. Werfen wir einen Blick auf den ganzen Source:

#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define PARTIKELNUM 10000

GLuint texture;

struct Particle { 
  /* siehe oben */ 
};

float color[5][3] = {
		{1.0, 0.0, 0.0},
		{0.5, 0.0, 0.0},
		{1.0, 0.1, 0.0},
        	{1.0, 0.5, 0.0},
        	{1.0, 1.0, 1.0},
	 };

struct Particle GiveLife(struct Particle p)
{
  /* siehe oben */
}

struct Particle* DrawParticles(struct Particle* p)
{
  /* siehe oben */
}

int LoadTexture(char* filename)
{
  /* bekannt */
}

int main(int argc, char** argv)
{
  SDL_Event event;
  struct Particle* p;
  int i;

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  atexit(SDL_Quit);

  if (!SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE | SDL_OPENGL)) {
    printf("Error: %s\n", SDL_GetError());
    exit(1);
  }

  SDL_WM_SetCaption("MeinFenster", "MeinFenster");

  /* Texturen laden */
 LoadTexture("ptex.bmp");

  /* OpenGL initiieren */
  glViewport(0, 0, 640, 480);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE);
  glMatrixMode(GL_PROJECTION);
  gluPerspective(45.0f, (GLfloat) 640/480, 0.1f, 100.0f);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  glTranslatef(0.0f, -0.4f, -1.0f);

  srand((unsigned int)time(0));

  p = (struct Particle*)calloc(PARTIKELNUM, sizeof(struct Particle));
  
  for (i = 0; i < PARTIKELNUM; i++)
    p[i] = GiveLife(p[i]);

  while(1) {
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   p = DrawParticles(p);
   SDL_GL_SwapBuffers();
    /* Eventloop */
    while(SDL_PollEvent(&event)) {
      switch(event.type) {
        case SDL_QUIT: exit(0);
        case SDL_KEYDOWN:
	      if (event.key.keysym.sym == SDLK_ESCAPE)
	        exit(0);
	      break;
      }
    }
  }
  return 0;
}
Fangen wir oben an. Wir müssen einen weiteren Header time.h einbinden, um nachher die Funktion rand(); und srand(); zu verwenden. Kurz darauch haben wir PARTIKELNUM definiert. Dabei handelt es sich um die Anzahl unserer Partikel. 10000 ist eine ganze Menge, doch dafür sieht das Feuer auch etwas besser aus. Die einzelnen Funktionen habe ich oben schon erklärt, und daher sehen wir uns die Main-Funktion genauer an. Zu Beginn deklarieren wir einen Zeiger auf die Partikelstruktur. Es folgt die bereits bekannte Routine zum Fensteraufbau.
Jeder Partikel wird texturiert, und daher laden wir mit LoadTexture("ptex.bmp"); eine Textur, die wir dafür verwenden werden.
In der Manpage zu srand() ist die Funktion in etwa so beschrieben "Die srand() Funktion erwartet einen 'Keim' als Beginn für eine neue Folge von Pseudo-Zufallsganzzahlen, die von rand() zurückgegeben werden.". Und was eignet sich besser als Keim oder Samen als die aktuelle Uhrzeit.
Folgend erzeugen wir unser Partikelarray für PARTIKELNUM Partikel. Jedem Partikel geben wir dann ein Leben. In jedem Schleifendurchlauf unserer Mainloop wird dann jeder Partikel gezeichnet, bis er stirbt, und wiederbelebt wird. Das ganze könne so aussehen.

Ich gebe zu, daß das Feuer nicht sehr realistisch aussehen mag, aber es bietet einen Einblick, in das Mögliche.

4. Links & Literatur

Hier einige sehr hilfreiche Links zu Literatur und weiterführenden Tutorials:

Das Redbook - Programming Guide:
http://www.opengl.org/documentation/red_book_1.0/

Das Bluebook - Reference Manual:
http://www.opengl.org/documentation/blue_book_1.0/

NeHe Gamedevelopment:
http://gamedev.nehe.net

5. Anmerkung des Autors

Ich hoffe, das Tutorial gefällt. Mit der Zeit werde ich es noch um einige Kapitel erweitern.

Bei Fragen, Kritiken oder sonstigen Bemerkungen bitte an nixon@excluded.org oder ggf. das Forum (http://www.excluded.org/board) besuchen.
Gelegentlich bin ich auch im IRC anzutreffen: euirc.net #excluded

Feedback ist übrigens sehr erwünscht ;)

Und natürlich nicht zu vergessene Grüße an maze2k sowie l0om, Sirius, ProXy, FE2k, Takt und DNA und dem Rest des excluded teams - haut rein Jungs :P

MfG
  nixon

"Niemand weiß, wie weit seine Kräfte gehen, bis er sie versucht hat." -- Goethe