Advanced post-processing

Введение

This tutorial describes an advanced method for post-processing in Godot. In particular, it will explain how to write a post-processing shader that uses the depth buffer. You should already be familiar with post-processing generally and, in particular, with the methods outlined in the custom post-processing tutorial.

In the previous post-processing tutorial, we rendered the scene to a Viewport and then rendered the Viewport in a ViewportContainer to the main scene. One limitation of this method is that we could not access the depth buffer because the depth buffer is only available in spatial shaders and Viewports do not maintain depth information.

Full screen quad

In the custom post-processing tutorial, we covered how to use a Viewport to make custom post-processing effects. There are two main drawbacks of using a Viewport:

  1. The depth buffer cannot be accessed

  2. The effect of the post-processing shader is not visible in the editor

To get around the limitation on using the depth buffer, use a MeshInstance with a QuadMesh primitive. This allows us to use a spatial shader and to access the depth texture of the scene. Next, use a vertex shader to make the quad cover the screen at all times so that the post-processing effect will be applied at all times, including in the editor.

Сначала создайте новый MeshInstance и установите его сетку на QuadMesh. Это создаст квадрат с центром в позиции (0, 0, 0) с шириной и высотой 1. Установите ширину и высоту на 2. Сейчас квад занимает позицию в мировом пространстве в точке начала координат, но мы хотим, чтобы он перемещался вместе с камерой так, чтобы всегда охватывать весь экран. Для этого мы обойдем преобразования координат, которые переводят позиции вершин через различные координатные пространства, и будем обращаться с вершинами так, как будто они уже находятся в пространстве клипа.

Вершинный (вертексный) шейдер ожидает вывода координат в пространстве клипов, которые представляют собой координаты от -1 в левой и нижней части экрана до 1 в верхней и правой части экрана. Вот почему QuadMesh должен иметь высоту и ширину 2. Godot обрабатывает трансформацию от модели к пространству представления к пространству клипа за кулисами, поэтому нам нужно свести на нет эффекты трансформаций Godot. Мы сделаем это, установив встроенный параметр POSITION в нужное нам положение. POSITION обходит встроенные трансформации и устанавливает позицию вершины напрямую.

shader_type spatial;

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

Даже с этим вершинным (вертексным) шейдером квадрант продолжает исчезать. Это происходит из-за выборки фрустума, которая выполняется на CPU. Фрустумная очистка использует матрицу камеры и AABBs сетки, чтобы определить, будет ли сетка видимой до передачи ее на GPU. CPU не знает, что мы делаем с вершинами, поэтому он предполагает, что указанные координаты относятся к мировым позициям, а не к позициям пространства клипа, что приводит к тому, что Godot выбраковывает квад, когда мы отворачиваемся от центра сцены. Для того чтобы квадрант не сворачивался, есть несколько вариантов:

  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.

Depth texture

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;

Примечание

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.

Значения, возвращаемые DEPTH_TEXTURE, находятся в диапазоне от 0 до 1 и являются нелинейными. При отображении глубины непосредственно из DEPTH_TEXTURE, все будет выглядеть почти белым, если только не находится очень близко. Это происходит потому, что буфер глубины хранит объекты, расположенные ближе к камере, используя больше битов, чем те, что дальше, поэтому большая часть деталей в буфере глубины находится близко к камере. Для того чтобы значение глубины совпадало с координатами мира или модели, нам необходимо линеаризовать значение. Когда мы применяем матрицу проекции к позиции вершины, значение z становится нелинейным, поэтому для его линеаризации мы умножаем его на обратную матрицу проекции, которая в Godot доступна с помощью переменной 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;
}

An optimization

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.

Установите Mesh в MeshInstance на ArrayMesh. ArrayMesh - это инструмент, который позволяет легко построить сетку из массивов вершин, нормалей, цветов и т.д.

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)

Примечание

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.

Единственным недостатком использования ArrayMesh по сравнению с QuadMesh является то, что ArrayMesh не виден в редакторе, поскольку треугольник не строится до запуска сцены. Чтобы обойти это, постройте один треугольник Mesh в программе моделирования и используйте его в MeshInstance.