Posts mit dem Label OpenGL Leitfaden werden angezeigt. Alle Posts anzeigen
Posts mit dem Label OpenGL Leitfaden werden angezeigt. Alle Posts anzeigen

Texture Buffer Geometry Instancing

Im Rahmen der Entwicklung eines Prototypen für ein Weltraum-Strategiespiel galt es die Frage zu klären, wie sich eine prozedural erzeugte Galaxie mit tausenden Sternen und 3D-Nebelwolken mit nur wenigen Draw Calls darstellen lässt. Nach einer längeren Testphase mit Uniform-Buffer-basiertem Geometry Instancing fiel die Wahl letztlich auf den Einsatz von Texture-Buffer-Objekten, da sich auf diese Weise alle Sterne und alle Nebelwolken mit jeweils einem einzigen Draw Call rendern lassen.

OpenGL-Texture-Buffer-Objekte

Texture-Buffer-Objekte wurden mit der OpenGL-Spezifikation 3.0 eingeführt und dienen im Unterschied zu den Uniform-Buffer-Objekten dem dynamischen Austausch sehr großer Datenmengen zwischen dem Haupt- und dem Grafikspeicher. Zu den Einsatzmöglichkeiten zählt die Speicherung der Geometriedaten eines 3D-Modells oder die der Höhenwerte und Textur-Gewichtungsfaktoren eines Terrains sowie die Darstellung einer sehr großen Anzahl von 3D-Objekten im Rahmen des Geometry-Instancings mit nur einem einzigen Draw Call.
Wie im nachfolgenden Beispiel gezeigt, erfolgt der Zugriff auf einen Texture Buffer in einem Shader-Programm mithilfe der texelFetch()-GLSL-Shaderfunktion:

uniform samplerBuffer SunInstancesTextureBuffer;

[...]


void main()
{
int Index = 4*gl_InstanceID;

mat4 matWorldViewProjection = mat4(texelFetch(SunInstancesTextureBuffer,
                                   Index++),
                                   texelFetch(SunInstancesTextureBuffer,
                                   Index++),
                                   texelFetch(SunInstancesTextureBuffer,
                                   Index++),
                                   texelFetch(SunInstancesTextureBuffer,
                                   Index));

[...]

}


OpenGL-Texture-Array-Objekte

Geometry Instancing bietet uns die Möglichkeit, eine große Anzahl von 3D-Objekten (Terrain Tiles, Nebelwolken, Sterne, Asteroiden, etc.) mit nur wenigen Draw Calls darzustellen. Während man sich zunächst darauf beschränkte, die einzelnen Objekte lediglich individuell zu skalieren, wurde es mit der Einführung der Texture-Arrays zum ersten Male möglich, die Objekte auch gesondert zu texturieren, ohne dass man hierfür jede Textur einzeln an einen Shader übergeben muss. Auch kann bei der Auswahl der zu verwendenden Textur auf komplexe Conditional Statements verzichtet werden. Wie das nachfolgende Fragment-Shader-Beispiel zeigt, ist der Zugriff auf ein Texture-Array denkbar einfach:

uniform sampler2DArray NebulaTextureArray;

[...]

void main()
{
// Hinweise:
// gs_TexCoord[0].z:  Index der zu verwendenden Textur
// gs_TexCoord[0].xy: die normalen Texturkoordinaten

vec4 NebulaColor = texture(NebulaTextureArray, gs_TexCoord[0].xyz);

[...]

}

Parallax Mapping (Offset Mapping, Virtual Displacement Mapping) + Ambient Occlusion

Bereits in OpenGL-Tutorial 12 haben Sie eine Technik kennengelernt, mit deren Hilfe sich im Vergleich zum Normal Mapping mit nur wenig mehr Berechnungsaufwand deutlich realistischere Oberflächenstrukturen simulieren lassen – das sogenannte Parallax Mapping (Offset Mapping oder Virtual Displacement Mapping). Berücksichtigt man darüber hinaus die in der Normal Map gespeicherten Oberflächen-Informationen zur Modifizierung des ambienten Beleuchtungsanteils (Stichwort Ambient Occlusion), so wird die räumliche Tiefenwirkung der Oberflächen noch einmal deutlich verbessert.

Geometry Instancing Teil 2 – Verwendung von Uniform-Buffer-Objekten

Im ersten Artikel zum Thema Geometry Instancing wurden die zum Rendern benötigten Daten (Position in der Spielewelt aus Sicht der Kamera, Orientierung sowie Skalierung) der einzelnen Asteroiden-Instanzen in einem uniform-Array gespeichert. Aufgrund der limitierten Anzahl von uniform-Variablen, die von einem Shader verarbeitet werden können, kann jedoch nur eine verhältnismäßig geringe Zahl von Instanzen pro Draw Call gerendert werden.

Geometry Instancing Teil 1

Durch den Einsatz von Geometry Instancing ist es möglich, eine große Anzahl von 3D-Objekten (Instanzen) gleicher Geometrie mit nur einem einzigen Draw Call (Renderaufruf) zu rendern.
Doch warum genau führt das Geometrie Instancing zu so einem großen Performanceschub? Worin besteht überhaupt der Vorteil, mehrere Instanzen auf einmal zu rendern?
In diesem Zusammenhang müssen Sie wissen, dass auf modernen Grafikkarten die Darstellung eines einfachen 3D-Modells (mit mehreren hundert bis hin zu wenigen tausend Dreiecksflächen) inklusive aller erforderlichen Vertex- und Fragment-Shader-Berechnungen deutlich weniger Zeit beansprucht als die Verarbeitung und Weiterleitung (CPU => GPU) eines Draw Calls.
Da sich mit zunehmender Leistungsfähigkeit der Grafikkarten die für die Darstellung eines 3D-Modells benötigte Zeit immer weiter verringert, während gleichzeitig die für einen Draw Call erforderliche Zeit mehr oder weniger unverändert bleibt, vergrößert sich mit jeder neuen Generation von Grafikkarten der Performancegewinn, der sich durch Geometry Instancing erzielen lässt. Darüber hinaus führt das Instancing auch bei zunehmend komplexeren 3D-Modellen zu einem messbaren Geschwindigkeitszuwachs.

OpenGL-Uniform-Buffer-Objekte

Im heutigen Artikel werden wir uns mit der Verwendung der in OpenGL 3.1 eingeführten Uniform-Buffer-Objekte befassen. Nehmen wir einmal an, wir müssten eine komplexe 3D-Szene unter Verwendung verschiedener Shader rendern. Eine in diesem Zusammenhang recht triviale Angelegenheit ist die Einstellung der Beleuchtungsparameter für die korrekte Ausleuchtung der Szene. Vor Einführung des Uniform-Buffer-Objektes dienten uniform-Variablen zum Speichern dieser Parameter. Die Verwendung dieser Variablen ist bei Licht betrachtet jedoch recht ineffizient, da man für jedes verwendete Shader-Programm dieselben Daten aufs Neue an die GPU schicken muss.

World/Camera Space Normal Mapping

Wie bereits im letzten Artikel angekündigt, werden wir uns heute mit dem sogenannten World Space Normal Mapping befassen. Gegenüber dem altbekannten Tangent (Texture) Space Normal Mapping bietet diese Technik einige entscheidene Vorteile:

Normal Mapping – eine kurze Übersicht über die einzelnen Techniken

Object (Model) Space Normal Mapping: Die verwendete Normal Map muss im Object bzw. Model Space des 3D-Modells definiert sein und dessen Gesamt-Geometrie berücksichtigen. Als Beispiel diene das OpenGL-Tutorial über sphärisches Normal Mapping, in welchem die kugelförmige Gestalt der Asteroiden bereits bei der Erstellung der Normal Map berücksichtigt wird. Beim Texturieren eines Asteroiden zeigen die in der Normal Map gespeicherten Normalen bereits in die korrekte Richtung (im Object Space). Lediglich mögliche Rotationsbewegungen der Asteroiden müssen bei den Beleuchtungsberechnungen berücksichtigt werden.

Normal Maps und Height Maps

In unseren OpenGL-Tutorials erfolgt die Beleuchtung der 3D-Modelle auf pixelbasis im Fragment Shader (Normal Mapping). Hierbei werden für alle Pixel der Modelloberfläche die für diese Berechnungen benötigten Normalenvektoren aus einer speziellen Textur – der sogenannten Normal Map – ausgelesen.
In gewisser Weise verwandt mit den Normal Maps sind die sogenannten Height Maps, welche den Höhenverlauf einer Modelloberfläche speichern. Die Verwandtschaft begründet sich darin, dass sich Height Maps problemlos in Normal Maps umrechnen lassen.
Doch woher kommen eigentlich die zusätzlichen Oberflächendetails, die in einer Normal bzw. einer Height Map gespeichert sind?

Ambiente, diffuse und spiegelnde Reflexion (Lokale Beleuchtungsmodelle)

Zwei Voraussetzungen sind notwendig, damit man einen Körper sehen kann. Mindestens eine Lichtquelle muss ihr Licht in Richtung des Körpers aussenden, und die Oberfläche des Körpers muss so beschaffen sein, dass zumindest ein Teil des Lichts wieder reflektiert wird. Auf den ersten Blick hört sich das zwar sehr einfach an, in der Praxis bedeutet die Umsetzung realistischer Beleuchtungsmodelle jedoch einen nicht zu unterschätzenden Programmieraufwand. Die Durchführung der eigentlichen Berechnungen in den (GLSL)-Shadern ist dementsprechend rechenintensiv.

GLSL Vertex Shader und Fragment Shader Grundlagen

Im heutigen Artikel werden wir uns mit den Grundlagen der GLSL Shader Programmierung vertraut machen. Ausgangspunkt für unsere Betrachtungen ist das OpenGL Tutorial Nr. 2. Ziel dieses Tutorials ist die Darstellung eines texturierten Vertexquads. Zugegeben, dass hört sich nicht nach besonders viel an, nichtsdestotrotz beinhaltet dieses Programmbeispiel alles, was zum Einstieg in die Shader Programmierung erforderlich ist.

GLSL Shader – Aktivieren eines Shader Programms, Übergabe von Variablen und Texturen

Bereits im Artikel GLSL Shader Framework Entwurf wurde Ihnen an einem konkreten Beispiel die Übergabe von Variablen und Texturen an ein Shader Programm demonstriert. Auf die Details der Parameterübergabe sind wir zum damaligen Zeitpunkt jedoch noch nicht eingegangen. Im heutigen Artikel werden wir dies unter anderem nachholen. Darüber hinaus werden wir uns mit den Einzelheiten der Implementierung unseres Shader Frameworks – der CGLSLShader-Klasse – befassen.

GLSL Shader – Schnittstelle zu der in den OpenGL-Tutorials verwendeten Mathematik Bibliothek

In unserem heutigen Artikel gehen wir der Frage nach, wie sich Vektoren, Matrizen (einzeln und als Arrays) an ein Shader Programm übergeben lassen. Mit den Vektor- und Matrizen-Klassen der von uns verwendeten Mathematik Bibliothek (D3DXVECTOR3, D3DXVECTOR4, D3DXMATRIXA16, usw.) können die Shader natürlich nichts anfangen. Kurzum, wir benötigen eine Schnittstelle zwischen den GLSL Shader Programmen auf der einen und der verwendeten Mathematik Bibliothek auf der anderen Seite. Realisiert wird dies mithilfe einer Reihe von Helper-Funktionen, welche die bibliotheksspezifischen Vektor- und Matrizen-Klassenobjekte in ein für die Shader verständliches Format konvertieren.

GLSL Shader Programme in eine OpenGL-Anwendung laden

Aufbauend auf den vorangegangenen Artikel befassen wir uns heute damit, wie sich GLSL Shader Programme in eine OpenGL-Anwendung laden lassen.
In unseren Programmbeispielen wird der Source Code der verwendeten Shader in separaten Dateien im Ordner Shader gespeichert, der seinerseits im Verzeichnis Bin zu finden ist. Vertex Shader Dateien sind durch die Dateiendung vert gekennzeichnet, Fragment Shader Dateien durch die Endung frag.

GLSL Shader Framework Entwurf

Eine der wichtigsten Neuerungen mit Einführung der OpenGL Version 3 war zweifelsohne der Wegfall der Fixed-Function-Pipeline. Zwar konnte bereits in früheren OpenGL Versionen die komplette Szenendarstellung (Beleuchtungsberechnungen, Animationen, Schatten, usw.) mithilfe von sogenannten Shader-Programmen realisiert werden, jedoch blieb ihr Einsatz optional. Besonders für Programmieranfänger bedeutet die Verwendung der OpenGL Versionen 3 und 4 einen nicht zu unterschätzenden Mehraufwand, da die Shader Programmierung nun zu einem Pflichtfach wird und man gezwungen ist, Transformations- und Beleuchtungsroutinen selbstständig zu implementieren.

OpenGL-Frame-Buffer- und -Render-Buffer-Objekte

Frame-Buffer- und Render-Buffer-Objekte sind für uns OpenGL-Programmierer die Eintrittskarte in die Welt der modernen Grafik-Programmierung, ermöglichen sie doch Render-To-Texture-Techniken wie Shadow Mapping, Environment Mapping, Post Processing, eine realistische Wasserdarstellung, Deferred Lighting (Shading), usw.

OpenGL-Sampler-Objekte

Thema des heutigen Artikels sind die mit den OpenGL Versionen 3.3 und 4.0 neu eingeführten Sampler-Objekte. Gegenüber früheren OpenGl Versionen ist es nun das erste Mal möglich, Texturfilter-Einstellungen separat von den eigentlichen Grafikdaten zu handhaben, wodurch im Wesentlichen die folgenden beiden Nachteile beseitigt werden. Da viele der verwendeten Texturen naturgemäß die gleichen Filtereinstellungen (z.B. Mip Mapping, anisotropische Filterung, usw.) nutzen, ist es völlig unnötig, diese mehrfach (zigfach) zu speichern. Darüber hinaus ist es der Performance abträglich, wenn bei jedem Texturwechsel auch die Filtereinstellungen neu gesetzt werden müssen, selbst wenn sich diese gar nicht ändern. Die mithilfe eines Sampler-Objekts festgelegten Filtereinstellungen werden so lange verwendet, bis man die Einstellungen mithilfe eines anderen Sampler-Objekts wieder ändert.

OpenGL-Textur-Objekte

Nachdem wir uns im letzten Artikel damit beschäftigt haben, Grafikdateien mithilfe von SDL-Image zu laden, befassen wir uns heute mit den OpenGL-Textur-Objekten.

Erzeugt wird ein neues Textur-Objekt mithilfe der glGenTextures()-Funktion. Wie wir es bereits beim Anlegen von anderen OpenGL-Objekten her gewohnt sind, erhalten wir als Rückgabewert die Objekt-ID in Form einer vorzeichenlosen 32-Bit-Ganzzahl vom Typ unsigned int, mit deren Hilfe wir im weiteren Verlauf auf unser neu erzeugtes Textur-Objekt zugreifen können. Wird eine Textur schließlich nicht mehr benötigt, so muss der von ihr belegte Speicherplatz mithilfe der glDeleteTextures()-Funktion wieder freigegeben werden.

Grafikdateien (Textur-Daten) mithilfe von SDL-Image laden

Was den Umgang mit Texturen angeht, kommt DirectX insbesondere den Programmieranfängern sehr entgegen. Beschäftigt man sich jedoch mit OpenGL, ist schon ein wenig mehr Aufwand erforderlich, um eine Textur zu laden.

Zunächst müssen die in einer Grafikdatei gespeicherten Daten ausgelesen werden, wobei auf das verwendete Grafikformat zu achten ist. Im zweiten Schritt müssen diese Daten mehr oder weniger aufwändig bearbeitet werden (definieren der transparenten Bereiche mithilfe eines Color Keys, berechnen einer Normal Map aus einer Height Map, usw.). Im letzten Schritt werden die Texturdaten in den Speicher der Grafikkarte kopiert, wo sie uns dann als „Textur“ zur Verfügung stehen.