Впервые узнав про шейдеры я почему-то сразу воспринял технологию как средство для быстрого искажения изображений для создания определённых эффектов. Это отнюдь, точнее, не совсем, так, что сейчас и будет продемонстрировано.
Постановка задачи
- Разработка компонента 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; на остальных поддерживаемых платформах всё работает замечательно.
ссылка на оригинал статьи http://habrahabr.ru/post/166813/
Добавить комментарий