Шейдеры чтения экрана
Введение
Часто требуется создать шейдер, считывающий данные с того же экрана, на который он выводит данные. 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, перекрываются, второй не будет использовать результат первого, что приведет к неожиданным визуальным эффектам:
На изображении выше вторая сфера (справа вверху) использует тот же источник для текстуры экрана, что и первая ниже, поэтому первая "исчезает" или не видна.
В 2D это можно исправить с помощью узла BackBufferCopy, который можно создать между обеими сферами. BackBufferCopy может работать, указывая как область экрана, так и весь экран:
При правильном копировании обратного буфера две сферы смешиваются правильно:
Предупреждение
В 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;