OpenGL in C#: Teil 1

Yeah, Heute machen wir OpenGL!
Im Vergleich zum zwei dimensionalen SDL arbeiten wir jetzt mit der Tao-Implementierung von OpenGL, was das ganze leider ein wenig komplizierter macht.
Solltet ihr das SDL-Tutorial befolgt haben, könnt ihr die Projektmappe nehmen (ggf. kopieren und Pfade in der Solution anpassen) und den Code der Program.cs auf Standard zurücksetzen, da wir das meiste nicht brauchen, und es für mich einfacher ist neu anzufangen.
Ich gehe also ab nun davon aus, dass ihr die Assemblies alle habt, die restlichen dlls im debug/release-Ordner liegen und ihr Einsatzfähig seit!
Zusammenfassung
Folgende Assemblies müsst ihr Importiert haben: SdlDotNet, System, System.Drawing, Tao.OpenGl, Tao.OpenGl.Glu, Tao.Sdl. Solltet ihr diese nicht haben, ergänzt diese, sie sollten bei den anderen Assemblies liegen!

Hier tun sich nun Abgründe auf: Wir haben Code-Technisch extreme Nachteile, wenn wir keine Engine verwenden. Während bei XNA (Ich werde auf XNA öfter zurückkommen, bitte entschuldigt) ein Fenster direkt im Projekt verankert ist (genau wie die draw, die initialize, die update und die loadContent Methoden), gibt es für OpenGl keinerlei vordefinierten Funktionen.
Wir werden diverse Variablen verwenden (Zur besseren Übersicht verwende ich #region-Tags):

#region def
int width = 600;
int height = 400;
bool fullscreen = false;
bool resizeable = false;
int bpp = 16;
Surface screen;
#endregion

Da die Namen in meinen Augen ziemlich selbsterklärend sind, gehe ich nicht näher auf sie ein.
Vorerst gehen wir wie bei einer normalen SDL-Anwendung vor (streng genommen ist dies ja auch eine normale SDL-Anwendung ;)): Wir instanzieren zuerst die Hauptklasse (hier: Program) und rufen ggf. Funktionen darin auf. Hier werde ich direkt zum laufen die Schleife aus der statischen Main-Methode starten. (Ihr erinnert euch: Events.Run())
In meinem Fall sieht der Aufruf aus wie folgt:

Program app = new Program();
app.Initialize();
app.Reshape();
app.InitGL();
Events.Run();

Etwas mehr, als man erwarten würde, oder? Ich habe dies gemacht, damit ich keinen Konstruktor selber schreiben muss. Nicht das gelbe vom Ei, aber es tuts.
Fangen wir von oben an, mit der Initialize()-Funktion. Wie der Name schon sagt, soll es lediglich etwas initialisieren, nämlich unsere Events, sowie das Fenster. Der Code ist dementsprechend ziemlich einfach:

Events.Tick += new EventHandler(Events_Tick); // Event auf jeden Frame
Events.Quit += new EventHandler(Events_Quit); // Event beim Schließen
Events.KeyboardDown += new EventHandler(Events_KeyboardDown); // Event beim Tastendruck
Events.VideoResize += new EventHandler(Events_VideoResize); // Event beim Verändern der Fenstergröße (Auch beim Wechseln in Vollbildmodus!)
screen = Video.SetVideoMode(width, heigh, bpp, resizeable, true, fullscreen); // Ja, wir erstellen ein Fenster

Erstellt für diese Handler ersteinmal Dummies, wir werden sie später füllen!
Als nächstes kannst du die Reshape() Funktion erkennen. Diese werde ich allerdings nicht nur einmal definieren, sondern zweimal, da man eigentlich noch eine Variable mit angeben muss.

protected virtual void Reshape()
{
 Reshape(100.0f);
}

Falls du es nicht weißt: das f zeigt an, dass es sich um einen Float-Wert handelt.
Nun erkläre ich die richtige Reshape-Funktion.

protected virtual void Reshape(float distance)
{
 ...

Es wird nötig sein, jede Zeile einzeln zu erklären.

Gl.glViewport(0, 0, width, height);

Wie du sicher bemerkt hast, geben die Tooltips keine Hilfe. Ich denke, dass es daran liegt, dass der Code aus original C++ einfach nur kopiert wurde, und man nicht daran dachte, die Paramter netterweise umzubenennen.
Doch zurück zu der Funktion.
glViewport(…) setzt den Viewport zurück. Folgende Parameter sind von nöten:

  • P1: Der Wert, wo der Viewport auf der X-Achse anfangen soll. Für uns einfach nur 0, da wir ja keine Experimente durchführen wollen.
  • P2: Der Wert, wo der Viewport auf der Y-Achse anfangen soll.
  • P3: Die Breite des Viewports. Idealerweise die des Fensters, da wir sonst Grafikfehler ausserhalb des Viewports erhalten.
  • P4: Die Höhe des Viewports.
Gl.glMatrixMode(Gl.GL_PROJECTION);

Diese Funktion gibt an, welche Matrix du als nächstes bearbeitest. Während du in XNA einfach mehrere Matrizen anlegst, um diesen etwas zuzuweisen, gibt es in OpenGl vordefinierte Matrizen. Folgende verwende ich in diesem Beispiel:

  • GL_PROJECTION: Die Matrix, die zur Projektion/zur Anzeige der Szene dient. Dieser wird eine Perspektive verpasst, damit man ein möglichst realistisches Bild erreicht.
  • GL_MODELVIEW: Die Matrix, die verwendet wird, um Objekte auch abzuspeichern.

Der Unterschied zwischen beiden ist folgender: In der ersten landen die Grafiken, in der zweiten die Daten, aus denen die Grafiken errechnet werden. Einfach, oder?

Gl.glLoadIdentity();

Diese Funktion ist wirklich eine einfache 🙂 Sie setzt die verwendete Matrix auf den Ursprungszustand zurück, sie resetet sie also.

Glu.gluPerspective(45.0f, (width / (float)height), 0.1f, distance);

Ehm…. Kann ich das erklären, wenn wir zu 3D-Objekten kommen? Ich kann dir aber sagen, dass eine Veränderung an diesen Werten eine Veränderung der Perspektive verursacht.
Die Parameter bedeuten auf jeden Fall das folgende:

  • P1: Der Winkel der Projektion.
  • P2: Der Wert, auf den der Winkel angewandt wird. Wie du sehen kannst, das Ergebniss der Division von Breite und Höhe des Fensters.
  • P3: Der Wert, ab dem die “Kamera” etwas sehen kann.
  • P4: Der Wert, bis dem die “Kamera” etwas sehen kann. Die Sichtweite also.
Gl.glMatrixMode(Gl.GL_MODELVIEW);
Gl.glLoadIdentity();

Hausaufgabe: Was macht dieser Code? Zur Lösung einfach die nächsten beiden Zeilen markieren!
Du hast nicht wirklich geschummelt, oder?
Der Code wählt die MODELVIEW-Matrix aus und resettet sie. So einfach is det.

Unsere Reshape-Funktionen sehen zusammengefasst aus wie folgt:

protected virtual void Reshape()
{
 Reshape(100.0F);
}

protected virtual void Reshape(float distance)
{
 Gl.glViewport(0, 0, width, height);
 Gl.glMatrixMode(Gl.GL_PROJECTION);
 Gl.glLoadIdentity();
 Glu.gluPerspective(45.0F, (width / (float)height), 0.1F, distance);
 Gl.glMatrixMode(Gl.GL_MODELVIEW);
 Gl.glLoadIdentity();
}

Doch wozu braucht man diese Funktion?
Wenn du das Fenster erstellst oder veränderst, muss die Matrix auch neu eingestellt werden. Sonst kommt es zu komischen Fehlern, und das wäre nicht Schön.

Schau mal nach oben. Was haben wir noch nicht abgearbeitet?
Die InitGl-Funktion!!!!!!!
Brav aufgepasst.
War denn das bisher kompliziert? Eigentlich nocht nicht, oder?
Jetzt wird es schon ein wenig stressiger 😀

Gl.glShadeModel(Gl.GL_SMOOTH);

Wenn du ein wenig mitdenkst, wirst du dir sicherlich denken können, was diese Funktion bewirkt, sie setzt nämlich das ShadeModel. Das Modell GL_SMOOTH bewirkt einen schönen “smoothen” Effekt der Farben; Ich werde aber jetzt nicht darauf eingehen, such dir dafür eher eine OpenGL-Spezifikation.

Gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

Diese Funktion setzt die Farbe, die verwendet werden soll, um den Bildschirm zu leeren. Anders als bei 2D-Techniken wirkt das hier schon ein wenig logischer und nativer.
Die Parameter sind nach RGBA-Prinzip aufgebaut:

  • P1: Der Rot-Anteil der Farbe, mit der der Bildschirm überschrieben wird
  • P2: Der Grün-Anteil der Farbe.
  • P3: Der Blau-Anteil.
  • P4: Der Alpha-Wert.

Frag mich nicht aus über Farben, ich kenne mich mit Farben gar nicht aus. Frag meine lieben Hacking-Kollegen wie tomtom, der wird es dir bestätigen können 😛

glClearDepth(1.0f);
Gl.glEnable(Gl.GL_DEPTH_TEST);
Gl.glDepthFunc(Gl.GL_LEQUAL);

Diese Aufrufe sind ungeheuer wichtig. Später.
Allgemein sind sie für nichts anderes zu gebrauchen als zum Unterscheiden, in welcher Tiefe ein Objekt gezeichnet werden soll. Da wir aber noch nichts zeichnen, kann uns das relativ egal sein 😛

Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);

Hiha. Diese Funktion gibt OpenGl an, welche Methode wir verwenden möchten, um die Perspektive zu korrigieren. Würden wir das nicht machen, kommt es zu Fehlern. (Unglaublich)
Die hier gewählte Methode ist eine etwas rechenaufwändigere, allerdings sieht es so schöner aus.
Mensch, du hast es geschafft! Wieder eine Funktion abgeschlossen!

protected void InitGL()
{
 Gl.glShadeModel(Gl.GL_SMOOTH);
 Gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 Gl.glClearDepth(1.0f);
 Gl.glEnable(Gl.GL_DEPTH_TEST);
 Gl.glDepthFunc(Gl.GL_LEQUAL);
 Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);
}

Mensch! Alles an Initialisierung abgeschlossen!
Jetzt hau mal auf Ausführen. Du solltest ein Fenster sehen, bzw. eher nur den Rand des Fensters. Wenn du das Fenster bewegst oder vergrößerst, wirst du ein interessantes Merkmal sehen: Das Bild behält den Inhalt beim Bewegen!
Das liegt daran, dass ein OpenGL-Fenster zuallererst einfach nur ein Fenster erstellt, ohne einen Inhalt festzulegen. Das Fenstersystem (unterschiedlich, je nach OS) versucht aber zwingend, dem Fenster einen Inhalt zu geben. Also nimmt es das, was bereits vorhanden ist, nämlich das, was hinter dem Fenster ist, und legt es als Inhalt fest.
Na, was brauchst du wohl, um selber einen Inhalt festzulegen? Genau, eine Draw-Methode. Und wo landet die? Richtig, im Tick-Eventhandler. (Du erinnerst dich: In der Initialize-Funktion festgelegt!)
Meine Funktion heißt DrawGLScene(). Hier landet nun jede Anweisung zum zeichnen.

Gl.glClear((Gl.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT));
Gl.glLoadIdentity();

Die zweite Funktion kennst du ja bereits. Sie setzt einfach die zuletzt verwendete Matrix zurück. Erinnerst du dich noch, welche das war? Wenn nicht schau nach!
[spoiler=’Lösung’]GL_MODELVIEW
Warum? Siehe die Funktion Reshape an. Der letzte Aufruf einer Matrix ist zwingend die Matrix GL_MODELVIEW.[/spoiler]
Wenn du die Lösung nicht alleine herauskriegst, solltest du dringenst nachschauen!
Aber nicht verwirren lassen! Die Funktion Gl.glClear(…) löscht den Bildschirm und die Projektion, während glLoadIdentity() die letzte Matrix löscht!
Der erfahrene Programmierer wird sicherlich erkennen: Ach, da werden Flags übergeben.
Genau genommen zwei.
Diese beiden Flags sind das BUFFER_BIT und das DEPTH_BUFFER_BIT, zwei Flags, die angeben, welche Bereiche nun eigentlich gecleart werden sollen. Der Grafikbuffer und der Tiefenbuffer. Löschen wir diese, haben wir wieder ein komplett leeres Bild!
Nun fehlt noch genau ein Aufruf, um alles auf den Bildschirm zu kriegen. Weißt du schon, welcher das ist?
Bei 2D hatten wir doch auch immer einen screen.Update-Befehl, oder? Nun, einen ähnlichen verwenden wir hier auch! Nur heißt er nun

Video.GLSwapBuffers();

Das war es auch schon! Du solltest nun ein Fenster haben, das zwar nur schwarz ist, aber du weißt, dass das Fenster mit jedem Frame aktualisiert wird, und du hast schon eine Matrix, in die du Objekte eintragen kannst! Du kannst stolz auf dich sein!

Hausaufgaben (Werden mit dem nächsten Artikel gelöst)

  • Wie musst du das ganze abändern, um den Hintergrund halbrot zu machen?
  • Was musst du ändern, um ein Gelb zu erreichen?
  • Wie kannst du den Rand des Fensters entfernen?
Advertisements
Tagged , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: