Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Fortgeschrittenes Post Processing

Einführung

Dieses Tutorial beschreibt eine fortgeschrittene Methode zur Nachbearbeitung in Godot. Insbesondere wird erläutert, wie ein Post Processing-Shader geschrieben wird, der den Tiefenpuffer verwendet. Sie sollten bereits mit der Nachbearbeitung im Allgemeinen und insbesondere mit den Methoden vertraut sein, die in der folgenden Anleitung beschrieben sind Anleitung zur benutzerdefinierten Nachbearbeitung.

Im vorherigen Post-Processing-Tutorial haben wir die Szene in ein Viewport gerendert und dann das Viewport in einem SubViewportContainer zur Hauptszene gerendert. Eine Einschränkung dieser Methode ist, dass wir nicht auf den Tiefenpuffer zugreifen konnten, da der Tiefenpuffer nur in Shadern verfügbar ist und Viewports keine Tiefeninformationen verwalten.

Vollbild-Quad

Im Benutzerdefiniertes Post-Processing-Tutorial wurde erläutert, wie Sie mit einem Viewport benutzerdefinierte Nachbearbeitungseffekte erzielen. Die Verwendung eines Ansichtsfensters hat zwei Hauptnachteile:

  1. Auf den Tiefenpuffer kann nicht zugegriffen werden

  2. Der Effekt des Post Processing-Shaders ist im Editor nicht sichtbar

Um die Einschränkung bei der Verwendung des Tiefenpuffers zu umgehen, verwenden Sie ein MeshInstance3D mit einem QuadMesh-Primitiv. Dies ermöglicht uns die Verwendung eines Shaders und den Zugriff auf die Tiefentextur der Szene. Als Nächstes verwenden wir einen Vertex-Shader, damit das Quad jederzeit den Bildschirm bedeckt, so dass der Nachbearbeitungseffekt jederzeit angewendet wird, auch im Editor.

Erstellen Sie zunächst eine neue MeshInstance und stellen Sie als Mesh ein QuadMesh ein. Dies erzeugt ein Quad, das an der Position (0, 0, 0) mit einer Breite und Höhe von 1 zentriert ist. Stellen Sie die Breite und Höhe auf 2 ein und aktivieren Sie Oberflächen wenden. Im Moment befindet sich das Quadrat am Ursprung des World Space. Wir möchten jedoch, dass es sich mit der Kamera so bewegt, dass es immer den gesamten Bildschirm abdeckt. Um das zu erreichen, umgehen wir die Koordinatentransformationen, welche die Vertex-Positionen durch die unterschiedlichen Koordinaten-Spaces verschieben und behandeln die Eckpunkte so, als wären sie bereits im Clip Space.

Der Vertex-Shader erwartet, dass die Koordinaten im Clip Space ausgegeben werden, das sind Koordinaten, die von -1 am linken und unteren Rand des Bildschirms bis 1 am oberen und rechten Rand des Bildschirms reichen. Aus diesem Grund muss das QuadMesh eine Höhe und Breite von 2 haben. Godot handhabt die Transformation vom Model Space zum View Space zum Clip Space hinter den Kulissen, daher müssen wir die Auswirkungen von Godots Transformationen aufheben. Wir tun dies, indem wir den Built-in-Parameter POSITION auf unsere gewünschte Position setzen. POSITION umgeht die Built-in-Transformationen und setzt die Position des Vertex direkt.

shader_type spatial;

void vertex() {
  POSITION = vec4(VERTEX, 1.0);
}

Selbst mit diesem Vertex-Shader verschwindet das Quad immer wieder. Das liegt am Frustum Culling, das auf der CPU durchgeführt wird. Frustum Culling verwendet die Kameramatrix und die AABBs von Meshes, um zu bestimmen, ob das Mesh sichtbar sein wird, bevor es an die GPU weitergegeben wird. Die CPU weiß nicht, was wir mit den Vertices machen, also nimmt sie an, dass sich die angegebenen Koordinaten auf World Space-Positionen und nicht auf Clip Space-Positionen beziehen, was dazu führt, dass Godot das Quad abschneidet, wenn wir uns vom Zentrum der Szene wegdrehen. Um zu verhindern, dass der Quad gecullt wird, gibt es ein paar Optionen:

  1. Fügen Sie das QuadMesh als Child-Element der Kamera hinzu, so dass die Kamera immer auf das QuadMesh gerichtet ist

  2. Setzen Sie die Geometrie-Property extra_cull_margin so groß wie möglich im QuadMesh

Die zweite Option stellt sicher, dass das Quad im Editor sichtbar ist, während die erste Option gewährleistet, dass es auch dann noch sichtbar ist, wenn sich die Kamera außerhalb des Cull-Bereichs bewegt. Sie können auch beide Optionen verwenden.

Tiefentextur

Um aus der Tiefentextur zu lesen, müssen wir zuerst ein Textur-Uniform für den Tiefenpuffer erstellen, indem wir hint_depth_texture benutzen.

uniform sampler2D depth_texture : source_color, hint_depth_texture;

Einmal definiert, kann die Tiefentextur mit der Funktion texture() gelesen werden.

float depth = texture(depth_texture, SCREEN_UV).x;

Bemerkung

Ähnlich wie der Zugriff auf die Bildschirmtextur ist auch der Zugriff auf die Tiefentextur nur möglich, wenn sie aus dem aktuellen Viewport gelesen wird. Auf die Tiefentextur kann nicht von einem anderen Viewport aus zugegriffen werden, in das Sie gerendert haben.

Die von depth_texture zurückgegebenen Werte liegen zwischen 0.0 und 1.0 und sind nichtlinear. Wenn die Tiefe direkt von der depth_texture angezeigt wird, sieht alles fast weiß aus, es sei denn, es ist sehr nah. Das liegt daran, dass der Tiefenpuffer Objekte, die sich näher an der Kamera befinden, mit mehr Bits speichert als solche, die weiter entfernt sind, so dass die meisten Details im Tiefenpuffer in der Nähe der Kamera zu finden sind. Damit der Tiefenwert mit den Welt- oder Modellkoordinaten übereinstimmt, müssen wir den Wert linearisieren. Wenn wir die Projektionsmatrix auf die Scheitelpunktposition anwenden, wird der z-Wert nichtlinear. Um ihn zu linearisieren, multiplizieren wir ihn mit der Inversen der Projektionsmatrix, die in Godot über die Variable INV_PROJECTION_MATRIX zugänglich ist.

Zunächst werden die Bildschirmkoordinaten in normalisierte Gerätekoordinaten (NDC) umgewandelt. NDC verlaufen von -1.0 bis 1.0 in x und y Richtung und von 0.0 bis 1.0 in z Richtung, wenn das Vulkan-Backend benutzt wird. Rekonstruieren Sie den NDC unter Verwendung von SCREEN_UV für die x- und y-Achse, und den Tiefenwert für z.

Bemerkung

Dieses Tutorial setzt die Verwendung des Vulkan-Renderers voraus, der NDCs mit einem Z-Bereich von [0.0, 1.0] verwendet. Im Gegensatz dazu verwendet OpenGL NDCs mit einem Z-Bereich von [-1.0, 1.0].

void fragment() {
  float depth = texture(depth_texture, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
}

Konvertieren Sie den NDC in den View Space, indem Sie den NDC mit INV_PROJECTION_MATRIX multiplizieren.Erinnern Sie sich, dass der View Space Positionen relativ zur Kamera angibt, so dass der z-Wert die Entfernung zum Punkt angibt.

void fragment() {
  ...
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;
}

Da die Kamera in die negative z-Richtung zeigt, wird die Position einen negativen z-Wert haben.Um einen brauchbaren Tiefenwert zu erhalten, müssen wir view.z negieren.

Die Weltposition kann mit dem folgenden Code aus dem Tiefenpuffer konstruiert werden. Man beachte, dass der INV_VIEW_MATRIX benötigt wird, um die Position vom View Space in den World Space zu transformieren, daher muss er dem Fragment Shader mit einer Variation übergeben werden.

varying mat4 CAMERA;

void vertex() {
  CAMERA = INV_VIEW_MATRIX;
}

void fragment() {
  ...
  vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;
}

Eine Optimierung

Es kann von Vorteil sein, ein einzelnes großes Dreieck zu verwenden, anstatt ein bildschirmfüllendes Quad zu verwenden. Der Grund dafür wird hier erklärt. Der Vorteil ist jedoch recht gering und nur dann von Vorteil, wenn besonders komplexe Fragment-Shader verwendet werden.

Stellen Sie das Mesh in der MeshInstance3D auf ein ArrayMesh ein. Ein ArrayMesh ist ein Werkzeug, mit dem man leicht ein Mesh aus Arrays für Vertices, Normalen, Farben, etc. konstruieren kann.

Hängen Sie nun ein Skript an die MeshInstance3D an und verwenden Sie den folgenden Code:

extends MeshInstance3D

func _ready():
  # Create a single triangle out of vertices:
  var verts = PackedVector3Array()
  verts.append(Vector3(-1.0, -1.0, 0.0))
  verts.append(Vector3(-1.0, 3.0, 0.0))
  verts.append(Vector3(3.0, -1.0, 0.0))

  # Create an array of arrays.
  # This could contain normals, colors, UVs, etc.
  var mesh_array = []
  mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
  mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array

  # Create mesh from mesh_array:
  mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)

Bemerkung

Das Dreieck wird in normalisierten Gerätekoordinaten angegeben. Erinnern Sie sich, dass NDC von -1.0 bis 1.0 sowohl in der x- als auch in der y-Richtung reichen. Dies macht den Bildschirm 2 Einheiten breit und 2 Einheiten hoch. Um den gesamten Bildschirm mit einem einzigen Dreieck zu bedecken, nehmen Sie ein Dreieck, das 4 Einheiten breit und 4 Einheiten hoch ist, und verdoppeln Sie seine Höhe und Breite.

Weisen Sie den gleichen Vertex-Shader von oben zu, und alles sollte genau gleich aussehen.

Der einzige Nachteil bei der Verwendung eines ArrayMesh gegenüber einem QuadMesh ist, dass das ArrayMesh im Editor nicht sichtbar ist, da das Dreieck erst beim Ausführen der Szene konstruiert wird. Um dies zu umgehen, konstruieren Sie ein einzelnes Dreiecks-Mesh in einem Modellierungsprogramm und verwenden Sie dieses stattdessen in der MeshInstance3D.