Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
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.
Vollbild-Quad
Eine Möglichkeit, benutzerdefinierte Post-Processing-Effekte zu erstellen, ist die Verwendung eines Viewports. Die Verwendung eines Viewports birgt jedoch zwei wesentliche Nachteile:
Auf den Tiefenpuffer kann nicht zugegriffen werden
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 im Clip Space.
shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
Bemerkung
In Godot-Versionen vor 4.3 empfahl dieser Code die Verwendung von POSITION = vec4(VERTEX, 1.0);, was implizit voraussetzte, dass die Near-Ebene des Clip Space bei 0.0 lag. Dieser Code ist jetzt nicht mehr korrekt und wird in den Versionen 4.3+ nicht mehr funktionieren, da wir jetzt einen "umgekehrten-z" Tiefenpuffer verwenden, bei dem die Near-Ebene bei 1.0 liegt.
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:
Fügen Sie das QuadMesh als Child-Element der Kamera hinzu, so dass die Kamera immer auf das QuadMesh gerichtet ist
Setzen Sie die Geometrie-Property
extra_cull_marginso 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 : 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.
The values returned by depth_texture are between 1.0 and 0.0 (corresponding to
the near and far plane, respectively, because of using a "reverse-z" depth buffer) and are nonlinear.
When displaying depth directly from the depth_texture, everything will look almost
black unless it is very close due to that nonlinearity. 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.
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.
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
}
Bemerkung
This tutorial assumes the use of the Forward+ or Mobile renderers, which both
use Vulkan NDCs with a Z-range of [0.0, 1.0]. In contrast, the Compatibility
renderer uses OpenGL NDCs with a Z-range of [-1.0, 1.0]. For the Compatibility
renderer, replace the NDC calculation with this instead:
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
You can also use the CURRENT_RENDERER and RENDERER_COMPATIBILITY
built-in defines for a shader that will work in all renderers:
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
#endif
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.
The world position can be constructed from the depth buffer using the following code, using the
INV_VIEW_MATRIX to transform the position from view space into world space.
void fragment() {
...
vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 world_position = world.xyz / world.w;
}
Example shader
Once we add a line to output to ALBEDO, we have a complete shader that looks something like this.
This shader lets you visualize the linear depth or world space coordinates, depending on which
line is commented out.
shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;
uniform sampler2D depth_texture : hint_depth_texture;
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
view.xyz /= view.w;
float linear_depth = -view.z;
vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 world_position = world.xyz / world.w;
// Visualize linear depth
ALBEDO.rgb = vec3(fract(linear_depth));
// Visualize world coordinates
//ALBEDO.rgb = fract(world_position).xyz;
}
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(3.0, -1.0, 0.0))
verts.append(Vector3(-1.0, 3.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.