Im heutigen Artikel wird sich alles um das Thema Rendering drehen. Wir werden darüber sprechen, wie das sogenannte Offscreen-Rendering die Komposition komplexer 3-D-Szenen für immer verändert hat, und Sie erhalten einen Einblick in die Funktionsweise des in unseren Vulkan-Demoprogrammen zum Einsatz kommenden Rendering-Gerüsts.
Vorbei sind die Zeiten, in denen die Beleuchtung der in einer 3-D-Szene sichtbaren Objekte stets zeitgleich mit ihrer Positionierung in der Spielewelt (Transform and Lighting) erfolgte oder man sich mit unschönen Transparenzfehlern herumschlagen musste, die daraus resultierten, dass man das komplette Szenenbild auf direktem Wege in den Back-Buffer gerendert hat. Schauen Sie sich in diesem Zusammenhang einmal den in Abbildung 1 gezeigten Screenshot an, der uns eine typische Szene aus einem Weltraumspiel vor Augen führt.
Früher wäre es praktisch unmöglich gewesen, so etwas wie ein explodierendes Raumschiff, das von einer Wolke aus Rauch und Partikeln eingehüllt wird, ohne irgendwelche Darstellungsfehler zu rendern. Man kann nicht einfach mal so zuerst das Raumschiff und dann die Partikel, Explosionen sowie die zugehörigen Rauchwolken in den Back-Buffer zeichnen. Würde sich eine der Explosionen beispielsweise vom Spieler aus gesehen hinter dem Schiff ereignen, so würde sie durch den Schiffsrumpf nicht verdeckt werden, da man bei der Darstellung von transparenten Objekten generell auf die Verwendung eines Z-Buffer-Tests verzichten sollte. Transparenzfehler lassen sich erst dann vermeiden, wenn man für jedes transparente Szenenpixel überprüft, ob es durch ein opakes (undurchsichtiges) Pixel verdeckt wird oder nicht. An dieser Stelle kommt nun das eingangs genannte Offscreen-Rendering ins Spiel. Wie in der Bildunterschrift zu lesen, werden im Zuge einer modernen Szenenkomposition zunächst die Teilbilder einer 3-D-Szene getrennt voneinander zwischengespeichert und erst zu einem späteren Zeitpunkt – im Verlauf des sogenannten Post Processings – miteinander verrechnet. In diesem Zusammenhang müssen Sie wissen, dass man vor der Berechnung der gewünschten Post-Processing-Effekte zunächst einmal alle wichtigen Geometrie- und Farbinformationen einer 3-D-Szene in diversen Render Targets (Renderzielen, Texturen, Framebuffer-Attachments) zwischenspeichert. Verzichtet man auf eine zusätzliche Schattenberechnung, sind für die Darstellung des in Abbildung 1 gezeigten Raumschiffs die fünf nachfolgend aufgelisteten Render Targets erforderlich:
Render Target 1: Farbe der texturierten, unbeleuchteten, sichtbaren Szenenpixel
Render Target 2: Kameraraumpositionen und Tiefenwerte der sichtbaren Szenenpixel
Render Target 3: Kameraraumnormalen der sichtbaren Szenenpixel
Render Target 4: Reflexionsvermögen (Farbe und Intensität) der sichtbaren Szenenpixel
Render Target 5: zusätzliche Lichtfarbe der (z. B. mit einer Light Map texturierten und daher) selbstleuchtenden Szenenpixel
Unter Berücksichtigung der in den jeweiligen Targets gespeicherten Geometrie- und Farbinformationen lassen sich die einzelnen Explosionen und Partikeleffekte nun wie folgt darstellen:
Im ersten Schritt erfolgt die Darstellung aller opaken (nicht transparenten) Szenenobjekte (Raumschiff).
Im zweiten Schritt werden die Beleuchtungsberechnungen unter Berücksichtigung der in den jeweiligen Render Targets gespeicherten Geometrie- und Farbinformationen durchgeführt (Deferred Lighting).
Im dritten Schritt steht die Darstellung aller transparenten Objekte (Explosionen, Rauch und Partikel) unter Berücksichtigung des im ersten Renderingschritt erstellten Tiefenabbilds auf dem Programm. Anhand der gespeicherten Tiefenwerte kann ermittelt werden, welche der Explosions-, Rauch- und Partikelpixel durch die opake Szenengeometrie (durch das Raumschiff) verdeckt werden.
Bevor wir uns nun ein wenig ausführlicher mit den einzelnen Aspekten des Offscreen-Renderings auseinandersetzen werden, gilt es, zunächst einmal die alles entscheidende Frage zu beantworten: Was zum Teufel ist eigentlich ein Render Target? Damit wir im Rahmen einer Vulkan-Anwendung überhaupt irgendetwas rendern können, benötigen wir zunächst einmal ein sogenanntes VkFramebuffer-Objekt (Framebuffer, dt.: Bildspeicher). Doch Vorsicht: Was man gemeinhin als Render Target bezeichnet, ist jedoch nicht das Framebuffer-Objekt an sich, sondern ein sogenanntes Framebuffer-Attachment, das wir an eine VkFramebuffer-Instanz anbinden müssen. Für die Verwaltung der besagten Framebuffer-Attachments steht uns in unseren Vulkan-Demoprogrammen die in Listing 1 deklarierte CFrameBufferAttachment-Struktur zur Verfügung. Die Anzahl der an ein VkFramebuffer-Objekt angebundenen Framebuffer-Attachments entspricht nun der Anzahl an Render Targets, auf die wir innerhalb eines Fragment-Shader-Programms beim Speichern der diversen Berechnungsergebnisse zurückgreifen können. Für eine möglichst einfache Handhabung der VkFramebuffer-Instanzen samt der zugehörigen Attachments greifen wir darüber hinaus auf eine Reihe von CRenderTarget_*NonDepthAttachments-Datentypen zurück, so zum Beispiel auf die in Listing 2 deklarierte CRenderTarget_5NonDepthAttachments-Struktur. Was nun die Durchführung des eigentlichen Offscreen Renderings betrifft, stehen uns in unseren Demoprogrammen die nachfolgend aufgelisteten Frameworkklassen zur Verfügung:
Die CVulkanOffScreenRenderingStep_1NonDepthAttachment-Klasse vereinfacht die Verwaltung und Handhabung eines einzelnen Render Targets.
Der CVulkanOffScreenRenderingStep_5NonDepthAttachments-Klasse obliegt die Verantwortung für die Verwaltung und Handhabung von fünf Render Targets, in denen sich die Geometrie- und Farbinformationen einer 3-D-Szene zwischenspeichern lassen. Die zugehörige Klassendeklaration können Sie anhand von Listing 3 nachvollziehen.
Die CVulkanOffScreenRenderingStep_6NonDepthAttachments-Klasse kann für die Verwaltung und Handhabung von sechs Render Targets eingesetzt werden.
Listing 1: Deklaration eines Framebuffer-Attachments (Render Target)
struct CFrameBufferAttachment
{
VkImage image;
VkImageView view; /*für den Zugriff auf das VkImage-Objekt*/
VkDeviceMemory mem; /*GPU-Speicher*/
VkFormat format;
};
Listing 2: Deklaration eines Framebuffers mit fünf Attachments (Render Targets)
struct CRenderTarget_5NonDepthAttachments
{
int32_t width, height;
VkFramebuffer frameBuffer;
CFrameBufferAttachment attachment0;
CFrameBufferAttachment attachment1;
CFrameBufferAttachment attachment2;
CFrameBufferAttachment attachment3;
CFrameBufferAttachment attachment4;
CFrameBufferAttachment depth;
};
Listing 3: Offscreen-Rendering mit fünf Render Targets (Frameworkklasse)
class CVulkanOffScreenRenderingStep_5NonDepthAttachments
{
public:
CBaseVulkanApp* pUsedBaseVulkanApp;
CRenderTarget_5NonDepthAttachments RenderTarget;
uint32_t Width;
uint32_t Height;
VkFormat FormatAttachment0;
VkFormat FormatAttachment1;
VkFormat FormatAttachment2;
VkFormat FormatAttachment3;
VkFormat FormatAttachment4;
/* Rendering-spezifische Anweisungen lassen sich nur innerhalb eines aktiven Renderpasses in einem Command-Buffer speichern: */
VkRenderPass RenderPass;
/* Sampler-Objekt, auf das wir im Rahmen der Texturfilterung zurückgreifen, wenn die Framebuffer-Attachments als Textur verwendet werden:*/
VkSampler ColorSampler;
VkCommandBuffer OffscreenCommandBuffer;
/* zusätzliche sekundäre Command-Buffer-Objekte für das Multithreaded-Rendering: */
VkCommandBuffer SecondaryOffscreenCommandBuffer_Thread[NUM_VULKAN_THREADS_MAX];
/* Damit sich die sekundären Command-Buffer-Objekte erzeugen lassen, müssen die Eigenschaften des zugrunde liegenden primären Command-Buffers in der nachfolgenden Struktur gespeichert werden: */
VkCommandBufferInheritanceInfo
CmdBufferInheritanceInfo_MultithreadedRendering;
/* Semaphore-Objekt für die spätere synchronisierung der Rendering-Schritte: */
VkSemaphore OffscreenSemaphore;
bool MultiThreadedRendering;
long SeparateThreadRenderingPossible;
bool Initialized;
CVulkanOffScreenRenderingStep_5NonDepthAttachments();
~CVulkanOffScreenRenderingStep_5NonDepthAttachments();
void Initialize(CBaseVulkanApp* pBaseVulkanApp, uint32_t width, uint32_t height,
VkFormat formatAttachment0, VkFormat formatAttachment1,
VkFormat formatAttachment2, VkFormat formatAttachment3,
VkFormat formatAttachment4, bool multiThreadedRendering = false);
void CleanUp(void);
void RecordTask_Prepare_Rendering(void);
void RecordTask_Begin_SinglethreadedRendering(
float ClearValue0_Attachment0 = 0.0f,
float ClearValue1_Attachment0 = 0.0f,
float ClearValue2_Attachment0 = 0.0f,
float ClearValue3_Attachment0 = 0.0f,
[...]
float ClearValue0_Attachment4 = 0.0f,
float ClearValue1_Attachment4 = 0.0f,
float ClearValue2_Attachment4 = 0.0f,
float ClearValue3_Attachment4 = 0.0f);
void RecordTask_End_SinglethreadedRendering(void);
void RecordTask_Begin_MultithreadedRendering(
float ClearValue0_Attachment0 = 0.0f,
float ClearValue1_Attachment0 = 0.0f,
float ClearValue2_Attachment0 = 0.0f,
float ClearValue3_Attachment0 = 0.0f,
[...]
float ClearValue0_Attachment4 = 0.0f,
float ClearValue1_Attachment4 = 0.0f,
float ClearValue2_Attachment4 = 0.0f,
float ClearValue3_Attachment4 = 0.0f);
void RecordTask_End_MultithreadedRendering(
uint32_t numOfUsedSeparateRenderThreads);
void RecordTask_Begin_PartialRendering_SeparateThread(
uint32_t vulkanThreadID);
void RecordTask_End_PartialRendering_SeparateThread(
uint32_t vulkanThreadID);
};
Greift man im Rahmen der Hintergrunddarstellung auf eine einfache Sky Sphere bzw. Box zurück, kommt man normalerweise mit einem einzigen Offscreen-Render-Target aus. Um jedoch die Geometrie- und Farbinformationen zwischenspeichern zu können, die für die Beleuchtung der in den Abbildungen 2 und 3 gezeigten Objekte im Zuge des Deferred Lightings erforderlich sind, benötigen wir fünf separate HDR Render Targets. Für die Initialisierung der zugehörigen CVulkanOffScreenRenderingStep_5NonDepthAttachments-Klasseninstanz ist lediglich der nachstehend gezeigte Methodenaufruf erforderlich:
GBufferRenderTargets.Initialize(&g_BaseVulkanApp,
g_BaseVulkanApp.Width, g_BaseVulkanApp.Height,
/*BaseColorTexture:*/ VK_FORMAT_R32G32B32A32_SFLOAT,
/*CameraSpacePositionTexture:*/ VK_FORMAT_R32G32B32A32_SFLOAT,
/*NormalTexture:*/ VK_FORMAT_R32G32B32A32_SFLOAT,
/*SpecularTexture:*/ VK_FORMAT_R32G32B32A32_SFLOAT,
/*EmissiveTexture:*/ VK_FORMAT_R32G32B32A32_SFLOAT,
/*Multithreaded Rendering:*/ true);
Das VkFramebuffer-Objekt, in dessen Attachments man die Geometrie- und Farbinformationen einer 3-D-Szene zwischenspeichert, wird in der Literatur als G-Buffer...