Mit dem heutigen Artikel endet unser Streifzug durch die Welt der Vulkan-Programmierung. Unser Ziel, auf das wir im Verlauf der Serie Schritt für Schritt hingearbeitet haben, ist beinahe erreicht: die Entwicklung eines eigenen Frameworks, mit dessen Hilfe wir zukünftig einfache Vulkan-basierte Anwendungen erstellen können. In diesem abschließenden Teil wollen wir uns noch mit drei wichtigen Themen auseinandersetzen: dem sogenannten Billboard-Rendering, der Speicherverwaltung und dem Prototyping.
Die Einstiegshürden beim Erlernen einer neuen Programmiersprache sind im Allgemeinen recht überschaubar, da sich die Grundelemente der betreffenden Sprache in der Regel Schritt für Schritt mithilfe von einfachen Programmbeispielen nachvollziehen lassen. Wer von uns kennt sie nicht, die berühmt-berüchtigte Hallo-Welt-Anwendung, die einen Grünschnabel mit einer kurzen Grußbotschaft in der Welt der Programmierer willkommen heißt. Eine grafische Alternative zu einem Hallo-Welt-Programm – eine Art „Hallo-Dreieck-Anwendung“ – sucht man im Bereich der Grafikprogrammierung heutzutage leider vergebens. Es ist allerdings noch gar nicht einmal so lange her, da ließ sich die Darstellung einer schlichten ein- bzw. mehrfarbigen Dreiecksfläche noch mit einigen wenigen Zeilen Sourcecode bewerkstelligen, sofern man hierbei beispielsweise auf die GLUT Library (OpenGL Utility Toolkit) zurückgriff. Wie kompliziert der Umgang mit einem modernen Grafik-API mittlerweile geworden ist, dürfte Ihnen als aufmerksamer Leser dieser Artikelserie wahrscheinlich nicht entgangen sein. Zunächst einmal hat es mehrerer Artikel bedurft, in denen wir uns überhaupt erst mit der Funktionsweise des Vulkan-API vertraut machen mussten. Man reibt sich vor Verwunderung die Augen, wenn man erkennt, wie zeitaufwendig die Entwicklung einer Vulkan-basierten Anwendung doch ist. Nur um das klarzustellen, ich spreche hier von der Darstellung einer schlichten Dreiecksfläche ohne irgendwelchen zusätzlichen Firlefanz wie Animationen, Texturen oder Beleuchtungsberechnungen.
Da der Umgang mit einem modernen Grafik-API alles andere als trivial ist, haben wir uns im Zuge der vorangegangenen Artikel damit beschäftigt, unser kleines Grafikframework Schritt für Schritt zu erweitern, damit wir in Zukunft einfache Vulkan-basierte Anwendungen erstellen können, ohne uns hierfür mit den zugrunde liegenden API-spezifischen Details auseinandersetzen zu müssen.
Die Softwareindustrie im Allgemeinen und die Spielebranche im Speziellen leben vom Einfallsreichtum ihrer Mitarbeiter. Aber das mit den Ideen ist so eine Sache, denn viele der Einfälle, die uns tagtäglich durch den Kopf gehen, geraten normalerweise bereits nach kurzer Zeit wieder in Vergessenheit. In der Theorie zumindest scheint die Lösung für dieses Problem recht einfach zu sein: Notiere deine Einfälle, dann kannst du jederzeit auf sie zurückgreifen! In der Realität hingegen wird niemand von uns sämtliche seiner Ideen aufschreiben können – schon allein aus Zeitgründen nicht. Wie gut oder schlecht die eigenen Einfälle tatsächlich sind, lässt sich letzten Endes nur in einem Praxistest beurteilen. Obwohl es im Grunde genommen lediglich darum geht, einige neue Ideen auszuprobieren, spricht man in diesem Zusammenhang heutzutage lieber vom sogenannten Prototyping, was im Endeffekt jedoch mehr oder weniger das Gleiche ist:
Exploratives Prototyping: Man testet zunächst einmal aus, ob eine Idee grundsätzlich etwas taugt.
Evolutionäres Prototyping: Eine Idee wird kontinuierlich weiterentwickelt und verfeinert.
Experimentelles Prototyping: Die Erfahrungen, die man mithilfe verschiedener Prototypen gesammelt hat, fließen in ein neues Projekt ein. Die zugrunde liegenden Prototypen werden hingegen nicht mehr weiterentwickelt.
Im Bereich der Softwareentwicklung handelt es sich bei einem (Spiele-)Prototyp häufig um eine einfache, gemäß der Quick-and-Dirty-Methode programmierte Testanwendung. Man sollte der Versuchung widerstehen, einen Prototyp über die Maßen hinaus weiterzuentwickeln. Prototypen werden normalerweise als reine Wegwerfanwendungen konzipiert, bei denen man aus Kostengründen möglichst wenige Arbeitsstunden in die Ausgestaltung des Sourcecodes investiert. Die Entwicklung einer modular aufgebauten, flexiblen und erweiterbaren Codebasis, wie man sie normalerweise in einem releasefähigen Endprodukt findet, erfordert im Unterschied dazu einen ungleich höheren Zeitaufwand. Was unser Vulkan-basiertes Framework betrifft, so haben wir im Verlauf der letzten Artikel eine Menge Arbeit in die Implementierung einer Reihe von Komponenten investiert, die sich für die Entwicklung eines grafikbasierten Prototyps als überaus nützlich erweisen:
Darstellung und Beleuchtung (Deferred Lighting) von einfachen 3-D-Modellen.
Hintergrunddarstellung, bei der die zugrunde liegenden Texturen auf die Innenflächen eines würfelförmigen (Background-Box) bzw. kugelförmigen 3-D-Modells (Background-Sphere) gemappt werden.
Textdarstellung
Darstellung von interaktiven grafischen Benutzeroberflächen (GUI-Rendering)
Darstellung von zweidimensionalen Objekten mittels texturierter Bildraum-Vertex-Quads
Liniendarstellung
Post-Processing
Eine moderne Form der Szenenkomposition, bei der mehrere Teilbilder einer 3-D-Szene miteinander verrechnet werden
Im Zuge dieses Artikels müssen wir uns darüber hinaus noch mit dem Thema Speicherverwaltung auseinandersetzen. In Echtzeitgrafikanwendungen sollte man sich wenn möglich nicht auf die in einer Programmiersprache integrierten Bordmittel – hierzu zählen beispielsweise die C++-Operatoren new (Speicheranforderung) und delete (Speicherfreigabe) – verlassen, da diese, neben einigen weiteren Nachteilen, leider nicht auf Geschwindigkeit optimiert worden sind. Der zweite noch offene Punkt betrifft den Umgang mit sogenannten Billboard-Objekten, die eine zentrale Rolle bei der Darstellung von gigantischen Explosionen, Energiewaffen, Positionslichtern, Sternen, galaktischen Hintergrundnebeln, Wolken, Sonnenstrahlen, Lens Flares, Feuer, Rauch und Partikeleffekten spielen. Zu Erklärung: Bei einem Billboard handelt es sich für gewöhnlich um ein texturiertes Vertex-Quad (Abb. 1) bzw. Vertexkreuz, das entsprechend der Blickrichtung des Spielers im Raum positioniert und ausgerichtet wird. Auch im Zuge der Prototypentwicklung erweist sich der Einsatz von Billboard-Objekten als überaus nützlich. Bei der Implementierung des heutigen Demoprogramms haben wir beispielsweise auf die Verwendung von 3-D-Objekten vollkommen verzichtet, wodurch sich die Entwicklungszeit immens verkürzt hat. Bei den in den Abbildungen 2 und 3 gezeigten Asteroiden und Ufos handelt es sich folgerichtig ausnahmslos um einfache zweidimensionale Billboard-Objekte, die sich durch den dreidimensionalen Raum bewegen.
Dank der zuvor vorgestellten Komponenten unseres Grafikframeworks müssen wir uns bei der Programmierung einer Vulkan-basierten Anwendung nicht mehr mit den ganzen API-spezifischen Details herumschlagen, wodurch uns eine Menge an Arbeit abgenommen wird. Mit den grundlegenden Funktionsprinzipien eines modernen Grafik-API muss man aber natürlich dennoch vertraut sein, da die Entwicklung einer Grafikanwendung ansonsten von vornherein zum Scheitern verurteilt wäre. Wer sich beispielsweise nicht nur auf vorgefertigte Shader-Programme verlassen möchte, wird sich wohl oder übel in das Thema Shader-Programmierung einarbeiten müssen. Des Weiteren muss man das Konzept der modernen Szenendarstellung verinnerlicht haben und verstehen, auf welche Weise heutzutage die Kommunikation zwischen dem Hauptprogramm und der Grafikkarte erfolgt:
Im ersten Schritt müssen sämtliche Anweisungen (GPU-Instruktionen), die wir an eine Grafikkarte übermitteln wollen, in einem Command-Buffer-Objekt zwischengespeichert werden. Hierbei kann es sich um Rendering-spezifische Operationen handeln, um Anweisungen für eine von der GPU durchzuführenden Berechnung (z. B. für eine Physiksimulation) oder um Anweisungen für ein Ressourcenupdate (Beispiel 1: Austausch von nicht mehr benötigten Texturen und 3-D-Modellen, Beispiel 2: Aktualisierung der in einem Buffer gespeicherten Daten).
Nach der Aufzeichnung aller erforderlicher GPU-Instruktionen muss ein Command-Buffer-Objekt finalisiert werden, wodurch ein weiteres Work Recording unmöglich gemacht wird.
Sobald ein Command-Buffer-Objekt finalisiert worden ist, können wir es schließlich so oft wie erforderlich an die Grafikkarte übermitteln. Hat sich beispielsweise im Hintergrund einer 3-D-Szene nichts verändert, können wir auf die Aktualisierung des für die Hintergrunddarstellung verantwortlichen Command-Buffers verzichten.
Bei der Szenendarstellung ist ein wenig Handarbeit gefragt. Allerdings müssen wir das Rad nicht immer neu erfinden, denn in vielen Fällen können wir einfach auf das gleiche Ablaufschema zurückgreifen, das wir bereits in den zuvor gezeigten Programmbeispielen verwendet haben:
Im ersten Schritt werden die Geometrie- und Farbinformationen der zu beleuchtenden 3-D-Objekte in den fünf Framebuffer Attachments eines als G-Buffer bezeichneten Render-Targets zwischengespeichert.
Im Anschluss daran wird der Szenenhintergrund in das Framebuffer Attachment eines zweiten...