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


Так получилось, что в нашем флагманском продукте 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.


Прототип


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


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



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


Посмотрел 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


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



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



Баг с clip: true


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



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


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


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



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

Комментарии (10)


  1. KanuTaH
    23.12.2019 17:34
    +1

    А QtCharts не пробовали воспользоваться для отрисовки графиков?


    1. gshep Автор
      23.12.2019 17:45

      в списке возможных альтернатив у нас он мелькал, но если мне не изменяет память, он тоже рисует (или рисовал тогда) на CPU.


      1. KanuTaH
        23.12.2019 17:55

        1. gshep Автор
          23.12.2019 18:12

          сейчас прочитал и вспомнил — из-за перечисленных там ограничений и не взяли.


          в статье я среди плюсов упоминаю, что кривые можно мешать с контролами. Допустим нужен TextInput в слое определённого графика, но как ребёнок Scene — с этим велосипедом это возможно. Если соберёте самый первый черновой сэмпл graph — там на сцене как раз такая ситуация.


          Добавлю также, что аналогичная ситуация справедлива и при отрисовке через FBO — мы могли каждый трек рисовать в FBO, потом сдёргивать текстуру и рисовать её в нужном прямоугольнике. В этом случае также встраивать контролы нельзя — только поверх (думаю под они не особо пригодятся).


          Сразу ограничивать себя мы не стали.


    1. Deranged
      24.12.2019 13:56
      +1

      QtCharts не поддерживает виртуализацию данных. В сериях просто хранятся вектора семплов. И от этого на практике толку от него мало. Потому что обычно в источнике мноооооого семплов и что бы график работал быстро, надо узнать видимые диапазоны времени / значений по шкалам, выбрать соответствующие семплы и произвести их мат. обработку на стороне источника — понизить sample rate, схлопнуть накладывающиеся участки, добавить сглаживающий пост-фильтр и т.д. Еще хорошо бы иметь кешированные потоки семплов под разный LOD (Level Of Detail) с предфильтрацией. А уже потом можно быстренько нарисовать. В общем система получается навороченной. И в архитектуру без поддержки виртуализации вписывается очень плохо. Можно закостылить перехватом изменений положения вьюпорта и запускать асинхронную подготовку данных серий, где-то рисовать что, мол, грузим, подождите и т.д. Все равно придется постоянно спамить передачей векторов семплов в график. В общем решение несерьезное. У нас (команды) тоже были размышления на тему использования QChart в нашем продукте, но по вышеназванным причинам решили делать свой контрол. О чем не жалеем.


      1. gshep Автор
        24.12.2019 14:35

        для чего контрол написали: Widgets или QML/Quick?


        1. Deranged
          24.12.2019 14:58
          +1

          QML. QQuickPaintedItem. На подложке QPainter'ом рисуются шкалы, а сверху для вывода серий накладываются QImage с кешем отрисованных серий. Сами серии рисуются параллельно в фоновых потоках, перерисовка инициируется при изменении положения вьюпорта. В итоге получается эффект, что при увеличении масштаба на графике на какое-то время появляется «мыло», которое заменяется более четким изображением по мере перерисовки серий. Типа как при увеличении масштаба в картографических приложениях.
          Самое главное, что в график «засовываются» источники «сырых» потоков семплов с простым интерфейсом а ля QFuture read(DateTime start, DateTime end, const std::function<void(Sample*, size_t)>&), а весь хардкор с кешированием, ресемплингом, распараллеливанием и т.д. уже реализован в графике. А самих источников обычно много разных видов. Архив такой, архив сякой, адаптер для внешних систем и т.д.
          Знаю, что QQuickPaintedItem — это очень плохо, но рисовать шкалы на SceneGraph API это то еще удовольствие. Так же был тестовый вариант в котором данные серий считались как буфера точек, а выводились через QSGGeometryNode + GL_LINE_STRIP, но от этого варианта пришлось отказаться, т.к. требовался еще отключаемый вывод точек, меток, поддержка стилей пера (точка, тире). На Scene Graph это всё можно, но уж больно много геморроя. А профита не так уж и много, т.к. все равно требуется время CPU на подготовку буферов геометрии, upload на GPU и т.д. Может когда нибудь дойдут руки перевести на него, но на удивление даже при использовании софтверной отрисовки производительность очень хороша, благодаря мат. обработке и параллелизму, а учитывая тенденцию по наращиванию количества ядер CPU…


          1. KanuTaH
            24.12.2019 15:12

            QQuickPaintedItem может быть не так уж плох, если задать ему opengl framebuffer в качестве render target. Но, опять же, это зависит.


  1. werewol
    24.12.2019 13:16
    +1

    QtChart лучше не использовать. Возможно вам захочется между скважинами что то рисовать ( связи, кривые, пропластки), которые надо будет по разному соединять. И тогда будет переписывание практически с нуля.
    З.Ы.: основано на личном опыте =)


    1. gshep Автор
      24.12.2019 13:17

      личный опыт тоже для нефтянки? )