Tutorial Lektion 1

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Nicht zu weit aus dem Fenster lehnen

Vorwort

Hallo, in dem folgenden Tutorial möchte ich Euch etwas näher mit OpenGL vertraut machen. Der Großteil dieses Tutorials besteht aus bloßer Theorie, die jedoch absolut notwendig ist. Es ist nicht leicht über ein so trockenes Thema zu schreiben, denn in aller Regel sind Eure Erwartungen groß und wenn es schon keine aufwendige Grafikdemo wird, so wollt Ihr doch wenigstens ein Dreieck am Ende auf den Bildschirm gezaubert sehen. Seit nicht frustriert, wenn ich Euch nun verrate, dass das eigentliche Zeichnen (Rendern) erst im zweiten Tutorial behandelt wird. Ein leeres Fenster ist doch auch nicht zu unterschätzen oder ;)?

Info DGL.png Hilfreich für das Verständniss ist es wenn man das Tutorial_Quickstart gelesen hat.

Was ist (die) OpenGL?

Womit wir schon bei der ersten Frage wären. Ist es der, die oder das OpenGL. Um diese Frage zu beantworten müssen wir wissen, dass OpenGL ein Akronym für "Open Graphics Library" ist. Der englischen Begriff "Library" bedeutet Bibliothek, womit wir auf "die OpenGL" schließen. Häufig findet OpenGL aber auch ganz ohne Pronomen Verwendung. Nachdem wir diese Sache klargestellt haben kommen wir zur eigentlichen Frage zurück. Was ist OpenGL?

OpenGL ist eine API, die es uns erlaubt relativ einfach Grafiken auf dem Bildschirm auszugeben ohne jedoch zu wissen, was im Hintergrund genau geschieht. Das ganze ist in gewisser Weise mit der Windows-API vergleichbar. Wir wissen zwar, wie man ein Fenster erzeugt, was im Hintergrund jedoch genau geschieht wissen wir nicht.

Weiterhin ist OpenGL plattformunabhängig. Theoretisch könntet Ihr OpenGL also unter jedem Betriebssystem nutzen und Eure Programme würden auch auf jeder Hardware laufen. Dies bringt jedoch den Nachteil mit sich, dass bestimmte Dinge wie die Fensterverwaltung, Tastatur- oder Maussteuerung aus der Bibliothek ausgeschlossen sind, da diese sich von Betriebssystem zu Betriebssystem unterscheiden können. Um diese Dinge müssen wir uns also selbst kümmern. Zum Glück bietet uns die VCL (Visual Component Library) von Delphi ein mächtiges Werkzeug um solchen Herausforderungen entgegenzutreten.

OpenGL selbst besitzt nur einen relativ kleinen Sprachumfang. Die wenigen Funktionen, die jedoch vielseitig einsetzbar sind lassen sich sehr leicht erlernen. Die Folge hiervon ist aber auch, dass es keine Funktion zum Zeichnen komplexer Objekte wie z. B. die eines Vogels gibt. Solche Objekte muss man sich daher selbst aus den so genannten Primitiven, wie z. B. Punkten, Linien oder Dreiecken zusammensetzen.

OpenGL ist ein riesiger Zustandsautomat. Je nachdem wie Ihr die einzelnen Zustände schaltet können die Bilder, welche ihr auf dem Bildschirm ausgebt völlig unterschiedlich aussehen. Beispielsweise sieht ein Würfel bei aktiviertem Licht gezeichnet wurde anders aus als derselbe Würfel ohne Licht. Verändert Ihr einmal einen Zustand wie z. B. die Farbe so werden sämtliche Primitive, die Ihr anschließend zeichnet mit dieser Farbe versehen, bis ihr die Farbe erneut wechselt.

Von Tausend und einer Funktion

 glColor3b, glColor3d, glColor3f, glColor3i, glColor3s, 
 glColor3ub, glColor3ui, glColor3us, glColor4b, glColor4d, 
 glColor4f, glColor4i, glColor4s, glColor4ub, glColor4ui, 
 glColor4us, glColor3bv, glColor3dv, glColor3fv, glColor3iv, 
 glColor3sv, glColor3ubv, glColor3uiv, glColor3usv, 
 glColor4bv, glColor4dv, glColor4fv, glColor4iv, glColor4sv, 
 glColor4ubv, glColor4uiv, glColor4usv

Es sind nicht ganz tausend und eine geworden, allerdings kommt dies schon sehr nahe dran ;). Sprach ich nicht gerade noch von einem kleinen Sprachumfang? Schnell werdet Ihr aber erkannt haben, dass das Grundgerüst dieser Funktion glColor heißt. All diese Funktionen verhalten sich gleich und unterscheiden sich lediglich in der Übergabe der Parameter. So werden bei glColor3i 3 Parameter vom Typ Integer erwartet. glColor4b verlangt hingegen 4 Parameter vom Typ Byte.

Alle OpenGL-Funktionen besitzen den Präfix "gl" gefolgt von dem Befehlsstamm. Einige Funktionen besitzen noch einen Suffix, der den Typ oder auch die Anzahl der übergebenen Parameter wieder spiegelt. (siehe auch diese Erklärung)

OpenGL-Konstanten sind immer an ihren Großbuchstaben erkennbar und beginnen mit der Vorsilbe "GL_". Obgleich Delphi keine Unterschiede zwischen Groß- und Kleinschreibung macht empfehle ich doch diese Schreibweise beizubehalten, denn sie steigert die Übersicht ungemein.

Wie sind OpenGL-Programme aufgebaut?

Um ein Dreieck zu rendern reicht es vielleicht das Bild einmal zu Zeichnen und auszugeben. Der Großteil von Euch wird sich jedoch nicht mit einem Dreieck zufrieden geben und möchte doch wenigstens Bewegungen und Animationen in seinen Programmen verwenden. Die Vorgehensweise ist so einfach wie genial: Um Bewegungen zu ermöglichen ist es notwenig, das Bild mehrmals zu zeichnen und auszugeben. Stellt Euch vor Ihr möchtet ein Dreieck von links nach rechts über den Bildschirm bewegen! Ihr zeichnet Euer Dreieck ganz links und gebt es aus. Noch bevor das menschliche Auge genug Zeit hat dieses Bild als einzelnes Bild zu deuten habt ihr dank der hohen Geschwindigkeit der heutigen PCs das Dreieck bereits um ein paar Pixel nach rechts verschoben und erneut gezeichnet. Ist Euer Rechner schnell genug, so erscheint den Betrachter die Bewegung als völlig flüssig und die eigentlichen Sprünge von wenigen Pixeln sind als solche nicht erkennbar. Das menschliche Auge kann etwa 25-30 Bilder pro Sekunde unterscheiden, also solltet Ihr immer versuchen mehr als 30 Bilder pro Sekunde (FPS = Frames per Second) zu zeichnen.

Spätestens jetzt sollte Euch klar sein, warum es bei modernen Spielen manchmal ruckeln kann, wenn Euer Rechner zu langsam ist.

Soweit zur Theorie. Wie aber realisiert man diesen Vorgang. Ganz klar: Wir zeichnen in einer Schleife. Nun stellt sich die Frage wie man dieses Problem elegant löst. Man könnte sich z.B. eine eigene Hauptprogrammschleife schreiben, in der man rendert. In dieser müsste man dann aber auch die Botschaften des Betriebssystems verarbeiten.

Eine elegantere Lösung ist meiner Meinung nach das OnIdle-Ereignis der Anwendung hierfür zu nutzen. Wenn Ihr darin den Parameter "Done" auf "False" setzt wird diese Nachricht so oft ausgeführt, wie es nur irgendwie möglich ist. Das OnIdle-Ereignis wird im Hauptthread der Anwendung aufgerufen, was den positiven Nebeneffekt hat, dass die Nachrichten des Betriebssystems von der VCL verarbeitet werden und man die VCL in vollem Umfang nutzen kann.

Nachteil der vorgestellten Methoden ist, dass die Anwendung nie zur Ruhe kommt, da die Schleife permanent durchlaufen wird. Damit steigt die Prozessorauslastung in der Regel auf 100%. Wenn man aus bestimmten Gründen in keiner echten Schleife rendern möchte könnte man sich auch mit einem Timer Abhilfe schaffen.

Die Initialisierung

Wer sofort loslegen möchte, den kann ich beruhigen, denn die Initialisierung von OpenGL ist mit unserem Header sehr einfach. Ihr müsst einfach bevor Ihr OpenGL nutzen möchtet, z. B. im OnCreate-Ereignis Eures Formulars die Funktion "InitOpenGL" aufrufen, nachdem Ihr die Unit "dglOpenGL" in Eurer Usesklausel eingebunden habt.

Damit OpenGL auch weiß, wohin gezeichnet werden soll müssen wir ihr das irgendwie mitteilen. Hierfür ermitteln wir den Gerätekontext unseres Formulars mit Hilfe der Funktion "GetDC" und dem Handle unseres Formulars.

Anschleißend erstellen wir einen Zeichenkontext mit Hilfe der Funktion "CreateRenderingContext".

Nachdem wir diesen erstellt haben müssen wir Ihn noch aktivieren. Dies geschieht mit der Funktion "ActiveRenderingContext". Das ganze sieht dann in etwa so aus:

var
  DC, RC:HDC;
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  DC:=GetDC(Handle);
  RC:=CreateRenderingContext(DC,          //Device Contest
                             [opDoubleBuffered], //Optionen
                             32,          //ColorBits
                             24,          //ZBits
                             0,           //StencilBits
                             0,           //AccumBits
                             0,           //AuxBuffers
                             0);          //Layer
  ActivateRenderingContext(DC, RC);
end;

Eine Änderung der Parameterwerte von CreateRenderingContext bzw. das genaue Verständniss dieser ist für Anfänger nicht nötig. Für alle die es trotzdem wissen wollen:

  • DC ist der Gerätekontext an dem der Rendercontext hängt. Zum Beispiel der Kontext eines Formulars oder aber der eines Panels. Je nachdem wo später drauf gezeichnet werden soll.
  • Optionen sind hinweise wie z.B: opDoubleBuffered. Diese Option gibt an, dass Doppelpufferung genutzt werden soll. TODO Bitte weitere Optionen bzw. Link wo man nachlesen kann.
  • ColorBits, hier 32, gibt die Farbtiefe an. 32 bedeutet, dass für die 4 Farbkanäle (Rot, Grün, Blau, Alpha(Transparenz)) jeweils 1Byte=8Bit zur Verfügung stehen. Das bedeutet für jeden Kanal können 256 Abstufungen genutzt werden. Das macht insgesamt 256^4 = ... verdammt viele!
  • ZBits, hier 24, gibt an wie viele Bits für den Tiefenpuffer reserviert werden. 24Bits bedeutet, dass Einträge von 0 bis 2^24=16,7Mio möglich sind. Je höher der Wert, desto feiner/genauer der Tiefentest. (Mehr dazu im Tiefetest-Artikel)
  • StencilBits werden für den Stencil Test benötigt. (Maskieren von Bildschirmteilen)
  • AccumBits geben an, wie viele Bits im Akkumulationspuffer gespeichert werden können.
  • AuxBuffer gibt an, wie viele Bits im Hilfspuffer gespeichert werden können.
  • Layer gibt die Anzahl ebenen an. (Für OpenGL fällt mir hier kein Nutzen ein. Falls jemand Ideen hat, bitte ins Forum posten.)


Im Prinzip könntet Ihr nun loslegen. Wenn Ihr Euer Bild nun im OnIdle-Ereignis zeichnet werdet Ihr schnell feststellen, dass Ihr nichts zu sehen bekommt. Das ist auch ganz normal, denn Ihr müsst das Bild auch noch ausgeben. Das geschieht mit Hilfe der Funktion "SwapBuffers".

Da wir sauber programmieren wollen müssen wir den Renderingkontext und den Gerätekontext wieder freigeben, wenn wir unser Programm beenden oder aber OpenGL nicht mehr benötigen. Die Funktionen "ReleaseDC" und "DestroyRenderingContext" erfüllen diese Aufgaben.

Jetzt müssen wir nur noch die Größe des Bildes, welches wir rendern möchten definieren. Mit dem Befehl glViewport definieren wir die Zeichenfläche. Im Gegensatz zu Windows liegt der Ursprung bei OpenGL unten links. Da die Größe unserer Zeichenfläche der des Fensters entsprechend soll rufen wir diesen Befehl im OnResize-Ereignis unseres Formulars auf:

glViewport(0, 0, ClientWidth, ClientHeight);

Die vier kommenden Zeilen solltet Ihr ganz schnell wieder aus Eurem Gedächtnis streichen und erst einmal als gegeben hinnehmen. In Tutorial 2 und 3 werden wir näher darauf eingehen. Soviel sei jedoch gesagt: Es wird nichts weiter unternommen als unsere dreidimensionale Welt aufzuspannen. Damit ist sichergestellt, dass Objekte in großer Entfernung kleiner erscheinen, so wie wir es gewohnt sind.

glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(60, ClientWidth/ClientHeight, 0.1, 100);
glMatrixMode(GL_MODELVIEW);

Mit diesem Wissen könnt Ihr nun direkt in Tutorial 2 einsteigen.

Nachwort

Genug dazu. Ich freue mich sehr, dass Ihr Euch die Zeit genommen habt, dieses Tutorial zu lesen und OpenGL lernen wollt. Ich hoffe, dass wir die nächsten Tutorials genauso gut durchbekommen und dass am Ende jeder etwas gelernt hat und die Zeit nicht total vergebens war. Ich schreibe sicherlich nicht immer alles auf dem direktesten Weg, sondern rede gerne mal um den Brei herum. Wer das nicht mag, soll sich die OpenGL-Dokumentation zu Herzen nehmen, dort ist alles kurz und schmerzlos beschrieben ;). Für alle, die es nicht so trocken mögen, ist mein Schreibstil hoffentlich eine gute Alternative ;).

Euer
Magellan

Dateien


Vorhergehendes Tutorial:
Tutorial Quickstart
Nächstes Tutorial:
Tutorial Lektion 2

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.