Tutorial 2D
Inhaltsverzeichnis
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. Das dies nicht der Fall ist möchte ich mit diesem (vor allem an Einsteiger gerichtet, 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 das 2D mit OpenGL nicht nur möglich ist, sondern auch noch sehr viel einfacher (selbst mit der GDI ist 2D komplizierter) ist 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 das 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 das man selbst auf älteren Grafikkarten sehr komplexe 2D-Szenen mit Geschwindigkeiten jenseits der 100 FpS (=Frames per Seconds ~ 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 z.B. ü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 das 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 das 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 das 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 zur OpenGL unter 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 das 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 das 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 das 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 das 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 stellen die komplett über die CPU ablaufen. So kann man dann OpenGL-Anwendungen mit einer etwas schnelleren CPU trotz fehlendem 3D-Beschleuniger nutzen, oder auf 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 Problem 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 :
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 :
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 :
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 :
Das sollte man immer in Hinterkopf behalten, und unter 3D ist es genauso. Während ein positiver Y-Wert Objekte in eine, 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 das 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 das 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 das dann besser nochmal anhand einer kleinen Bilderserie :
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 das 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. Ausserdem 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.
- DirectDraw Surface (*.dds)
- 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.
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 Texture.pas (die im Original von Jan Horn stammt. Ein weitere guter Loader ist glBitmap), 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 das 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 dank einer gut durchdachten API wie OpenGL gehen. Jetzt wo wir der API erstmal gesagt haben das 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 :
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, dann 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 :
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 :
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 obiger 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 das 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 immernur 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 das 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 Richtung 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.