Пользовательский интерфейс в Qt

Кастомные шейдеры в QML через ShaderEffect: простые визуальные эффекты

Введение

Для визуальных эффектов, которые нельзя получить комбинацией стандартных QML-элементов (свечение, искажение, кастомное смешивание цветов, процедурные паттерны), QML предоставляет ShaderEffect — элемент, позволяющий применить собственный шейдер, написанный на языке шейдеров Qt (основанном на синтаксисе, близком к GLSL, компилируемом через qsb в кросс-платформенный байткод для Qt Quick 3D Renderer / RHI). В статье разберём простой пример — эффект градиентного затемнения краёв (vignette).

Концепция

Начиная с Qt 6, шейдеры для ShaderEffect пишутся на варианте GLSL и компилируются заранее инструментом qsb (Qt Shader Baker) в файл .qsb, содержащий байткод для разных графических backend’ов (Vulkan, Metal, Direct3D, OpenGL) — это часть архитектуры RHI (Rendering Hardware Interface), абстрагирующей Qt Quick от конкретного графического API. ShaderEffect в QML ссылается на скомпилированный .qsb-файл через свойство fragmentShader/vertexShader и передаёт значения в шейдер через обычные QML-свойства, автоматически сопоставляемые с uniform-переменными шейдера по имени.

Пример кода: Кастомные шейдеры в QML через ShaderEffect

// vignette.frag — фрагментный шейдер (GLSL для Qt Shader Baker)
#version 440

layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;

layout(std140, binding = 0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;
    float intensity; // кастомный uniform, управляемый из QML
};

layout(binding = 1) uniform sampler2D source;

void main()
{
    vec4 color = texture(source, qt_TexCoord0);

    vec2 center = qt_TexCoord0 - vec2(0.5);
    float distanceFromCenter = length(center);
    float vignette = smoothstep(0.8, 0.2, distanceFromCenter * intensity);

    fragColor = vec4(color.rgb * vignette, color.a) * qt_Opacity;
}
# Компиляция шейдера в кросс-платформенный байткод
qsb --glsl "100 es,120,150" --hlsl 50 --msl 12 -o vignette.frag.qsb vignette.frag
// main.qml — применение скомпилированного шейдера к изображению
import QtQuick

Item {
    width: 400
    height: 400

    Image {
        id: sourceImage
        anchors.fill: parent
        source: "photo.jpg"
        visible: false // источник для шейдера, сам не отображается напрямую
    }

    ShaderEffect {
        anchors.fill: parent
        property variant source: sourceImage
        property real intensity: 1.5 // автоматически сопоставляется с uniform "intensity"

        fragmentShader: "vignette.frag.qsb"
    }
}

Пояснения к коду

Фрагментный шейдер vignette.frag вычисляет расстояние текущего пикселя от центра текстуры (qt_TexCoord0, стандартная переменная координат текстуры, предоставляемая Qt Quick) и применяет затемнение, усиливающееся к краям, через функцию smoothstep для плавного перехода вместо резкой границы. Uniform-переменная intensity объявлена в блоке buf рядом со стандартными qt_Matrix/qt_Opacity, которые Qt Quick предоставляет автоматически для каждого ShaderEffect.

В QML свойство property real intensity: 1.5, объявленное непосредственно на ShaderEffect, автоматически связывается с одноимённым uniform внутри шейдера — изменение этого QML-свойства в рантайме (включая через Behavior/анимации, как в предыдущей статье про анимации) обновляет значение, передаваемое в шейдер на каждом кадре, что позволяет анимировать интенсивность эффекта обычными средствами QML без необходимости пересобирать шейдер.

Подводные камни

  • Шейдер должен быть предварительно скомпилирован через qsb перед использованием — в отличие от старого OpenGL-only API Qt Quick (ShaderEffect до Qt 6 с прямым GLSL-кодом как строкой), современный RHI-based подход требует обязательного шага компиляции в .qsb-файл как части процесса сборки, что нужно явно интегрировать в CMake (qt_add_shaders) или выполнять вручную.
  • Несовпадение имён и типов uniform-переменных между QML-свойствами и шейдером. Если имя QML-свойства не совпадает точно с именем переменной в блоке uniform шейдера (с учётом регистра), значение просто не передастся в шейдер без явной ошибки компиляции — эффект будет выглядеть так, как если бы свойство имело значение по умолчанию (обычно 0), что сложно диагностировать без явной проверки соответствия имён.
  • Производительность шейдеров на встраиваемых/мобильных GPU. Сложные фрагментные шейдеры, выполняемые для каждого пикселя на каждом кадре, могут заметно нагружать GPU слабых устройств — для мобильных приложений стоит профилировать реальную производительность шейдерных эффектов на целевом классе устройств, а не только на десктопе разработчика.
  • Различия в поведении между графическими backend’ами (Vulkan, Metal, Direct3D, OpenGL). Хотя qsb генерирует байткод для всех целевых платформ из единого исходного шейдера, тонкие различия в точности вычислений или порядке операций между разными графическими API иногда приводят к едва заметным визуальным отличиям на разных платформах — для критичных к точности эффектов стоит тестировать на всех целевых backend’ах, которые приложение поддерживает.