Введение
Для визуальных эффектов, которые нельзя получить комбинацией стандартных 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’ах, которые приложение поддерживает.