© EVILEG 2015-2018
Рекомендует хостинг
TIMEWEB

Написание пользовательского 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г.

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
15 июня 2018 г. 12:42
Nicky

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат 100 баллов
  • Очки рейтинга 10
15 июня 2018 г. 12:36
Nicky

C++ - Тест 003. Условия и циклы

  • Результат 57 баллов
  • Очки рейтинга -2
15 июня 2018 г. 12:29
Nicky

C++ - Тест 001. Первая программа и типы данных

  • Результат 46 баллов
  • Очки рейтинга -6
Последние комментарии
18 июня 2018 г. 7:12
EVILEG

PyQt5 - Урок 007. Работаем с QML QtQuick (Сигналы и слоты)

Я вот сейчас банальность скажу, но у меня всё работало. Так что даже и не знаю, надо на код смотреть, что ещё у вас добавлено или отсутствует из библиотек. P/S/ Извините, вы сейчас вс...
18 июня 2018 г. 7:10
EVILEG

Qt/C++ - Урок 042. PopUp уведомление в стиле Gnome с помощью Qt

Недоработки, вряд ли этот зверь вообще является официально поддерживаемым
18 июня 2018 г. 7:01
EVILEG

QML - Урок 016. База данных SQLite и работа с ней в QML Qt

что-то мне сдаётся, что здесь просто пересобрать проект нужно с удалением build каталога
18 июня 2018 г. 7:00
EVILEG

Qt - WinAPI. Как показать запущенное приложение поверх своего приложения

Если зарыться в API системы, то, думаю, что можно, тут тоже использовался WinAPI.
16 июня 2018 г. 15:19
pro100belik

Qt - WinAPI. Как показать запущенное приложение поверх своего приложения

А можно по ID процесса  выводить на передний план окно? myProcess->processId();
Сейчас обсуждают на форуме
19 июня 2018 г. 7:56
EVILEG

как редактировать порядок обхода этементов по нажатию TAB в Qt5 qml

Что-то наподобие такого TextField { Keys.onReturnPressed: nextItemInFocusChain().forceActiveFocus()}
19 июня 2018 г. 6:31
kabanov

Как сохранить фокус в TextField после перезагрузки модели

Rectangle { ListView { id: listView delegate: Item { id: cDelegate Item { Row { ComboBox { ...
18 июня 2018 г. 10:51
alex_lip

Qml and JavaScript

В том то и дело что просто в JS так нельзя Если использовать state - onReleased - не нужен вот так все работает Text { ...
18 июня 2018 г. 7:16
EVILEG

почему не выполняется код после вызова слота?

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

Рекомендуемые страницы