QtQuick 2.0 и шейдеры OpenGL

в 12:45, , рубрики: Digia, glsl, OpenGL, QML, qt, Qt 5, qt quick, Qt Software, qt5, shaders, метки: , , , , , , , , ,

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

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

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

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

Часть первая: LedScreen компонент

QtQuick 2.0 и шейдеры OpenGL
В качестве светодиодов будут использованы эти два изображения:
QtQuick 2.0 и шейдеры OpenGL — включенное состояние QtQuick 2.0 и шейдеры OpenGL — выключенное состояние
Изображение строки — битовый массив, по которому можно сформировать выходное изображение. Естественно, можно обойтись вовсе без шейдеров, но грамотное их использование может сильно облегчить разработку: увеличить производительность приложения в целом и уменьшить количество исходного кода. Итак, 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)

Нам понадобятся следующие изображения:
Фон с щитом
QtQuick 2.0 и шейдеры OpenGL
Текстура для эффекта затемнения
QtQuick 2.0 и шейдеры OpenGL
Тут всё просто: накладываем фон, создаём 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js