Введение
Основной цикл статей про Qt Widgets/QML (статья 89) уже касался базовых shader-эффектов через ShaderEffect. В этой статье углубимся в применение shader-эффектов специально для визуальных переходов между экранами приложения — эффектов растворения, пикселизации, искажения при смене состояния StackView или смене целого экрана, которые сложно реализовать через обычные QML-анимации свойств.
Концепция
ShaderEffect в Qt Quick позволяет применить произвольный фрагментный шейдер (написанный на GLSL или, в более новых версиях Qt с RHI, скомпилированный через инструмент qsb) к визуальному содержимому QML-элемента. Для переходов между экранами типичный подход — захватить оба экрана (старый и новый) как текстуры через ShaderEffectSource, и передать обе текстуры в шейдер, который вычисляет, как именно смешивать пиксели одного изображения с другим в зависимости от анимируемого параметра прогресса перехода (от 0.0 — полностью старый экран, до 1.0 — полностью новый).
Пример кода: Shader-эффекты глубже
// dissolve.frag.qsb — шейдер эффекта "растворения" одного экрана в другой
// (исходный GLSL-код, компилируемый через qsb в формат, используемый Qt Quick RHI)
/*
#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 progress; // 0.0 — полностью текущий экран, 1.0 — полностью новый
};
layout(binding = 1) uniform sampler2D currentScreen;
layout(binding = 2) uniform sampler2D nextScreen;
layout(binding = 3) uniform sampler2D noiseTexture; // случайный шум для неравномерного растворения
void main() {
vec4 noise = texture(noiseTexture, qt_TexCoord0);
vec4 fromColor = texture(currentScreen, qt_TexCoord0);
vec4 toColor = texture(nextScreen, qt_TexCoord0);
// Пиксель "переключается" на новый экран не равномерно, а в момент,
// когда прогресс перехода превышает случайное значение шума для этого пикселя —
// создаёт неравномерный, органичный эффект растворения, а не плоское затухание
float threshold = noise.r;
fragColor = (progress > threshold) ? toColor : fromColor;
fragColor *= qt_Opacity;
}
*/
// ScreenTransition.qml — использование скомпилированного шейдера для перехода между экранами
import QtQuick
Item {
id: root
anchors.fill: parent
property Item currentScreen
property Item nextScreen
ShaderEffectSource {
id: currentScreenSource
sourceItem: root.currentScreen
hideSource: true
live: false // захватываем как статичный снимок именно в момент начала перехода
}
ShaderEffectSource {
id: nextScreenSource
sourceItem: root.nextScreen
hideSource: true
live: false
}
ShaderEffect {
id: transitionEffect
anchors.fill: parent
property variant currentScreen: currentScreenSource
property variant nextScreen: nextScreenSource
property variant noiseTexture: noiseTextureSource
property real progress: 0.0
fragmentShader: "qrc:/shaders/dissolve.frag.qsb"
}
Image {
id: noiseTextureImage
source: "qrc:/textures/noise.png"
visible: false
}
ShaderEffectSource { id: noiseTextureSource; sourceItem: noiseTextureImage; live: false }
function startTransition()
{
transitionEffect.progress = 0.0;
progressAnimation.start();
}
NumberAnimation {
id: progressAnimation
target: transitionEffect
property: "progress"
from: 0.0
to: 1.0
duration: 800
easing.type: Easing.InOutQuad
onFinished: root.nextScreen.visible = true // финальная фиксация состояния после перехода
}
}
Пояснения к коду
Закомментированный GLSL-код шейдера показывает логику «органичного» растворения — вместо равномерного линейного смешивания (mix(fromColor, toColor, progress), что дало бы плоский, равномерный crossfade) используется текстура случайного шума: для каждого пикселя сравнивается текущий прогресс перехода с предварительно сгенерированным случайным значением шума именно для этого пикселя, и пиксель «переключается» на новое изображение в момент, когда прогресс превышает это случайное значение — пиксели с низким значением шума переключаются первыми, с высоким — последними, создавая неравномерный, визуально интересный эффект растворения.
ScreenTransition.qml показывает интеграцию шейдера в QML — два ShaderEffectSource захватывают визуальное содержимое старого и нового экрана как текстуры (с live: false, поскольку нужен статичный снимок на момент начала перехода, а не постоянно обновляемое живое содержимое), и NumberAnimation плавно изменяет свойство progress от 0 до 1, при каждом шаге передавая новое значение в шейдер через привязанное свойство ShaderEffect, что и создаёт анимированный переход.
Подводные камни
- Компиляция шейдеров через
qsb— обязательный шаг для современных версий Qt Quick, использующих RHI (Rendering Hardware Interface) вместо прямого OpenGL — простой.frag-файл с GLSL-кодом сам по себе не работает напрямую какfragmentShaderв современных версиях Qt, и требует предварительной компиляции инструментомqsbиз Qt SDK в промежуточный, платформонезависимый байткод-формат.qsb, который и используется в реальном QML-коде. hideSource: trueдляShaderEffectSourceскрывает оригинальный элемент, что может создать путаницу, если тот же элемент (currentScreen/nextScreen) используется где-то ещё в иерархии сцены, ожидая, что он останется видимым — нужно явно учитывать, что захват элемента как источника текстуры для шейдера имеет побочный эффект скрытия оригинала, если не отключить это поведение явным указаниемhideSource: false, когда оригинал должен остаться видимым отдельно.- Производительность shader-эффектов на embedded-устройствах со слабым GPU может быть значительно ниже, чем на десктопе — сложные шейдеры с несколькими текстурными выборками (как в примере с тремя текстурами — текущий экран, новый экран, шум) умножают нагрузку на каждый отрисовываемый пиксель, и для embedded-целевых платформ (основной цикл статей про embedded, статья 242) такие визуально насыщенные переходы стоит явно профилировать и при необходимости заменять на более простые, менее ресурсоёмкие альтернативы.
- Отсутствие fallback-варианта для платформ или конфигураций, где кастомные шейдеры не поддерживаются или работают нестабильно (некоторые старые встроенные GPU, нестандартные графические драйверы) — для критичной по надёжности функциональности переходов стоит предусмотреть более простой, гарантированно работающий запасной вариант перехода (обычный crossfade через
opacity), активируемый при обнаружении проблем с шейдерным рендерингом на конкретном устройстве.