- PVSM.RU - https://www.pvsm.ru -

QtQuick 2.0 и шейдеры OpenGL

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

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

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

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

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

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

Часть вторая: рекламный щит (billboard)

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