Расширенная пост-обработка

Введение

В этом руководстве описывается продвинутый метод пост-обработки в Godot. В частности, будет объяснено, как написать шейдер пост-обработки, использующий буфер глубины. Вы уже должны быть знакомы с пост-обработкой в целом и, в частности, с методами, описанными в custom post-processing tutorial.

Полный экран Quad

Один из способов создания пользовательских эффектов постобработки — использование Viewport. Однако у неё есть два основных недостатка:

  1. Невозможно получить доступ к буферу глубины

  2. Эффект шейдера постобработки не виден в редакторе

Чтобы обойти ограничение на использование буфера глубины, используйте MeshInstance3D с примитивом QuadMesh. Это позволяет использовать шейдер и получить доступ к текстуре глубины сцены. Затем используйте вершинный шейдер, чтобы квадрат покрывал экран постоянно, и эффект постобработки применялся всегда, в том числе в редакторе.

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

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

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);
}

Примечание

В версиях Godot до 4.3 этот код рекомендовал использовать POSITION = vec4(VERTEX, 1.0);, что неявно предполагало, что ближняя плоскость пространства отсечения находится в точке 0.0. Этот код теперь неверен и не будет работать в версиях 4.3+, поскольку теперь мы используем буфер глубины с "reversed-z", где ближняя плоскость находится в точке 1.0.

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

  1. Добавьте QuadMesh как дочерний элемент к камере, чтобы камера всегда была направлена на него

  2. Установите свойство геометрии extra_cull_margin как можно больше в QuadMesh

Второй вариант гарантирует, что квадрат будет виден в редакторе, а первый — что он останется видимым, даже если камера выйдет за пределы области отбраковки. Вы также можете использовать оба варианта.

Текстура глубины

Чтобы выполнить чтение из текстуры глубины, нам сначала нужно создать единообразный набор текстур для буфера глубины с помощью hint_metre_texture.

uniform sampler2D depth_texture : hint_depth_texture;

После определения текстуру глубины можно прочитать с помощью функции texture().

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

Примечание

Подобно доступу к текстуре экрана, доступ к текстуре глубины возможен только при чтении из текущего окна просмотра. Текстура глубины недоступна из другого окна просмотра, в котором вы выполнили рендеринг.

Значения, возвращаемые deep_texture, находятся в диапазоне от 1.0 до 0.0 (что соответствует ближней и дальней плоскостям соответственно из-за использования буфера глубины с "обратной осью z") и являются нелинейными. При отображении глубины непосредственно из deep_texture всё будет выглядеть почти чёрным, если только не находится очень близко, из-за этой нелинейности. Чтобы привести значение глубины в соответствие с мировыми или модельными координатами, нам необходимо линеаризовать его. При применении матрицы проекции к положению вершины значение "z" становится нелинейным, поэтому для его линеаризации мы умножаем его на обратную матрицу проекции, которая в Godot доступна через переменную INV_PROJECTION_MATRIX.

Сначала преобразуем координаты экранного пространства в нормализованные координаты устройства (NDC). NDC изменяется от -1.0 до 1.0 по осям x и y и от 0.0 до 1.0 по осям z при использовании бэкенда Vulkan. Восстановите NDC, используя SCREEN_UV для осей x и y и значение глубины для z.

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

Примечание

В этом руководстве предполагается использование рендеров Forward+ или Mobile, которые используют NDC Vulkan с Z-диапазоном [0.0, 1.0]. В отличие от этого, рендер совместимости использует NDC OpenGL с Z-диапазоном [-1.0, 1.0]. Для рендера совместимости замените расчёт NDC следующим образом:

vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;

Вы также можете использовать встроенные определения CURRENT_RENDERER и RENDERER_COMPATIBILITY для шейдера, который будет работать во всех рендерерах:

#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

Преобразуйте NDC в пространство обзора, умножив NDC на INV_PROJECTION_MATRIX. Напомним, что пространство обзора задаёт положение относительно камеры, поэтому значение z будет указывать расстояние до точки.

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

Поскольку камера направлена в отрицательном направлении оси z, позиция будет иметь отрицательное значение z. Чтобы получить полезное значение глубины, нам нужно изменить view.z на отрицательное.

Положение в мире можно построить из буфера глубины с помощью следующего кода, используя INV_VIEW_MATRIX для преобразования положения из пространства вида в мировое пространство.

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

Пример шейдера

После добавления строки для вывода в ALBEDO мы получаем готовый шейдер, который выглядит примерно так. Этот шейдер позволяет визуализировать линейную глубину или координаты в мировом пространстве, в зависимости от того, какая строка закомментирована.

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;
}

Оптимизация

Использование одного большого треугольника вместо полноэкранного квадрата может дать преимущество. Причина этого объяснена здесь: <https://michaldrobot.com/2014/04/01/gcn-execution-patterns-in-full-screen-passes>_. Однако преимущество довольно незначительно и заметно только при запуске особенно сложных фрагментных шейдеров.

Установите сетку в MeshInstance3D в ArrayMesh. ArrayMesh — это инструмент, позволяющий легко создать сетку из массивов вершин, нормалей, цветов и т. д.

Теперь прикрепите скрипт к MeshInstance3D и используйте следующий код:

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)

Примечание

Треугольник задаётся в нормализованных координатах устройства. Напомним, что NDC изменяется от -1.0 до 1.0 как в направлениях x, так и y. Это увеличивает ширину экрана до 2 единиц, а высоту до 2 единиц. Чтобы покрыть весь экран одним треугольником, используйте треугольник шириной 4 единиц и высотой 4 единиц, удвоив его высоту и ширину.

Назначьте тот же вершинный шейдер, что и выше, и все должно выглядеть точно так же.

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