Tutorial 2D

Aus DGL Wiki
Wechseln zu: Navigation, Suche

2D mit OpenGL - "Nicht jeder benötigt 3 Dimensionen"

Einleitung

Bei der Erwähnung einer API wie OpenGL denken die meisten eigentlich eher an 3D, und sind der festen (aber sehr wohl falschen) Überzeugung eine solche API sei für reine 2D-Anwendung überdimensioniert oder gar ungeeignet. Dass dies nicht der Fall ist, möchte ich mit diesem vor allem an Einsteiger gerichteten (denn die Könner wissen wohl, was man mit der GL so alles machen kann) Tutorial zeigen und auch gleich mit mehreren praktischen Beispielen aufweisen, dass 2D mit OpenGL nicht nur möglich, sondern auch noch sehr viel einfacher ist (selbst mit der GDI ist 2D komplizierter) und dabei jede Menge Vorteile mit sich bringt.

Welche Vorteile bringt mir die GL für eine 2D-Anwendung?

Dies ist wohl das wichtigste Kapitel und sollte zugleich auch mit diversen Vorurteilen und Missverständnissen aufräumen. Denn gerade der 3D-Bereich ist es, in dem seit Jahren fast monatlich neue Techniken entworfen werden und der dafür sorgt, dass v.a. Grafikkarten immer leistungsstärker werden, während der 2D-Bereich seit seligen VLB-Zeiten (= Vesa-Local-Bus, alte Haudegen kennen diese Grafikkartengeneration sicherlich noch) keine Innovationen mehr erlebt hat (und warum auch? Im 2D-Bereich reicht ein gutes Bild zusammen mit passabler Darstellungsgeschwindigkeit).

Deshalb gibt es jetzt gleich mal die wichtigsten Punkte, warum man denn gerade OpenGL (D3D würde hier auch zählen, aber das haben wir GL'ler ja nicht so gerne) für die 2D-Darstellung nutzen sollte:

  • Hardwarebeschleunigung
Moderne Grafikkarten können inzwischen über 200 Millionen Dreiecke pro Sekunde rendern und besitzen brachiale Füllraten jenseits der 2.000 M(Texel/Pixel)/s. Das bedeutet also, dass man selbst auf älteren Grafikkarten sehr komplexe 2D-Szenen mit Geschwindigkeiten jenseits der 100 FpS (= Frames per Second ~ Bilder pro Sekunde) darstellen kann, während man mit der GDI schon bei einfachen 2D-Grafiken Geschwindigkeitsprobleme bekommen würde.
  • "Kostenlose" Objektsortierung
Eine 3D-API braucht einen Tiefenpuffer, um zu erkennen, ob Fragmente verdeckt sind oder nicht und damit Overdraw zu vermeiden. Eine 2D-Anwendung kann diesen Tiefenpuffer aber auch nutzen, nämlich um Objekte zu sortieren. Man nutzt dann die Z-Koordinate der Objekte (= Tiefenkoordinate) quasi als Layer, um zu kennzeichnen, welches Objekt auf welcher "Höhe" liegt. Wenn man also z.B. einen 2D-Topdown-Shooter entwickelt, bei dem der Spieler mit seinem Flugzeug über den Boden fliegt, dann nutzt man den Z-Puffer, um die API (die das dann der Hardware überlässt) seine Objekte sortieren zu lassen. Das Flugzeug bekommt dann einen niedrigen Z-Wert (=oben) und Objekte auf dem Boden einen hohen Tiefenwert (=unten/hinten). Die Sortierung übernimmt dann die Grafikkarte und wir müssen uns darum keine Gedanken machen. Würden wir die Anwendung stattdessen über die GDI realisieren, müssten wir diese Objekte selbst entsprechend ihrer Höhe sortieren.
  • Jede Menge hardwarebeschleunigte Spezialeffekt
Wie schon oben erwähnt haben im 3D-Bereich innerhalb der letzten Jahre sehr viele Innovationen stattgefunden. Warum sollte man diese also nicht auch für seine 2D-Anwendung nutzen? Klingt logisch und macht auch Sinn! So bietet OpenGL alle Arten von Effekten, die auch in einer 2D-Anwendung nützlich sein können. Darunter solche Sachen wie den Alphatest (der dafür sorgt, dass maskierte Teile eines Objektes transparent sind), Blending und natürlich (auch wenn das jetzt für erfahrene GL'ler sehr trivial klingt) hardwarebeschleunigte Rotation und Skalierung; was zur Folge hat, dass man seine Objekte nicht für verschiedene Auflösungen in verschiedenen Größen erstellen muss. Für Fortgeschrittene gibt es dann natürlich noch solche Sachen wie Shader, mit denen man Teile der OpenGL-Pipeline durch eigene (kleine) Programme ersetzen kann (entweder in Assemblerform oder aber in der neuen GL-HLSL). Dadurch bietet sich dann ein quasi unendlich großes Spektrum an möglichen Effekten, und das wohlgemerkt alles hardwarebeschleunigt!
  • Plattformübergreifend
Auch ein großer Vorteil von OpenGL. Die Tatsache, dass die GL unter diversen Betriebssystemen verfügbar ist (im Gegensatz zu GDI oder gar DirectX), macht die eigenen Programme recht portabel (einschränkend ist hier halt nur die Verfügbarkeit der genutzten Programmiersprache auf dem passenden OS). Unterstützt werden alle größeren Betriebssysteme wie Windows, Linux, MacOS und Solaris.
Ganz nebenbei wurde vor kurzem mit OpenGL ES ein mobiler Standard für OpenGL geschaffen, wodurch es dann auch möglich ist, auf mobilen Geräten (Handys, PDAs, Handhelds) OpenGL zu nutzen. Und gerade dort sind 2D-Spiele (aufgrund der oft mangelnden Leistung der Geräte) ja noch stark verbreitet.

So viel also zu den wichtigsten Vorteilen von OpenGL in 2D. Natürlich gibt es noch weiter Dinge, die OpenGL für 2D-Anwendungen attraktiv machen, aber allein die oben genannten Gründe sollten jedermann überzeugt haben. Und alle, die wirklich mal wissen wollen, wie gut OpenGL denn für solche Anwendungen geeignet ist, sollten sich unbedingt eine neuere Version des MacOS ansehen, denn das benutzt OpenGL zur Darstellung seiner GUI.

Und welche Nachteile gibt es?

Nichts, was der Mensch bisher erfunden hat (mal abgesehen von der Spaltung des Atoms ;) ), hat nur Vorteile. Genauso sieht es auch aus, wenn man die OpenGL für seine 2D-Anwendung nutzen will. Welche genau das sind, will ich hier grob auflisten.

  • Ohne 3D-Beschleuniger mit passenden Treibern geht nichts
Klingt logisch, oder? OpenGL ist eine 3D-API und da 2D nichts weiter als die (fast vollständige) Vernachlässigung der Z-Koordinate ist, kommen wir um einen 3D-Beschleuniger nicht herum, der dazu auch noch einen Treiber mitbringen muss, der OpenGL unterstützt. Allerdings schritt der Fortschritt auf diesem Gebiet der IT-Technik in den letzten Jahren so rasant voran wie sonst nirgendwo, und wir werden nur sehr selten auf Rechner stoßen, in denen Hardware agiert, die keine 3D-Beschleunigung bietet. Ergänzend dazu sollte allerdings trotzdem immer der neuste Treiber installiert sein, denn besonders die in WindowsXP integrierten Grafikkartentreiber wurden ihrer OpenGL-Funktionalität entraubt (Man riecht hier förmlich die Konkurrenz zwischen D3D und der GL). Also ist dies im Endeffekt ein Nachteil, der inzwischen kaum noch Halt findet und in Zukunft total vernachlässigt werden kann.
  • Hardwarelimitationen
Einer der größten Nachteile einer jeden 3D-API, die auf Hardwarebasis arbeitet, sind die Limitationen, die die Hardware mitbringt. Jeder Grafikkartentyp hat andere, was mitunter dazu führen kann, dass die selbstverfassten OpenGL-Anwendungen nicht auf allen Rechnern laufen. Da wir uns in diesem Tutorial (2D ist ja recht anspruchslos) allerdings in den Niederungen der OpenGL-Funktionalität bewegen, dürfte es hier kaum Probleme geben. Einzig die Tatsache, dass vor allem ältere 3D-Beschleuniger mit großen Texturen Probleme, haben könnte hier und da Schwierigkeiten machen. Wer aber keine Texturen größer 1024x1024 Pixel nutzt und dazu noch sparend mit dem Speicher der Grafikkarte umgeht (nicht jede Grafikkarte hat 128 Mbyte Grafikspeicher oder gar mehr). Einige Leute werden sich übrigens evtl. dadurch verunsichert fühlen, dass ihnen jemand gesagt hat, man könnte unter OpenGL nur Texturen nutzen, die der Dimension 2^n*2^n entsprechen. Das ist grundlegend korrekt, aber wir nutzen hier einen Texturenloader, der gluBuildMipMaps benutzt, um MipMaps (verschiedene Detailstufen) für unsere Texturen zu erstellen. Diese Funktion schluckt jede Größe (sofern diese kleiner oder gleich dem Hardwarelimit ist) und passt die Texturen dann entsprechend an eben genanntes Limit an, also müssen wir uns um diese so oft erwähnte Limitation keine Sorgen machen. Wer zu dem Thema Hardwarelimitation mehr wissen will, der sollte unbedingt mal auf Tom Nuydens Seite vorbei schauen. Dort gibt es eine riesige Datenbank, in der fast alle Grafikkarten mit ihren entsprechenden OpenGL-Fähigkeiten vertreten sind.
  • Filtering
Zugleich ein großer Vorteil, aber je nach Situation auch ein Nachteil. OpenGL filtert Texturen (sofern man das via GL_LINEAR so will) bilinear, was man auch tunlichst aktiviert lassen sollte (GL_NEAREST filtert nicht, sieht dann aber auch scheußlich blockig aus). Dadurch wirken Texturn meist etwas verschwommen. Ich für meinen Teil umgehe dies aber ganz einfach, denn in fast jedem Bildbearbeitungsprogramm gibt es eine Funktion mit der man ein Bild scharfzeichnen kann. Das sieht auf den ersten Blick dann zwar überzeichnet aus, aber wenn OpenGL das Bild dann als Textur bilinear filtert, heben sich Filtering und Scharfzeichnung gegenseitig fast auf. Das hat sich in meinen Anwendungen bisher bewährt und ist nicht wirklich viel Aufwand.

Um dieses Kapitel hier abzuschließen, sei noch gesagt dass man ohne 3D-Beschleuniger nicht unbedingt auf OpenGL verzichten muss. Brian Paul hat mit Mesa3D nämlich ein Projekt am Laufen, das OpenGL-DLLs zur Verfügung stellt, die komplett über die CPU ablaufen. So kann man dann OpenGL-Anwendungen mit einer etwas schnelleren CPU trotz fehlendem 3D-Beschleuniger nutzen, oder auch Funktionen ausprobieren, die von der (zu alten) Grafikkarte nicht unterstützt werden.

Die Grundlagen

Sollte sich der geneigte Leser nun also doch für die GL entschieden haben, so widmen wir uns dann jetzt den Grundlagen der 2D-Darstellung unter OpenGL. Viele Sachen, die man bei einer 3D-Anwendung beachten muss, sind hier eigentlich zu vernachlässigen. Wer also schon mal eine kleine 3D-Anwendung unter OpenGL geschrieben hat, wird hier sicherlich keine Probleme bekommen. Da sich dieses Tutorial aber an blutige (mhh, lecker) Einsteiger richtet, versuche ich so genau und einfach wie möglich zu erklären, was man machen muss und v.a. warum. Genau deshalb habe ich auch für einen Großteil der hier erwähnten Techniken im Download zu diesem Tutorial (siehe unsere Files-Sektion und dort unter VCL-Source) jeweils ein eigenes Beispielprogramm + Quellcode (und natürlich ausgiebigen Kommentaren) geschrieben. Wenn zu dem jeweiligen Kapitel ein solches im Download enthalten ist, dann steht das kursiv unter der Überschrift des Kapitels.

Die Projektion

Wie bekannt (sein sollte), besitzt OpenGL im Groben zwei wichtige Matrizen. Zum einen die Modellansichtsmatrix, in der man im Normalfall seine Szene (egal ob 2D oder 3D) rendert und (für dieses Kapitel wichtiger) die Projektionsmatrix. Diese Matrix lässt sich am besten mit der Linse einer Kamera vergleichen und legt fest wie die Objekte auf den Bildschirm projiziert werden (wer mitdenkt wird jetzt wissen warum diese Matrix so genannt wurde). In einer 3D-Anwendung setzen wir (meist über gluPerspective) eine Projektionsmatrix die dafür sorgt das unsere Objekte perspektivisch korrekt verzerrt werden (so wie es im echten Leben auch ist). Da Bilder aber mehr als tausend Worte sagen zeige ich das anhand der unteren Bildreihe, die einen Würfel an verschiedenen Positionen auf der X-Achse zeigt:

Tutorial 2D illustration 1.jpg

Der Würfel wurde auf den beiden Bildausschnitten links und rechts jeweils um 40 Einheiten auf der X-Achse verschoben und man kann sehr gut sehen das die Seiten des Würfels perspektivisch verzerrt werden, also weiter entfernte Kanten kleiner erscheinen (wie im realen Leben, das kann man ja prima mit nem würfelähnlichem Objekt nachprüfen). Diese Art der Darstellung ist für 3D gut geeignet, aber für unseren Zweck nicht. Denn wir wollen ja das unser Objekt, egal an welcher Bildschirmposition es sich befindet, gleich aussieht.

Dazu gibt es unter OpenGL den sog. orthogonalen Modus, der dafür sorgt das unser Frustum (Sichtkegel) nicht wie bei der 3D-Projektion kegelförmig ist (kleine Seite beim Betrachter, große Seite am Ende des Sichtfeldes), sondern wie eine Box aussieht. Für technisch interessierte hier der Vergleich zwischen dem 3D- und dem 2D-Frustum :

Tutorial 2D illustration 2.jpg

Links sehen wir das Frustum (~Sichtbereich) für die orthogonale Projektion (also 2D) und rechts für die perspektivische Projektion (3D). In diesem Tutorial interessieren wir uns wie gesagt für ersteres Frustum, welches sich über die Funktion glOrtho erstellen lässt. Diese Funktion will von uns die Dimensionen haben die wir unserem Viewport geben wollen. Ich empfehle hier übrigens immer einen festen Wert der einer der gängigen Auflösungen (z. B. 640x480, 800x600) entspricht. Der feste Wert hat übrigens den Vorteil das unsere Anwendung von der vom Nutzer gewählten Bildschirmauflösung unabhängig ist. Wir müssen dann also nicht mehr umrechnen wo unser Objekt jetzt in der gewählten Auflösung wäre und wie groß es dort sein müsste. Dadurch das wir immer die selben virtuellen Koordinaten haben, überlassen wir der GL (bzw. der Grafikkarte) die Umrechnung. Wenn wir also eine virtuelle Auflösung von 640x480 an glOrtho übergeben, und ein Objekt zentriert bei 320x240 rendern, dann wird dieses egal in welcher Auflösung immer in der Mitte des Schirms gerendert. Die Umrechnung macht wie gesagt OpenGl (oder besser gesagt die Grafikkarte). Zusätzlich übergeben wir der Funktion dann noch die Z-Reichweite. Hier kann man beliebig wählen, und muss nicht wie in 3D darauf achten Z-Near und Z-Far so zu wählen das die Auflösung des Tiefenpuffers nicht unnötig verschwendet wird (z. B. mit einem Z-Near von 0.1 oder gar kleiner). Für Z-Near nehme ich gewöhnlich 0 und für Z-Far einen Wert der dafür sorgt das ich alle Objekte so sortieren kann das ihre Z-Position auf einen Integerwert fällt. Als kleines Codebeispiel könnte unsere Projektionsmatrix nun so aussehen:

glMatrixMode(GL_PROJECTION);
glLoadIdentity;
glViewport(0,0,ClientWidth,ClientHeight);
glOrtho(0,640,0,480,0,128);

Um den optischen Vergleich zur oben erwähnten 3D-Projektion zu zeigen, gibt es wieder ein Bild des Würfels, diesmal allerdings mit 2D-Projektion:

Tutorial 2D illustration 3.jpg

Das sieht auf den ersten Blick zugegeben recht langweilig aus, stellt aber genau den selben Szenenverhalt dar wie die Ansicht ein paar Zeilen weiter oben. Diesmal allerdings mit der gerade besprochenen orthogonalen Ansicht, die als Grundlage für unsere 2D-Projektion dient.

So viel also zur 2D-Projektion und hoffentlich hat das hier jeder verstanden. Die orthogonale Projektion ist ein essentieller Bestandteil einer jeden 2D-Anwendung unter OpenGL und sollte daher allen Interessierten ein Begriff sein. Falls das hier jemandem zu technisch war, im Forum werden weitergehende Fragen gerne beantwortet.

Noch als kleiner Nachtrag: Wer sich mal die Parameter angesehen hat die glOrtho will, wird bemerkt haben das wir in obigem Quellcode (zumindest augescheinlich) Top mit Bottom verwechselt haben (sprich es sollte 0,640,480,0 statt 0,640,0,480) heißen. Das hat allerdings seine Richtigkeit, denn in OpenGL liegt der Ursprung des Koordinatensystems in der unteren linken Bildschirm(oder Fenster)ecke, wobei er bei Windows in der oberen Ecke liegt. Unter OpenGL liegt also quasi der "Boden" oben, genau umgekehrt wie unter Windows. Genau deshalb übergeben wir als "Oben" an glOrtho den eigentlichen Boden des Viewports. Das klingt verwirrend, aber ist im Endeffekt gar nicht so schwer zu behalten, besonders dann nicht wenn man sich folgende Illustration mal näher ansieht:

Tutorial 2D illustration 11.jpg

Das sollte man immer in Hinterkopf behalten, und unter 3D ist es genauso. Während ein positiver Y-Wert Objekte in einem Windowsfenster nach unten verschiebt, geschieht unter OpenGL genau das Gegenteil. Wer sich mit dieser Tatsache nicht anfreunden kann, der kann auch gerne glOrtho dazu nutzen die 2D-Matrix von OpenGL an die Windowsgegebenheiten anzupassen:

glOrtho(0,640,480,0,0,128);

Und schon verhält es sich unter OpenGL genauso wie unter Windows. Positive Y-Koordinaten zeigen nach unten. Allerdings muss man hier dann auch drauf achten die gerenderten Objekte an diese Gegebenheit anzupassen. Man muss diese also quasi auf den Kopf stellen, damit sie mit der neuen Matrix korrekt angezeigt werden. Das geht aber ganz leicht, indem man beim rendern von Quads oder anderen texturierten Primitiven ganz einfach die T-Texturkoordinaten vertauscht.

Darstellung der 2D-Objekte

Einige 2D-Interessierte haben sich sicherlich schon mal im Funktionsumfang von OpenGL umgesehen und bemerkt, dass es dort eigentlich gar keine Funktionen gibt um Dinge in 2D zu zeichnen. Auf den ersten Blick sieht das auch wirklich so aus, aber man darf halt nie vergessen dass OpenGL primär für den 3D-Bereich entworfen wurde und sich 2D-Sachen dann nur über 3D-Techniken realisieren lassen. So auch die Darstellung unserer 2D-Objekte, für die wir aus genau diesem Grund eine 3D-Technik anwenden müssen, nämlich das sog. Texturemapping (Den Begriff "Textur" gibt's übrigens auch im deutschen Sprachgebrauch, aber gängiger ist die korrekte Übersetzung "Oberfläche"). Unter OpenGL werden ja alle Objekte aus verschiedenen Primitiventypen zusammengesetzt (Dreiecke, Rechtecke, usw.) und diese Objekte kann man mit einer Textur belegen die dann auf dieser Oberfläche "angezeigt" wird. Diese Textur lädt man im Normalfall aus einer vorher erstellten Bilddatei unter Nutzung eines Texturenloaders (alternativ kann man den sich natürlich auch selbst schreiben), der diese Textur für die Grafikkarte vorbereitet (also z. B. ein BMP-Bild vom BGR-Format ins RGB-Format bringt) und dann auf dieser ablegt. Danach kann diese Textur an jeder Stelle im Programm auf eine Primitve "geklebt" werden, und genauso machen wir das auch in unserer 2D-Anwendung.

Allerdings müssen wir keine komplexen Formen darstellen, da unsere Objekte ja nicht 3D sind, sondern (meistens) schon in einem anderen Programm erstellt (oder vorgerendert wurden) und als Bilddatei abgelegt wurde. Wir laden und stellen dann also nicht die 3D-Daten dieses Modells dar (was bei komplexen 2D-Objekten wohl eh zu viel wäre), sondern kleben diese schon fertige Bilddatei mittels einer Textur auf ein Rechteck (in der GL-Terminologie "Quad" genannt, vom Primitiventyp GL_QUADS). Hoffe mal das kam gut rüber, aber ich verdeutliche dass dann besser nochmal anhand einer kleinen "Bilderserie":

Tutorial 2D illustration 4.jpg

Oben sei mal kategorisch der Vorgang geschildert um ein vorgerendertes 3D-Objekt als Textur in seine 2D-Anwendung zu bekommen. Rechts sieht man das 3D-Modell, das dann aus der gewünschten Ansicht (im obigen Falle von der Seite) im 3D-Modellierungsprogramm gerendert wird. Dieses Rendering speichert man dann in einem Format ab das der Texturenloader verarbeiten kann, lädt dies in seine OpenGL-Anwendung und stellt dies dann auf z. B. einem Quad dar (siehe letztes Bild). Natürlich spielt es keine Rolle ob man seine 2D-Objekte vorrendert oder diese von Hand zeichnet, wobei den meisten wohl Ersteres besser von der "Hand" geht.

Welches Bildformat ist das richtige?

Bevor wir nun weiter auf das Thema eingehen kümmern wir uns um die Frage nach dem richtigen Bildformat für unsere Texturen. Bildformate gibt's wie Sand am Meer, aber für unseren Zweck eignen sich nur sehr wenige (eigentlich nur ein einziges). Ich zähle die verbreitetsten Formate kurz auf und sag auch warum (oder warum nicht) und wofür man diese verwenden kann:

  • Joint Photographic Experts Group (*.jpg; *.jpeg)
Das in den Weiten des WWWs wohl verbreitetste Format ist für Texturen generell eher weniger zu empfehlen, und für 2D-Objekte erst recht nicht. Zum einen ist die in diesem Format genutzte Kompression verlustbehaftet (also verlieren unsere Texturen an Qualität) und außerdem hat dieses Format keine Möglichkeit Transparenzinformationen zu speichern. Diese benötigt man aber für 2D-Objekte, denn im Normalfall wollen wir den Hintergrund des Objektes ja durchsichtig machen (dazu gleich mehr). Also sollte dieses Format nur verwendet werden wenn wir etwas darstellen wollen das sehr viele Details enthält (dann fällt die verlustbehaftete Kompression nicht so stark auf) und keine transparenten Bereiche enthält.
  • Graphical Interchange Format (*.gif)
Direkt aus der Steinzeit des IT-Sektors kommt das (im Netz noch weit verbreitet) GIF-Format. Für unsere Zwecke ist es total unbrauchbar. Es ist eine Palettenformat, das maximal 256 verschiedene Farben unterstützt, allerdings inklusive Transparenzinformationen. Aber die maximal 256 Farben und die kaum vorhandene Kompression machen es für unseren Zweck nutzlos. Die Tatsache dass es Animationen unterstützt ist zwar im Internet für kleine animierte Sachen ganz toll, aber hilft uns auch nicht, denn dazu überwiegen die Nachteile zu stark
  • Portable Network Graphic (*.png)
Der Nachfolger des GIF-Formates. Eigentlich auch sehr gut für Texturen geeignet, denn neben einem Alphakanal (maximal 8 Bit) und 8 Bit pro Farbkanal unterstützt es auch verlustfreie Kompression (mit einem recht hohen Kompressionsfaktor). Nachteil ist allerdings der Aufbau des Formates, denn der Chunkaufbau macht das Laden recht schwer und bisher gibt es nur Loader die eine DLL-Datei mit sich schleppen. Alternativ kann man auf neuen Betriebssystemen jedoch via GDI+ auch PNG-Dateien direkt laden.
  • TARGA (*.tga)
Dieses Format werden sicherlich nicht viele Einsteiger kennen, allerdings ist dies das perfekte Format für unsere Bedürfnisse bzw. Texturen im Allgemeinen. Es kann nämlich bis zu 8 Bit pro Farbkanal (also das was man heute als 32-Bit Farbtiefe bezeichnet) speichern (= 24 Bit für Farben) und dazu noch einen Alphakanal (maximal 8 Bit). Der Alphakanal ist sehr nützlich, denn in ihm kann man die Transparenzinformationen eines Bildes ablegen. Auch viele kommerzielle Titel nutzen dieses Format (u. a. Quake3), und das aus gutem Grund. Kompression wird auch unterstützt, und zwar verlustfrei in Form einer LZW-Kompression. Alles in allem ist das momentan das geeignetste Format für Texturen, zumal so gut wie jedes Bildbearbeitungsprogramm damit umgehen kann. Nebenbei ist dies Format auch recht einfach aufgebaut und damit auch recht leicht einzulesen.
  • BITMAP (*.bmp)
Das BMP-Format dürfte sicherlich jedem ein Begriff sein, ist aber genauso wie GIF ein Relikt aus der Steinzeit. Es kann zwar genauso wie das TGA-Format neben den 8 Bits pro Farbkanal auch einen maximal 8 Bits großen Alphakanal anbieten, allerdings kommen damit nur recht wenige Bildbearbeitungprogramme klar. Außerdem sind die Bilddaten hier im BGR-Format abgelegt, statt dem eher üblichem RGB-Format (Red, Green, Blue). Das ist zwar sehr leicht umzuwandeln, bzw. kann mit passender GL-Konstante auch direkt übergeben werden, aber trotzdem ist dieses Format in keinem Falle dem TARGA-Format vorzuziehen.
Ein sehr neues Format, das den Ursprung (wie am Namen zu erkennen) in Microsofts DirectX-Schnittstelle hat. Es ist ein recht modernes Format, das speziell für die Speicherung von Texturen entwickelt wurde. Allerdings ist das eher was für Fortgeschrittene, denn weder das Laden dieses Formates ist einfach, noch seine Speicherung (selbst teure Bildbearbeitungsprogramme brauchen ein passendes Plugin für DDS und bei der Erstellung des Formates muss man auf bestimmte Sachen achten). Aber ich wollte es hier trotzdem mal erwähnt haben, damit man sieht das es auch spezielle Formate für Texturen gibt, und wenn ihr euch eingearbeitet habt, dann könnt ihr im Forum mehr zu dem Format finden (Mars hat dort auch einen grundlegenden Loader gepostet), denn es ist recht interessant. Es unterstützt feste Kompressionsratios, Mip-Maps, 3D-Texturen, uvm. Für fortgeschrittene 3D-Programmierer sollte DDS also das Format der Wahl sein, denn es wurde als einziges der verbreiteten Bildformate speziell für diesen Bereich konzipiert. Vor allem die Tatsache dass die Kompression dieses Formates von den Grafikkarten direkt unterstützt wird prädestiniert es für diesen Anwendungsfall. So spart man sowohl beim Hochladen als auch beim Rendern viel Busbandbreite, da die GPU die Textur auch intern komprimiert behandelt und spart logischerweise auch VRAM. Man darf sich allerdings nicht vom Platzverbrauch der eigentlichen Datei irrtieren lassen, denn das Kompressionsformat ist fest (z.B. 4:1, 6:1, etc.) statt variable wie u.A. bei JPGs. Aber dafür entspricht dann die Dateigröße exakt der Größe die auch im VRAM belegt wird. Allerdings sollte man hier genau wissen was man tut, da es sehr viele Foramte innerhalb des DDS geben kann (DXT1, DXT3, DXT5, Palette, Fließkomma, etc.) und es durchaus Texturen gibt die durch den festen Kompressionsratio schlecht aussehen können. Ein gutes Beispiel ist hier ein blauer Himmel mit Sonne und Wolken, der durch einen weichen Farbverlauf gekennzeichnet ist. Da bei DXT1/3/5 fest komprimiert wird kann dort die Bildqualität sehr stark leiden. Dann sollte man im DDS unkomprimiert ablegen. Außerdem gibt es inzwischen auch einen recht brauchbaren Loader für DDS in Delphi, und Lossys glBitmap kann dieses Format auch laden.

Die obige Liste dürfte also einen recht (groben) Überblick über die verbreiteten Bildformate geben, und für dieses Tutorial begnügen wir uns erstmal mit dem TARGA-Format. Das wurde übrigens bereits 1984 erfunden, ist aber trotzdem noch nicht veraltet, sorgt aber dafür das so ziemlich jedes Programm damit umgehen kann.

Wer sich übrigens nicht auf einen fremden Texturenloader verlassen möchte, sondern sich selbst um das Einlesen der Bildformate kümmern will, der sollte mal eine Blick auf wotsig.org werfen, einer recht großen Bibliothek die es sich zur Aufgabe gemacht hat Spezifikationen für Dateiformate zu sammeln. Dort wird man zu jedem der oben gennannten Bildformate eine solche Spezifikation finden, anhand derer man dann selbst Laderoutinen schreiben kann.

Textur laden

Auch wenn es ein sehr simples Unterfangen ist eine Textur zu laden, werde ich hier trotzdem nochmal kurz darauf eingehen. Das Tutorial richtet sich ja an Einsteiger, und von daher kann es nicht schaden auch mal kurz zu zeigen wie man so eine Textur lädt. Nutzen tun wir dazu die Textures.pas (die im Original von Jan Horn stammt. Ein weitere guter Loader ist glBitmap.pas), die sich im Download des Beispiels für dieses Tutorial befindet. Der Loader kann JPG, BMP und TGA laden. Außerdem lädt er auch den Alphakanal aus einer TGA-Textur.

Bevor wir die Textur laden können, benötigen wir eine Variable in der wir den Bezeichner der Textur speichern. OpenGL erstellt für alle Ressourcen eindeutige Bezeichner (Bezeichner hier in Form eines Integerwertes, also einer eindeutigen ID), so auch für Texturen. Dieser Bezeichner ist vom Typ glUInt (U=unsinged Int=Integer, also vorzeichenloser Ganzzahlwert, was in Delphi dem Variablentyp Cardinal entpricht). Deshalb deklarieren wir unseren Texturenbezeichner auch so:

var
 MyTex : glUInt;

Wenn wir mehrere Texturen laden und verwalten wollen, bietet sich natürlich ein (dynamisches) array of glUInt an, aber das sind Delphigrundlagen die in diesem Tutorial nichts zu suchen haben.

Das Laden der Textur geht nun dank der Textures.pas ganz einfach:

LoadTexture('MeineTextur.tga', MyTex, False);

Die Parameter sollten recht logisch sein, der erste gibt den Dateinamen der Textur an, der zweite das Texturobjekt (in das die ID der Textur geschrieben wird) und der letzte Parameter gibt an ob die Textur aus einer dem Programm angehängten Ressource geladen werden soll. Sollte der Ladevorgang erfolgreich gewesen sein, so müsste sich in MyTex ein Wert > 0 befinden, nämlich der eindeutige Bezeichner dieses Texturenobjektes.

Textur anwenden

Wer sich schonmal ein wenig über OpenGL schlau gemacht hat, der wird wissen dass die GL eine Statemachine ist. Das trifft auch auf Texturen zu, denn wenn eine Textur gebunden wurde, wird sie solange auf alle Primitiven angewendet, bis entweder eine andere Textur gebunden wurde oder das Texturemapping über glDisable abgeschaltet wird. Das hat besonders dann den Vorteil, wenn man viele Objekte mit der gleichen Textur rendern muss, denn Texturenwechsel sind recht kostspielig. Von daher sollte man also bei vielen Objekten eine Sortierung nach Textur vornehmen, dann diese Textur binden und danach dann alle Objekte die diese Textur besitzen rendern.

Doch bevor Texturen überhaupt angezeigt werden, müssen wir OpenGL erstmal mitteilen das es diese auch anzeigen soll. Dazu gibt es die Funktion glEnable, der man mit der Konstante GL_TEXTURE_2D mitteilt das wir die 2D-Texturierung aktivieren wollen (1D oder 3D-Texturen benötigen wir ja in diesem Tutorial nicht):

glEnable(GL_TEXTURE_2D);

Unglaublich einfach, oder? So schnell kann das dank einer gut durchdachten API wie OpenGL gehen. Jetzt wo wir der API erstmal gesagt haben dass wir gerne Texturen sehen möchten, müssen wir auch noch sagen welche Textur als nächstes auf unserer Primitiven gezeigt werden soll. Dazu bindet (~ aktiviert) man das passende Texturobjekt:

glBindTexture(GL_TEXTURE_2D, MyTex);

Von nun an werden alle folgenden Primitiven solange mit der hinter MyTex abgelegten Textur gerendert, bis das Texturemapping entweder deaktiviert wird oder wir eine andere Textur binden.

Transparenz

Eine wichtige Sache die es noch zu klären gibt ist Transparenz. Oben habe ich ja gesagt das wir unsere Objekte auf Quads kleben (über eine Textur), aber unsere vorgefertigten Objekte nur selten auch genau die Form eines Quads haben. Der Panzer auf der oben gezeigten Textur wird z. B. von sehr viel schwarz umgeben, das wir da natürlich nicht sehen wollen. Aber auch um die Transparenz brauchen wir uns unter OpenGL keine Sorgen zu machen, denn dafür gibt es den sog. Alphakanal der Textur, der angibt welche Teile einer Textur später transparent (oder besser gesagt gar nicht, aber dazu gleich mehr) gezeigt werden sollen. Aus diesem Grund haben wir uns mit dem TGA-Format auch ein Format gewählt das diesen Kanal direkt im Bild speichern kann, sodass wir diesen nicht extra erstellen oder aus einer seperaten Bilddatei laden müssen.

Wie man den Alphakanal nun in die Textur bekommt hängt davon ab wie man seine Textur erstellt. Wer seine Objekte von Hand malt, der muss den Alphakanal im Bildbearbeitungsprogramm selbst erstellen. Wer seine Objekte allerdings vorrendert, der kann diese Arbeit im Normalfall von der 3D-Software erledigen lassen, die Alphainformationen direkt mitexportieren kann. Als kleiner Hinweis sei übrigens gesagt das man beim Rendering des Objektes im 3D-Programm die Kantenglättung deaktivieren muss, da man sonst an den Rändern Artefakte hat (die logischerweise Teile der Hintergrundfarbe enthalten) die dann in der OpenGL-Anwendung zu unschönen Effekten führen. Um das zu verbildlichen hier nochmal unsere Panzertextur, allerdings begleitet vom (im Bildformat gespeichertem) Alphakanal:

Tutorial 2D illustration 5.jpg

Links also unsere Textur und rechts der Alphakanal. Den sieht man normalerweise nicht, aber fast jedes Bildbearbeitungsprogramm gibt einem die Möglichkeit sich diesen anzeigen zu lassen. Wie zu sehen befinden sich in unserem Falle nur zwei Werte im Alphakanal. Und zwar Schwarz (= 0) für komplett transparent und Weiß (= 1) für komplett Sichtbar.

Unter OpenGL nutzen wir jetzt für die Transparenz den Alphatest. Transparenz ließe sich auch über Blending realisieren, allerdings hat Blending den Nachteil das man dann die transparenten Objekte nach Tiefe sortieren müsste, da Blending im Framepuffer abläuft und nicht wie der Alphatest auf Fragmentbasis, wo wir uns dann keine Sorge um die Reihenfolge unserer Objekte machen müssen. Wollen wir nun also den Alphakanal der Textur nutzen (natürlich muss dieser vorher im Texturenloader geladen werden, sonst geht's nicht) müssen wir vor dem rendern des mit der Objekttextur belegten Quads den Alphatest aktiveren und festlegen wie der Test auszusehen hat :

glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.1);

Zuerst aktiveren wir also den Alphaest (dazu gibt es die Konstante GL_ALPHA_TEST), bevor wir dann der GL mittels glAphaFunc sagen wie der Test aussehen soll. Der erste Parameter (GL_GREATER) gibt an, das nur Fragmente (also Teile der Textur) gerendert werden sollen deren Alphawert größer ist als der im zweiten Wert angegebene (0.1). Wie auch Farbwerte wird der Alphawert unter OpenGL "geclampt", also in eine bestimmte Reichweite gebracht, nämlich 0 (= Schwarz) bis 1 (= Weiß). Die 0,1 (statt der 0) als Vergleichswert nehmen wir quasi aus Toleranz. Wenn wir das obige also getan haben, dürften wir auf unserem Quad (sofern der Alphakanal korrekt erstellt und geladen wurde) also nur noch das eigentliche Objekt sehen, und der Hintergrund müsste an den transparenten (Alpha <= 0,1) zu sehen sein:

Tutorial 2D illustration 6.jpg

Links sehen wir unsere Bohne ohne aktiven Alphatest, was zur Folge hat das der eigentlich transparente (schwarze) Teil der Objekttextur den Hintergrund überdeckt. Rechts wuede der Alphatest aktiviert und wir sehen nur den Teil unseres Objektes den wir auch sehen wollen.

Das Objekt anzeigen

Nachdem wir nun unsere Textur mit passendem Alphakanal geladen haben und den Alphatest auch aktiviert haben, müssen wir schlussendlich noch unser(e) Objekt(e) rendern. Wie schon mehrfach gesagt nutzen wir dazu die GL_QUADS-Primitive. Bei diesem Primitiventyp beschreiben wir mit vier Eckpunkten ein Rechteck (das von der Grafikkarte dann in zwei Dreiecke zerlegt wird), wobei jeder Eckpunkt auch eine Texturkoordinate zugewiesen bekommt. Diese Koordinate gibt an, aus welchem Teil der Textur dieser Eckpunkt seine Bilddaten beziehen soll, und sie wird über das gesamte Quad hinweg interpoliert. Also haben wir in der genauen Mitte des Quads als interpolierte Texturkoordinate das genaue Mittel der übergebenen Texturkoordinaten. Um diese Interpolation müssen wir uns allerdings keine Sorgen machen, das macht die Grafikkarte.

Und in Sachen Texturkoordinaten sind wir auch schnell fertig, denn für den Anfang haben wir pro Textur immer nur ein Objekt (später zeige ich dann wie man mehrere Objekt in eine Textur packt) und müssen dementsprechend auch nur eine 1 (= Ende der Textur) bzw. 0 (= Anfang der Textur vergeben. Wenn man sich das bildlich vorstellt, sieht das dann so aus:

Tutorial 2D illustration 7.jpg

Da wir nur mit 2D-Texturen arbeiten, müssen wir pro Eckpunkt auch nur zwei Koordinaten angeben. Und zwar einmal in X-Richtung auf der Textur (unter OpenGL auch S-Richtung genannt, oft auch mit "U" betitelt) und in Y-Richtung (unter OpenGL T-Richtung, oft auch "V" genannt). Wenn S=0 und T=0, bedeutet das also das dieser Eckpunkt seinen Texel (wie Pixel, bloß im Bezug auf Texturen) aus der oberen linken Ecke unserer Textur (X=0/Y=0) bezieht, während S=1 und T=1 dafür sorgt das der Eckpunkt sich den Texel in der untersten rechten Ecke der Textur schnappt. Wie bereits oben erwähnt müssen wir uns um den Raum zwischen den vier Eckpunkten nicht kümmern, das wird ja von der Hardware linear interpoliert.

Der Quellcode zu obigem Beispiel sieht dann so aus:

glBegin(GL_QUADS);
 glTexCoord2f(0,0); glVertex3f(-Breite/2, -Höhe/2, -Tiefe);
 glTexCoord2f(1,0); glVertex3f(+Breite/2, -Höhe/2, -Tiefe);
 glTexCoord2f(1,1); glVertex3f(+Breite/2, +Höhe/2, -Tiefe);
 glTexCoord2f(0,1); glVertex3f(-Breite/2, +Höhe/2, -Tiefe);
glEnd;

Wer obigen Text aufmerksam gelesen hat, sollte eigentlich problemlos verstehen was der Quellcode denn bewirkt. Wer damit Problem hat, der sollte sich das obige Kapitel nochmal unbedingt sorgfältig durchlesen, denn das ist eine sehr wichtige Sache.

Soviel also zu den Grundlagen von 2D unter OpenGL. Wer nämlich hier angelangt ist, sollte zumindest 2D-Objekte unter OpenGL anzeigen können. Evtl. ist es hier angebracht das Tutorial einige Minuten ruhen zu lassen und ein wenig mit dem Erlerntem herumzuprobieren. Danach geht's nämlich mit etwas fortgeschritteneren (aus Sicht des Einsteigers) Themen weiter.

Die Rolle des Tiefenpuffers

Trotz der Tatsache dass wir in 2D die dritte Dimension vernachlässigen, bedeutet dies nicht das wir den Tiefenpuffer nicht doch nutzen können. Und zwar nutzen wir diesen in 2D zur hardwarebeschleunigten Sortierung unserer Objekte. In 2D-Anwendungen kann es ja genauso vorkommen das ein Objekt unter ("hinter") einem anderen liegt und da wäre es ziemlich dumm diese selbst zu sortieren, wo OpenGL uns doch einen Tiefenpuffer anbietet der dies für uns macht.

Genau deshalb haben wir mittels glOrtho auch die Reichweite für unseren Tiefenpuffer angegeben. Haben wir dann auch noch den Tiefentest mittels glEnable(GL_DEPTH_TEST) und dem passenden Tiefentest via glDepthFunc(GL_LESS oder GL_EQUAL) aktiviert, so können wir über die Z-Koordinate unserer Objekte angeben wo die nun genau liegen. Ein Objekt dessen Z-Koordinate also näher an Z-Near ist, wird dann über einem an gleicher Stelle befindlichem Objekt mit einer Z-Koordinate näher an Z-Far gerendert, und zwar egal welches dieser Objekte wir als erstes an die GL übergeben haben.

Backface Culling

Auch diese von OpenGL angebotene Funktionalität sollte hier nicht verschwiegen werden. Denn in OpenGL bestehen Flächen immer aus einer Vorder- und Rückseite (wie im echten Leben, ein Blatt Papier hat ja auch zwei Seiten, egal wie dünn es ist), was spätestens dann Sinn macht wenn man bedenkt das man sich in einer 3D-Umgebung ja komplett frei bewegen kann. Doch in unserer 2D-Welt sehen wir immer nur eine Seite unserer Objekte, egal was wir anstellen. Und genau deshalb sollten wir OpenGLs Backface Culling (zu Deutsch heisst das wörtlich "Rückseiten Ausschluß", aber solche Fachbegriffe deutsch man auch besser nicht ein) aktivieren, denn ist dies nicht der Fall, so werden alle Berechnungen immer für beide Seiten eines Polygons ausgeführt. Und dabei spielt es keine Rolle welche der Seiten sichtbar ist oder nicht. Wir aktivieren also das Backfaceculling und sagen OpenGL dass die Rückseiten der Primitiven nicht dargestellt werden soll (letzteres könnte man sich sparen, da das die Voreinstellung ist, allerdings sollte man lieber auf Nummer sicher gehen):

glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

Solltet ihr jetzt übrigens eure Objekte nicht mehr sehen, dann habt ihr die Eckpunkte eurer Primitiven in der falschen Reihenfolge (Standard ist für OpenGL GL_CCW (CounterClockWise, gegen den Uhrzeigersinn) angegeben. Ich empfehle dann entweder einen Blick ins Reedbook oder in eine der Beispielanwendungen, wo es eine Funktion namens DrawQuad gibt, die ein korrektes Quad rendert. Bei modernen Grafikkarten sollte das Backface Culling zwar nur recht wenig Performance bringen, aber es wäre trotzdem Performanceverschwendung dieses nette Feature einfach ungenutzt zu lassen.

Animation

Bisher können wir zwar unsere 2D-Objekte mittels texturierter Quads auf dem Bildschirm darstellen, aber das ist natürlich noch recht statisch; und was wäre ein Computerspiel ohne Animationen? Und genau darum kümmern wir uns jetzt. Aber zuerst kurz zum Unterschied "Animation in 3D" und "Animation in 2D". In 3D ist es normalerweise so, dass man ein 3D-Modell lädt und dann die einzelnen Teile des Modells animiert, sei dies über die GL-Befehle oder Animationsdaten im 3D-Format (Bones, Keyframes). Das bedeutet also das man in 3D quasi unendlich viele Freiheiten hat, denn man kann ja z. B. den Turm eines Panzers über glRotatef in jeden beliebigen Winkel bringen. In 2D ist das anders, denn da sind ja alle Grafiken vorgefertigt und werden nur abwechselnd auf unsere Quads geklebt. Würden wir dort also den Turm eines vorgefertigten Panzers rotieren lassen wollen, so müssten wir jeden Animationsframe vorfertigen und als Textur laden. Man muss sich also vorher Gedanken drüber machen ob 2D denn im Endeffekt wirklich weniger Aufwand bedeutet. Aber diese Entscheidung muss jeder für sich selbst fällen, weshalb wir uns nun zwei verschiedenen Animationsmöglichkeiten widmen werden.

Animation über Einzeltexturen

(Projektdatei : openGL2D_demo1)

Tutorial 2D illustration 8.jpg

Die wohl offensichtlichste (und auch einfachste) Art ein 2D-Objekt über Texturen zu animieren, ist über in einzelnen Texturen abgelegte Frames. Besonders aus einem 3D-Animationsprogramm (3D Studio, Maya) geht das besonders einfach. Denn diese Programme können eine Animation als Einzelbilder auf die Platte rendern. Dadurch hat man dann für eine Animation mit 60 Bildern 60 Texturen die dann einfach in ein Array geladen werden:

var
 Frame : array of glUInt;

SetLength(Frame, NumFrames);
for i := 0 to NumFrames-1 do
 LoadTexture('animframe'+IntToStr(i)+'.tga', Frame[i], False);

Das zu animierende Objekt verpasst man dabei mit einem Fließkommawert der den aktuell anzuzeigenden Frame darstellt, und erhöht diesen dann über die Zeit. Beim Rendern des Objektes nutzt man diesen Wert gerundeten (Indizes müssen ja Ganzzahlwerte sein) als Index in das Texturenarray:

glBindTexture(GL_TEXTURE_2D,Frame[Round(CurrentFrame)]);
...
CurrentFrame := CurrentFrame + 0.025 * TimeFactor;
if CurrentFrame > Length(Frame) then
 CurrentFrame := 0;

Der Vorteil dieser Methode ist das man diese Animationen direkt aus seinem 3D-Programm heraus speichern kann und das die Animationen quasi unendlich lange (wobei sowohl Speicherplatz als auch Speicherausbau der Grafikkarte hier limitierende Faktoren sind) sein können. Nachteil ist der hohe Speicherverbrauch, sowohl auf der Platte als auch im Grafikspeicher, sowie die Tatsache das man dann recht oft die Textur wechseln muss.

Animation in einer einzigen Textur

(Projektdatei: openGL2D_demo2)

Diese Art der Animation ist wie oben schon angedeutet recht speichersparend und vermindert auch die Zahl der Texturenwechsel. Allerdings ist sie auch aufwendiger zu implementieren und unterliegt einigen Einschränkungen die man bei der Erstellung der Animationstextur berücksichtigen sollte. Statt also jeden Frame der Animation in einer eigenen Datei (und damit später auch Textur, obwohl sich das natürlich auch kombinieren lässt) abzulegen, wird versucht bei dieser Animationsart alle Frames in einem Gitter auf einer Textur anzuordnen.

Wenn man nicht noch mit der Erstellung komplexer Texturkoordinaten rumfuchteln will, dann sollte man drauf achten dass das gewählte Gitter so hoch wie breit ist und die Texturen an diesem Gitter ausgerichtet sind. So habe ich bei dieser Beispieltextur (bevor sie für das Tutorial etwas verkleinert wurde) die Frames in einem Raster von 256x256 Pixeln untergebracht.

Explosion det.jpg

Zu sehen sind hier alle Animationsframes einer Explosionstextur aus der Beispielanwendung. Diese wurd mit einem entsprechenden Tool generiert, aber kaum ein 3D-Porgramm kann Animationen direkt in eine einzelne Textur exportieren. Hier muss man sich dann mit passenden Plug-Ins oder Drittprogrammen aushelfen.

Das "Abspielen" dieser Animation ist nun recht einfach, denn wir erinnern uns ja daran das die Texturkoordinaten in OpenGL immer im Bereich 0 bis 1 liegen, unabhängig (es gibt Ausnahmen, aber interessiert hier nicht) von der Größe unserer Textur. Also müssen wir im Endeffekt nur wissen wie viele Spalten und Zeilen unsere Animationstextur enthält um für jeden Frame die passenden Texturkoordinaten errechnen zu können. Um es konkret zu machen nehmen wir obiges Beispiel. Es besteht aus drei Reihen und drei Spalten, wobei die Animationen von links nach rechts durchlaufen. Also bedeutet dies das unser erster Animationsframe bei S=0 / T=0 beginnt und bei S=1/4 / T=1/4 Ende, Frame 2 beginnt dann bei S=1/4 / T=0 und endet bei S=2/3 / T=1/4, usw. Dies zu errechnen sollte als keine Probleme bereiten, genauso wie das Rendern.

Doch dürfen wir die Nachteile dieser Animationsart auch nicht verschweigen. Das es oft schwierig ist (wenn man die Objekte und Animationen sowieso von Hand zeichnet dann natürlich nicht) aus den Animationsframes eine einzelne Textur zu machen habe ich bereits gesagt, aber man muss auch wie immer auf die Hardwarelimitationen achten. Denn wenn man viele Animationen in einer Textur unterbringen will, wird diese oft recht groß und selbst auf modernen Karten sollte die Textur nicht größer als 2048x2048 Pixel sein. Wenn wir also ein Objekt der Größe 256x256 haben, bekommen wir im besten Falle (2048 wird auf älteren Karten entweder nicht machbar sein, deren Grafikkartenspeicher fast ganz aufbrauchen, oder zu langsam sein) 64 Animationsframes auf eine Textur. Alles darüber müsste man dann in eine weitere Textur auslagern, was den Verwaltungsaufwand stark erhöhen würde. Außerdem muss man drauf achten, dass sich die Animationsframes nicht direkt berühren, also das zwischen einem sichtbarer Objektteil in Frame N mindestens ein Pixel Abstand zum sichtbaren Objektteil in Frame N+1 besteht. Denn dadurch das die Textur von OpenGL gefiltert wird, verlaufen Pixel die direkt aneinander liegen ineinander; wodurch dann unschöne Effekte entstehen. Ein weitere (allerdings nicht so gravierender) Nachteil ist die Tatsache dass man solche Animationen nicht über Texturenwiederholung mehrfach auf ein Objekt legen kann. Wenn man bei der erstgenannten Animationsart z. B. statt S=1/T=1 S=2/T=2 nutzt, dann wird dann Animation insgesamt viermal auf dem Quad wiederholt. Da man hier aber über die Texturkoordinaten quasi einen Frame aus der Textur herauspickt ist das nicht möglich, was im Normalfall aber kaum Gewicht haben sollte.

Rotation und Skalierung

(Projektdatei: openGL2D_demo3)

Wenn man alles selbst zeichnet (also z. B. mit der GDI arbeitet), dann hat man für die Rotation (oder Skalierung) eines Objektes im Normalfall nur zwei Möglichkeiten: Entweder man schreibt sich eigene Routinen die den Pixelsalat rotieren (was meist langsam ist, solange man es nicht in Assembler schreibt) oder man erstellt Bilddateien auf denen die Objekte schon vorrotiert sind. Erstere Methode ist wie gesagt langsam und sieht hässlich aus (es sei denn man filtert auch noch selbst) und die zweite Methode schränkt einen auf die vorberechneten Drehwinkel ein.

Alle gerade genannten Probleme kann man unter OpenGL dank der hardwarebeschleunigung ad acta legen. Nicht nur dass man dank Hardware unendlich schnell rotieren kann, diese Rotation auch noch frei ist, nein, man bekommt sogar das Filtering dank Hardware umsonst:

Tutorial 2D illustration 10.jpg

Links ist das um 15° gegenüber dem Ausgangsbild gedrehte Raumschiff ohne Filtering zu sehen. Gut ist hier die dadurch entstehende "Körnung" erkennbar, die in Bewegung noch sehr viel unschöner aussieht. Rechts sieht man das gleiche, diesmal allerdings mit Filtering. Der Unterschied dürfte direkt auffallen, und besonders auf hellen Hintergründen ist er noch frapierender. Natürlich sollte man die Grundlagen von OpenGL beherrschen um die Rotation korrekt anwenden zu können, denn folgender Code:

glRotatef(45, 0,0,1);
glTranslatef(320, 240, 0);
DrawQuad(0,0,0, 256,256);

Bewirkt etwas total anderes als folgender Code :

glTranslatef(320, 240, 0);
glRotatef(45, 0,0,1);
DrawQuad(0,0,0, 256,256);

Wer sich die OpenGL-Grundlagen bereits angeeignet hat, der wird sicher schnell erkannt haben wo der Unterschied liegt. Denn in OpenGL bewegt man Objekte nicht direkt, sondern immer nur den Ursprung der Matrix. Ersterer Code rotiert also unsere Matrix um 45° und verschiebt diese dann um 320 Einheiten auf der X- und 240 auf der Y-Achse, was dazu führt das unser Objekt in einem recht großen Kreis um den Ursprung rotiert wird. Der zweite Codeschnippsel hingegen verschiebt unser Objekt an den Punkt 320/240 (in unserem Falle genau die Bildschirmmitte) und rotiert dann dort unser Objekt um die eigene Achse. Das ist auch der Rotationscode den wir im Normalfall nutzen um ein Objekt um sich selbst rotieren zu lassen. In unserer 2D-Anwendung müssen wir auch prinzipiell nur um die Z-Achse rotieren, denn die ragt in OpenGL "in" den Bildschirm hinein. Um das nachzuvollziehen einfach mal den rechten Arm grade ausstrecken und "um" den ausgestreckten Zeigefinger rotieren lassen. Euer Arm stellt dann in diesem Falle die Z-Achse dar.

Das Skalieren eines Objektes gestaltet sich noch leichter, denn hier muss man nichts weiter machen als vor dem Rendern des Quads ein glScalef aufzurufen und diesem dann den Skalierungsfaktor (1=keine Skalierung) pro Achse zu übergeben. Da wir hier allerdings nur in 2D arbeiten sollten wir die Z-Skalierung immer bei 1 belassen:

glScalef(2,2,1);
DrawQuad(0,0,0, 256,256);

Obiger Quellcode zeichnet unser Objekt in der doppelten Größe (also statt 256x256 Einheiten 512x512 Einheiten groß). Über die Skalierung kann man schön in 2D einen Tiefeneffekt realisieren, zum Beispiel ein Raumschiff das mit einer Skalierung von 0,5 /0,5 / 1 auf einem Träger startet und dann zu Beginn des Levels langsam in Richtung 1 / 1 / 1 skaliert wird. Dadurch wird das Schiff größer und es entsteht beim Betrachter der Eindruck, das Schiff würde hinaufsteigen.

Scrollen und zoomen

(Projektdatei: openGL2D_demo4)

Selbst viele kommerzielle 3D-Spiele benötigen oft einen 2D-Teil um solche Sachen wie Übersichtskarten (man will ja gerne wissen wo man hin will) oder evtl. Credits und ähnliche größere 2D-Anzeigen zu tätigen, die oft den ganzen Bildschirm einnehmen oder gar größer sind. Besonders bei solchen Sachen wie z. B. einer großen Übersichtskarte (sei es nun die Strategiekarte für einen Schlachtplatz im Zweiten Weltkrieg, oder die Übersichtskarte eines Fantasyreiches) sind die Vorteile von OpenGL (wie an dem im Download enthaltenem Beispiel zu erkennen) gut ersichtlich. Das Scrollen wird durch einen einfachen Aufruf an glTranslatef erledigt (um die außerhalb des sichtbaren Bereichs liegenden Teile der Karte muss man sich dabei keine Sorgen machen), während man das vergrößern der Karte (~ zoomen) mit einem einfachem glScalef vollbringen kann.

Und dank der Hardwarebeschleunigung wird diese Karte schneller angezeigt als man dies von Hand (oder über andere 2D-APIs je tun) könnte. Auf einer halbwegs modernen Grafikkarte sollten locker an die 1000 FpS drin sein.

Nachdem man die Übersichtskarte geladen hat, gestaltet sich das Rendern selbiger als sehr einfach:

glBindTexture(GL_TEXTURE_2D, MapTex);
glTranslatef(MapPos.x, MapPos.y, 0);
glScalef(MapScale, MapScale, MapScale);
DrawQuad(0,0,0, MapSize.x,MapSize.y);

Sieht schockierend einfach aus. Mehr braucht es nicht um eine Übersichtskarte zu scrollen und zu zoomen. Allerdings sollte man wie bereits einige Male erwähnt wurde auf die Hardwarelimiationen achten. Wenn die Übersichtskarte also allzu groß sein sollte, dann kann man diese ja ohne Probleme unterteilen und als mehrere Quads rendern. Außer der etwas abgeänderten Positionierung der einzelnen Kartenteile ändert sich aber am Grundprinzip nichts.

Tiling (Kachelung)

(Projektdatei: openGL2D_demo5)

Die oben genannte Technik mag zwar gut genug für statische Übersichtskarten sein, aber wenn man eine dynamische Karte aus der Vogelperspektive realisieren will, so kommt man mit einer riesigen vorgefertigten Textur nicht weit. Dafür gibt es dann aber eine Technik namens "Tiling", was zu Deutsch so viel wie "Kachelung" heißt. Hier hat man ähnlich einem Schachbrett ein zweidimensionales Spielfeld das aus unterschiedlichen Kacheln besteht die als Texturen vorliegen. In einem zweidimensionalem Array wird dann für jedes Feld gespeichert welche Textur zu ihm gehört. Diese Technik ist immer noch recht weit verbreitet (zumindest im Hobbybereich) und für 2D-Spiele aus der Vogelperspektive recht gut geeignet, wobei hier der Großteil der Arbeit eher beim Grafiker als beim Programmierer liegt, denn da die Teils nur Vierecke darstellen müssen Übergänge von z. B. einem Terraintyp zum anderen in die Textur gezeichnet werden.

Das Rendern eines solchen Spielfeldes gestaltet sich dabei sehr einfach:

for x := 0 to MapWidth-1 do
 for y := 0 to MapHeight-1 do
  begin
  glBindTexture(GL_TEXTURE_2D, MapTexture[Map[x,y]]);
  DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);
  end;

Allerdings ist obige Methode alles andere als optimiert. Das erste (und größte) Problem ist die Tatsache das unabhängig von aktuellen Betrachtungsposition immer die komplette Karte gerendert wird. Bei großen Karten (z. B. 128x128 Kacheln) gehen dann selbst moderne Karten in die Knie. Die Kacheln außerhalb des Sichtbereiches werden zwar von der Grafikkarte nicht gerendert (da sie außerhalb des Viewports liegen), aber müssen dennoch in jedem Frame über den Bus gesendet werden, genauso wie die Texturenwechsel in jedem Frame stattfinden müssen. Das erste Problem lässt sich recht schnell lösen, in dem wir einfach prüfen ob die aktuell zu rendernde Kachel auch im momentan sichtbarem Bereich liegt:

glScalef(MapScale, MapScale, MapScale);
for x := 0 to High(Map) do
 for y := 0 to High(Map[x]) do
  if (MapPos.x + x*32 >= -16*MapScale) and (MapPos.x + x*32 <= SizeX/MapScale+16) and
     (MapPos.y + y*32 >= -16*MapScale) and (MapPos.y + y*32 <= SizeY/MapScale+16) then
      begin
      glBindTexture(GL_TEXTURE_2D, MapTex[Map[x,y]]);
      DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);
      end;

In der If-Abfrage, die wir vor dem Rendern einer jeden Kachel machen, prüfen wir jetzt ganz einfach ob die aktuelle Kachel im Sichtfeld liegt. Da wir eine dynamische Karte haben und die Vorteile von OpenGL nutzen wollen, müssen wir bei dieser Abfrage natürlich auch den Zoomfaktor (MapScale) und das Scrolling (MapPos) mit einbeziehen.

Dank dieser offensichtlichen Optimierung dürften wir jetzt einen starken Performancegewinn von mehreren hundert Prozent sehen, denn sehr viele Texturenwechsel fallen weg und es wird sehr viel weniger Geometrie über den Bus gesendet. Aber natürlich sind wir noch nicht am Ende der Fahnenstange angelangt, denn wie gesagt sind Texturenwechsel recht performancelastig. Also liegt es nahe diese zu optimieren:

for t := 0 to High(MapTex) do
 begin
 glBindTexture(GL_TEXTURE_2D, MapTex[t]);
 for x := 0 to High(Map) do
  for y := 0 to High(Map[x]) do
   if Map[x,y] = t then
    if (MapPos.x + x*32 >= -16*MapScale) and (MapPos.x + x*32 <= SizeX/MapScale+16) and
       (MapPos.y + y*32 >= -16*MapScale) and (MapPos.y + y*32 <= SizeY/MapScale+16) then
        DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);
 end;

Wie zu sehen haben wir unsere Schleife etwas umgebaut. Wir gehen jetzt in der Hauptschleife durch alle unsere Kacheln (abgelegt in MapTex) und dann für jede Kachel die komplette Karte in beide Dimensionen durch. So binden wir jede Texur nur einmal und rendern dann alle Kacheln die diese Textur nutzen. Das klingt erstmal so (zumindest vom Schleifenaufbau her) als würden wir hier Performance vernichten, denn schliesslich gehen wir jetzt nicht einmal die komplette Karte durch, sondern so viele Male wie wir Kacheltexturen haben. Allerdings ist es meistens so das der CPU-Overhead durch die zusätzlichen Schleifendurchläuft weniger kostet als uns die gesparten Texturenwechsel eingebracht haben. Im Normalfall (es gibt natürlich Situationen wo der CPU-Overhead zu groß wird, z. B. bei wirklich riesigen Karten) sollten wir auch durch diese Optimierung einen Performancezuwachs im zweistelligen Prozentbereich erkennen.

So viel also zum Thema Kachelung. Besonders für kleinere Projekte ist diese Technik ein guter Start und vor allem ist diese Technik leicht umzusetzen. Ist natürlich nicht mehr zeitgemäß, aber bei den meisten Hobbyprojekten geht es ja eher um den Inhalt und nicht nur um das Äußere.

Spezialeffekte

Nach diesem langen Marsch sind wir jetzt so ziemlich mit allen im 2D-Bereich verwendeten Techniken durch und können uns nun einem interessanterem Kapitel zuwenden; den Spezialeffekten. Hier ist der eigenen Kreativität natürlich keine (oder kaum, je nach Hardware) Grenze gesetzt, weshalb ich nur einige häufig unter 2D genutzte Effekte erwähnen möchte.

Beleuchtung ("Per-Pixel")

(Projektdatei: openGL2D_demo6)

Beleuchtung ist ja (auch wenn man das nicht so wahrnimmt, da es ja im echten Leben selbstverständlich ist) eine Sache die man in keinem Spiel vernachlässigen sollte. Ohne korrekte Beleuchtung geht viel Atmosphäre flöten, aber besonders in einer 3D-Umgebung ist korrektes Per-Pixel-Licht (die OpenGL-Beleuchtung arbeitet auf Vertexbasis) oft schwer zu implementieren (was dank Shader aber leichter geworden ist). In einer 2D-Umgebung ist das aber zum Glück sehr viel einfacher, denn dort haben wir z. B. im Falle einer 2D-Karte ja keine dritte Dimension um die wir uns kümmern müssten, müssen unsere Lichtquellen also nicht auf die Szene draufprojizieren.

In 2D geht das also ganz einfach und wir machen die Beleuchtung bequem über eine Lichttextur in der die Intensität der Lichtquelle abgelegt ist. Für die Demo habe ich dazu einen einfachen radialen Verlauf gewählt, aber der Fantasie sind da keine Grenzen gesetzt:

Tutorial 2D illustration 12.jpg

Farbinformationen speichern wir nicht in der Textur, denn wir wollen ja dynamisch bleiben und realisieren die Färbung des Lichtes später in unserem Programm über einen glColor3f – Aufruf. Bevor wir allerdings loslegen, sollten noch ein paar Kleinigkeiten im Bezug auf diese (einfache Form der) Beleuchtung geklärt werden. Damit wir unsere Szene mit einer solchen Textur "beleuchten" können, nutzen wir additives Blending. Das bedeutet also das wir zur Farbe im Framepuffer (der unsere Karte zeigt) den Farbwert in unserer Lichttextur addieren. Dort wo unsere Textur also komplett weiß ist, haben wir auf der Karte die volle Lichtfarbe. Blending hat aber einen Nachteil, der in 3D oft sehr viel Kopfzerbrechen macht, aber in 2D leicht gelöst werden kann: Blending arbeitet "auf" dem Framepuffer, was also bedeutet das OpenGL in diesem Falle nicht für uns sortiert. Wenn wir dann also eine Lichttextur rendern die nahe am Betrachter ist und dahinter (im Raum) eine zweite Lichttextur rendern die weiter entfernt ist, werden wir diese nicht sehen, da im Framepuffer bereits die erste Lichttextur "liegt" (sprich dort der Z-Wert der ersten Textur abgelegt wurde und die Fragmente der zweiten Textur deshalb aufgrund des Z-Tests verworfen werden). Das ist natürlich nicht korrekt, aber in 2D lässt sich dieses Problem mit dem passenden Tiefentest lösen:

glDepthFunc(GL_ALWAYS);

Normalerweise sollte man diese Tiefenfunktion erst setzen bevor man die Lichtquellen zeichnet, sofern man vorher den Tiefenpuffer nutzen will um die Objekte auf seiner Karte in der Höhe zu sortieren. Denn wie der Name vermuten lässt bewirkt diese Form des Tiefentests das Fragmente (="Pixel auf Probe") immer rasterisiert werden, egal ob sie jetzt durch ein bereits im Frampuffer liegendes Fragment verdeckt werden würden oder nicht. Wenn wir mit diesem Tiefentest nun also zwei Lichtquellen übereinander rendern würden, werden diese korrekt angezeigt. Aber wie immer soll euch dieser etwas technisch klingende Text nicht aus dem Konzept bringen, weshalb ich dann hier folgende zwei Bilder reden lasse:

Tutorial 2D illustration 13.jpg

Links sehen wir zwei sich überlappende Lichtquellen mit einem normalerweise verwendetem Tiefentest (GL_LEQUAL oder GL_LESS). Dass die grüne Lichtquelle die zweite verdeckt ist reiner Zufall und liegt wohl daran das diese als erstes gerendert wurde. Wie oben gesagt (vereinfacht) steht dann im Z-Puffer drin das an dieser Stelle (des grünen Lichtes) bereits ein Objekt im Z-Puffer liegt. Danach wird dann irgendwann die zweite (blaue) Lichtquelle gerendert, aber die Stellen an denen bereits steht dass dort im Z-Puffer ein Fragment liegt werden nicht mehr gerendert, aufgrund des Tiefentests. Das zweite Bild ist hingegen korrekt, einzig der Tiefentest wurde auf GL_ALWAYS umgestellt. Dann werden die mit der Lichttextur belegten Quads immer gezeichnet, egal ob sich an der Z-Position bereits ein Fragment befindet oder nicht, und bei Lichtquellen ist das reichlich egal welche zuerst gerendert wird. Denn ob ich jetzt Blau mit Grün addiere, oder Grün mit Blau ist egal, denn beides ergibt im Ende Türkis. Das dürfte dann hoffentlich verstanden worden sein, wenn nicht dann einfach mal das Beispielprogramm öffnen und den Tiefetest wie hier angesprochen ändern; Probieren geht je bekannter weise meist über Studieren.

Abschließend auch noch kurz zum verwendeten Blendmodus:

glBlendFunc(GL_ONE, GL_ONE);

Wer sich schon mal ein wenig mit dem Thema Blending beschäftigt hat, wird schnell erkennen was hier abläuft. Sowohl der Quellfaktor als auch der Zielfaktor stehen auf GL_ONE, was bedeutet dass bei unserer Blendoperation die bereits im Framepuffer befindliche Farbe mit der Farbe unseres Lichtes addiert wird (additives Blending genannt). Bei einer grünen Lichtquelle haben wir in der Mitte also RGB = 0/1/0 und wenn wir das dann auf einen Pixel legen würden der halb grau ist (RGB = 0,5/0,5/0,5) hätten wir als Ergebnis RGB = 0,5/1/0,5 (1+0,5 ist eigentlich 1,5 , aber OpenGL zwängt Farbwerte in den Bereich 0..1).

Texturenverläufe

(Projektdatei: openGL2D_demo7)

Im Kapitel "Kachelung" habe ich erwähnt, dass es eigentlich Aufgabe des Grafikers ist die Überläufe zwischen zwei unterschiedlichen Kacheln zu erstellen. Allerdings lässt sich das auch mit OpenGL bewerkstelligen, und zwar dank Blending und des Alphakanals. Dazu rendern wir zuerst ein Quad mit der ersten Kachel, und zwar dort wo diese Kachel komplett erscheinen soll mit vollem Alpha (vierter Parameter von glColor4f = 1) und dort wo später die andere Kachel komplett erscheinen soll mit Alpha = 0. Danach rendern wir an exakt der gleichen Stelle die zweite Kachel, müssen aber natürlich darauf achten das wir den richtigen Tiefentest aktiviert haben damit diese auch sichtbar ist (GL_ALWAYS oder GL_LEQUAL), allerdings mit genau umgekehrten Alphawerten und aktiviertem Blending. Fürs Blending nutzen wir als Quellfaktor GL_SRC_ALPHA und als Zielfaktor dann in logischer Konsequenz GL_DST_ALPHA. Dadurch wird dann im finalen Ergebnis dort wo die erste Kachel eine Alpha von 1 hat ihr kompletter Farbwert übernommen und dort wo die zweite Kachel einen Alpha von 1 hat deren voller Farbwert. Dazwischen wird in gewohnter Weise interpoliert:

glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, TileA);
glBegin(gl_Quads);
 glColor4f(1,1,1,1); glTexCoord2f(0,0); glVertex3f(-128, -128, 0);
 glColor4f(1,1,1,0); glTexCoord2f(1,0); glVertex3f( 128, -128, 0);
 glColor4f(1,1,1,0); glTexCoord2f(1,1); glvertex3f( 128,  128, 0);
 glColor4f(1,1,1,1); glTexCoord2f(0,1); glvertex3f(-128,  128, 0);
glEnd;

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
glBindTexture(GL_TEXTURE_2D, TileB);
glBegin(gl_Quads);
 glColor4f(1,1,1,0); glTexCoord2f(0,0); glVertex3f(-128, -128, 0);
 glColor4f(1,1,1,1); glTexCoord2f(1,0); glVertex3f( 128, -128, 0);
 glColor4f(1,1,1,1); glTexCoord2f(1,1); glVertex3f( 128,  128, 0);
 glColor4f(1,1,1,0); glTexCoord2f(0,1); glVertex3f(-128,  128, 0);
glEnd;

Allerdings wird das bei komplexeren Kachelübergängen recht aufwendig und endet in jeder Menge Overdraw, da man dann viele Übergänge rendern muss. Aber wer sich mal näher damit beschäftigen will, sollte einen Blick auf das "Verblendet"-Tutorial (Einsteiger) von Phobeus werfen, in dem auch diese Technik hier näher besprochen wird.

Schatten

(Projektdatei : openGL2D_demo8)

Im 3D-Bereich sind Schatten eigentlich immer noch die "Königsdisziplin" für jeden Programmierer, besonders wenn diese volumetrisch und vor allem auch korrekt sein sollen. Meist muss man dazu die Szene analysieren, Umrisse von 3D-Objekten generieren und auch noch diverse Spezialfälle beachten (Stichwort "Betrachter im Schattenvolumen"). In unserer heilen 2D-Welt geht das aber schon wie bei der Beleuchtung sehr viel einfacher und hat mit dreidimensionalen Schatten rein gar nichts gemeinsam.

Hier gehen wir nämlich ganz einfach hin und täuschen Schatten vor indem wir unser Objekt (das dazu natürlich einen Alphakanal besitzen muss) mittels glColor3f(0, 0, 0) schwarz machen und dann unter dem eigentlichen Objekt leicht versetzt (die Richtung des Versatzes kann man je nach Lichteinfall verändern) rendern. Über glScalef passen wir dann noch die Größe des Schattens an um einfach vermitteln zu können wie hoch sich unser Objekt befindet. Wenn wir also den Schatten eines Spielers rendern, dann kann der genauso groß sein wie der Spieler selbst, aber bei einem Flugzeug das hoch über dem Boden fliegt sollte der Schatten schon etwas verkleinert werden um einen Eindruck von der Flughöhe zu vermitteln.

Rein programmiertechnisch sind Schatten in 2D (auch wenn diese keineswegs physikalisch korrekt sind) also genau das Gegenteil von 3D-Schatten: Einfach und ohne jegliche Hindernisse:

glPushMatrix;
 glBindTexture(GL_TEXTURE_2D, ShipTex);
 glEnable(GL_ALPHA_TEST);
 glAlphaFunc(GL_GREATER, 0.1);
 glScalef(0.75, 0.75, 0.75);
 glColor3f(0, 0, 0);
 DrawQuad(60,-60,1, 256,192);
glPopMatrix;

glPushMatrix;
 glColor3f(1, 1, 1);
 glBindTexture(GL_TEXTURE_2D, ShipTex);
 glEnable(GL_ALPHA_TEST);
 glAlphaFunc(GL_GREATER, 0.1);
 DrawQuad(0,0,0, 256,192);
glPopMatrix;

Im ersten Block rendern wir unser Objekt etwas verkleinert und nach unten/rechts versetzt, natürlich komplett schwarz. Danach müssen wir nur noch unser Objekt drüberrendern (in gewohnter Weise) und fertig ist unser Schatten:

Tutorial 2D illustration 14.jpg

Schlusswort

Das war es also zum Thema 2D in OpenGL. Ich hoffe stark das dieser doch recht ausführliche Bericht so ziemlich alle Bereiche abgedeckt hat und damit auch häufig gestellte Fragen beantwortet. Natürlich war das nicht alles was in diesem Bereich machbar ist, allerdings kann ich hier schlecht auf jeden Effekt eingehen, denn sonst könnte ich das Tutorial gleich als ein einige hundert Seiten starkes Buch verkaufen.

Doch eines sei noch am Schluss gesagt: 2D ist zwar programmiertechnisch fast immer (besonders bei in 3D komplexen Sachen wie Beleuchtung und Schatten) sehr viel einfacher und man kommt dann auch meist schneller ans Ziel, allerdings ist hier die Erstellung des Inhalts meist aufwendiger. Während z. B. ein 3D-Terrain zwar von der Programmierung her aufwendiger ist, muss man dort am Ende nur eine Terraintextur draufkleben und fertig ist die Sache. In 2D muss man dies hingegen über Kacheln lösen und dann für jeden Übergang verschiedene Kacheln in einem Bildbearbeitungsprogramm erstellen. Oder zum Beispiel das in unserem letzten Beispiel verwendete Raumschiff; wenn man das in 3D über ein 3D-Modell animiert (Keyframes), dann ist es kaum Aufwand was am Modell zu verändern (andere Textur, Form ändern), aber in 2D liegen diese Animationen vorberechnet auf der Platte, und sobald man dann was am Raumschiff ändert, müssen auch alle Animationen geändert werden.

Wie so oft im Leben muss man also abwägen was jetzt für die eigene Anwendung richtig ist. Essentiell reicht es sich die Frage "Ist es für mich aufwendiger in die 3D-Programmierung einzusteigen, oder es ist es aufwendiger den Content für meine 2D-Anwendung zu erstellen" zu beantworten, aber das kann ich euch nicht abnehmen, da müsst ihr selbst drauf antworten können. Außerdem sei noch gesagt das die moderne Generation der Computerspieler inzwischen 3D gewohnt ist, was dazu führt das 2D-Anwendungen dann spielerisch sehr überzeugen müssen.

In dem Sinne also viel Spaß in der zweiten Dimension (die auch Spaß machen) kann. Und vergesst nicht: Ich WILL Feedback zu diesem Tutorial und außerdem will das DGL-Team sehen was ihr so mit unseren Tutorials auf die Beine stellt. Wenn dabei also was halbwegs brauchbares raus gekommen ist, lasst es uns wissen!

Euer

Sascha Willems (webmaster AT delphigl.de)

Dateien



Vorhergehendes Tutorial:
Tutorial Lektion 8
Nächstes Tutorial:
Tutorial_Matrix2

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