Шейдеры чтения экрана

Введение

Часто требуется создать шейдер, считывающий данные с того же экрана, на который он выводит данные. 3D API, такие как OpenGL или DirectX, значительно затрудняют это из-за внутренних аппаратных ограничений. Графические процессоры работают исключительно параллельно, поэтому чтение и запись вызывают всевозможные проблемы с кэшем и когерентностью. В результате даже самое современное оборудование не поддерживает эту функцию должным образом.

Обходной путь — скопировать экран или его часть во вторичный буфер, а затем считывать оттуда данные во время рисования. Godot предоставляет несколько инструментов, упрощающих этот процесс.

Текстура экрана

Godot Язык шейдеров имеет специальную текстуру для доступа к уже отрисованному содержимому экрана. Она используется с указанием подсказки при объявлении юниформы sampler2D: hint_screen_texture. Специальный встроенный вариативный SCREEN_UV может использоваться для получения UV-координат относительно экрана для текущего фрагмента. В результате этот фрагментный шейдер canvas_item создаёт невидимый объект, поскольку отображает только то, что находится за ним:

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

void fragment() {
    COLOR = textureLod(screen_texture, SCREEN_UV, 0.0);
}

Здесь используется textureLod, поскольку мы хотим читать только из нижней MIP-карты. Если вы хотите читать из размытой версии текстуры, можно увеличить третий аргумент до textureLod и изменить подсказку filter_nearest на filter_nearest_mipmap (или любой другой фильтр с поддержкой MIP-карт). При использовании фильтра с MIP-картами Godot автоматически рассчитает размытую текстуру.

Предупреждение

Если режим фильтра не изменен на режим фильтра, содержащий mipmap в своем названии, textureLod с параметром LOD больше 0.0 будет иметь тот же вид, что и с параметром LOD 0.0.

Пример текстуры экрана

Текстуру экрана можно использовать для множества целей. Существует специальная демоверсия Screen Space Shaders, которую вы можете скачать, чтобы посмотреть и изучить. Один из примеров — простой шейдер для настройки яркости, контрастности и насыщенности:

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

uniform float brightness = 1.0;
uniform float contrast = 1.0;
uniform float saturation = 1.0;

void fragment() {
    vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;

    c.rgb = mix(vec3(0.0), c.rgb, brightness);
    c.rgb = mix(vec3(0.5), c.rgb, contrast);
    c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);

    COLOR.rgb = c;
}

За кулисами сцен

Хоть это и кажется магией, это не так. В 2D, когда hint_screen_texture впервые обнаруживается в узле, готовящемся к отрисовке, Godot делает полноэкранное копирование во вторичный буфер. Последующие узлы, использующие его в шейдерах, не будут копировать экран, поскольку это становится неэффективным. В 3D экран копируется после прохода непрозрачной геометрии, но до прохода прозрачной геометрии, поэтому прозрачные объекты не будут захвачены в текстуре экрана.

В результате в 2D, если шейдеры, использующие hint_screen_texture, перекрываются, второй не будет использовать результат первого, что приведет к неожиданным визуальным эффектам:

../../_images/texscreen_demo1.png

На изображении выше вторая сфера (справа вверху) использует тот же источник для текстуры экрана, что и первая ниже, поэтому первая "исчезает" или не видна.

В 2D это можно исправить с помощью узла BackBufferCopy, который можно создать между обеими сферами. BackBufferCopy может работать, указывая как область экрана, так и весь экран:

../../_images/texscreen_bbc.png

При правильном копировании обратного буфера две сферы смешиваются правильно:

../../_images/texscreen_demo2.png

Предупреждение

В 3D материалы, использующие hint_screen_texture, сами по себе считаются прозрачными и не будут отображаться в результирующей текстуре экрана других материалов. Если вы планируете создать сцену, использующую материал с hint_screen_texture, вам потребуется узел BackBufferCopy.

В 3D-графике решение этой проблемы менее гибкое, поскольку экранная текстура захватывается только один раз. Будьте осторожны при использовании экранной текстуры в 3D, так как она не захватывает прозрачные объекты и может захватывать некоторые непрозрачные объекты, находящиеся перед объектом, использующим экранную текстуру.

Вы можете воспроизвести логику заднего буфера в 3D, создав Viewport с камерой в том же положении, что и ваш объект, а затем использовать текстуру Viewport's вместо текстуры экрана.

Логика обратного буфера

Итак, чтобы было понятнее, вот как работает логика копирования обратного буфера в 2D в Godot:

  • Если узел использует hint_screen_texture, весь экран копируется в дальний буфер перед отрисовкой этого узла. Это происходит только в первый раз; последующие узлы не вызывают этого.

  • Если узел BackBufferCopy был обработан до ситуации, описанной в пункте выше (даже если hint_screen_texture не использовался), описанное в пункте выше поведение не происходит. Другими словами, автоматическое копирование всего экрана происходит только в том случае, если hint_screen_texture используется в узле впервые и ранее в дереве не было найдено ни одного узла BackBufferCopy (не отключённого).

  • BackBufferCopy может копировать как весь экран, так и его область. Если BackBufferCopy копирует только область (а не весь экран), и ваш шейдер использует пиксели, не входящие в копируемую область, результат чтения будет неопределённым (скорее всего, это мусор из предыдущих кадров). Другими словами, BackBufferCopy можно использовать для обратного копирования области экрана, а затем для чтения текстуры экрана из другой области. Избегайте такого поведения!

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

Для 3D-шейдеров также возможен доступ к буферу глубины экрана. Для этого используется подсказка hint_metre_texture. Эта текстура нелинейна и должна быть преобразована с помощью матрицы обратной проекции.

Следующий код извлекает 3D-позицию под отрисовываемым пикселем:

uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;

void fragment() {
    float depth = textureLod(depth_texture, SCREEN_UV, 0.0).r;
    vec4 upos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth, 1.0);
    vec3 pixel_position = upos.xyz / upos.w;
}

Текстура нормальной шероховатости

Примечание

Текстура с нормальной шероховатостью поддерживается только в методе рендеринга Forward+, но не в Mobile или Compatibility.

Аналогично, текстура нормалей и шероховатости может использоваться для считывания нормалей и шероховатости объектов, визуализируемых в предварительном проходе глубины. Нормаль хранится в каналах .xyz (соответствующих диапазону 0-1), а шероховатость — в канале .w.

uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;

void fragment() {
    float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
    vec3 screen_normal = texture(normal_roughness_texture, SCREEN_UV).xyz;
    screen_normal = screen_normal * 2.0 - 1.0;

Переосмысление текстур экрана

Подсказки текстуры экрана (hint_screen_texture, hint_metre_texture и hint_normal_roughness_texture) можно использовать с несколькими униформами. Например, может потребоваться несколько раз прочитать текстуру с разными флагами повторения или фильтра.

В следующем примере показан шейдер, который считывает нормаль экранного пространства с помощью линейной фильтрации, но считывает шероховатость экранного пространства с помощью фильтрации ближайшего соседа.

uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;
uniform sampler2D normal_roughness_texture2 : hint_normal_roughness_texture, repeat_enable, filter_linear;

void fragment() {
    float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
    vec3 screen_normal = texture(normal_roughness_texture2, SCREEN_UV).xyz;
    screen_normal = screen_normal * 2.0 - 1.0;