LU Computergraphik 2 SS 4.0, 186.165

Michael Wimmer


 

Allgemeines

Prinzipiell ist zu sagen, daß die meisten Fragen bezüglich OpenGL, die man sich während der Programmierung des Spiels stellen wird, sich durch eingehendes Studium des sogenannten Red Book klären lassen können. Es gibt aber einige Punkte, die eher systemabhängig sind und daher nicht in der allgemeinen OpenGL-Literatur zu finden sind, diese wollen wir hier erläutern.


Effizientes Programmieren

Es ist kein "rasend schnelles" Spiel verlangt, Geschwindigkeit ist aber trotzdem ein wichtiger Bewertungspunkt. Anders als beim reinen Softwarerendering geht es jedoch nicht darum, jede Zeile Code zu optimieren oder sogar C-Code durch Assembler-Code zu ersetzen. Normalerweise verbringt ein OpenGL-Programm nämlich nicht die meiste Zeit im eigenen C-Code, sondern im OpenGL-Treiber! Die wichtigste Aufgabe beim effizienten Programmieren mit OpenGL ist daher, die richtigen OpenGL-Befehle in der richtigen Reihenfolge zu verwenden! Hierfür gibt es einige Richtlinien:

  • State-changes sind teuer: Der häufige Wechsel zwischen verschiedenen Texturen, Materialien und vor allem Shadern kann viel Zeit kosten - minimiert daher die Anzahl der nötigen State Changes und vermeidet redundantes Setzen von gleichen Zuständen.

  • Vertex Arrays statt glVertex: Jeden einzelnen Eckpunkt mit einem glVertex plus dazugehörigem glNormal etc. spezifizieren zu müssen, kostet Rechenzeit (für die Funktionsaufrufe). Bei Vertex Arrays gibt man stattdessen einen Zeiger auf ein Array an, in dem alle Eckpunkte und dazugehörigen Daten schon vorbereitet stehen. Am effektivsten ist es, die Geometrie-Daten direkt auf der GPU als Vertex Buffer Object(VBO) zu speichern und aufzurufen.

  • Durch die Anforderung OpenGL 3.x in dieser LU verwenden zu müssen ergibt sich, dass glNormal und glVertex - Befehle ohnehin nicht mehr verwendet werden dürfen. Ihr braucht mindestens ein Vertex-Array oder auch ein VBO.
  • Testen, testen, testen! Ob ein Programm auf einer bestimmten Plattform problemlos läuft ist, kann man nur sagen, wenn man es dort ausprobiert! Befehle, die auf einer Karte langsam sind, können auf einer anderen sehr schnell sein. Außerdem ist es möglich, dass unterschiedliche Treiber (und sogar einzelne Treiberversionen vom selben Hersteller) bestimmte Befehle unterschiedlich verarbeiten. Auf ATI-Grafikkarten wird beispielsweise GL_TEXTURE_2D für non-power-of-2-Texturen nur teilweise unterstützt.

Moderne Graphikkarten sind oft so schnell, dass die CPU sie nicht schnell genug mit neuen Aufgaben versorgen kann. Deshalb kostet es oft nichts, komplexere Geometrie oder komplexere Beleuchtung zu verwenden, da man damit sowieso nur ungenutzte Resourcen der Graphikkarte verwendet. So sind beispielsweise Vertex Arrays bis ~200 Dreiecke rein CPU-limitiert. Das heißt auch oft, dass es schneller ist, ein komplexes Objekt an die Graphikkarte zu schicken, als es vorher mit der CPU zu vereinfachen oder auszurechnen, ob es überhaupt sichtbar ist. Wie üblich muss man das durch probieren austesten.


Objektorientiertes Programmieren und STL

Designed Euer Programm, wie man es in Software Engineering lernt, mit objektorientierten Methoden - es zahlt sich aus!

Verwendet außerdem die Standard Template Library (STL). Sie ist bei jedem C++ Compiler schon dabei, weil sie Teil des C++ Standards ist. Die STL enthält effiziente Implementierungen von vielen wichtigen Datenstrukturen. Es ist heutzutage eigentlich nicht mehr zu entschuldigen, wenn man List-, Hashtable-, Resizable Array- oder ähnliche Klassen selbst programmiert. All dies ist in der STL schon enthalten, viel getestet und effizient. Eine gute Dokumentation zur STL gibt es bei SGI, und im 1. Repetitorium wird auch darauf eingegangen.


Transformationen

Um in OpenGL verschiedenste Transformationen durchzuführen wurden früher Befehle, wie glPop/PushMatrix, glLoadIdentity, glRotate(), glTranslate(), glScale() .... verwendet. Durch die Verwendung von OpenGL 3.x  fallen alle diese Fixed-Function calls weg, d.h. ihr müsst für die Berechnung der Vertices im Vertex-Shader sorgen. Am Besten definiert ihr uniform mat4 Matrizen im Shader, um die View-Transformation durchzuführen. Wie man Parameter an die Shader übergeben kann wurde euch bereits im 1. Repetitorium vorgeführt.

Hier sei nun eindringlich auf das Kapitel 'Viewing and Modeling Transformations' im Redbook (Seite 104-108) hingewiesen - das Verständnis dieser Seiten ist essentiell für ein Verständnis jeglicher Kamera und Objektmanipulation. Für die meisten Anwendungen eignet sich der 'Moving a Local Coordinate System' besser - man denkt sich einfach ein Koordinatenkreuz im Raum, das zu Beginn im Augpunkt verankert ist. Jede weitere Transformation bewegt dieses Koordinatenkreuz, und jegliche Geometrie wird in diesem lokalen Koordinatensystem spezifiziert.ziert.


Kamera

In OpenGL gibt es keinen Befehl, eine 'Kamera' zu positionieren. Die Kamera ist einfach das erste lokale Koordinatenkreuz, von dem aus man alle anderen Transformationen durchführt. Will man doch mit einem Weltkoordinatensystem arbeiten, macht man sich zunutze, daß Kamerabewegungen ja relativ zur Welt sind: ob sich die Kamera auf ein Objekt zubewegt oder das Objekt auf die Kamera zu, macht im Ergebnis keinen Unterschied. Deswegen ist die erste Transformation, die man im Programm spezifiziert, die INVERSE Kameratransformation (siehe dazu das RedBook ab Seite 113)!

Sehr einfach kann man die Position mit der gluLookAt()-Routine spezifizieren, indem man einfach den Blickpunkt und die Blickrichtung mit je einem einem Vektor angibt, und zusätzlich noch einen Vektor, der 'oben' definiert (gluLookAt() übernimmt die Berechnung der oben angesprochenen inversen Kameratransformation, man braucht sich darum also nicht zu kümmern).

Für die Berechnung dieser Vektoren gibt es viele Methoden. Man kann sie z. B. als Definition für die Betrachterposition im Programm speichern und bei Eingaben durch den Benutzer bei Bedarf verändern. Bewegt sich z.B. ein Raumschiff mit einer gewissen Geschwindigkeit vorwärts, addiert man den Blickrichtungsvektor - skaliert mit der Geschwindigkeit - zum Blickpunktvektor. Bei Rotationen wird es schon gefinkelter: Möchte man um eine 'globale' Achse im Weltkoordinatensystem rotieren, multipliziert man den Blickrichtungsvektor mit einer Rotationsmatrix für die jeweilige Achse. Aber Achtung: das funktioniert nur gut für Rotationen um die senkrechte Achse! Möchte man um z.B. die x-Achse (die parallel zum Boden liegt) rotieren, meint man damit praktisch immer eine Rotation um die lokale x-Achse, nicht die globale!

Deswegen ist es oft einfacher, man speichert gleich die aktuelle Rotationsmatrix für die Betrachterposition. Bewegungen im globalen Koordinatensystem (also um eine globale Achse) kann man dann durch Multiplikation einer Rotationsmatrix von links, Bewegungen im lokalen mit einer Rotationsmatrix von rechts erreichen. So erhält man dann eine neue Kameramatrix, die man jetzt aber mit glMultMatrix() spezifiziert. Allerdings muß man hier wieder aufpassen, weil man ja die INVERSE Kameramatrix benötigt. Das ist für Rotationsmatrizen aber kein Problem, weil hier gilt, daß die Inverse gleich der Transponierten ist.

Empfohlene Datenstrukturen

Objekt-Geometrie

Es macht Sinn, Geometrie als Indexed Face Sets zu speichern: in verschiedenen Arrays werden Koordinaten, Normalvektoren und Texturkoordinaten gespeichert, und ein zusätzliches Array enthält für jedes Primitiv (z.B. für Dreiecke 3) Indizes, die die Koordinaten, Normalvektoren und Texturkoordinaten angeben, die aus den jeweiligen Arrays verwendet werden sollen.

Generell läßt man sich beim Design von Datenstrukturen davon leiten, wie man die Daten an OpenGL übergeben wird. Verwendet man (was sehr empfohlen ist) Vertex Arrays, wird man die Koordinaten und Indizes schon in einer Form speichern, daß man sie OpenGL nur noch mit dem entsprechenden Aufruf von glVertexPointer() usw. angeben muß. Es wäre nämlich sehr aufwendig, dafür die Daten jedesmal im Hauptspeicher verändern (verschieben, umordnen o.ä.) zu müssen. Das bedeutet insbesondere, daß klassische zeigerbasierte Datenstrukturen wie B-Reps oder BSP-Bäume für schnelles und effizientes Rendering ungeeignet sind! Unabhängig davon kann es notwendig sein, für andere Aufgaben (etwa Kollisionsbehandlung) zusätzliche Daten oder etwa die gesamten Daten nochmals in anderer Form speichern zu müssen - das hängt dann sehr vom jeweiligen Spiel ab. 


Globale Geometrie

Wenn man eine große Welt simuliert, wird man nicht immer alle Objekte gleichzeitig darstellen können/wollen. Man muß sich daher eine globale Strukturierung der Szene überlegen, die erlaubt, schnell herauszufinden, welche Teile der Szene sichtbar sind oder gezeichnet werden sollen. Die einfachste Möglichkeit dazu ist hierarchisches View-Frustum Culling: Jeder Koten im Szenengraph (also jedes Gruppen-Objekt und jedes eigentliche Objekt) enthält eine Bounding Sphere. Bevor man einen Knoten rendert, testet man, ob die Bounding Sphere überhaupt das View Frustum schneidet (dazu benötigt man natürlich Daten über die Kameratransformation, wie etwa den Blickpunkt, die Blickrichtung, den Kameraöffnungswinkel usw.). Dadurch können ganze Objekte oder sogar Gruppen von Objekten, die sich nicht im Blickfeld befinden, schnell 'geculled' werden.

Aufwendigere Methoden hängen dann immer von der Art des Spiels und der Szene ab: für Spiele in geschlossenen Räumen kann man (wie in Quake) BSP-Bäume (als volumetrische Unterteilung der Innenräume) generieren, die in jedem Knoten eine Liste von sichtbaren Objekten speichert - das ist aber ziemlich aufwendig. Alternativ kann man in Innenräumen auch auf sogenanntes Portal-Culling zurückgreifen, welches durch die hohe Geometrieverdeckungsicher sehr effizient ist. Eine Implementation ist allerdings nicht so einfach wie z.B. View Frustum Culling.

Für die Übung sollt ihr View Frustum Culling (nicht unbedingt hierarchisch, also ev. nur mit Bounding-Spheres oder Bounding-Boxes für die echten Objekte) implementieren, aufwendigere Methoden können als Spezialeffekt Punkte bringen.

Beliebte Fehler die ihr vermeiden solltet

Die Erfahrung aus den letzten Jahren hat gezeigt, dass es einige Möglichkeiten gibt, sich bei Programmierung und Design des Spiels zu verzetteln. Ein paar dieser Fallstricke werden nun beschrieben und sollten tunlichst vermieden werden:


Over-Engineering

Grundsätzlich ist es gut, sich ein objekt-orientiertes Design zu überlegen und die Features von C++ zu nutzen, da ein wohlüberlegtes System letztendlich viel Zeit und Nerven sparen kann. Allerdings sollte man es auch nicht übertreiben. Zeit ist knapp bei dieser Übung, und wenn man einen Großteil davon damit verbringt, ein komplexes Framework mit allen möglichen und unmöglichen und vor allem nie benutzten Features zu implementieren statt sie für die Lösung der eigentlichen Aufgabenstellung zu nutzen, kann man schnell in Probleme geraten. Ziel der Lehrveranstaltung ist es, ein spaßiges Spiel zu machen, schöner Code ist nur Mittel zum Zweck!

Zu Features, die möglicherweise nett und praktisch sind, aber von uns nicht bewertet werden, zählen Dinge wie XML-basierte Menüs oder In-Game-Kommandokonsolen mit Autovervollständigung. Auch Lauffähigkeit auf unterschiedlichen Plattformen ist für uns nicht relevant, bisher hatte ausnahmslos jede Gruppe, die das versucht hat, ihre Probleme damit. Konzentriert euch auf eine funktionierende Version für Windows 7 64bit, denn das ist die Anforderung. Ihr könnt das Spiel nach der LU immer noch auf andere Plattformen portieren, falls ihr das wollt.


Über-ambitioniertes Gamedesign

Denkt daran, dass ihr nur ein Semester Zeit habt, ein vollständiges Spiel zu programmieren und plant das auch beim Gamedesign ein. Beispielsweise sind Multiplayer-Spiele übers Netzwerk ein enormer Programmieraufwand, der sich bisher nur selten ausgezahlt hat. Bis man hier ein robustes, funktionierendes System entwickelt hat, vergeht viel Zeit - in der Regel zu viel, um sich noch ausreichend um den Rest des Spiels kümmern zu können. Außerdem bereiten derartige Spiele aufgrund ihres Konzepts immer wieder Probleme bei der (durchaus wichtigen) Präsentation.

Natürlich soll das Spielkonzept nicht extrem simpel ausfallen, allgemein sollte man sich jedoch gut überlegen, was man sich vornimmt, denn ein übermäßig komplexes, nicht abgeschlossenes Projekt macht in der Regel weniger Eindruck als ein kleines, aber feines Spiel, das letztendlich auch aufgrund seiner Komplettheit Spaß bereitet.

Ressourcen

Entwicklungsumgebung und Compiler

Für die Entwicklung empfehlen wir Visual Studio 2008. Informatik-Studenten haben über die MSDNAA kostenlos Zugang zur Vollversion. Außerdem lässt sich bei Microsoft kostenlos die Visual Studio 2008 Express Edition herunterladen. Achtet darauf dass die verwendeten Libraries mit dem Visual Studio 2008-Library-Format kompatibel sind!


Content Creation

Empfohlene Tools:

  • CrazyBump ist ein Gratis-Tool zum Generieren von Normalmaps, Heightmaps, Occlusionmaps und Specularmaps von Texturen und Fotos, das auch von kommerziellen Spielen verwendet wird. Dank vielen Detail-Einstellungen und Echtzeit-Preview kann man mit dem Tool schnell und effizient sehr gute Texturen erzeugen.
  • Blender ist ein Open Source-Modellierungstool, das auch Animation erlaubt. Blender ist auf Spiele ausgerichtet, deshalb wird sogar eine Engine mitgeliefert - diese sollte natürlich nicht verwendet werden.
  • 3D Studio Max.
  • Terragen zum Erstellen von Skyboxen.
  • Milkshape 3D, ein Modellierungsprogramm, das über viele Import- und Exportmöglichkeiten (md2, md3, 3ds usw.) verfügt. Dazu gibt es auch ein Tutorial.

Empfohlene Modelformate:

  • Das Quake 2-Modelformat (md2) und das Quake 3-Modelformat (md3), da hier viele Loader existieren und keyframe-basierte Animation sehr einfach zu implementieren ist.
  • Das Doom3-Modelformat (md5), für das ebenfalls Importer und Exporter für alle gängigen Modelling-programme existieren. Im Unterschied zu MD3 erlaubt dieses Format weighted vertex skinning. Das Format ist human-readable und damit leicht zu parsen.
  • FBX, das proprietäre Autodesk-Format für 3DS Max und Maya, für das ein SDK mit Sample-Code zum Import und Export von ganzen Szenen existiert. Mit diesem Format lassen sich damit neben einzelnen Models auch ganze Szenen oder Level importieren.
  • Ogre XML (und Ogre Tools). Auch für die die Nicht-Engine-Gruppen kann man die Ogre3D Tools empfehlen. Darunter gibt es Export-Plugins für so ziemlich jede Modellierungssoftware, die meistens in das human-readable Ogre XML Format speichert, das sich dann leicht auslesen lässt, um Modelle in die eigene Engine zu bekommen.
  • Collada: Zum schnellen und unkomplizierten Laden von Collada-Dateien empfehlen wir die FCollada-Library. Zum Download muss man sich auf der Seite registrieren.
  • Milkshape 3D (ms3d)

Libraries

Bei einem Spiel fallen eine Menge von Programmieraufgaben an, die nicht direkt mit Computergraphik oder Spieldesign zu tun haben, aber leider unerlässlich sind. Wir bieten daher hier eine Übersicht über die wichtigsten Libraries, die einem eine Menge Programmierarbeit abnehmen können.


OpenGL Helper:

GLEW

Zusatzfunktionalität in OpenGL, die über die Core-Spezifikation hinausgehen, wird über so genannte Extensions gelöst. Dafür muss man mittels wglGetProcAddress() (bzw. den Äquivalenten unter anderen Fenstersystemen) einen Funktionspointer auf die gewünschte Extension-Funktion holen. Unter Windows braucht man weiters aktualisierte glext.h und wglext.h Header Files. Da dieser Prozess mühsam ist und keinen wirklichen Lehrcharakter besitzt, empfehlen wir die Verwendung von GLEW (GL Extension Wrangler). Diese Library kann mit wenigen Zeilen Code alle Extension-Funktionen verfügbar machen und spart einiges an Arbeit.

GLFW

OpenGL enthält grundsätzlich keine Funktionen, die ein passendes Fenster im jeweiligen Fenstersystem erzeugen. Diese werden über eine plattformabhängige Schnittstelle implementiert (WGL unter Windows, GLX unter Unix, usw.). Damit ihr diese Funktionen nicht selbst implementieren müsst, empfehlen wir die Verwendung von GLFW. Der Vorteil liegt darin, dass GLFW sich auch um die Behandlung von Tastatur-, Maus- und Joystick-Ereignissen kümmert.

SDL

Etwas älter und mit mehr Features ausgestattet ist SDL, eine ebenfalls plattformunabhängige Library, die auch Sound-Unterstützung enthält, und sehr häufig eingesetzt wird.


Image Loading:

FreeImage

Die FreeImage-Library unterstützt alle gängigen Bildformate (inklusive HDR-Bildformaten)

DevIL

DevIL ist eine sehr umfangreiche Library zum Laden und Speichern von Bilddateien mit OpenGL-artiger Syntax. DevIL unterstützt alle gängigen Formate: JPEG, BMP, TGA und PNG (für Alpha-Kanäle) und DDS. Außerdem können Mipmaps und Animationen leicht erstellt werden. Allerdings hat die DevIL-Library Bugs bei der Unicode-Implementation, so dass ihr immer auf die ANSI-Funktionen zum Bildladen zurückgreifen solltet.

Auf der Homepage der Library findet ihr auch zahlreiche Tutorials.

Für die Übung sollten allerdings die ilutGL-Funktionen nicht verwendet werden!


Sound:

FMOD

Sehr gute Erfahrungen haben Studenten mit FMOD gemacht, für nicht-kommerzielle Verwendung ist die Lizenz frei und diese Library unterstützt sogar 3D Sound, MP3 und ähnliche Features. FMOD EX bietet sogar ein objektorientiertes Framework an und ist damit noch einfacher zu benutzen.

OpenAL

OpenAL ist eine plattformübergreifende Open Source Audio Library für Sound, die ebenfalls 3D Sound unterstützt.


Collision Detection/Physik:

PhysX

Falls ihr euch nicht allzu sehr mit Kollision und Physik auseinandersetzen wollt, kann die Verwendung einer Physik-Library sinnvoll sein. Gute Erfahrungen haben wir mit der nVidia PhysX Engine gemacht.

OPCODE

Bevor man beginnt PhysX herunterzuladen und sich einmal eine Woche mit dem Einarbeiten beschäftigt, sollte man sich überlegen, ob es nicht ausreicht, wenn man das einfache Objekt-Verhalten selber schreibt.
Dabei kann es aber immer noch nützlich sein, wenn man eine fertige, schnelle Kollisionserkennungs-Library verwendet. Hier wäre OPCODE zu empfehlen.


Vektor Mathematik:

Imath

Imath bietet eine ganze Reihe von generischen Klassen an (Vektoren, Matrizen, Quaternionen, etc. etc) an, die einem die Arbeit sehr erleichtern.
Ihr findet die Library im ilmbase Packet von OpenEXR: http://www.openexr.com/downloads.html

GLM

Eine Library, ähnlich in Umfang und Anwendung wie Imath, ist GLM. Sie ist darauf ausgelegt das Verhalten der in GLSL eingebauten Vektor und Matrix Typen zu simulieren. Deshalb integriert sie sich besonders gut in OpenGL Code. GLM ist eine Header-only Library. Das heißt, dass man die Files nur im eigenen Code includen muss, um sie zu verwenden.

Unterlagen


Einführung in C++

OpenGL Basiswissen:

  • OpenGL Programming Guide, Sixth Edition (das sogenannte "Red Book") (auch die 5th, 4th, 3rd oder 2nd Edition sind ok): Das ist eindeutig die wichtigste Quelle für alle Fragen, die OpenGL betreffen. Dieses Buch ist für die Laborübung unverzichtbar, es enthält genaue Anleitungen, wie man in OpenGL die komplette Rendering-Pipeline steuern kann. Frühere Versionen des Red Book findet man auch leicht mit Google im Netz als PDF oder HTML. Gegen Studentenausweis ist es im Lehrmittelzentrum etwas vergünstigt zu erhalten.
  • OpenGL Spezifikationen: Die verbindliche Stelle wenn man klare Fragen hat. Die C API und GLSL haben jeweils ein eigenes Dokument. Ihr bekommt die Spezifikationen aller Versionen in PDF Form hier. Die aktuellste Version gibt es hier: API, GLSL
  • Es gibt mittlerweile eine Unmenge an Büchern zur Spieleprogrammierung mit OpenGL. Ein paar davon gibt es bei uns in der Bibliothek zum Anlesen.

OpenGL Spezialeffekte:

  • Nvidias Developer Website enthält in den Sections "White Papers" und "Technical Presentations" zu einigen der Spezialeffekte sehr gute Erläuterungen.
  • Die "GPU Gems"(auch online verfügbar)- und "ShaderX"-Bücher, die ihr in der Institutsbibliothek findet, beinhalten viele nützliche Artikel zur effizienten Implementierung von State of the Art-Effekten.
  • Die "Game Programming Gems"-Serie enthält auch viele Tipps und Tricks für Spieleprogrammierer. Die Bücher sind in der Bibliothek verfügbar.

OpenGL- und Spiele-Tutorials:

Vorträge im Rahmen der LU:

Code Snippets

GLFW Fenster initialisieren

GLFW macht die Initialisierung eines Fensters sehr einfach:

#include <GL/glfw.h>
#include <iostream>

void renderScene(void)
{
    static const GLfloat vertices[] = { 0.000f, 1.0f, 0.0f, // top
                                       -0.866f,-0.5f, 0.0f, // lower left
                                        0.866f,-0.5f, 0.0f};// lower right

    static const GLfloat colors[] = {1.0f, 0.0f, 0.0f, // Red
                                     0.0f, 1.0f, 0.0f, // Green
                                     0.0f, 0.0f, 1.0f};// Blue

    /* not using Shaders here, but remember NOT to use
       glPushMatrix(), glRotatef() etc. with OpenGL3.x*/
    glPushMatrix();

    glRotatef(glfwGetTime()*15.0, 0,0,1); // Rotate around Z axis

    glEnable(GL_VERTEX_ARRAY);
    glEnable(GL_COLOR_ARRAY);

    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glColorPointer(3, GL_FLOAT, 0, colors);

    glDrawArrays(GL_TRIANGLES, 0, 3);

    glDisable(GL_VERTEX_ARRAY);
    glDisable(GL_COLOR_ARRAY);

    glPopMatrix();
}

int main (void)
{
    bool running = true;

    // Initialise GLFW
    glfwInit();

    // Open an OpenGL window
    if ( !glfwOpenWindow(300, 300, 0, 0, 0, 0, 24, 8, GLFW_WINDOW)) {
        std::cerr << "Could not create window" << std::endl;
        glfwTerminate();
        return 1;
    }

    // Main loop
    while (running) {

        // Game logic goes here...



        // OpenGL rendering goes here...
        glClear(GL_COLOR_BUFFER_BIT);

        renderScene();

        // Swap front and back buffers
        glfwSwapBuffers();


        // Check if ESC key was pressed or window was closed
        running = !glfwGetKey( GLFW_KEY_ESC ) &&
                   glfwGetWindowParam( GLFW_OPENED );

    }

    // Close window and terminate GLFW
    glfwTerminate();

    // Exit program
    return 0;
}

Eine ausführlichere und empfehlenswerte Einführung zu GLFW ist hier zu finden:
GLFW User's Guide
GLFW Reference Manual

GLFW Vollbildmodus

Man kann in GLFW einen Vollbild OpenGL-Context erzeugen, indem man glfwOpenWindow() statt mit GLFW_WINDOW mit GLFW_FULLSCREEN aufruft.

glfwOpenWindow(800,600,0,0,0,0,24,8,GLFW_FULLSCREEN);

Mit glfwGetVideoModes() kann man die vom System unterstützten Video-Modes abfragen.

GLFWvidmode modes[100];
int mode_cnt;

mode_cnt = glfwGetVideoModes(modes, 100);

for (int i = 0; i < mode_cnt; i++) {
    std::cout << modes[i].Width << "x" << modes[i].Height << std::endl;
}

Tastatur-/Mausabfrage in GLFW

Am einfachsten realisiert man die Steuerung eines Spiels über die Tastatur. Dazu gibt es in prinzipiell 2 Möglichkeiten (dasselbe gilt für die Maus):

  • Ereignis (Event)-gesteuerte Abfrage
  • Punktuelle Abfrage (Polling)

Tastaturabfrage

Ereignisgesteuert

Ereignisorientiertes Abfragen eignet sich für alle Tasten, die eine einmalige Wirkung haben sollen (wie z.B. die ESC-Taste, oder die "Schuss"-Taste, oder das Ein/Ausschalten einer Option). Dazu registriert man eigene Callback-Funktionen, die von GLFW aufgerufen werden, wenn eine Taste gedrückt worden ist.

GLFW stellt dafür die Funktion glfwSetKeyCallback zur Verfügung. Eine einfache Callback-Funktion kann man z.B. so einrichten:

void GLFWCALL myKeyCallback(int key, int action) {

    switch (key) {
    case 'A':
        if (action == GLFW_PRESS)
            std::cout << "A pressed" << std::endl;
        break;

    case GLFW_F1:
        if (action == GLFW_RELEASE)
            std::cout << "F1 released" << std::endl;
        break;

    default:
        if (action == GLFW_PRESS)
            std::cout << "Some key pressed" << std::endl;
        else
            std::cout << "Some key released" << std::endl;
        break;
    }
}

...

glfwSetKeyCallback(myKeyCallback);

Polling

Polling hingegen benötigt man für Tasten, die für die Dauer, die sie gedrückt sind, eine Wirkung haben sollen. Das gilt z.B. für die Cursor-Tasten - hier ist es nicht korrekt, wenn sich das Raumschiff nur ein einziges Mal vorwärts bewegt, wenn die Pfeil-hinauf-Taste gedrückt wird!

Keyboard-polling geht in GLFW einfach mit der Funktion glfwGetKey():

if (glfwGetKey(GLFW_KEY_UP))
    std::cout << "Up key is pressed" << std::endl;

if (glfwGetKey('T'))
    std::cout << "T key is pressed" << std::endl;

Mausabfrage

Für die Maus hat man gleich mehrere Polling und Callback-Funktionen, mit dem man Bewegungen vom Mauszeiger, dem Mausrad als auch Button-Klicks abfangen und abfragen kann.

Ereignisgesteuert

void GLFWCALL myMousePosCallback(int x, int y)
{
    std::cout << "Mouse moved to (" << x << ", " << y << ")" << std::endl;
}

void GLFWCALL myMouseButtonCallback(int button, int action)
{
    switch (button) {
    case GLFW_MOUSE_BUTTON_LEFT:
        std::cout << "Left mouse button ";
        break;
    case GLFW_MOUSE_BUTTON_RIGHT:
        std::cout << "Right mouse button ";
        break;
    case GLFW_MOUSE_BUTTON_MIDDLE:
        std::cout << "Middle mouse button ";
        break;
    default:
        // There are mice with more than 3 buttons!
        std::cout << "Some mouse button ";
    }

    if (action == GLFW_PRESS) {
        std::cout << "pressed." << std::endl;
    } else {
        std::cout << "released." << std::endl;
    }
}

...

glfwSetMousePosCallback(myMousePosCallback);
glfwSetMouseButtonCallback(myMouseButtonCallback);

Polling

int x,y;

glfwGetMousePos(&x, &y)

std::cout << "Mouse is at position (" << x << ", " << y << ")." << std::endl;

if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) {
    std::cout << "Left mouse button is pressed." << std::endl;
} else {
    std::cout << "Left mouse button is released." << std::endl;
}

Extensions laden mit GLEW

Zu Beginn des Programmes, nachdem man ein OpenGL Fenster erzeugt hat, muss GLEW initialisiert werden:

#include <GL/glew.h >

GLenum err = glewInit();

if (err != GLEW_OK)
{
    cerr << "GLEW Error: " << glewGetErrorString(err);
    exit(1);
}

Zur Laufzeit kann man nun die Verfügbarkeit der Extensions folgendermaßen überprüfen:

if (GLEW_ARB_depth_clamp)
{
    // it is safe to use the ARB_depth_clamp here
    glEnable(GL_DEPTH_CLAMP);
}

Timer für framerateunabhängige Spiellogik

GLFW stellt die Funktion double glfwGetTime() zur Verfügung, die die seit dem Start des Programmes vergangenen Sekunden zurückgibt.

Eine gute Zeitmessung braucht man für mehrere Dinge:

  • Berechnen der Framerate, um sie am Bildschirm anzuzeigen (damit kann man die allgemeine Performance beurteilen)
  • Genaues Timing von bestimmten Code-Stücken
  • Abstimmen der Animationen und Bewegungen auf die Framerate. Man kann die Geschwindigkeit der einzelnen Objekte dann so berechnen, dass sie unabhängig von der Framerate ist. Die Geschwindigkeit ist dann also wirklich in "Einheiten pro Sekunde" und nicht "Einheiten pro Frame" bestimmbar.

Kommen in dem Stück Code, das man messen möchte, auch OpenGL-Befehle vor, so muss man (vor allem bei Hardware-Beschleunigung) beachten, dass OpenGL Befehle in einem Kommando-Puffer speichert und nicht sofort abarbeitet. Um also sicherzugehen, dass man die gesamte Zeit misst, die OpenGL zur Ausführung benötigt, fügt man vor den Aufrufen zur Timing-Routine jeweils ein glFinish() ein. In der Endversion des Programmes sollten aber keine glFinish-Befehle mehr vorkommen (außer ev. für die Frameratenanzeige), da sie das Programm verzögern.

Noch ein Tipp: Beachtet dass Physik-Engines wie PhsyX sehr wohl eine fixe Update-Rate pro Sekunde benötigen um korrekte Resultate zu liefern!

Game Programming Primer

Wenn ihr noch nicht wisst wie ihr das Programmgerüst für euer Spiel aufbauen wollt dann lest euch am besten den Game Programming Primer durch.