QtQuick 2.0 и шейдеры OpenGL

от автора

На хабрахабре уже была статья «Применение шейдеров OpenGL в QML», в которой рассмотрены теория и примеры использования шейдеров в Qt Quick 1.0. Прошло больше года, фреймворк претерпел массу изменений: состоялся релиз Qt 5 и шейдеры теперь являются частью Qt Quick 2.0, а не вынесены в отдельный модуль и синтаксис их использования, естественно, также изменился. Сразу оговорюсь, что с GLSL я сам знаком весьма посредственно, зато имею опыт работы с QML, поэтому в этой статье хочу разобрать работу с фрагментным шейдером на примере компонента LedScreen, разработанного сообществом QUIt Coding (наверняка многие из вас видели его в демо-ролике на YouTube):


Впервые узнав про шейдеры я почему-то сразу воспринял технологию как средство для быстрого искажения изображений для создания определённых эффектов. Это отнюдь, точнее, не совсем, так, что сейчас и будет продемонстрировано.

Постановка задачи

  1. Разработка компонента LedScreen, который будет любую заданную строку представлять в виде текста на светодиодном графическом экране;
  2. Создание сцены, на которой этот компонент будет использован с наложением на него определённых преобразований.

Часть первая: 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; на остальных поддерживаемых платформах всё работает замечательно.

ссылка на оригинал статьи http://habrahabr.ru/post/166813/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *