Fortgeschrittene Nachbearbeitung

Einführung

Diese Anleitung beschreibt eine fortschrittliche Methode zur Nachbearbeitung in Godot. Insbesondere wird erläutert, wie ein Nachbearbeitungs-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.

In der vorherigen Anleitung zur Nachbearbeitung haben wir die Szene in einem :ref:`Viewport <Klassenfenster> und dann das Ansichtsfenster in einem :ref: ViewportContainer <KlassenportContainer> in die Hauptszene gerendert. Eine Einschränkung dieser Methode besteht darin, dass wir nicht auf den Tiefenpuffer zugreifen konnten, da der Tiefenpuffer nur in räumlichen Shadern verfügbar ist und in Ansichtsfenstern keine Tiefeninformationen verwaltet werden.

Vollbild Quadrat

In der Anleitung Benutzerdefinierte Nachbearbeitung wurde erläutert, wie Sie mit einem Ansichtsfenster benutzerdefinierte Nachbearbeitungseffekte erzielen. Die Verwendung eines Ansichtsfensters hat zwei Hauptnachteile:

  1. Auf den Tiefenpuffer kann nicht zugegriffen werden
  2. Der Effekt des Nachbearbeitungs-Shaders ist im Editor nicht sichtbar

Um die Einschränkung bei der Verwendung des Tiefenpuffers zu umgehen, verwenden Sie eine MeshInstance mit einem QuadMesh Primitiv. Dies ermöglicht es uns einen räumlichen Shader zu verwenden und auf die Tiefenstruktur der Szene zuzugreifen. Verwenden Sie als Nächstes einen Vertex-Shader, damit das Quadrat den Bildschirm jederzeit abdeckt, sodass der Nachbearbeitungseffekt jederzeit angewendet wird, auch im Editor.

Erstellen Sie zunächst eine neue MeshInstance und setzen Sie das Mesh auf ein QuadMesh. Dies erzeugt ein Quadrat, 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. Im Moment nimmt das Quadrat am Ursprung eine Position im Raum ein; Wir möchten jedoch, dass es sich mit der Kamera so bewegt, dass es immer den gesamten Bildschirm abdeckt. Dazu umgehen wir die Koordinatentransformationen, die die Vertex-Positionen durch die die unterschiedlichen Koordinatenräume verschieben und behandeln die Eckpunkte so, als wären sie bereits im abgegrenzten Raum.

The vertex shader expects coordinates to be output in clip space, which are coordinates ranging from -1 at the left and bottom of the screen to 1 at the top and right of the screen. This is why the QuadMesh needs to have height and width of 2. Godot handles the transform from model to view space to clip space behind the scenes, so we need to nullify the effects of Godot's transformations. We do this by setting the POSITION built-in to our desired position. POSITION bypasses the built-in transformations and sets the vertex position directly.

shader_type spatial;

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

Even with this vertex shader, the quad keeps disappearing. This is due to frustum culling, which is done on the CPU. Frustum culling uses the camera matrix and the AABBs of Meshes to determine if the Mesh will be visible before passing it to the GPU. The CPU has no knowledge of what we are doing with the vertices, so it assumes the coordinates specified refer to world positions, not clip space positions, which results in Godot culling the quad when we turn away from the center of the scene. In order to keep the quad from being culled, there are a few options:

  1. Add the QuadMesh as a child to the camera, so the camera is always pointed at it
  2. Set the Geometry property extra_cull_margin as large as possible in the QuadMesh

The second option ensures that the quad is visible in the editor, while the first option guarantees that it will still be visible even if the camera moves outside the cull margin. You can also use both options.

Tiefentextur

To read from the depth texture, perform a texture lookup using texture() and the uniform variable DEPTH_TEXTURE.

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

Bemerkung

Similar to accessing the screen texture, accessing the depth texture is only possible when reading from the current viewport. The depth texture cannot be accessed from another viewport to which you have rendered.

The values returned by DEPTH_TEXTURE are between 0 and 1 and are nonlinear. When displaying depth directly from the DEPTH_TEXTURE, everything will look almost white unless it is very close. This is because the depth buffer stores objects closer to the camera using more bits than those further, so most of the detail in depth buffer is found close to the camera. In order to make the depth value align with world or model coordinates, we need to linearize the value. When we apply the projection matrix to the vertex position, the z value is made nonlinear, so to linearize it, we multiply it by the inverse of the projection matrix, which in Godot, is accessible with the variable INV_PROJECTION_MATRIX.

Firstly, take the screen space coordinates and transform them into normalized device coordinates (NDC). NDC run from -1 to 1, similar to clip space coordinates. Reconstruct the NDC using SCREEN_UV for the x and y axis, and the depth value for z.

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

Convert NDC to view space by multiplying the NDC by INV_PROJECTION_MATRIX. Recall that view space gives positions relative to the camera, so the z value will give us the distance to the point.

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

Because the camera is facing the negative z direction, the position will have a negative z value. In order to get a usable depth value, we have to negate view.z.

The world position can be constructed from the depth buffer using the following code. Note that the CAMERA_MATRIX is needed to transform the position from view space into world space, so it needs to be passed to the fragment shader with a varying.

varying mat4 CAMERA;

void vertex() {
  CAMERA = CAMERA_MATRIX;
}

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

Eine Optimierung

You can benefit from using a single large triangle rather than using a full screen quad. The reason for this is explained here. However, the benefit is quite small and only beneficial when running especially complex fragment shaders.

Set the Mesh in the MeshInstance to an ArrayMesh. An ArrayMesh is a tool that allows you to easily construct a Mesh from Arrays for vertices, normals, colors, etc.

Now, attach a script to the MeshInstance and use the following code:

extends MeshInstance

func _ready():
  # Create a single triangle out of vertices:
  var verts = PoolVector3Array()
  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

The triangle is specified in normalized device coordinates. Recall, NDC run from -1 to 1 in both the x and y directions. This makes the screen 2 units wide and 2 units tall. In order to cover the entire screen with a single triangle, use a triangle that is 4 units wide and 4 units tall, double its height and width.

Assign the same vertex shader from above and everything should look exactly the same.

The one drawback to using an ArrayMesh over using a QuadMesh is that the ArrayMesh is not visible in the editor because the triangle is not constructed until the scene is run. To get around that, construct a single triangle Mesh in a modelling program and use that in the MeshInstance instead.