ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4420807
Jetzt (Chat):
36 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - TutorialsDruckversion

3D Spieleentwicklung mit OpenGL
Das Tutorials von Oliver Düvel

© Copyright 2002 [whole tutorial] by Oliver Düvel
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


OpenGL - 1 - Grundlagen.Basecode()



OpenGL - 1 - Grundlagen.Basecode()
von Oliver Düvel aka "Qbound"




die Einleitung

Mit diesem Tutorial kann der interessierte Leser solide Grundlagen, zur Programmierung der OpenGL API erlernen.
OpenGL selbst, ist eine 3D Graphics Library (Bibliothek) die in sehr vielen aktuellen Spielen als Renderdevice (zur Darstellung) eingesetzt wird. Um dem Tutorial in Gänze zu folgen, sind Kenntnisse in der Programmiersprache C notwendig.

unser Ziel

Wir wollen ein einfaches Rahmenprogramm entwickeln, damit wir uns besser auf die OpenGL relevanten Zeilen in unserem Quellcode konzentrieren können. Das Rahmenprogramm soll ein einfaches Fenster erzeugen und OpenGL nach unseren Wünschen initialisieren. Es wird in dem Fenster ein einfaches Dreieck (Triangle), siehe Abbildung '1. Dreieck im Basecode', gerendert.


Abbildung 1. Dreieck im Basecode

die Vorbereitungen

Bevor wir mit dem programmieren der ersten Zeilen Quellcode beginnen, müssen ein paar Kleinigkeiten gemacht werden. Dazu zählen das Erstellen von einer Win32-Anwendung mit dem Visual Studio (mit dem Produkt arbeite ich), das Einrichten von unserem Projektordner und das Vornehmen der notwendigen Einstellungen im Visual Studio.

Step 01: Erstellen der Win32-Anwendung

Das VS (Visual Studio) öffnen und dort mit CTRL+N in den Neu-Dialog springen, in der Registerseite Projekte die Win32-Anwendung auswählen. Dann den Projektnamen BaseCode und den gewünschten Projektpfad angeben.
Mit 'OK' weiter. 'Ein leeres Projekt.' auswählen und mit dem Button 'Fertigstellen' das Projekt erzeugen. Es folgt noch ein Hinweisdialog den wir mit 'OK' schliessen, der kann eh getrost ignoriert werden.
An dem Speicherort, in dem unser Projekt erstellt wurde, befinden sich neben dem Debug-Ordner alle notwendigen Dateien, die unseren Arbeitsbereich definieren.

Step 02: Anlegen der Ordnerstruktur

Im Laufe der Zeit hat sich bei mir eine Ordnerstruktur bewährt, mit der ich sehr große Projekte, die teilweise mehr als 100 Dateien sowie mehr als 70.000 Zeilen Quellcode haben, sauber handeln kann. Die Abbildung '2. Ordnerstruktur' zeigt den Grundordner des Projektes BaseCode. Darin befinden sich die Unterordner debug, welcher standardmässig von VS angelegt wird, include, source und release. Die letzten 3 Unterodner wurden von mir angelegt.


Abbildung 2. Ordnerstruktur

In dem Ordner include befinden sich allen Headerdateien *.h, der Ordner source enthält dagegen die Sourcecodedateien *.cpp und der Ordner release beinhaltet das Kompilat (die kompilierte EXE-Datei) sowie alle notwendigen Laufzeitdaten. Oftmals befindet sich dort der data, mission oder savegame Ordner. Und diesen Ordner release würden wir auch an unsere Kunden als gepackte *.zip Datei weitergeben.

Es besteht natürlich keine Pflicht die Struktur so zu übernehmen, wer das aber macht, wird es später nicht mehr missen wollen.

Step 03: Einstellungen im Visual Studio

Alle Einstellungen werden in dem Registerdialog Projekteinstellungen der über die Tastenkombination ALT+F7 erreichbar ist, vorgenommen. Da es 2 Modis gibt, einmal der DEBUG- und einmal der RELEASE-Mode und diese auch unterschiedlich konfiguriert werden können, springen wir als erstes mit Hilfe der Tastenkombination ALT+E zu den Einstellungen für: und wechseln dort auf 'Alle Konfigurationen'. Wir müssen die folgenden Einstellungen ändern:

Das Arbeitsverzeichnis:
Als erstes die Registerkarte Debug aufschlagen und dort mit der Tastenkombination ALT+V in das Einfabefeld 'Arbeitsverzeichnis:' springen. Dort wird unser eigens anglegter Ordner release eingetragen. Also einfach release rein und fertig.

Der zusätzliche Includepfad:
Hier auf die Registerkarte C/C++ springen. Dann in dem Dropdownfeld Kategorie: auf 'Präprozessor' wechseln. Die Tastenkombination ALT+V bringt uns in das Eingabefeld 'Zusätzliche Include-Verzeichnisse:' und hier tragen wir einfach unseren Include-Ordner include ein.
Jetzt werden alle *.h Dateien auch in diesem Ordner gesucht. Daher können wir im Quellcode später einfach #include "MeinHeader.h" eintragen und das VS würde diese Header auch in unserem eigenen Include-Ordner suchen und einbinden.

Der Name der Ausgabedatei:
In dem Registerdialog die Seite Linker auswählen. Dann durch die Tastenkombination ALT+N in das Eingabefeld 'Name der Ausgabedatei:' springen, wo wir dann relase/BaseCode.exe eintragen.
Wir sind schon fast an der richtigen Position. Das Einfabefeld 'Objekt-/Bibliothek-Module:' befindet sich direkt darunter und ist ansonsten über die Tastenkombination ALT+B erreichbar. Hier müssen die beiden statischen Linklibraries opengl32.lib und winmm.lib eintragen.

In der opengl32.lib Bibliothek befinden sich alle notwendigen Befehle um OpenGL zu nutzen. Die Bibliothek winmm.lib bietet uns verschiedene Win32 Multimediafunktionen an.

Das waren unsere Vorbereitungen, jetzt gehts ans Programmieren. :)

unsere Headerdatei

Wir starten damit, eine Headerdatei in unserem eigenen Include-Ordner zu erstellen. Also einfach in dem Arbeitsbereich auf die Registerkarte Dateien wechseln. Das ist dort, wo uns VS die BaseCode Dateien anbietet. Dann gehen wir auf den Menüpunkt Datei->Neu und wählen dort die 'C/C++ Header-Datei' aus. In dem Eingabefeld Dateinname, das wir über ALT+D erreichen, geben wir den Namen der Headerdatei BaseCode an. Jetzt noch schnell den Speicherpfad angeben, den geben wir mit Hilfe der Tastenkombination ALT+. (Punkt) an. In dem Speicherdialog einfach den Pfad zu unserem Include-Ordner suchen, den Ordner anwählen und mit dem Button 'OK' beenden. So und nun noch einmal auf den nächsten 'OK' Button und somit ist unsere Headerdatei erstellt. Sie sollte sich jetzt automatisch in dem Abschnitt 'Header-Dateien' dem Arbeitsbereich befinden.



Ab in die Headerdatei.....


Das erste Sinnvolle, was wir in der Headerdatei machen, ist zu überprüfen ob das Define __BASECODE__ gesetzt ist. Das machen wir mit dem Präprozessorbefehl #ifndef. Dieser Befehl setzt sich ganz einfach aus If not defined zusammen.

Jetzt könnte natürlich die Frage kommen, warum wir das überprüfen?
Der Grund ist, das wir das Define, wenn es nicht gesetzt ist, gleich in der nächsten Zeile mit der Präprozessoranweisung #define setzen.

Puhh, nun aber welchen Sinn hat das Ganze?
Am Ende der Headerdatei befindet sich die schliessende Präprozessoranweisung #endif. Alles was sich an Zeilen zwischen der ersten Präprozessoranweisung also #ifndef __BASECODE__ und dieser befindet, wird nur ein einziges mal durchlaufen und das einmalige Durchlaufen regulieren wir mit dem Setzen der Definition __BASECODE__. Wenn diese also noch nicht gesetzt ist, dann wird als erstes die Definition gesetzt und dann die gesamten Zeilen bis zum dazugehörigen #endif abgearbeitet. Beim nächsten Auftreffen auf die erste Präprozessoranweisung wird bis zum Ende (#endif) gesprungen.

Ja, aber jetzt mal ehrlich, warum?
Wir sind dadurch in der Lage, an jeder beliebigen Stelle in unserem Projekt diese Headerdatei zu inkludieren, ohne dass uns der Compiler lästige Fehler auswirft. Der Compiler kommt dann mit sowas nervigen wie... Blaaa.. is already defined. Jetzt aber nicht mehr.

Weiter im Kontext...

	//-----------------------------------------------------------------------------
	// Include
	//-----------------------------------------------------------------------------
	#include "windows.h";		// Windows Header
	#include "gl/gl.h";		// OpenGL Header
	//-----------------------------------------------------------------------------

Hier werden einfach die beiden notwendigen Headerdateien inkludiert. Das eine ist der globogalaktische Windowsheader. Und das andere ist der wichtigste OpenGL Header. Sollte dieser nicht gefunden werden, dann einfach mal auf www.OpenGL.org rumstöbern.

Als nächstes erwarten uns die Variablen, mit denen unser Programm gesteuert wird.


Die Bedeutungen der einzelnen Variablen, stehen immer fein säuberlich im Kommentar.

Als nächstes die Prozeduren, denn ohne die passiert gar nichts.


Jetzt noch einen Schwank zu Prozdural vs. OOP.
Hier bin ich der Meinung, dass es viele sinnvolle Anwendungen von beidem gibt. Wenn man in der Lage ist, diese beiden Smart miteinander zu kombinieren, dann gelangt man schneller ans Ziel. Jetzt könnte natürlich so ein Schlaumich kommen und sagen: Aber warum du nicht gleich GLut nutzen? (Der Angriff der Klonkrieger begonnen er hat!). GLut als Hinweis, ist eine Utilitylibrary für OpenGL, mit der man sehr einfach Windows initialisieren kann, Benutzereingaben abfragen und noch schneller zum Ergebnis kommen kann und das ganze noch plattformunabhängig. Also auch auf Linux usw. Aaaaber.....
Quake, was ja nicht mit DirectX sondern mit OpenGL (die Wissen schon warum :)) programmiert wurde, nutzt kein GLut und wir auch nicht. Es macht da Sinn, wo ich schnell so nebenbei mal einen kleinen Hack machen will, aber nicht dort, wo ich erstens was lernen und zweitens selber machen will.

unsere Sourcecodedatei

Wir sind soweit klar im Kopf, äh ich meine in der Headerdatei. Dann können wir ja gleich mal den fluffigen Quellcode runterschreiben.
Starten wir aber erst einmal damit, dass wir wie in unserer Headerdatei eine Datei erstellen. Das macht ihr bitte genauso wie bei der Headerdatei, mit dem Unterschied, dass Ihr jetzt nicht eine 'C/C++ Headerdatei', sondern eine 'C++ Quellcodedatei' auswählt und als Erstellungsort (ALT+.) den Ordner Source angebt.


Jetzt aber mal ganz fix Quellcode her, sonst knallt's.


Hier inkludieren wir unseren eigenen Header, man achte bitte darauf, dass wir keinerlei Pfadangaben angeben. Wir wissen ja jetzt, dass der Compiler auch unseren eigenen Include-Ordner durchsucht und daher die Datei auch darin findet.


Program_Init()

	//-----------------------------------------------------------------------------
	// Name: Program_Init( void )
	// Info: Initialisiert das Rahmenprogramm
	//
	// Return	= true / false
	//-----------------------------------------------------------------------------
	bool Program_Init( void )
	{
		// Erzeugen des Fensters
		Program_CreateWindow();

		// Das Renderdevice erstellen
		oglInit();

		// und wech
		return true;
	}
	//-----------------------------------------------------------------------------

Das ist unsere Hauptinitialisierungsprozedur (was für ein Wort). Darin werden alle für die Initialisierung notwendigen Prozeduren aufgerufen. Es können hier natürlich auch Settings durchgeführt werden (z.B. Bildschirmeinstellungen...) Auch wenn wir hier OpenGL erlernen wollen, müssen wir doch ein Mindestmaß an Wissen über die Windowswelt haben. Immer vorausgesetzt, dass unser OpenGL-Program auch auf Windows laufen soll. Wer daran denkt, mit OpenGL plattformübergreifende Programme zu erstellen, der unterliegt harten Restriktionen, die am Anfang nicht leicht sind.

Wir wollen aber mit Windows arbeiten und somit schauen wir uns die Prozedur Program_CreateWindow() an.


Program_CreateWindow()


Unser Fenster soll sich genau in der Mitte des Bildschirms befinden, dazu werden wir jetzt die Koordinaten berechnen. Wenn wir ein FullScreen erstellen wollen, ist das recht einfach, da die Mitte einfach die gewünschten Bildschirmgrössen geteilt durch 2 ist.


Der gesamte obere Teil berechnet uns die notwendigen Bildschirmkoordinaten für unser Fenster oder für den FullScreen. Die WindowsAPI Funktion GetClientRect() füllt die RECT Struktur sWinRect aus. In dieser stehen dann die Koordinaten für Oben, Links, Unten und Rechts. Anhand dieser 'Bildschirm-'Koordinaten können wir unser Fenster im Fenstermodus sauber in der Mitte positionieren. In den Variablen g_iCenter_X und g_iCenter_Y befindet sich der exakte Mittelpunkt. Später setzen wir unseren Mauscursor genau darauf. Das machen wir, damit bei einem Mausklick (z.B. Feuern oder selektieren) unser Fenster nicht auf einmal den Eingabefokus verliert.

Ok wir haben jetzt genug Informationen gesammelt, um die Fensterklasse zu registrieren. Windows braucht diese, um unsere Applikation (unser Window) in Windows selbst anzumelden.


Die Klassenstruktur sWC ist unsere Fensterklasse. Diese müssen wir mit Daten füllen, die Windows zum starten und aktualisieren von unserem Fenster/Program braucht. Eine wichtige Information ist der Hinweis auf unsere programminterne Nachrichtenprozedur (WinProc). An diese sendet Windows alle Nachrichten, die für uns relevant sein könnten.

Am besten stellt Ihr Euch das mal als Euren Posteingang vor. Die Registrierung und das Betanken der Klassenstruktur könnte Euer Personalbogen in Euerer Personalakte sein. Die wird automatisch angelegt, wenn Ihr eingestellt werdet, quasi im Unternehmen / in der Schule registriert werdet. Jetzt kann der Big Boss, Euer Chef oder Rektor, einfach in seine gesammelten Personalakten schauen und findet dort auch Eure. Jetzt sollt Ihr über einen neuen Feiertag informiert werden und dazu sucht er Eure Emailadresse, die interne Nachrichtenprozedur (WinProc) und an diese sendet er jetzt die Mail, dass morgen ein neuer Feiertag ist und Ihr zuhause bleiben dürft.

Ihr seht, es lohnt sich registriert zu sein. :)

Die Registrierung übernimmt dann die WindowsAPI Funktion RegisterClass(), sollte das fehlschlagen, so erscheint eine freundliche Hinweisbox und das Programm wird abgebrochen.

So, jetzt kommen die Einstellungen für den Windows- bzw. dem Fullscreenmode. Die Modis unterscheiden sich hier sehr stark.


Auch hier wird in dem oberen Teil eine Struktur gefüllt. Die Struktur DEVMODE dmScreenSettings wird mit den Device- und Umgebungsinformationen gefüllt. Da diese Umgebung natürlich zwischen dem Fullscreen- und dem Windowedmode unterschiedlich sind, wird sie auch unterschiedlich betankt. Beim Erstellen des Fullscreens kann es passieren, dass z.B. der Windowsmode initialisiert werden konnte, der FullScreenmode aber nicht. Das geht natürlich auch anders herum. Eine freundliche Hinweisbox meldet das Misslingen.

Jetzt werden wir endlich das Fenster erstellen.


Zugegeben, der Funktionsaufruf von CreateWindowEx() ist ziemlich umfangreich. Es muss Windows genauestens gesagt werden, wo das Fenster in welcher Art und Form erstellt werden soll. Natürlich darf unsere erzeugte Windowsinstanz (g_hInstance) nicht fehlen. Denn Windows muss ja wissen, zu wem das Fenster gehört. Wenn der Aufruf erfolgreich ist, dann befindet sich in g_hWnd das heißbegehrte Windowshandle. Man könnte dazu auch das Autokennzeichen vom Fenster sagen. Wir überprüfen, ob das Handle erfolgreich erstellt wurde. Wenn nicht, dann werden wir wie üblich eine kleine Hinweisbox darstellen und das Programm beenden. Da es aber in den allermeisten Fällen erfolgreich sein sollte, nutzen wir das Handle, um uns ein Device Context mit dem Befehl GetDC() zu erzeugen. Auch hier wieder die Überprüfung und ggf. der Programmabbruch.
Ein Device Context ist eine Datenstruktur, die Informationen über Devices wie Display oder Drucker hält. Wenn wir also etwas malen oder drucken wollen, dann brauchen wir diese Informationen aus der Datenstruktur. Später in dem OpenGL-Init werden wir noch sehen, dass der Render Context auf dem Device Context aufsetzt. Was ja logisch ist, da rendern was mit dem Display oder Drucker anstellt.

So langsam können wir unser Fenster mal darstellen.

	// Unser Fenster anzeigen
	ShowWindow( g_hWnd, SW_SHOW );

	// Unser Fenster nach vorn
	SetForegroundWindow( g_hWnd );

	// Unser Fenster soll den Eingabefokus haben
	SetFocus( g_hWnd );

	// Cursorausblenden
	ShowCursor( false );

	// Den Cursor wieder auf die Mitte setzen
	SetCursorPos( g_iCenter_X, g_iCenter_Y );

	// und wech
	return true;
	}
	//-----------------------------------------------------------------------------

Der Befehl ShowWindow() bringt unser Fenster zum Vorscheinen. Danach setzen wir nur noch ein paar Kleinigkeiten wie den Eingabefokus, ob der Cursor gezeigt werden soll oder nicht. Das können wir mit dem Befehl ShowCursor(true/false) machen. Wenn wir true übergeben, dann wird der Cursor angezeigt, mit false wird er ausgeblendet. Zum Schluss positionieren wir noch den Mauscursor in der Mitte unseres Fensters/Screens.

Das war die grösste Funktion in dem BaseCode. Die Anderen sind nur noch Peanuts. (wie einige Bänker Deutschlands zu sagen pflegen)


WinProc()

Die nächste Funktion ist die mysteriöse Nachrichtenprozedur. Aber letztlich ist es doch alles ganz einfach. In der Funktion Program_CreateWindow() wurde von uns eine Windowsklassenstruktur gefüllt. In dieser haben wir unsere interne Nachrichtenprozedur angemeldet. Erinnert Euch mal an den Emaileingang. Naja und genau diesen Emaileingang haben wir vor uns.

Wir wollen auf die folgenden Nachrichten reagieren:
1. WM_ACTIVE
2. WM_SYSCOMMAND
3. WM_CLOSE
4. WM_KEYDOWN
5. WM_KEYUP
Das bedeutet, dass diese Nachrichten eine Aktion auslösen. WM_ bedeutet WindowMessage. Wer noch mehr Nachrichten abfragen will, der sollte einfach mal in der MSDN nach WM_CLOSE oder Window Messages suchen.


In meinen Programmen und Spielen, nutze ich derzeit kein DirectInput. Daher gibt es bei mir immer das 256er Bool Array in dem für jeden Tastencode True oder False gesetzt werden kann. Das Setzen übernimmt die Nachricht WM_KEYDOWN. Da in dem Parameter wParam der Tastencode enthalten ist, müssen wir einfach die Taste auf True setzen. In der Hauptschleife WinMain() prüfen wir dann einfach die Tasten, die wir getestet haben wollen auf True oder False ab. Die Nachricht WM_KEYUP wird von Windows gesendet, wenn eine Taste losgelassen wurde. Hier ist die beste Gelegenheit, diese Taste im Boolarray wieder auf False zu setzen.
Wenn unser Programm geschlossen werden soll, dann bekommen wir die Nachricht WM_CLOSE zugestellt. Ist das nicht freundlich... Naja und wir senden dann unsererseits auch eine Close-Nachricht, um uns zu beenden.


WinMain()

Nachdem unser Programm kompiliert ist, wollen wir es ausführen. Zu diesem Zeitpunkt muss Windows der Einstiegspunkt bekannt sein. Diesen Punkt nennt man APIENTRY. Stellt Euch das wie eine Tür vor, denn wenn Ihr in ein geniales Haus wollt, wo es viele schöne Sachen gibt, dann müsst Ihr schon wissen wo die Eingangstür ist um das Haus zu nutzen und genau aus diesem Grund ist in der Deklaration das Wort APIENTRY zu finden.
Sobald Windows unser Programm startet, springen wir in die Funktion Program_Init() die uns schon bekannt ist. Dann folgt die eigentliche Hauptschleife, diese wird solange durchlaufen, wie die Variable g_bShutdown von uns nicht auf true gesetzt wurde.


In der Hauptschleife fragen wir beständig nach, ob die Nachricht WM_QUIT gesendet wurde. Ist das nicht der Fall, dann können wir unsere eigenen Programmbelange abarbeiten.

Die interne Nachrichtenprozedur (der Emailposteingang) von uns (WinProc()) setzt uns fleissig, dank der Nachricht WM_KEYDOWN, die gedrückten Tasten auf true. Und hier in der Hauptschleife fragen wir dann einfach die gewünschte Taste ab, ob die auf true steht. Sollte das so sein, dann wurde sie gedrückt. Die Codes können in dem Header Winuser.h nachgeschaut werden. Hier jedoch mal kurz die Wichtigsten:



Die restlichen Tastenkürzel einfach in der Winuser.h nachschauen.

Die Variable g_bActive zeigt an, ob unser Fenster, in dem unser Programm läuft, aktiv ist oder nicht. Wenn der Nutzer zum Beispiel auf ein anderes Programm umgeschaltet hat, unser Programm jedoch noch läuft, dann müssen wir den Eingabefokus (SetFocus(NULL)) freigeben. Sonst werden die Eingaben nicht an das gewünschte Programm weitergeleitet, sondern landen immer bei uns.


Program_ToggleScreenMode()

Diese Funktion ist wie ein Wechselschalter. Eimal drücken, Licht ein und ein zweitesmal drücken Licht wieder aus.


Als erstes invertieren wir unser Fullscreenflag. Das geht ziemlich einfach mit dem Ausrufezeichen. Es bedeutet ja ungleich. Also wenn g_bFullScreen gleich ungleich g_bFullScreen ist, wird es bei jeden Aufruf dieser Zeile invertiert. Es gibt ja nur true und false bei einer Boolvariablen. Da jetzt das Fullscreenflag invertiert wurde, müssen wir einfach unser Hauptfenster zerlegen und neu erstellen. Die Funktionen sind für beide Modi ausgelegt. Wenn das Fenster dann steht, dann wechseln wir auch im OpenGL den ScreenMode. Das können wir erst dann machen, weil mit dem Zerstören von unserem Fenster ja unser Device Context flöten geht und diesen brauchen wir zwingend für unseren Render Context. Also muss dieser erst wieder neu erstellt werden.

Sollte davon etwas fehlgeschlagen sein, so springen wir vor dem Ende einfach mit false raus. Ansonsten gibts ein true zurück.


Program_Shutdown()

Ein Programm muss nicht nur sauber initialisiert werden, sondern auch sauber entfernt werden. Dabei schreiben wir uns für jedes Subsystem eine eigene ShutDown-Prozedur. Diese werden dann von dieser zentralen Funtkion aufgerufen. Später wenn mal Klassen dazukommen, dann werden diese Klasse hiern aus dem Speicher entfernt. Die Klassen rufen dann durch ihren Destruktor (der wird beim Entfernen automatisch aufgerufen), alle notwendigen Methoden auf, die die Klasse sauber beenden. Ein guter Programmierer unterscheidet sich von einem schlechten dahingehend, dass seine Programme schnell und schlank gestartet und sauber sowie stabil laufen und um das ganze wirklich abzurunden auch sauber verlassen. Ich hasse es wie die Pest, wenn ein Programm beim Beenden noch mein System blockiert. Oder ich das Gefühl vermittelt bekomme, doch mal lieber einen Reboot zu machen, um weiter arbeiten zu können. Das unterscheidet professionelle Produkte von Müll.

	//-----------------------------------------------------------------------------
	// Name: Program_Shutdown( void )
	// Info: Beendet das Rahmenprogramm
	//-----------------------------------------------------------------------------
	void Program_Shutdown( void )
	{
	// Registry Displaysettings einstellen
	if( g_bFullScreen ) Program_ToggleScreenMode();

	// Unser Fenster schliessen
	Program_DestroyWindow();

	// Renderdevice freigeben
	oglShutdown();

	// Cursor anzeigen
	ShowCursor( true );
	}
	//-----------------------------------------------------------------------------

Bevor das Programm beendet wird, schauen wir nach, ob es sich gerade im Fullscreenmode befindet. Sollte das so sein, dann wechseln wir erst in den Fenstermode, um dann sofort das Fenster zu schliessen und zu beenden. Damit wird der Desktop in seiner ursprünglichen Darstellung wieder hergestellt.


Program_DestroyWindow()

Wer Fenster erstellen kann, der kann auch Fenster schliessen. Wir werden in dieser Funktion das Device Context entfernen, unser Fenster zerstören und die Fensterklasse freigeben.


In meinen Programmen pflege ich die Pointer, die von mir erstellt wurden, immer beim Löschen auf NULL zu setzen. Das erleichtert später beim Programmieren das Überprüfen, ob ein Pointer gültig ist oder nicht. Dazu ein kurzes Beispiel, was allerdings nicht in das Programm gehört. Daher nur lesen und verstehen und nicht abtippen.



oglInit()

Endlich was mit OpenGl... In dem Ganzen Quellcode hier waren ja nur kleine Stücke von OpenGL enthalten, jetzt kommt aber endlich Butter aufs Brot.
Die Initialisierung von OpenGL nutzt eine Unterfunktion oglGetPixelformat() die sucht uns das, naja was soll ich sagen!?! das Pixelformat raus. Sie wird im Anschluss erläutert.

	//-----------------------------------------------------------------------------
	// Name: oglInit( void )
	// Info: Initialisiert die OpenGL API
	//
	// Return			= True / False
	// ----------------------------------------------------------------------------
	bool oglInit( void )
	{
	// Pixelformat holen
	if( !oglGetPixelformat() )
			return false;		// und wech FEHLER

	// RenderContext erstellen
	if( !( g_hRC = wglCreateContext( g_hDC ) ) )
			return false;		// und wech FEHLER

Hier ist die schon öfters erwähnte Stelle, an der das Device Context zur Erstellung des Render Contextes genutzt wird. Nachdem der Render Context erstellt wurde, wird er aktiviert.

	// Renderkontext aktivieren
	if( !wglMakeCurrent( g_hDC, g_hRC ) )
			return false;		// und wech FEHLER

	// Bildschirm und den Viewport anpassen
	oglReSize( 0, 0, g_iScreenWidth, g_iScreenHeight );

	// einschalten von Smooth Shading
	glShadeModel( GL_SMOOTH );

OpenGL kennt insgesamt 2 Shading Metoden. GL_SMOOTH sorgt für eine weiche Licht- und Farbberechnung. Die 3 Farben, der einzelnen Vertices (so nennt man die Eckpunkte) des Dreiecks von uns, werden im GL_SMOOTH Mode sauber vermischt. Im GL_FLAT Mode wird nur eine Farbe und auch nur eine Lichtberechnung gemacht. Für alle Primitiven, die OpenGL kennt, also Triangle, Punkte, Linien usw. nimmt die OpenGL API beim Rendern mit GL_FLAT die Farbe des letzte Vertex (Einzahl von Vertices). Aber hier machen die Polygone eine Ausnahme. Würden wir das Dreieck als Polygon also mit GL_POLYGON rendern, dann würde die Farbe des ersten Vertex genommen werden, in diesem Falle Rot.

GL_SMOOTH
weicher Berechnung
GL_FLAT
flacher Berechnung
mit GL_TRIANGLES
GL_FLAT
flacher Berechnung
mit GL_POLYGON
Abbildung 3. GL_SMOOTH
Abbildung 4. GL_FLAT
und GL_TRIANGLES
Abbildung 5. GL_FLAT
und GL_POLYGON

GL_SMOOTH ist übrigens standardmässig eingestellt. Es macht aber Sinn es noch einmal explizit zu setzen.
Als nächstes setzen wir das Backface Culling.

	// Backface Culling
	glEnable( GL_CULL_FACE );

	// CounterClockWise order als Vorderseite angeben
	glFrontFace( GL_CCW );

Mit Culling ist das Entfernen von geometrischen Objekten gemeint. Wir können einzelne Dreiecke cullen, Teile von Objekten oder ganze Objekte selbst. Je mehr wir vor dem Rendern durch die Grafikkarte cullen können, desto schneller ist unsere 3D Engine / unser Programm. Das einfachste cullen, was von OpenGL selbsttätig durchgeführt wird, ist das sogenannte Backface Culling. Backface heist Rückseite und genau die wollen wir von einen Dreieck niemals sehen, da wir uns, wenn das passieren sollte, in illegaler Geometrie befinden. Nur in den seltensten Fällen macht es Sinn, die Rückseiten eines Polygons mit zu rendern. In den 3D Engine's die heute so genutzt werden, rendert keiner diese Rückseiten mit. Das Verfahren dahinter ist recht simpel. Nehmen wir mal ein einfaches Dreieck, das aus 3 Vertices (Eckpunkten) besteht. Am besten zeichnet Euch das Dreieck mal auf einem Blatt auf. Nummeriert die 3 Vertices durch, von 1 bis 3. Wenn ihr jetzt einen Pfeil (auch Vektor genannt) von dem ersten Vertex (Eckpunkt) zu dem zweiten Vertex zeichnet und dann noch von dem zweiten zu dem dritten und weil es so schön ist auch noch von dem dritten zu dem ersten Vertex zurück, dann stellt Ihre eine Reihenfolge fest. Diese ist entweder im Uhrzeigersinn (Clockwise CW) oder gegen den Uhrzeigersinn (CounterClockWise CCW). Jetzt müsst Ihr nur noch eine Reihenfolge als Vorderseite definieren, zum Beispiel die gegen den Uhrzeigersinn. Wenn Ihr jetzt das Blatt umdreht und die Vertices von hinten betrachtet, dann ist auf einmal die Reihenfolge der Vertices von 1 bis 3 im Uhrzeigersinn und somit habt Ihr den Beweis erbracht, dass es die Rückseite ist.
Genauso geht auch OpenGL vor, mehr steckt da nicht dahinter.

Als nächstes setzen wir die Löschfarbe, wir können sie aber auch als Hintergrundfarbe bezeichnen. Mit dieser Farbe wird jeder neue Frame übermalt. In Weltraumspielen ist diese zumeist schwarz, in Outdoor spielen entweder blau oder weiss. Farben können in OpenGL auf verschiedene Arten angeben werden. Eine ist die Methode mit den Floats. Da bedeutet 0.0f (das kleine f steht für Float 'Fliesskommazahl') kein Farbanteil und 1.0f bedeutet maximaler Farbanteil. Wenn wir also für Rot 1.0f, Grün 1.0f, Blau 1.0f und Alpha 1.0f angeben, dann ist das die Farbe Weiß. Wenn wir hier für Rot, Grün und Blau jeweils 0.0f angeben, dann wäre das Schwarz. Alle Werte dazwischen, bedeuten auch Werte dazwischen. So ist zum Beispiel 0.5211103f ein klein wenig mehr als die Hälfte der möglichen Farbanteile. Der Alphawert wird für die Transparenz genutzt. Um das nutzen zu können, muss das Alphablending eingeschaltet sein. glEnable( GL_BLEND ); da wir das aber hier nicht brauchen, war das nur ein kleiner Hinweis.


Der Depthbuffer oder auch Z-Buffer genannt, sorgt dafür, dass unsere Szenen die wir rendern, korrekt dargestellt werden. Ein kurzes Beispiel dazu. Mal angenommen, wir wollen 2 Dreiecke rendern. Das Dreieck A liegt vor dem Dreieck B. Wenn wir den Depthbuffer ausgeschaltet lassen, mittels glDisable( GL_DEPTH_TEST ); und dann als erstes das Dreieck A rendern, das liegt vor B und erst dann das Dreieck B, so würde das Dreieck B durch das A durchscheinen. Das passiert daher, weil OpenGL niemand sagt, dass dort, wo er was rendern will, schon jemand was gerendert hat.
Diese Tiefensortierung mussten pfiffige Programmierer früher mit Hilfe des Painteralogryhtmus lösen. Der BSP-Tree wurde auch aus diesem Grunde entwickelt. Dort werden die Dreiecke so zerlegt, dass konvexe Räume entstehen. Konvex bedeutet, dass von jedem beliebigen Punkt aus alle Dreiecke sichtbar sind. Mit dem BSP-Tree sind wir in der Lage, eine Tiefensortierung durchzuführen. Daher kann man wenn ein BSP-Tree genutzt wird den Z-Buffer ausschalten. Allerdings mit dem geschulten Auge sieht man dort die häßlichen Splits. Das passiert bei einem eingeschalteten Dept_Test nicht. Und wenn wir mal auf den Aufwand schauen, dann brauchen wir für den Depth_Test 3 Zeilen und der BSP nimmt locker 2500 Zeilen. Der BSP-Tree wird ja heute auch ganz anders eingesetzt, insofern macht er teilweise noch Sinn.

Mal kurz bremsen.... Also der Z-Buffer oder Depth_Buffer sorgt dafür, dass durch OpenGL eine Tiefensortierung erfolgt. Der Wert, den wir durch die Funktion glClearDepth() übergeben, wird jeweils beim Löschen des Buffers, in diesen rein geschrieben. Das passiert in jedem Frame. Danach schalten wir dann den Test mittels glEnable( GL_DEPTH_TEST ); ein. Das bringt uns aber noch nicht weiter, denn wir müssen noch definieren, wie unser Test erfolgen soll. Angenommen an einem Bildschirmpunkt soll noch ein weiterer Punkt gezeichnet werden. Dieser neue Punkt liegt hinter dem schon existenten Punkt. Jetzt braucht OpenGL eine klare Anweisung. Soll OpenGL den neuen Punkt an die gleiche Stelle rendern oder nicht? Es gibt hier mehrere Tests, die eingestellt werden können. Der am häufgsten genutzte ist GL_LESS. Wenn der neue Punkt jetzt vor dem existenten Punkt liegen würde, also kleiner (LESS) wäre, dann würde dieser auch gerendert werden. Hätten wir den Test auf GL_GREATER als grösser gesetzt, dann müsste der neue Punkt hinter dem alten liegen, um gerendert zu werden.

Dann muss OpenGL noch wissen, wie gerendert werden soll. Es gibt 3 Modi, wie unsere Polygone (damit ist alles gemeint was wir rendern) gerendert werden. Diese Modi können auf die Vorder- und Rückseite unterschiedlich angewendet werden. Die Auswahl der Seite erfolgt mit den folgenden Befehlen:


Und die Modi sind GL_FILL, GL_LINE und GL_POINT. Wobei ich die Punkte in der Darstellung GL_POINT vergrössert habe.

GL_FILL GL_LINE GL_POINT
Abbildung 6. GL_FILL
Abbildung 7. GL_LINE
Abbildung 8. GL_POINT
	// Solid polygons
	glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );

	// Licht ausschalten
	glDisable( GL_LIGHTING );

	// und wech, alles OK
	return true;
	}
	// ----------------------------------------------------------------------------

Und ganz zum Schluss schalten wir dann das Licht mit dem Befehl glDisable( GL_LIGHTING ); aus.

OpenGL ist eine Statemachine, das bedeutet, dass es verschiedene Stati in OpenGL gibt. Ein Status ist zum Beispiel ob Lichtberechnungen erfolgen sollen oder nicht. Diesen Status können wir über GL_LIGHTING mit den beiden Befehlen glEnable() und glDisable() ein bzw. ausschalten. Wir haben noch einen weiteren Status in OpenGL eingestellt. Das war unser Depth_Test. Dort war es GL_DEPTH_TEST, den wir eingeschaltet haben. Ein Status bleibt in OpenGL solange gesetzt, bis er von uns durch die beiden Befehle geändert wird. Statemachine bedeutet aber noch mehr. Zum Beispiel, wenn wir die Farbe für einen Vertex (Eckpunkt) auf rot setzen, dann wird jeder nachfolgende Vertex ebenfalls auf rot gesetzt, solange bis wir eine neue Farbe setzen. Ein anderes und letztes Beispiel, ist die Definition der Vorderseite. Es bleibt solange die Vorderseite im Uhrzeigersinn, bis wir sie auf entgengengesetzt dem Uhrzeigersinn stellen.

So, das war schon mal eine Menge Brot.... ich hole mir jetzt erst einmal einen Kaffee.


oglGetPixelformat()

Was bei DirectX die Enumeration ist, ist bei uns quasi das Pixelformat. Wir definieren in der Struktur pfd die vom Typ PIXELFORMATDESCRIPTOR ist, das gewünschte Pixelformat. In den Kommentaren steht jeweils was die Bedeutung der einzelnen Werte ist.


Jetzt ist das von uns gewünschte Pixelformat definiert und wir können uns ein ähnliches oder sogar das gewünschte Format raussuchen lassen. Dazu brauchen wir als Erstes mal wieder unser schönes Device Context, in dem ja jede Menge Informationen über das Display usw. enthalten sind. Als weiteres übergeben wir dem Befehl ChoosePixelFormat() noch das von uns gewünschte Pixelformat in der Struktur pfd. Und als Ergbnis bekommen wir eine einfache 'doofe' Nummer zurück. Diese Magicnumber ist das Pixelformat, welches wir direkt danach mit SetPixelFormat() setzen.


Wenn dann alles glatt lief, verlassen wir die Funktion mit true.


oglReSize()

Und jetzt mal ein kleiner Test.... Viewport, Matrix und Frustum.

Wie fühlt Ihr Euch jetzt? Lasst mich raten. Ihr sitzt stocksteif vor dem Monitor und könnt kaum noch atmen? Aber Ihr habt es überlebt ja? Das ist gut so, denn viel mehr braucht Ihr nicht um hinter diese Fremdwörter zukommen. Also los geht's.

Als Viewport wird der Bereich bezeichnet, in dem die Ausgabe von OpenGL stattfindet. Auf dem Bildschirm können auch mehrere Viewports genutzt werden. Denkt einfach mal an die ganzen Editoren, dort gibt es meistens 4 Viewports.

Wir werden zu Beginn mit OpenGL nicht viel mit den Matrizen in Kontakt kommen. Daher beschränke ich mich darauf, kurz zu erklären warum es diese Matrizen gibt. Wenn wir mit einem Modeler ein fluffigen Krieger erstellen, dann wollen wir diesen auch dargestellt bekommen. Der Krieger liegt in Form von Vertexinformationen also (X,Y,Z), sowie Texturinformationen im Speicher vor. Die ganzen Informationen müssen durch OpenGL so umgerechnet werden, dass sie in unserem Falle perspektivisch vor uns dargestellt werden können. Und das nicht nur als Standbild, sondern auch dann, wenn wir mit der Kamera durch die Szene brausen. Diese ganzen Umrechnungen, der Vertex- und Texturinformationen sowie die Bewegung (Translation) und die Rotation werden durch Matrizen erledigt. Und das macht OpenGL fasst alles alleine für uns. Mit dem Befehl glMatrixMode( GL_MODELVIEW ); setzen wir als aktuelle Matrix die Modelview Matrix. Damit sie sauber initialisiert ist, resetten wir diese mit Hilfe von dem Befehl glLoadIdentity();.

Um unser Frustum zu definieren, müssen wir die Matrix auf GL_PROJECTION setzen und resetten. Dann definieren wir den Frustum. Ein Frustum ist die abgesägte Sichtpyramide, dazu einfach Abbildung '9. Frustum' angeschaut.

Abbildung 9. Frustum
Das Frustum wird durch die Maße der Clippingplanes (Seitenwände), die Entfernung des Near und des Farplanes definiert. Das machen wir mit dem Befehl glFrustum(), wobei die ersten 4 Werte die Maße der Clippingplanes sind, gefolgt von der Entfernung des Nearplanes. Sie sollte niemals 0.0f sein. Ansonsten gibts grössere Probleme. Dann kommt nur noch die Entfernung des Farplanes.

Bei Erfolg, von dem wir einfach mal ausgehen, wird true returned.


oglShutdown()

Wir dürfen unser Programm nicht einfach so beenden, sondern es muss noch der Render Context entfernt werden. In grösseren Programmen würden hier auch die eigenen internen Buffer gelöscht werden, also das was unsere Szene ausmacht. Dazu zählen Vertexbuffer (X,Y,Z), Texturdaten und weitere Informationen wie zum Beispiel Gameentities (das sind Gegner, Gegenstände, Schalter usw.). Das bleibt uns aber alles im Moment erspart, weil wir nur den Render Context haben.

	//-----------------------------------------------------------------------------
	// Name: oglShutdown( void )
	// Info: Beendet OpenGL
	//-----------------------------------------------------------------------------
	bool oglShutdown( void )
	{
	// Ist unser Render Context noch da?
	if( g_hRC )
	{
	  // Versuchen wir mal die Contexte zu releasen
	  if( !wglMakeCurrent( NULL, NULL ) )
			return false;	// und wech FEHLER

	  if( !wglDeleteContext( g_hRC ) )
			return false;	// und wech FEHLER

	  // Rendercontext nullen
	  g_hRC = NULL;
	}

	// und wech, alles OK
	return true;
	}
	//-----------------------------------------------------------------------------

Sollte bei der Deinitialisierung etwas fehltschlagen, so springen wir mit false raus. Bei Erfolg gibt es ein true zurück.


oglSetScreenMode()

Ein Wechsel zwischen dem Fenster- und dem Vollbilschirmmodus ist dank unseren fluffigen Funktionen nicht so problematisch.


Das erste was gemacht werden muss ist blanke Zerstörung :). Es muss nämlich der alte und schäbige RenderContext gelöscht werden, das machen wir wie in der Funktion oglShutdown(). Dann wenn dieser Weg ist, erstellen wir uns mit einem einzigen Aufruf von oglInit() diesen neu, das wars.

Da wir so erfolgreich waren übergeben wir ein true beim verlassen.


oglFrameStart()

Die letzte Funktion die wir uns ansehen werden, wird die Funktion Program_Update() sein. In dieser finden letztendlich die Renderaufrufe statt. Um OpenGL auf Render vorzubereiten, rufen wir diese Funktion hier auf. Sie sorgt dafür, dass vor dem Frame, in dem unsere Szene gerendert wird, alles zum Rendern vorbereitet ist.


Zu diesen Vorbereitungen zählt, in unserem Falle erst einmal nur das Löschen des Framebuffers (GL_COLOR_BUFFER_BIT), genauer gesagt des Backbuffers und des Z-Buffers (GL_DEPTH_BUFFER_BIT). Nachdem Aufruf mit glClear(); enthalten diese beiden Buffer die von uns gewünschten Werte. Im Framebuffer (Backbuffer) befinden sich lauter Nullen und im Z-Buffer lauter Einzen (glClearDepth( 1.0f )). Dann wird noch die Matrix mit dem Befehl glLoadIdentity(); resettet.
Jetzt sind wir klar zum rendern.


oglFrameEnd()

Das Frameende ist nah. Nachdem unsere Szene in den Backbuffer gerendert wurde, müssen wir alle Befehle von OpenGL abschliessen und den Front- und Backbuffer austauschen. Das Austauschen erfolgt mit dem WindowsAPI-Befehl SwapBuffers();.

	//-----------------------------------------------------------------------------
	// Name: oglFrameEnd( void )
	// Info: Den Frame abschliessen, alle OGL-Befehle beenden und Swapbuffers()
	//-----------------------------------------------------------------------------
	void oglFrameEnd( void )
	{
	// Alle Befehle beenden
	glFlush();

	// Backbuffer zu Frontbuffer
	SwapBuffers( g_hDC );
	}
	//-----------------------------------------------------------------------------

Hmm... jede Menge Buffers. Z-Buffer, Backbuffer, Framebuffer usw. gibts da noch mehr und warum sind die da?
Den Z-Buffer haben wir schon kennengelernt. Der war ja zur automatischen Tiefensortierung da. Der Framebuffer ist einfach der aktuelle Buffer, der auf unserem Bildschirm angezeigt wird. Ein Buffer selbst ist nichts weiter, als ein grosses 2 Dimensionales (X,Y) Zahlenarray. Im Z-Buffer Falle steht an jeder 'Koordinate', in welcher Tiefe ein Pixel (Bildschirmpunkt) gemalt wurde. Im Framebuffer steht an jeder Koordinate, welche Farbe der Pixel hat. Daher auch der Befehl zum Löschen mittels glClear(GL_COLOR_BUFFER_BIT). Jetzt hab ich auch noch was vom Backbuffer erzählt und vom Austauschen mit dem Frontbuffer. Ui... wie gesagt viele, viele Buffers. Schauen wir doch einfach mal auf die Abbildung 10. Doppelbufferung.


Abbildung 10. Doppelbufferung
Das Doublebuffering haben wir uns übrigens in dem Pixelformat ausgesucht (PFD_DOUBLEBUFFER). Es dient dazu, dass wir schön in aller Seelenruhe in dem jeweiligen Backbuffer rummalen können, ohne das irgend jemand das 'Bild' sehen will, bevor es fertig ist. Erst damit erreicht man sanfte Animationen und smartes Scrolling.

Program_Update()


Um in unserem Programm die Geschwindigkeit messen zu können, nutzen wir den Befehl timeGetTime(). Dies ist der einzige Befehl, den wir aus der Biblioteht winmm.lib, die wir neben der opengl32.lib statisch gelinkt haben, nutzen. Hier speichern wir also pro Frame die aktuelle Zeit in g_ulCurrentTime ab und bilden die Differenz mit der alten Zeit g_ulOldTime in fDeltaTime. Wenn diese grösser 1000, also 1 Sekunde ist, dann setzen wir die aktuelle Zeit als die alte Zeit und übernehmen die Anzahl der gerenderten Frames. Da wir das ja nur machen, wenn wir 1 Sekunde voll haben, zählen wir jeweils pro Durchlauf einen Frame und die Anzahl der Durchläufe innerhalb dieser Sekunde ist die FPS-Rate (Frames Per Second).

	// Den neuen Frame initialisieren
	oglFrameStart();

	//
	// Hier erfolgen jetzt die einzelnen Renderaufrufe
	//

	// Translation auf der Z-Achse 'in den Monitor rein', damit wir was sehen
	glTranslatef( 0.0f, 0.0f, -3.0f ); 		// ( X, Y, Z )

	// Renderstart
	glBegin(GL_TRIANGLES);

	  // Den ersten Vertex setzen
	  glColor3f(  1.0f, 0.0f, 0.0f);		// Farbe erste Ecke
	  glVertex3f( 2.0f,-1.0f, 0.0f);

	  // Den zweiten Vertex setzen
	  glColor3f(  0.0f, 1.0f, 0.0f);		// Farbe zweite Ecke
	  glVertex3f( 0.0f, 1.0f, 0.0f);

	  // Den dritten Vertex setzen
	  glColor3f(  0.0f, 0.0f, 1.0f);		// Farbe dritte Ecke
	  glVertex3f(-2.0f,-1.0f, 0.0f);

	// Renderende
	glEnd();

	// Und hier wird der Frame beendet
	oglFrameEnd();

	// und wech
	return true;
	}
	//-----------------------------------------------------------------------------

Bevor wir die Vertices (Eckpunkte) und die Farben zum Rendern angeben, muss OpenGL wissen, was er genau rendern soll. Insgesamt sind OpenGL 10 Primitive bekannt. Ich werde hier die wichtgsten bildhaft aufführen.

Punkte gehören zu den einfachsten Primitiven. Einfach einzelne Vertices übergeben und fertig.

// Renderstart
glBegin(GL_POINTS);

  // Den ersten Vertex setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);

  // Den zweiten Vertex setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);

  // Den dritten Vertex setzen
  glColor3f(  0.0f, 0.0f, 1.0f);	// Farbe dritte Ecke
  glVertex3f(-2.0f,-1.0f, 0.0f);

// Renderende
glEnd();

Wenn einfache Linien gerendert werden sollen, dann müssen die Vertices immer paarweise angegeben werden. Auch eine Linie kann in OpenGL mit einem Farbverlauf versehen werden. Dazu muss der ShadeMode GL_SMOOTH gesetzt sein und der Startpunkt eine andere Farbe als der Endpunkt aufweisen. Wenn eine komplexe Szene als Drahtgittermodel gerendert werden soll, dann belassen wir den Primitiventyp dabei, was ursprünglich eingestellt war und ändern einfach den PolygonMode. Werft mal ein Blick auf oglInit() da haben wir das behandlet.

// Renderstart
glBegin(GL_LINES);

  // Linienanfang setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);

  // Linienende setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);
		
// Renderende
glEnd();

Die wohl wichtigste Primitivenart. Mit Dreiecken läst sich einfach alles erstellen. Es müssen, wie der Name es uns verrät, immer 3 Vertices angegeben werden. :)

// Renderstart
glBegin(GL_TRIANGLES);

  // Den ersten Vertex setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);
		
  // Den zweiten Vertex setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);

  // Den dritten Vertex setzen
  glColor3f(  0.0f, 0.0f, 1.0f);	// Farbe dritte Ecke
  glVertex3f(-2.0f,-1.0f, 0.0f);
		
// Renderende
glEnd();

Dies ist eine sparsame Methode, Dreiecke zu erzeugen. Wenn wir 3 Vertices übergeben, dann haben wir ein einfaches Dreieck. Übergeben wir jedoch ein 4tes Vertex, so haben wir ein weiteres Dreieck, was den ersten Übergebenen Vertex als seinen ersten Vertex nimmt und den Vorgänger Vertex als seinen zweiten Vertex nimmt. Wenn wir einfach mal auf die Abbildung 14. Primitive GL_TRIANGLES_FAN schauen, dann können wir fast einen Fächer erkennen. Daher rührt auch der Namen.

// Renderstart
glBegin(GL_TRIANGLE_FAN);

  // Den ersten Vertex setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);
		
  // Den zweiten Vertex setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);

  // Den dritten Vertex setzen
  glColor3f(  0.0f, 1.0f, 1.0f);	// Farbe dritte Ecke
  glVertex3f(-2.0f, 1.0f, 0.0f);
		
  // Den vierten Vertex setzen
  glColor3f(  0.0f, 0.0f, 1.0f);	// Farbe vierte Ecke
  glVertex3f(-2.0f,-1.0f, 0.0f);
		
// Renderende
glEnd();

Dieser Primitve ist der Schnellste und Sinnvollste. Er ist ähnlich dem Primitive GL_TRIANGLE_FAN mit dem Unterschied, dass er wie die Abbildung 15. Primitive GL_TRIANGLE_STRIP zeigt, für viele Anwendungsfälle günstiger anzuwenden ist.

// Renderstart
glBegin(GL_TRIANGLE_STRIP);

  // Den ersten Vertex setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);
		
  // Den zweiten Vertex setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);

  // Den dritten Vertex setzen
  glColor3f(  0.0f, 0.0f, 1.0f);	// Farbe dritte Ecke
  glVertex3f(-2.0f,-1.0f, 0.0f);
		
  // Den vierten Vertex setzen
  glColor3f(  0.0f, 1.0f, 1.0f);	// Farbe vierte Ecke
  glVertex3f(-2.0f, 1.0f, 0.0f);
		
// Renderende
glEnd();

Das letzte Primitive, das ich vorstelle ist GL_QUADS in verschiedenen Fällen macht es Sinn dieses zu nutzen. Performanceförderlich ist dieser jedoch nicht. Es gibt von diesem Primitive noch eine _STRIP Variante, die ähnlich dem Primitive GL_TRIANGLE_STRIP funktioniert.

// Renderstart
glBegin(GL_QUADS);

  // Den ersten Vertex setzen
  glColor3f(  1.0f, 0.0f, 0.0f);	// Farbe erste Ecke
  glVertex3f( 2.0f,-1.0f, 0.0f);
		
  // Den zweiten Vertex setzen
  glColor3f(  0.0f, 1.0f, 0.0f);	// Farbe zweite Ecke
  glVertex3f( 0.0f, 1.0f, 0.0f);

  // Den dritten Vertex setzen
  glColor3f(  0.0f, 1.0f, 1.0f);	// Farbe dritte Ecke
  glVertex3f(-2.0f, 1.0f, 0.0f);
		
  // Den vierten Vertex setzen
  glColor3f(  0.0f, 0.0f, 1.0f);	// Farbe vierte Ecke
  glVertex3f(-2.0f,-1.0f, 0.0f);
		
// Renderende
glEnd();


Es gibt jetzt noch 3 weitere Primitive. Dies sind GL_POLYGON, GL_LINE_LOOP und GL_LINE_STRIP. Da diese recht selten genutzt werden, will ich sie zumindest mal erwähnt haben.

Der Quellcode hinter den einzelnen Primitven soll dazu animieren, diesen mal gegen den in der Funktion Program_Update() auszutauschen. Dann könnt Ihr die einzelnen Primitven mal live und in Farbe erleben. Auch hatten wir uns mal in der Funktion oglInit() über die Shademodis unterhalten. Dort erwähnte ich, dass GL_POLYGON nicht die Farbe des letzten, sondern des ersten Vertex als solide Farbe nutzt. Probiert das doch auch mal aus. Die Primitiven werden in dem Funktionsaufruf glBegin(Primitive) angegeben.

das Ende

Irgendwie ist das 'kleine' Tutorial etwas länger geworden als ich gedacht hatte. Ich hoffe, dass Euch das Durcharbeiten genau so viel Spass gemacht hat, wie mir das Erstellen. Für Feedback zu diesem Turorial bin ich sehr dankbar. Auch wird sicher der Eine oder Andere kleine Fehler darin stecken. Wer also sachdienliche Hinweise zur Ergreifung von Fehlern hat, bitte einfach melden. Ich werde diese entsprechend ausräumen. Ob es in nächster Zukunft weitere Tutorials gibt, will ich nicht versprechen. Da es, wenn das Tutorial gründlich sein soll, eine zeitintensive Arbeit ist. Ich schätze den reinen Nettostundenanteil auf rund 25 Stunden. Wobei die Progammierung in weniger als 20 Minuten erledigt war. Wie gesagt Versprechungen gibt es keine, da ich lieber mit Tatsachen und Fakten überzeuge.

mein Dank gilt

Als erstes Euch, da Ihr dem Tutorial Eure wertvolle Zeit widmet.
Dann folgt auch gleich mein Schatz Janine. Sie sorgte dafür, dass Ihr nicht permanent über meine üble Rechtschreibung lacht.
Um die Sinnfälligkeit meiner Aussagen im Tutorial zu überprüfen, habe ich noch ein paar Freunde belästigt, denen gilt auch mein Dank:
Stefan Zerbst, Heiko Kalista, Jörg Winterstein, Eike Anderson

was richtig gut wäre...

Wenn es den Einen oder Anderen gibt, der den Quelltext des Tutorials in eine andere Programmiersprache übersetzt, wird dieser Held natürlich hier präsentiert.

zum Downloaden

Die Projektdaten:
Visual Studio 6.0 und 7.0 C/C++ rein prozedural Projektdateien von Oliver Düvel
BlitzBasic - Projektdateien von Oliver Skawronek aka Vertex
Delphi 5 - Projektdateien von Jan Michalowsky aka JanBacke
In der Zipdatei findet Ihr neben einem Visual Studio 6.0 Arbeitsbereich, auch einen Arbeitsbereich für das Visual Studio.NET. Das wird übrigens jetzt Solution genannt.
Die anderen Sprachen enthalten natürlich entsprechenden Quellcode ohne VC Arbeitsbereiche / Solutions.

Alle Rechte vorbehalten, Grafiken, Texte und andere Daten dürfen nicht ohne meine schriftliche Genehmigung veröffentlicht oder verbreitet werden.


Gruss Oliver Düvel
...und vielen Dank für den Fisch.



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick