Написание пользовательского Qt 3D аспекта - часть 2

Qt 3D, OpenGL, C++, Qt

Введение

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

В качестве напоминания о том, что мы имеем в виду, вот диаграмма архитектуры из части 1:

Создание бэкэнда

Одна из приятных вещей в Qt 3D заключается в том, что она способна к очень высокой пропускной способности. Это достигается за счет использования заданий, выполняемых в пуле потоков в бэкэнд. Чтобы иметь возможность сделать это без введения запутанной сети точек синхронизации (которая ограничивала бы параллелизм), мы воссоздаем классическую компьютерную ситуацию о компромиссе и жертве памяти в интересах скорости. Благодаря тому, что каждый аспект работает над собственной копией данных, он может планировать задания в безопасности, зная, что ничто другое не затронет его данные.

Это не так дорого, как это звучит. Бэкэнд-узлы не являются производными от QObject . Базовым классом для бэкэнд-узлов является Qt3DCore::QBackendNode , который является довольно легковесным классом. Кроме того, обратите внимание, что аспекты хранят только те данные, которые им нужны в бэкэнде. Например, аспект анимации не заботится о том, какой компонент Material имеет Entity , поэтому нет необходимости хранить какие-либо данные этого компонента. Напротив, аспект рендеринга не касается анимационных клипов или компонентов Animator .

В нашем маленьком пользовательском аспекте у нас есть только один тип фронтэнд компонента, FpsMonitor . Логически, у нас будет только один соответствующий бэкэнд тип, который мы образно назовем FpsMonitorBacken :

fpsmonitorbackend.h

class FpsMonitorBackend : public Qt3DCore::QBackendNode
{
public:
    FpsMonitorBackend()
       : Qt3DCore::QBackendNode(Qt3DCore::QBackendNode::ReadWrite)
       , m_rollingMeanFrameCount(5)
    {}
 
private:
    void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) override
    {
        // TODO: Implement me!
    }
 
    int m_rollingMeanFrameCount;
};

Декларация класса очень проста. Мы наследуем Qt3DCore::QBackendNode , как и следовало ожидать; добавляем элемент данных, чтобы отразить информацию из фронтэнд компонента FpsMonitor ; и переопределяем виртуальную функцию initializeFromPeer() . Эта функция будет вызвана сразу после того, как Qt 3D создаст экземпляр нашего бэкэнд-типа. Аргумент позволяет нам получить данные, отправленные из соответствующего фронтэнд объекта, как мы вскоре увидим.

Регистрация типов

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

Чтобы создать сопоставителя узлов, наследуемся от Qt3DCore::QNodeMapper и переопределяем виртуальные функции для создания, поиска и уничтожения бэкэнд-объектов по требованию. Способ создания, хранения, поиска и уничтожения объектов полностью зависит от вас как разработчика. Qt 3D не навязывает вам какую-либо конкретную схему управления. Аспект рендеринга делает некоторые довольно причудливые вещи с управляемыми менеджерами памяти и выравниванием памяти для SIMD-типов, но здесь мы можем сделать что-то гораздо более простое.

Мы будем хранить указатели на бэкэнд-узлы в QHash внутри CustomAspect и индексировать их с помощью Qt3DCore::QNodeId узла. Идентификатор узла используется для однозначной идентификации данного узла, даже между фронтэнд и всеми доступными бэкэндами. В Qt3DCore::QNode идентификатор доступен через функцию id() , тогда как для QBackendNode вы получаете доступ к нему через функцию peerId() . Для двух соответствующих объектов, представляющих компонент, функции id() и peerId() возвращают одно и то же значение QNodeId .

Давайте перейдем к нему и добавим некоторое хранилище для бэкэнд-узлов в CustomAspect вместе с некоторыми вспомогательными функциями:

customaspect.h

class CustomAspect : public Qt3DCore::QAbstractAspect
{
    Q_OBJECT
public:
    ...
    void addFpsMonitor(Qt3DCore::QNodeId id, FpsMonitorBackend *fpsMonitor)
    {
        m_fpsMonitors.insert(id, fpsMonitor);
    }
 
    FpsMonitorBackend *fpsMonitor(Qt3DCore::QNodeId id)
    {
        return m_fpsMonitors.value(id, nullptr);
    }
 
    FpsMonitorBackend *takeFpsMonitor(Qt3DCore::QNodeId id)
    {
        return m_fpsMonitors.take(id);
    }
    ...
 
private:
    QHash<Qt3DCore::QNodeId, FpsMonitorBackend *> m_fpsMonitors;
};

Теперь мы можем реализовать простой сопоставитель узлов как:

fpsmonitorbackend.h

class FpsMonitorMapper : public Qt3DCore::QBackendNodeMapper
{
public:
    explicit FpsMonitorMapper(CustomAspect *aspect);
 
    Qt3DCore::QBackendNode *create(const Qt3DCore::QNodeCreatedChangeBasePtr &change) const override
    {
        auto fpsMonitor = new FpsMonitorBackend;
        m_aspect->addFpsMonitor(change->subjectId(), fpsMonitor);
        return fpsMonitor;
    }
 
    Qt3DCore::QBackendNode *get(Qt3DCore::QNodeId id) const override
    {
        return m_aspect->fpsMonitor(id);
    }
     
    void destroy(Qt3DCore::QNodeId id) const override
    {
        auto fpsMonitor = m_aspect->takeFpsMonitor(id);
        delete fpsMonitor;
    }
 
private:
    CustomAspect *m_aspect;
};

Чтобы закончить этот кусочек головоломки, нам нужно рассказать о том, как эти типы и сопоставитель связаны друг с другом. Мы делаем это, вызывая функцию шаблона QAbstractAspect::registerBackendType() , передавая общий указатель сопоставителю, который будет создавать, находить и уничтожать соответствующие бэкэнд-узлы. Аргумент шаблона - это тип фронтэнд-узла, для которого должен вызываться этот сопоставитель. Удобное место для этого - в конструкторе CustomAspect. В нашем случае это выглядит так:

customaspect.cpp

CustomAspect::CustomAspect(QObject *parent)
    : Qt3DCore::QAbstractAspect(parent)
{
    // Register the mapper to handle creation, lookup, and destruction of backend nodes
    auto mapper = QSharedPointer<FpsMonitorMapper>::create(this);
    registerBackendType<FpsMonitor>(mapper);
}

Вот и все! При такой регистрации на месте, когда компонент FpsMonitor добавляется в дерево фронтэнд-объектов (сцена), аспект будет искать сопоставителя узлов для этого типа объекта. Здесь он найдет наш зарегистрированный объект FpsMonitorMapper и вызовет его функцию create() для создания бэкэнд-узла и управления его хранения. Аналогичная история и с уничтожением (технически, это удаление со сцены) фронтэнд-узла. Функция get() сопоставителя используется внутри, чтобы иметь возможность вызывать виртуальные функции бэкэнд-узла в соответствующие моменты времени (например, когда свойства сообщают, что они были изменены).

Фронтэнд-бэкэнд коммуникация

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

  1. Инициализация - Когда наш бэкэнд-узел создается, мы получаем возможность инициализировать его данными, отправленными из фронтэнд-узла.
  2. От фронтэнд к бэкэнд - Обычно, когда свойства на фронтэнд-узле меняются, мы хотим отправить новое значение свойства бэкэнд-узлу, чтобы он работал с актуальной информацией.
  3. От бэкэнд к фронтэнд - Когда наши задания обрабатывают данные, хранящиеся в бэкэнд-узлах, может случится ситуация, что это приведет к обновленным значениям, которые должны быть отправлены во фронтэнд-узел.

Здесь мы рассмотрим первые два случая. Третий случай будет отложен до следующей статьи, когда мы представим задания.

Инициализация бэкэнд узла

Вся связь между фронтэнд и бэкэнд объектами выполняется путем отправки подкласса Qt3DCore::QSceneChanges . Они аналогичны по своей природе и концепции для QEvent , но арбитр изменений, который обрабатывает изменения, имеет возможность манипулировать ими в случае конфликтов из нескольких аспектов, переориентировать их на приоритет или любые другие манипуляции, которые могут понадобиться в будущем.

Для инициализации бэкэнд-узла при создании мы используем Qt3DCore::QNodeCreatedChange , который является шаблоном, который мы можем использовать для обертывания данных определенного типа. Когда Qt 3D хочет уведомить бэкэнд о начальном состоянии вашего фронтэнд узла, он вызывает частную виртуальную функцию QNode::createNodeCreationChange() . Эта функция возвращает созданное узлом изменение, содержащее любую информацию, к которой мы хотим получить доступ в бэкэнд-узле. Мы должны сделать это, скопировав данные, а не просто разыменовав указатель на фронтэнд объект, потому что к моменту, когда бэкэнд обработает запрос, фронтэнд объект может быть удален - то есть классическая гонка данных. Для нашего простого компонента наша реализация выглядит так:

fpsmonitor.h

struct FpsMonitorData
{
    int rollingMeanFrameCount;
};

fpsmonitor.cpp

Qt3DCore::QNodeCreatedChangeBasePtr FpsMonitor::createNodeCreationChange() const
{
    auto creationChange = Qt3DCore::QNodeCreatedChangePtr<FpsMonitorData>::create(this);
    auto &data = creationChange->data;
    data.rollingMeanFrameCount = m_rollingMeanFrameCount;
    return creationChange;
}

Изменение, созданное нашим фронтэнд-узлом, передается на бэкэнд-узел (через арбитр изменений) и обрабатывается виртуальной функцией initializeFromPeer() :

fpsmonitorbackend.cpp

void FpsMonitorBackend::initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change)
{
    const auto typedChange = qSharedPointerCast<Qt3DCore::QNodeCreatedChange<FpsMonitorData>>(change);
    const auto &data = typedChange->data;
    m_rollingMeanFrameCount = data.rollingMeanFrameCount;
}

Коммуникация фронтэнд с бэкэнд

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

Хорошей новостью является то, что с этим легко справиться. Реализация Qt3DCore::QNode заботится о первой половине проблемы за нас. Внутренне он прослушивает сигналы уведомления Q_PROPERTY, и когда он видит, что свойство изменилось, он создает для нас QPropertyUpdatedChange и отправляет его в арбитр изменений, который, в свою очередь, доставляет его функции sceneChangeEvent() в бэкэнд-узле.

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

fpsmonitorbackend.cpp

void FpsMonitorBackend::sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e)
{
    if (e->type() == Qt3DCore::PropertyUpdated) {
        const auto change = qSharedPointerCast<Qt3DCore::QPropertyUpdatedChange>(e);
        if (change->propertyName() == QByteArrayLiteral("rollingMeanFrameCount")) {
            const auto newValue = change->value().toInt();
            if (newValue != m_rollingMeanFrameCount) {
                m_rollingMeanFrameCount = newValue;
                // TODO: Update fps calculations
            }
            return;
        }
    }
    QBackendNode::sceneChangeEvent(e);
}

Если вы не хотите использовать встроенную автоматическую отправку изменений свойств Qt3DCore::QNode , вы можете отключить ее, обернув излучение сигнала уведомления свойства при вызове QNode::blockNotifications() . Это работает точно так же, как QObject::blockSignals() , за исключением того, что он блокирует отправку уведомлений только на базовый узел, а не сам сигнал. Это означает, что другие соединения или привязки свойств, которые полагаются на ваши сигналы, будут по-прежнему работать.

Если вы блокируете уведомления по умолчанию таким образом, тогда вам необходимо отправить их, чтобы убедиться, что базовый узел имеет обновленную информацию. Не стесняйтесь наследовать любой класс в иерархии Qt3DCore::QSceneChange и подогнать его в соответствии с вашими потребностями. Общий подход заключается в наследовании Qt3DCore::QStaticPropertyUpdatedChangeBase , который обрабатывает свойство имя и в подклассе добавляет строго типизированный член класса для свойства значение полезной нагрузки. Преимущество этого перед встроенным механизмом состоит в том, что он избегает использования QVariant , который немного страдает от высокопоточных контекстов с точки зрения производительности. Обычно, свойства фронтэнд не изменяются слишком часто, и по умолчанию это нормально.

Заключение

В этой статье мы показали, как реализовать большую часть бэкэнд-узла; как регистрировать сопоставитель узлов с целью создания, поиска и уничтожения бэкэнд-узлов; как безопасно инициализировать бэкэнд-узел из фронтэнд-узла, а также как синхронизировать его данные с фронтэнд.
В следующей статье мы, наконец, сделаем наш пользовательский аспект, действительно выполним какую-то реальную работу и узнаем, как заставить бэкэнд-узел отправлять обновления на фронтэнд-узел (среднее значение fps). Мы обеспечим выполнение тяжелых частей в контексте пула потоков Qt 3D, чтобы вы поняли, как он может масштабироваться. До скорого.

Статья написана: Sean Harmer | Среда, Декабрь 13, 2017г.

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
Looking for a Job?
14,000.00 руб. - 40,000.00 руб.
Разработчик Qt
Annino, Moscow Oblast, Russia
5,000.00 руб. - 15,000.00 руб.
Дизайнер
Moskovskiy, Moscow, Russia
25,000.00 руб. - 30,000.00 руб.
Разработчик Qt/C++
Barnaul, Altai Krai, Russia

For registered users on the site there is a minimum amount of advertising

A
Aug. 22, 2019, 11:24 p.m.
Aleksandr73

Qt - Test 001. Signals and slots

  • Result:47points,
  • Rating points-6
Aug. 21, 2019, 10:23 a.m.
Andrej Ermoshin

C++ - Test 002. Constants

  • Result:58points,
  • Rating points-2
Aug. 21, 2019, 10:15 a.m.
Andrej Ermoshin

C++ - Test 001. The first program and data types

  • Result:86points,
  • Rating points6
Last comments
Aug. 19, 2019, 7:41 a.m.
Andrej Jankovich

это проблема дистрибутива, попробуйте установить через пакетный менеджер snap Суть проблемы: libQt5Core которая лежит в дистрибутиве требует версию glibc >= 2.25 у вас видимо …
b
Aug. 18, 2019, 6:09 a.m.
bbb116

cqtdeployer /home/aleks/CQtDeployer/bin/cqtdeployer: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found (required by /home/aleks/CQtDeployer/lib/libQt5Core.so.5) linux mint …
D
Aug. 17, 2019, 9:04 a.m.
Damir

github ChekableTView Правой групповая смена значения при перетаскивании левой как обычно.
Aug. 16, 2019, 1:03 p.m.
Evgenij Legotskoj

Потому, что в минуте 60 секунд
Aug. 16, 2019, 12:16 p.m.
Dmitrij

а почему делитель 60000, а не 1000?
Now discuss on the forum
Aug. 24, 2019, 7:21 a.m.
Evgenij Legotskoj

Не помню, давно уже с QML не работал, по-моему, обычно пишет в консоль, что не находит файл. В любом случае какую-то ошибку в консоль выкидывает. Но если честно, если у вас проект будет ак…
BG
Aug. 24, 2019, 4:27 a.m.
Brjus Gliff

Спасибо, вначале в документации было не понятно что к чему, теперь разобрался
I
Aug. 21, 2019, 8:36 a.m.
Intruder

Александр, мне не нужно перебирать. Вы говорите правильно, сначала я написал избыточный код просто не подумав. Задача такая, мне нужно просто переложить из QMap в атрибуты xml тега все, что там …
Aug. 21, 2019, 3:16 a.m.
nayk1982

Если Вы разрабатываете какую-то универсальную утилиту, которая вообще не привязана к логике, тогда как вариант: 1. Получить список таблиц через QSqlDatabase::tables 2. Для каждой табли…
EVILEG
About
Services
© EVILEG 2015-2019
Recommend hosting TIMEWEB