
Reverse Z-Buffer
Der Z-Buffer oder Depth-Buffer wird in der Computergrafik verwendet, um die Sichtbarkeit von Objekten in jedem Pixel zu bestimmen.
Jedes Mal, wenn wir ein Objekt zeichnen, speichern wir auch die Entfernung zur Kamera im Z-Buffer.
Beim Zeichnen des nächsten Objekts können wir im Z-Buffer nachsehen, welcher Wert dort zuvor gespeichert wurde. Ist der Wert im Z-Buffer näher als das Objekt, das wir gerade zeichnen, wissen wir, dass das Objekt verdeckt ist, und zeichnen das Pixel nicht. Diese Technik wird auch als Tiefentest bezeichnet.
Der Tiefentest ist in allen Grafik-Engines allgegenwärtig, so auch in Evergine. Wie wir jedoch sehen werden, liefert der naive Ansatz hinsichtlich der Genauigkeit nicht die besten Ergebnisse. In dieser Release haben wir Reverse Z-Buffering eingeführt, das die Präzision insbesondere bei weit entfernten Objekten erheblich verbessert.
Klassischer Z-Buffer
Beim klassischen Z-Buffering werden die Tiefenwerte normalisiert im Bereich [0, 1] gespeichert. Dabei ist 0 der nächste und 1 der am weitesten entfernte Wert.
Im folgenden Bild sehen wir eine 3D-Kamera, die auf einen Würfel gerichtet ist.
Die grüne Linie stellt die „Near“-Ebene dar; Pixel, die näher als die Near-Ebene liegen, werden abgeschnitten (verworfen).
Die rote Linie stellt die „Far“-Ebene dar; Pixel, die weiter entfernt als die Far-Ebene liegen, werden abgeschnitten.

Die magentafarbene Linie stellt die Entfernung zum gerade gezeichneten Pixel (d) dar.
Der normalisierte Wert im Depth-Buffer wird mit folgender Formel berechnet:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Beachten Sie: Ist d gleich near, ergibt sich der Wert null.
Ebenso ergibt sich der Wert eins, wenn d gleich far ist.
Im folgenden Bild sehen Sie, wie ein typischer Depth-Buffer aussehen kann.

Präzisionsprobleme
Um die Genauigkeitsprobleme zu zeigen, die beim klassischen Depth-Buffering auftreten können, haben wir die folgende Demo erstellt.

Bei den Kugeln in dieser Szene handelt es sich eigentlich um jeweils zwei Kugeln: eine innerhalb der anderen.
Die Anordnung dieser geschichteten Kugeln ist im folgenden Bild dargestellt. Die beiden Schichten liegen sehr dicht beieinander.

Natürlich sollte die innere rote Kugel nicht sichtbar sein. Wie Sie in unserer Demo jedoch sehen, treten mit zunehmender Entfernung zur Kamera rote Artefakte auf.
Dieses Phänomen wird als Aliasing bezeichnet. Aufgrund der begrenzten Genauigkeit der numerischen Darstellung werden die Tiefenwerte nach der Normalisierung auf denselben Zahlenwert abgebildet, sodass wir nicht erkennen können, welcher näher liegt.
Eine Möglichkeit wäre, die Anzahl der Bits für die Darstellung des Werts im Depth-Buffer zu erhöhen. Die meiste Hardware kann jedoch nicht über 32 Bit hinausgehen, und 32 Bit reichen nicht aus.
Reverse Depth
Dieses Genauigkeitsproblem lässt sich mit einer Technik namens Reverse Depth-Buffering lösen.
Die Idee dahinter ist sehr einfach. Statt der üblichen Formel zur Berechnung der normalisierten Tiefe:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Vertauschen wir die Near- und Far-Ebene:
depthBuffer=(near)/(near-far)-(near⋅far)/(near-far)⋅1/d
Mit dieser Formel wird die Near-Ebene auf 1 und die Far-Ebene auf 0 abgebildet. Der Depth-Buffer erscheint also invertiert.

Mit dieser kleinen Änderung, kombiniert mit einem Float-Tiefenformat, wird unsere Demo komplett grün.

Warum verbessert Reverse-Z die Genauigkeit?
Reverse-Z ist eine denkbar einfache Technik: Man vertauscht lediglich die Near- und die Far-Ebene. Dennoch sind die Ergebnisse hervorragend.
Zudem scheint die Technik äußerst robust zu sein; die Wahl der Near- und Far-Ebene erfordert keine Feinabstimmung mehr.
Tatsächlich können Sie die Far-Ebene einfach auf unendlich setzen, und alles funktioniert einwandfrei. Eine unendliche Sichtweite ist ein großer Vorteil für Anwendungen, die riesige Welten darstellen müssen – zum Beispiel beim Rendern der gesamten Erde, wie in unserem neuen Cesium Add-on. Die Near-Ebene kann ebenfalls deutlich näher liegen, wenn auch nicht bei null.
Falls Sie sich fragen, warum Reverse-Z funktioniert, gehen wir in diesem Abschnitt ausführlich auf die Mathematik und die technischen Details ein.
UNorm- und Float-Darstellung
Projizierte Tiefenwerte werden im Bereich [0, 1] gespeichert.
Zwischen 0 und 1 gibt es unendlich viele reelle Zahlen. Computer können jedoch nicht alle exakt darstellen – so wie Sie auch nicht alle Nachkommastellen von Pi auf Papier schreiben können. Computer verwenden daher verschiedene Darstellungen, um reelle Zahlen anzunähern, so wie Sie Pi beispielsweise durch die ersten 7 Ziffern annähern können.
Für die Darstellung der Tiefenwerte im Depth-Buffer werden zwei Arten von Formaten verwendet: UNorm und Float.
UNorm steht für „unsigned normalized“. Eine vorzeichenlose Ganzzahl wird verwendet, um reelle Zahlen im Bereich [0, 1] darzustellen.
Die darstellbaren Werte sind gleichmäßig im Bereich [0, 1] verteilt.
Das folgende Bild zeigt die mit UNorm darstellbaren Werte. Wie Sie sehen, ist der Abstand zwischen den Werten konstant.

Float steht für die Gleitkommadarstellung.
Die Float-Darstellung deckt den Bereich (-∞, +∞) ab.
Floats mit einfacher Genauigkeit verwenden 32 Bit, die in 3 Teile aufgeteilt sind: Vorzeichen, Exponent und Mantisse.

(Quelle: https://en.wikipedia.org/wiki/Single-precision_floating-point_format#/media/File:Float_example.svg)
Wir können in diesem Artikel nicht alle Details der Gleitkommanotation behandeln. Für Interessierte daher diese beiden Links: https://floating-point-gui.de, https://en.wikipedia.org/wiki/Single-precision_floating-point_format.
Wichtig für unser Problem ist, wie die darstellbaren Werte im Bereich [0, 1] verteilt sind, also in dem Bereich, auf den die projizierten Tiefenwerte abgebildet werden.

Wie Sie in der vorherigen Abbildung sehen, sind die Werte nicht gleichmäßig verteilt. Werten nahe null steht mehr Präzision zur Verfügung. Das ist eine interessante Eigenschaft für wissenschaftliche Berechnungen, und auch wir werden uns diese Besonderheit zunutze machen.
Kehren wir nun zu unserer Formel für die Tiefenprojektion zurück:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Wenn wir diese Formel grafisch darstellen, erhalten wir folgende Kurve:

Die X-Achse ist die lineare Tiefe. Die Y-Achse gibt die auf den Bereich [0, 1] projizierte Tiefe an.
Beachten Sie, wie schnell diese Funktion ansteigt.
Auf der Y-Achse sind die Werte gleichmäßig verteilt, da wir die UNorm-Darstellung verwenden.
Die X-Achse weist eine Reihe kleiner Markierungen auf. Diese entsprechen den gleichmäßig verteilten Markierungen auf der Y-Achse vor der Projektion. Beachten Sie, wie sich die meisten Markierungen nahe der Near-Ebene sammeln, während ihre Dichte zur Far-Ebene hin sehr gering wird.
Und das war bei einer sehr günstigen Wahl von Near- und Far-Ebene. Sehen Sie, was bei near = 0.1 und far = 100 passiert.

Das Problem ist hier, dass die Tiefenwerte von 20 bis 100 alle auf 1 abgebildet werden. Nach der Projektion können wir also nicht mehr feststellen, ob 45 kleiner als 46 ist.
Die Gleitkommanotation hilft hier nicht weiter. Im Gegenteil: Sie verschlimmert das Problem, da sich die darstellbaren linearen Tiefenwerte noch stärker in Richtung Near-Ebene konzentrieren.

Bisher haben wir gesehen, dass zwei Faktoren zur ungleichmäßigen Verteilung der Werte zugunsten der Near-Ebene beitragen: 1) die Kurve der Projektionsformel 2) die Gleitkommanotation
Die Idee von Reverse-Z ist, die Projektionsformel umzukehren. Dadurch entsteht die entgegengesetzte Verteilung der Werte, die Entfernungen nahe der Far-Ebene begünstigt. Die Gleitkommanotation mit ihrem gegenteiligen Effekt gleicht dies aus, sodass wir eine ausgewogenere Verteilung der Werte erhalten. Sehen Sie sich die Verteilung der Markierungen auf der X-Achse im folgenden Bild an.

Was Reverse Z für Sie als Evergine-Benutzer bedeutet
Nach dem Update auf die neue Version wird Reverse Depth automatisch aktiviert.
Für die meisten Projekte hat das keine Auswirkungen. Es gibt jedoch zwei Anwendungsfälle, in denen Probleme auftreten können.
Shader, die den Depth-Buffer verwenden
Wenn Sie eigene Shader geschrieben haben, die den unverarbeiteten Depth-Buffer auslesen, müssen Sie diese entsprechend anpassen.
Wir haben einige Shader-Parameter hinzugefügt, mit denen Sie Reverse Depth in Forward Depth umrechnen können.

ReverseDepth ist 0, wenn mit klassischem Forward Depth Buffering gerendert wird, und 1, wenn Reverse Depth verwendet wird. Ab sofort ist der Wert jedoch immer 1.
ReverseDepthFactor dient zur Umrechnung in Forward Depth mit folgender Funktion:
// This is a function that converts a reverse-depth value to forward-depth, if needed, without branching
inline float forwardDepth(float depth)
{
return depth * ReverseDepthFactor + ReverseDepth;
}Benutzerdefinierte Render Layers
Wenn Ihr Projekt benutzerdefinierte Render Layers enthält, müssen Sie die Bedingung des Tiefentests umkehren.

In der Evergine-Dokumentation zur neuen Release finden Sie einen Migrationsleitfaden mit Skripten, die diesen Vorgang automatisieren.
