Autor: Nicolai 'Prefect' Haehnle
Schon oft wurde ich gefragt, wann ich dieses Thema denn zu behandeln gedenke. Nun, es war langsam an der Zeit, und das letzte Tutorial ist sowieso schon relative lang her…
SDL kann von Natur aus mit OpenGL zusammenarbeiten, und wie alles andere bei SDL ist auch die Zusammenarbeit mit OpenGL äußerst einfach gestaltet. Um genau zu sein gibt es über die eigentliche SDL+OpenGL-Funktionalität gar nicht so viel zu erzählen (ich hab's trotzdem irgendwie geschafft, so viel zu schreiben, daß ich das Tutorial aufspalten mußte).
Genau wie SDL ist OpenGL eine extrem leicht zugängliche API, deren Grundzüge man innerhalb weniger Minuten erlernen kann - im Gegensatz zu einer gewissen konkurrierenden API arbeitet OpenGL für den Programmierer und nicht gegen ihn.
Nein, ich will hier nicht über die Vorzüge von OpenGL gegenüber Direct3D reden (das tu ich sonst schon genug ;)). Es geht mir viel mehr darum: Warum sollte jemand überhaupt eine API wie OpenGL verwenden?
Nun, der erste Grund ist natürlich offensichtlich: OpenGL ermöglicht den Zugriff auf 3D-Beschleunigerhardware. Und dieser Grund wird den meisten auch schon reichen.
Dennoch gibt es einen zweiten Grund zur Nutzung von OpenGL. OpenGL ist nämlich auch für 2D-Spiele nützlich. Hört sich zunächst etwas seltsam an, oder? Es ist nun aber so, daß sich die 2D-APIs seit Jahren technologisch praktisch nicht weiter entwickelt haben. So unterstützen die meisten von ihnen noch nicht einmal ordentliches Alphablending, geschweige denn Alphablending mit Hardwarebeschleunigung. Und genau hier kann OpenGL ansetzen. Was im zweidimensionalen Blits sind, das sind mit OpenGL lediglich texturierte Quads.
Habt ihr euch zum Beispiel schon einmal überlegt, wie ihr die Einheiten in einem Strategiespiel der Spielerfarbe entsprechend einfärben könntet? Auf diese Frage gibt es - außer dem Einfärben in Software und dem Erstellen von n verschiedenen Bitmaps für n verschiedene Farben - mit 2D-APIs keine Antwort. In OpenGL läßt sich dieses Problem ganz einfach in zwei Passes mit Textureinfärbung lösen.
Außerdem erhält man mit OpenGL auch noch frei Haus die Möglichkeit, extrem schnelle Rotationen und Streckungen durchzuführen.
All das ist Grund genug, OpenGL auch für 2D-Spiele zu verwenden.
Ich werde in diesem Tutorial die Grundlage für ein Programm schreiben, das OpenGL für 2D-Zwecke verwendet. Der Code läßt sich allerdings leicht auf die dritte Dimension ausweiten.
Langfristiges Ziel soll es sein, den Code aus dem zweiten Tutorial (also ein einfaches Hintergrundbild ohne zusätzliche Gimmicks) in OpenGL zu realisieren. Mir ist bewußt, daß es SDL selbst für diese simple Anwendung auch tut (um genau zu sein, es wäre sogar besser). Ich will lediglich die Grundlagen für eine Palette erweiterter Anwendungsmöglichkeiten legen.
Bevor wir anfangen, erstmal ein kurzer Einschub zum Kompilieren. Um OpenGL- Funktionen verwenden zu können muß schließlich die nötige Bibliothek in das Programm eingebunden werden.
Um die folgenden Programme zu kompilieren sollte folgendes Makefilestatement verwendet werden:
tut8: tut8.c gcc `sdl-config --libs --cflags` -lGL -Wall -o tut8 tut8.c
Beachtet, daß vor dem „gcc“ ein Tab stehen muß. Beim Copy&Paste aus dem Browser gehen Tabs leicht verloren.
Die umgekehrten Anführungszeichen ` ` um das `sdl-config –libs –cflags` teilen der Shell mit, daß die Ausgabe vom Programm sdl-config als Parameter an gcc weitergeben soll. Nun sollte die Makefile (im Gegensatz zu vorherigen Tutorials) auf allen Systemen, auf denen SDL installiert ist, funktionieren. Man lernt nie aus ;)
Der Parameter “-lGL“ teilt gcc mit, daß die libGL.so mit dem Programm gelinkt werden soll. In dieser Bibliothek finden sich alle OpenGL-Funktionen.
Im ersten Kapitel habe ich geschrieben, ihr sollt ein Projekt vom Typ „Win32-Konsolenanwendung“ erstellen. Das ist natürlich immer noch möglich, aber ich habe dazugelernt.
Ich werde nun Schritt für Schritt eine schönere Methode vorstellen.
Erstellt ein neues Projekt vom Typ „Win32-Anwendung“.
Fügt wie gewohnt die Quellcodedateien in das Projekt ein.
Öffnet noch einmal das Dialogfenster „Dateien in Projekt einfügen“. Wählt als Dateityp „Bibliothekdateien (.lib)“ aus, und fügt die SDLmain.lib in das Projekt ein. Diese Datei sollte sich im gleichen Verzeichnis wie die SDL.DLL und SDL.lib befinden.
Falls ihr schon mit SDL gearbeitet habt könnt ihr diesen Schritt wahrscheinlich überspringen: Wählt im Menü Extras→Optionen aus. Wählt die Karte „Verzeichnisse“ aus. Wählt „Verzeichnisse anzeigen für: Include-Dateien“ und fügt das Verzeichnis mit der SDL.h in die Liste ein.
Wählt „Verzeichnisse anzeigen für: Bibliothekdateien“ und fügt das Verzeichnis mit der SDL.lib in die Liste ein.
Geht im Menü auf Projekt→Einstellungen. Stellt links oben „Einstellungen für: Alle Konfigurationen“ ein. Wählt die Karte „Linker“ aus, und fügt im Eingabefeld „Objekt-/Bibliothek-Module“ sdl.lib und opengl32.lib hinzu.
Wählt die Karte „C/C++“ aus und stellt die Kategorie auf Code-Generation. Wechselt nun zuerst wieder auf „Einstellungen für: Win32 Debug“ und stellt als Laufzeit-Bibliothek „Multithreaded DLL debuggen“ ein. Stellt dann für „Einstellungen für: Win32 Release“ als Laufzeit-Bibliothek „Multithreaded DLL“ ein.
Natürlich müßt ihr dann noch dafür sorgen, daß Windows die SDL.DLL findet, zum Beispiel indem ihr sie in das Verzeichnis mit dem Projekt kopiert.
Nachdem die Formalitäten erledigt sind können wir loslegen. Zunächst einmal müssen wir OpenGL initialisieren. Das wird im folgenden Programm erledigt.
#include <stdlib.h> #include <stdio.h> #include <SDL.h> #include <SDL_opengl.h> int main(int argc, char **argv) { SDL_Surface *screen; int running; if (SDL_Init(SDL_INIT_VIDEO) /* OpenGL-Bildschirmmodus */ SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); screen = SDL_SetVideoMode(640, 480, 0, SDL_OPENGL); if (!screen) { fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n", SDL_GetError()); exit(1); } /* Programmschleife */ running = 1; while(running) { SDL_Event event; while(SDL_PollEvent(&event)) { switch(event.type) { case SDL_KEYDOWN: running = 0; break; case SDL_QUIT: running = 0; break; } } // Pageflip: Back- und Frontbuffer werden vertauscht SDL_GL_SwapBuffers(); } return 0; }
Zum Großteil sollte das Programm bekannt sein. Ein paar Anmerkungen sind trotzdem nötig.
Zunächst wird per #include die Datei SDL_opengl.h eingefügt. Man könnte theoretisch auch GL/gl.h verwenden, aber SDL_opengl.h kümmert sich um verschiedene Probleme, die auf unterschiedlichen Plattformen auftreten: unter Windows muß man z.B. noch die windows.h einbinden, und auf Macs liegen die Headerdateien anscheinend im OpenGL- statt im GL-Verzeichnis (Dank geht an Hellfire für die Info).
Das Einrichten des Bildschirmmodus läuft für OpenGL-Modi etwas anders ab als gewohnt. Vor dem Aufruf von SDL_SetVideoMode müssen wir ein paar Attribute setzen, die im Lauf der Ausführung nicht mehr geändert werden können. Dazu gehört die Farbtiefe, die in den ersten drei Aufrufen zu SDL_GL_SetAttribute gesetzt wird. In diesem Fall handelt es sich um einen 16Bit-Farbmodus, denn jeder Farbkanal hat 5 Bits. Für einen 32Bit-Farbmodus würde man statt 5 logischerweise jeweils 8 übergeben, denn bei 32Bit-Modi hat jeder Farbkanal 8 Bit.
Dann setzt das Programm die Tiefe des Z-Buffers, in diesem Fall 16 Bit. Je mehr Bits hier angegeben werden, desto genauer kann der Z-Buffer arbeiten. Er verbraucht dafür aber natürlich auch mehr Speicher auf der Grafikkarte. Eigentlich kann man den Z-Buffer bei einem Programm, das OpenGL lediglich für 2D-Grafik verwendet auch weglassen. Wobei sich der Z-Buffer sicher für interessante Effekte verwenden läßt, Umrisse von Einheiten hinter Gebäuden wie z.B. bei Age of Empires 2.
Schließlich teilen wir SDL noch mit, daß Doublebuffering gewünscht wird. Für OpenGL-Modi wird nicht mehr wie gewohnt das Flag SDL_DOUBLEBUF an SDL_SetVideoMode übergeben.
Es gibt noch mehr mögliche Attribute, die mit SDL_GL_SetAttribute gesetzt werden können. Dazu gehört unter anderem die Tiefe des Stencilbuffers. Diese Attribute sind alle in der SDL-Dokumentation aufgelistet.
SDL_SetVideoMode wird im Flagsparameter lediglich SDL_OPENGL übergeben. Dies veranlaßt SDL dazu, einen OpenGL-Kontext einzurichten. Im Zusammenhang mit OpenGL macht außerdem natürlich noch das Flag SDL_FULLSCREEN Sinn.
Nicht nur bei der Initialisierung verlaufen einige Dinge anders, wenn man OpenGL verwenden will. Normale SDL-Funktionen, die auf die Bildschirmsurface zugreifen, funktionieren in OpenGL-Modi nicht!
Dies ist eine wirklich grundlegende Tatsache, deswegen schreibe ich das hier lieber nochmal hin. Man kann in einem OpenGL-Modus nicht mehr mit den gewohnten SDL-Funktionen für Blitting usw. arbeiten. Unter anderem bedeutet das, daß SDL_LockSurface, SDL_Blit und SDL_Flip nicht mehr verwendet werden dürfen. Im Grunde ist das für die ersten beiden auch gar nicht notwendig, man hat ja schließlich OpenGL.
Für SDL_Flip braucht man allerdings einen Ersatz, und der heißt SDL_GL_SwapBuffers. SDL_GL_SwapBuffers erfüllt den gleichen Zweck wie SDL_Flip, nur für OpenGL-Modi.
Fairerweise sollte ich erwähnen, daß man SDL_SetVideoMode noch das Flag SDL_OPENGLBLIT übergeben kann. Dieses Flag ermöglicht es, auch in OpenGL-Modi die normalen Blitroutinen zu verwenden. Allerdings handelt es sich hierbei um einen ziemlichen Hack, dessen Performance weit unter dem liegt, was man erreichen kann, wenn man selbst OpenGL-Funktionen statt Blits verwendet. Generell sollte man SDL_OPENGLBLIT also vermeiden.
Genug der grauen Theorie. Das oben aufgelistete Programm sollte sich so wie es ist kompilieren lassen. Man erhält beim Ausführen ein einfaches Fenster, dessen Inhalt höchstwahrscheinlich ziemlicher Müll ist, da wir selber noch nichts in das Fenster hineinzeichnen).
Nun, da die OpenGL-Initialisierung abgeschlossen ist, wird sich meine Audienz wohl teilen ;)
Wer auf der Suche nach Tutorials für 3D-Rendering mit OpenGL ist wird hier nicht fündig werden. Hier gibt es schon exzellente Ressourcen, ganz vorneweg für Anfänger natürlich NeHe. Es gibt auch verstreut eine ganze Menge deutscher Übersetzungen der NeHe-Tutorials im Internet (die aber wahrscheinlich alle auf die gleiche Quelle zurückzuführen sind ;)). Eine Google-Suche nach „NeHe Tutorial Übersetzung“ dürfte da Wunder wirken.
Natürlich muß auch die offizielle OpenGL-Homepage genannt werden: opengl.org
Für die 3D-Programmierer gibt's im nächsten Tutorial vielleicht wieder etwas. Da stelle ich nämlich eine Möglichkeit vor, wie man Texturen aus SDL-Surfaces erstellen kann.
Ansonsten handelt der Rest dieses Tutorials davon, wie man mit OpenGL primitivste 2D-Funktionalität erhält.
Zunächst mag man sich erstaunt die Frage stellen, wie man denn mit einer API, die für 3D-Grafik ausgelegt ist, 2-dimensionales auf den Bildschirm bringen kann. Dabei ist es im Prinzip ganz einfach: man läßt die dritte Dimension, also die Z-Koordinaten, einfach weg.
Dazu wird die Projektion von OpenGL auf orthogonal gesetzt, d.h. es gibt keinen Fluchtpunkt.
Wir müssen OpenGL nun mitteilen, daß wir orthogonale Projektion erwünschen. Es reicht, dies einmal zu Beginn des Programms zu tun (es sei denn, man verändert die Projektion zwischendurch für 3D-Darstellungen).
Den entsprechenden Code füge ich oberhalb der Hauptschleife ein:
} glViewport(0, 0, screen->w, screen->h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, screen->w, screen->h, 0, -1.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); running = 1; while(running) {
Der Befehl glViewport teilt OpenGL mit, in welchem Bereich des SDL-Fensters gezeichnet werden soll. Wenn hier andere Werte eingegeben werden beschränken sich alle Ausgaben auf diesen Bereich (die Parameter sind x,y der linken oberen Ecke und Breite,Höhe des Viewports). Die Ausgaben erscheinen dann quasi gestaucht.
Die nächsten drei Befehle ändern die Projektionsmatrix, so daß sich der Punkt (0,0) in der linken oberen, der Punkt (640,480) in der rechten unteren Ecke befindet - also so, wie man es von 2D-APIs gewohnt ist.
Schließlich wird noch die Model-Matrix auf eine Identitymatrix gesetzt. In 3D-Anwendungen kann man mit der Model-Matrix die auf dem Bildschirm dargestellten Gegenstände drehen und verschieben, während mit der Projektionsmatrix die Kamerastellung geändert wird.
Nun fehlt noch die eigentliche graphische Darstellung. Ich fasse sie in der Funktion RefreshScreen zusammen:
void RefreshScreen() { glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_QUADS); glColor3ub(255, 0, 0); glVertex2i(0, 240); glColor3ub(0, 255, 0); glVertex2i(240, 240); glColor3ub(0, 0, 255); glVertex2i(240, 480); glColor3ub(255, 255, 0); glVertex2i(0, 480); glEnd(); }
Zunächst wird der Bildschirm mit schwarzer Farbe gelöscht. Die Parameter von glClearColor bestimmen natürlich die Farbe als RGBA-Wert. Um den Bildschirm mit rot zu löschen würde man also glClearColor(1.0, 0.0, 0.0, 0.0) verwenden. Erst glClear löscht dann wirklich den Framebuffer. Der Parameter GL_COLOR_BUFFER_BIT gibt an, welche der verschiedenen Buffer (Colorbuffer oder Framebuffer, Depthbuffer, Stencilbuffer, …) gelöscht werden sollen. Da wir nur mit dem normalen Framebuffer arbeiten wird nur das entsprechende Bit übergeben.
Die restlichen Zeilen der Funktion bringen ein Rechteck auf den Bildschirm. In OpenGL werden alle zu rendernden Figuren (Punkte, Linien oder Polygone) zwischen einem Begin/End-Aufrufpaar angegeben. Im Aufruf von glBegin teilen wir OpenGL mit, was für Figuren wir rendern wollen. In diesem Fall sind es Quads, also Vierecke, und für 2D-Zwecke wird dies auch meistens der Fall sein. Es gibt aber noch viele mehr, z.B. Triangles für Dreiecke, Triangle-Strips für zusammenhängende Dreiecke, Polygon für ein Polygon mit beliebig vielen Ecken, Lines für einzelne Linien, usw…
Wenn Quads gerendert werden kann man innerhalb der Begin/End-„Klammer“ beliebig viele Quads zum Rendering angeben.
Diese Funktion bringt nun erstmal ein einziges Quad auf den Bildschirm. Es wird immer zuerst die Farbe des Eckpunkts mit glColor3ub, und dann die Position des Eckpunkts mit glVertex2i angegeben. In der Polygonfläche selbst sorgt OpenGL dann für schöne Farbverläufe.
Übrigens kann man alle oder einzelne Aufrufe von glColor auch weglassen, oder aus der Begin/End-Klammer nach vorne verschieben. Läßt man z.B. das glColor in der zweiten Zeile weg, so werden die beiden oberen Eckpunkte mit der selben Farbe gerendert. Wenn man glColor zweimal aufruft ohne dazwischen ein glVertex einzufügen, so hat der erste Funktionsaufruf keinen Effekt.
Die Aufrufe von glVertex darf man logischerweise nicht weglassen, und man darf sie auch nicht aus der Begin/End-Klammer herausverschieben.
Wie viele andere OpenGL-Funktionen auch stecken die Parametertypen bei diesen beiden Funktionen übrigens im Funktionsnamen drin.
Das 3ub von glColor3ub heißt: die Funktion nimmt drei Parameter (RGB) vom Typ unsigned byte (d.h. Werte von 0 bis 255) an. Es gibt z.B auch glColor4f; diese Funktion erwartet vier floats (RGB und Alpha) im Bereich von 0 bis 1 als Parameter.
Entsprechend heißt das 2i in glVertex2i: die Funktion nimmt zwei Parameter (x und y) im Format int an. In 3D-Programm taucht meistens die Funktion glVertex3f auf, die drei Parameter (x, y und z-Koordinaten) als Floats erwartet. Es gibt auch glVertex3fv (und entsprechend ähnliche Funktionen), die als Parameter ein Array aus drei Floats erwartet.
So kann man sich die Funktionsnamen äußerst leicht merken, und man kann immer die Funktionen verwenden, die am besten zu den vorliegenden Datenstrukturen passen.
Die Funktion RefreshScreen muß jetzt nur noch innerhalb der Programmschleife aufgerufen werden:
} RefreshScreen(); SDL_GL_SwapBuffers(); }
Nun sollte in der linken unteren Ecke ein Quadrat mit schönen Farbverläufen entstehen. Der Rest des Fensters sollte schwarz ausgefüllt sein.
Wenn ihr noch nie mit OpenGL gearbeitet habt solltet ihr am besten verschiedene Dinge in RefreshScreen ausprobieren: andere Koordinaten, andere Farben, andere Reihenfolge der Funktionsaufrufe.
Was passiert, wenn man vier zusätzliche Koordinaten innerhalb des Begin/End-Blocks einfügt? Was ist mit zwei Begin/End-Blöcken hintereinander?
Polygone (wie unsere Quads) können in OpenGL jede beliebige Form annehmen solange sie konvex sind. Mit anderen Worten, keiner der Eckpunkte darf „nach innen“ zeigen. Anders formuliert, die Verbindungslinie zweier beliebiger Punkte innerhalb der Polygonfläche muß komplett innerhalb des Polygons selbst liegen. Konkave Polygone werden zwar meist gerendert, aber auf unvorhersagbare Weise.
Die Eckpunkte des Quads müssen natürlich auch in der richtigen Reihenfolge, das heißt entweder im Uhrzeigersinn oder im Gegenuhrzeigersinn angegeben werden. Andernfalls betrachtet OpenGL das Quad als konkav.
Wenn man etwas falsch macht erhält man von OpenGL übrigens selten einen Fehlercode (ich habe noch kein Programm gesehen, das die Fehlercodes von OpenGL ernsthaft überprüft), und Crashes gibt's auch nicht (es sei denn der Treiber ist verbuggt). Dafür erhält man einfach eine stumme, fehlerhafte Bildschirmausgabe…
Hoffentlich habe ich euch in diesem Tutorial nicht mit Text erschlagen. Auf jeden Fall solltet ihr nun dafür gerüstet sein, mit OpenGL loszulegen. In den nächsten zwei Tutorials erkläre ich, wie man mit OpenGL 2-dimensionale Blits macht.
Der Quellcode zu diesem Tutorial sowie die Makefile und MSVC++-Projektdateien sind zum Download verfügbar: (sdl_tut8.tgz sdl_tut8.zip)
Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.