Панель корреляции на QtQML-Quick

в 12:18, , рубрики: c++, CGI (графика), OpenGL, QML, qt, Qt Quick 2, shaders

Всем привет! Я — тимлид команды по разработке десктопных приложений в компании Роджии Европа. Мы разрабатываем программные решения для нефтегазовой отрасли.

Так получилось, что в нашем флагманском продукте StarSteer нет панели корреляции — классического инструмента проводчиков скважин. Задача долго откладывалась из-за других, более приоритетных, но осенью прошлого года мы наконец смогли к ней приступить.

Обходя вопросы наследия кодовой базы — о нём я упомяну в статье — был один фундаментальный вопрос — с помощью какой технологии делать? Однозначно нам был нужен OpenGL — который уже применяется в MapView и 3d view на базе OpenSceneGraph — но очевидно, что не голый, и с элементами графического интерфейса. OSG отвалился =(. Технологию, удовлетворяющую двум требованиям — граф сцены и GUI на OpenGL — я знал только одну — Qt QML/Quick. О том, что же у нас получилось и чем нам есть поделиться — внутри.

Вступление

Мы начала разрабатывать продукт осенью 2013 года. Каким набором библиотек пользоваться для меня, как фаната Qt, даже и вопроса не стояло. На тот момент мог возникнуть вопрос: использовать 4ю или 5ю (ещё довольно свежую) версию Qt. Мы выбрали пятую и с высоты своего полёта могу только сказать: слава богу!

Весь внешний вид разработан на QtGui/Widgets. Все сцены, где отображаются графики (гамма, пористость, сопротивление, прочее), сделаны на QGraphicsScene/View. Мой совет — не используйте эту связку для серьёзных вещей! Аргументы: скроллБары и не отключаемая снаружи (именно снаружи — без правок самой Qt) логика по центрированию сцены (qgraphicsview.cpp +458 и выше в этом же методе для горизонтали; qgraphicsview.cpp +3816 — какой контроль над матрицей). Если вам это не мешает, то используйте — много удобных штук из коробки.

Что ещё не использовать? NSIS.

Всё было отлично, продукт развивался, задачи делались, количество клиентов росло. Рефакторинг… В общем по прошествии некоторого времени нам стал мешать QGraphicsScene — оптимизировать отрисовку графиков в 40 000 точек мы не спешили, при включении "толстых" линий — всё это добро на ЦПУ тормозило очень сильно.

Попутно с этим устали разрабатывать ГУЙ на виджетах. Либо руками полностью в коде, либо чуть-чуть в дизайнере (из Креатора, .ui + .cpp). Захотелось самомоднейших штучек, вроде декларативного описания графического интерфейса.

Составили список технологий, на которых будем делать:

  • по старинке на QGraphicsView/Scene;
  • для каждого трека использовать отдельный голый QOpenGLWidget;
  • всё окно реализовать на QOpenGLWidget, но GUI самим (или что-то подыскать);
  • предыдущие два пункта + OSG соответственно;
  • Qt QML/Quick,

всего шесть пунктов; обсуждали. Моя харизма перевесила и решили попробовать посмотреть, как поведёт себя прототип на QML.

Прототип

Я открыл пример scenegraphgraph. Посмотрел и закрыл =). Втуплял несколько дней, смотрел другие примеры, но ничего меня не приблежало к заветной цели.

А что было нужно то? Вот как может выглядеть панель корреляции:

Довольно простая структура — список треков скважин, внутри него треки для кривых, шкал; ну и по мелочи. Много текста, в будущем какие-нибудь контролы — кнопки, выпадающие списки, поля ввода и прочее.

Посмотрел sgengine, научился создавать два графа сцены и рисовать их в отведённом вьюпорте. Позже осознал, что при таком варианте QML/Quick не будет, тогда зачем мне всё это?

На самом деле, я уже не помню, какие наркотики, но почему-то я решил обратиться к основам компьютерной графики. Так вот на последних этапах растеризации все координаты сцены переводятся в НКУ (Нормализованные Координаты Устройства; более известные как NDC = Normalized Device Coordinates). Да, я слышал о том, что выход вершинного шейдера — это на самом деле clip space (пространство отсечения) и после происходит ещё аффинное искажение, но всё это для трёхмерного представления, а в 2Д всегда w = 1 и поэтому можно считать, что выход сразу в NDC.

Хорошо, NDC, дальше то что? То, что если ширина вашего окна 800 пикселей, то NDC-координата центра нулевого пикселя есть -1; координата центра 799-го равна 1. Короче, ndcX = -1 + 2 * i / 799. Теперь представим, что есть прямоугольник от 100 до 300 и я хочу отрисовать всю сцену не в целое окно, а в него. Используя вот эти обрывочные знания, я посчитаю ndcX100, ndcX300, потом прокину их в вершинный шейдер и там, после стандартных

gl_Position = matrix * position;

линейно "заверну" gl_Position.x в [ndcX100; ndcX300]. Аналогично поступаем для вертикальной составляющей. Такой трюк позволит порождать сцены в любом выбранном прямоугольнике сцены. Вот с этими знаниями пример graph и стал подвергаться изменениям. Посмотреть на приход можно здесь — graph; вся соль в shaders/line.vsh.

SceneItem/Scene

Следующие три месяца было написание ТЗ, получилось 12 листов А4 =). Мы параллельно этому обдумывали архитектуру. Взяли MVC… он же MVP… он же Hierarchical MVC/MVP… или даже PAC — всё это условности, важна хорошая декомпозиция.

В общем, мы подготовили пример сцены. Исходники доступны тут — SceneSample. Получился некий framework для создания приложений с графиками на QtQML/Quick. Прошу не забывать, что этот код всё таки выступает в качестве примера. Да, уже в полуготовности и выглядит более-менее аккуратно, но не готов.

Scene — главный игрок. Этот класс следит за своими NDC-координатами и обновляет соответствующие матрицы. С ним плотно дружит SceneCamera. Следующая сущность, достойная упоминания — SceneItem. Сам по себе бесполезен, только содержит некую базовую логику; наследуйте ему — подобно LineStrip — и реализуйте необходимое. При этом в updatePaintNode нужно использовать производные от SceneMaterial — FlatColorMaterial в качестве эталона. Остальные сущности тоже чем-то занимаются =), всякие манипуляторы, тулы. Многие из классов не прокинуты в QML и без C++ с такой "либой" не обойтись; вы же помните, что не готово?

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

Плюсы подхода:

  • всё рисуется в одном графе сцены;
  • мы не правили Qt — остаётся возможность добавлять на Сцену обычные QML-контролы так, что z-order между ними и кривыми (или другими SceneItem) корректный;
  • меньшее расходование памяти в сравнении с другими подходами.

Минусы

  • сложная убермашина;
  • знания OpenGL и GLSL обязательны;
  • полуготовое решение.

Конечно же при разработке мы столкнулись с некоторыми трудностями. Одной из них был

Баг с z-order

Когда мы первый раз попробовали отображать сцену с кривыми, то увидели такие картины:
Панель корреляции на QtQML-Quick - 1

Панель корреляции на QtQML-Quick - 2

С первого взгляда было не понятно, "мы же всё сделали правильно!" Смутно догадывались, что gl_Position.z почему-то ошибочный, но почему именно ночью понимать было тяжело. Мы не сдавались: подглядели, что Qt правит шейдеры и дописывает код по изменению gl_Position.z, подумали. Через некоторое время осенило: мы испортили данные матрицы по изменению z, а Qt в них передаёт свои значения! Таким образом происходит отображение значения Item.z из QML в z из OpenGL (SceneMaterial.cpp +20):

Панель корреляции на QtQML-Quick - 3

Баг с clip: true

Однажды в чат бизнес-команда присылает скрин, где пропала левая линия координатной сетки.

Панель корреляции на QtQML-Quick - 4

Наши Q&A помучали программу и нашли шаги стабильного воспроизведения: ставим масштабирование для монитора не кратное 100% и линии "мелькают". Артём посидел, подумал и нашёл, что когда clip: true и айтем прямоугольный то используется glScissor, но его аргументы — целочисленные пиксельные координаты! У QML-х айтемов они вещественные и получалось, что растеризация линии попадала на следующий/предыдущий пиксель, а ножницы резали по текущему.

Починили сцену так: width: Math.round(metrics.width + leftPadding + 2 * rightPadding + 0.5). Соответственно айтем сцены должен всегда иметь целочисленные координаты, чтобы избежать подобных артефактов.

В заключении приведу КДПВ

Панель корреляции на QtQML-Quick - 5

Всем спасибо за внимание!

Автор: gshep

Источник

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


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