На хабрахабре уже была статья «Применение шейдеров OpenGL в QML», в которой рассмотрены теория и примеры использования шейдеров в Qt Quick 1.0. Прошло больше года, фреймворк претерпел массу изменений: состоялся релиз Qt 5 и шейдеры теперь являются частью Qt Quick 2.0, а не вынесены в отдельный модуль и синтаксис их использования, естественно, также изменился. Сразу оговорюсь, что с GLSL я сам знаком весьма посредственно, зато имею опыт работы с QML, поэтому в этой статье хочу разобрать работу с фрагментным шейдером на примере компонента LedScreen, разработанного сообществом QUIt Coding (наверняка многие из вас видели его в демо-ролике на YouTube):
Впервые узнав про шейдеры я почему-то сразу воспринял технологию как средство для быстрого искажения изображений для создания определённых эффектов. Это отнюдь, точнее, не совсем, так, что сейчас и будет продемонстрировано.
Постановка задачи
- Разработка компонента LedScreen, который будет любую заданную строку представлять в виде текста на светодиодном графическом экране;
- Создание сцены, на которой этот компонент будет использован с наложением на него определённых преобразований.
Часть первая: LedScreen компонент
В качестве светодиодов будут использованы эти два изображения:
— включенное состояние — выключенное состояние
Изображение строки — битовый массив, по которому можно сформировать выходное изображение. Естественно, можно обойтись вовсе без шейдеров, но грамотное их использование может сильно облегчить разработку: увеличить производительность приложения в целом и уменьшить количество исходного кода. Итак, LedScreen будет построен на основе базового элемента Item и будет содержать в себе два других компонента: ShaderEffectSource (производит рендеринг установленного sourceItem в текстуру и отображает её) и ShaderEffect (применяет заданный шейдер к прямоугольнику):
import QtQuick 2.0
Item {
id: root
property alias sourceItem: effectSource.sourceItem
property real ledSize: 48
property color ledColor: Qt.rgba(1.0, 1.0, 0.0, 1.0);
property bool useSourceColors: false
property real threshold: 0.5
ShaderEffectSource {
id: effectSource
hideSource: true
smooth: false
}
ShaderEffect {
id: effectItem
width: screenWidth * root.ledSize
height: screenHeight * root.ledSize
anchors.centerIn: parent
smooth: false
property real screenWidth: Math.floor(root.width / root.ledSize)
property real screenHeight: Math.floor(root.height / root.ledSize)
property var source: effectSource
property var sledOn: Image { source: "images/led_on.png"; sourceSize.width: root.ledSize; sourceSize.height: root.ledSize; visible: false }
property var sledOff: Image { source: "images/led_off.png"; sourceSize.width: root.ledSize; sourceSize.height: root.ledSize; visible: false }
property point screenSize: Qt.point(screenWidth, screenHeight)
property alias ledColor: root.ledColor
property real useSourceColors: root.useSourceColors ? 1.0 : 0.0
property alias threshold: root.threshold
fragmentShader: "
varying highp vec2 qt_TexCoord0;
uniform lowp float qt_Opacity;
uniform sampler2D source;
uniform sampler2D sledOn;
uniform sampler2D sledOff;
uniform highp vec2 screenSize;
uniform highp vec4 ledColor;
uniform lowp float useSourceColors;
uniform lowp float threshold;
void main() {
highp vec2 cpos = (floor(qt_TexCoord0 * screenSize) + 0.5) / screenSize;
highp vec4 tex = texture2D(source, cpos);
highp vec2 lpos = fract(qt_TexCoord0 * screenSize);
lowp float isOn = step(threshold, tex.a);
highp vec4 pix = mix(texture2D(sledOff, lpos), texture2D(sledOn, lpos), isOn);
highp vec4 color = mix(ledColor, tex, isOn * useSourceColors);
gl_FragColor = pix * color * qt_Opacity;
}"
}
}
Что тут происходит? Создаётся родительский Item, в котором определены несколько свойств (property) для удобной настройки отображения. Здесь стоит отметить только два момента: производится связывание (alias) свойства sourceItem с соответствующим свойством effectSource (таким образом достигается своего рода инкапсуляция шейдера) и задаётся пороговое значение threshold: если в исходном изображении прозрачность пикселя меньше этой величины, будем считать, что светодиод в этом месте выключен.
Теперь самое интересное: рассмотрим непосредственно шейдер. О том, как происходит связывание компонетов QML с текстурами внутри шейдера вы можете прочитать в вышеупомянутой статье, я же перейду непосредственно к разбору GLSL:
highp vec2 cpos = (floor(qt_TexCoord0 * screenSize) + 0.5) / screenSize;
Здесь происходит преобразование текстурных координат сцены в координаты текущего пикселя для исходного изображения (строки). Дело в том, что в самом шейдере координаты текстуры нормированы и представлены значениями от 0 до 1.
highp vec4 tex = texture2D(source, cpos);
В переменную tex записываем цвет текущего пикселя исходного изображения.
highp vec2 lpos = fract(qt_TexCoord0 * screenSize);
В переменную lpos записываем координаты текущего пикселя текстуры (sledOn или sledOff).
lowp float isOn = step(threshold, tex.a);
В переменную isOn записывается результат функции step(), которая возвращает значение 0.0 или 1.0 и служит здесь в качестве замены логического оператора if. Таким образом мы узнаём, как относится прозрачность текущего пикселя к нашему пороговому значению. Где-то было написано, что использование if в коде шейдера считается моветоном и способно сильно замедлить его выполнение; видимо, поэтому как раз здесь так и сделано.
highp vec4 pix = mix(texture2D(sledOff, lpos), texture2D(sledOn, lpos), isOn);
Продолжение хитрого приёма: обычно функция mix() используется для линейной интерполяции между двумя значениями (создание градиентов), тут же вызывается её сигнатура genType mix(genType x, genType y, float a)
и конечный цвет возвращаемого значения рассчитывается по формуле x*(1-a)+y*a
. Так как в качестве a
у нас служит isOn, то мы получаем следующий результат: если прозрачность пикселя исходной строки меньше 0.5, то цвет текущего пикселя берётся из текстуры sledOff, иначе — из sledOn.
highp vec4 color = mix(ledColor, tex, isOn * useSourceColors);
Если необходимо, задаём цвет color для колоризации выходного пикселя.
gl_FragColor = pix * color * qt_Opacity;
Цвет выходного пикселя складывается из цвета текстуры sledOn или sledOff и цвета колоризации color с применением прозрачности, заданной к компоненту шейдера из переменной qt_Opacity, доступной только для чтения.
Теперь использование компонента сводится всего лишь к заданию необходимых свойств и установке sourceItem, которым, естественно, может быть не только текст, но и изображение в качестве маски:
LedScreen {
id: ledScreen
anchors.fill: parent
sourceItem: Item {
id: sourceArea
width: 44
height: 10
Text {
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 14
font.family: "arial"
font.bold: true
smooth: false
text: "test"
}
}
ledSize: 18
ledColor: "#ff8800"
}
Часть вторая: рекламный щит (billboard)
Нам понадобятся следующие изображения:
Фон с щитом
Текстура для эффекта затемнения
Тут всё просто: накладываем фон, создаём Item с текстом, эффект движения достигается с помощью встроенных анимаций QML. Далее создаём компонент LedScreen, устанавливаем текст в качестве источника, накладываем поверх изображение-маску и к полученному выполняем геометрические преобразования. Исходный код сцены:
import QtQuick 2.0
import "ledscreencomponent"
Rectangle {
id: root
property int scrollSpeed: 500
width: 854
height: 480
color: "#000000"
Image {
id: backgroundImage
anchors.centerIn: parent
source: "images/billboard.png"
}
Item {
id: sourceArea
width: 41
height: 15
Text {
id: textItem
property int textXPos
x: textXPos
anchors.verticalCenter: parent.verticalCenter
font.family: "Fixedsys"
font.pixelSize: 14
font.bold: true
color: "#ffffff"
smooth: false
text: "Hello Habrahabr"
NumberAnimation on textXPos {
loops: Animation.Infinite
from: sourceArea.width; to: -textItem.paintedWidth; duration: textItem.text.length*scrollSpeed
}
}
}
LedScreen {
id: ledScreen
sourceItem: sourceArea
width: 656
height: 240
anchors.centerIn: backgroundImage
anchors.horizontalCenterOffset: 40
anchors.verticalCenterOffset: -10
transform: [
Rotation { origin.x: backgroundImage.width/2; origin.y: backgroundImage.height/2; axis { x: 0; y: 0; z: 1 } angle: -4 },
Rotation { origin.x: backgroundImage.width/2; origin.y: backgroundImage.height*2; axis { x: 0; y: 1; z: 0 } angle: 19 },
Rotation { origin.x: backgroundImage.width/2; origin.y: backgroundImage.height/2; axis { x: 1; y: 0; z: 0 } angle: 20 }
]
ledSize: 16
threshold: 0.48
Image {
anchors.fill: parent
source: "images/reflection.png"
}
}
}
Заключение
Лучше всего рассматривать мою статью как продолжение этой, так как тема шейдеров довольно непростая. Я учу Qt уже больше трёх лет и могу сказать, что Qt 5 привнёс очень много; как и другим адептам концепции виджетов, мне была непонятна мотивация разработчиков, когда впервые было заявлено, что виджеты заменит декларативное программирование. Сейчас же я нисколько не жалею, что пересилил себя и начал учить QML/JavaScript: удивительно, как просто с помощью этого языка создаются такие невероятно красивые и зрелищные эффекты.
В планах ещё две статьи: больше шейдеров и создание визуальных компонентов QML с помощью C++ (которые, к слову, оформляются в виде плагинов).
Надеюсь, вам было так же интересно читать эту статью, как мне её писать :)
Исходные коды, взятые за основу: http://quitcoding.com/?page=work#ledscreen
P. S. Под Windows рендеринг Qt Quick 2.0 может быть очень медленным из-за оверхеда в виде ANGLE; на остальных поддерживаемых платформах всё работает замечательно.
Автор: epicfailguy93