- PVSM.RU - https://www.pvsm.ru -
На хабрахабре уже была статья «Применение шейдеров OpenGL в QML» [1], в которой рассмотрены теория и примеры использования шейдеров в Qt Quick 1.0. Прошло больше года, фреймворк претерпел массу изменений: состоялся релиз Qt 5 [2] и шейдеры теперь являются частью Qt Quick 2.0, а не вынесены в отдельный модуль и синтаксис их использования, естественно, также изменился. Сразу оговорюсь, что с GLSL я сам знаком весьма посредственно, зато имею опыт работы с QML, поэтому в этой статье хочу разобрать работу с фрагментным шейдером на примере компонента LedScreen, разработанного сообществом QUIt Coding [3] (наверняка многие из вас видели его в демо-ролике на YouTube):
Впервые узнав про шейдеры я почему-то сразу воспринял технологию как средство для быстрого искажения изображений для создания определённых эффектов. Это отнюдь, точнее, не совсем, так, что сейчас и будет продемонстрировано.
В качестве светодиодов будут использованы эти два изображения:
— включенное состояние
— выключенное состояние
Изображение строки — битовый массив, по которому можно сформировать выходное изображение. Естественно, можно обойтись вовсе без шейдеров, но грамотное их использование может сильно облегчить разработку: увеличить производительность приложения в целом и уменьшить количество исходного кода. Итак, LedScreen будет построен на основе базового элемента Item [5] и будет содержать в себе два других компонента: ShaderEffectSource [6] (производит рендеринг установленного sourceItem в текстуру и отображает её) и ShaderEffect [7] (применяет заданный шейдер к прямоугольнику):
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 с текстурами внутри шейдера вы можете прочитать в вышеупомянутой статье [1], я же перейду непосредственно к разбору 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() [8], которая возвращает значение 0.0 или 1.0 и служит здесь в качестве замены логического оператора if. Таким образом мы узнаём, как относится прозрачность текущего пикселя к нашему пороговому значению. Где-то было написано, что использование if в коде шейдера считается моветоном и способно сильно замедлить его выполнение; видимо, поэтому как раз здесь так и сделано.
highp vec4 pix = mix(texture2D(sledOff, lpos), texture2D(sledOn, lpos), isOn);
Продолжение хитрого приёма: обычно функция mix() [9] используется для линейной интерполяции между двумя значениями (создание градиентов), тут же вызывается её сигнатура 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"
}
Нам понадобятся следующие изображения:
Фон с щитом
[10]
Текстура для эффекта затемнения
[11]
Тут всё просто: накладываем фон, создаём 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"
}
}
}
Лучше всего рассматривать мою статью как продолжение этой [1], так как тема шейдеров довольно непростая. Я учу Qt уже больше трёх лет и могу сказать, что Qt 5 привнёс очень много; как и другим адептам концепции виджетов, мне была непонятна мотивация разработчиков, когда впервые было заявлено, что виджеты заменит декларативное программирование. Сейчас же я нисколько не жалею, что пересилил себя и начал учить QML/JavaScript: удивительно, как просто с помощью этого языка создаются такие невероятно красивые и зрелищные эффекты [12].
В планах ещё две статьи: больше шейдеров и создание визуальных компонентов QML с помощью C++ (которые, к слову, оформляются в виде плагинов).
Надеюсь, вам было так же интересно читать эту статью, как мне её писать :)
Исходные коды, взятые за основу: http://quitcoding.com/?page=work#ledscreen [13]
P. S. Под Windows рендеринг Qt Quick 2.0 может быть очень медленным [14] из-за оверхеда в виде ANGLE [15]; на остальных поддерживаемых платформах всё работает замечательно.
Автор: epicfailguy93
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/opengl/25502
Ссылки в тексте:
[1] «Применение шейдеров OpenGL в QML»: http://habrahabr.ru/post/133828/
[2] релиз Qt 5: http://habrahabr.ru/post/163217/
[3] QUIt Coding: http://quitcoding.com/
[4] светодиодном графическом экране: http://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%B5%D1%82%D0%BE%D0%B4%D0%B8%D0%BE%D0%B4%D0%BD%D1%8B%D0%B9_%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D1%8D%D0%BA%D1%80%D0%B0%D0%BD
[5] Item: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-item.html
[6] ShaderEffectSource: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-shadereffectsource.html
[7] ShaderEffect: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-shadereffect.html
[8] step(): http://www.opengl.org/sdk/docs/manglsl/xhtml/step.xml
[9] mix(): http://www.opengl.org/sdk/docs/manglsl/xhtml/mix.xml
[10] Image: http://habrastorage.org/storage2/46b/ddc/e93/46bddce93940697ec13ae08d824dfb13.png
[11] Image: http://habrastorage.org/storage2/7a9/646/999/7a9646999101b8225ebff9a7ccdd019e.png
[12] невероятно красивые и зрелищные эффекты: http://youtu.be/2nYk2eO-wRE
[13] http://quitcoding.com/?page=work#ledscreen: http://quitcoding.com/?page=work#ledscreen
[14] может быть очень медленным: http://qt-project.org/forums/viewthread/23566/
[15] ANGLE: http://code.google.com/p/angleproject/
[16] Источник: http://habrahabr.ru/post/166813/
Нажмите здесь для печати.