АК
18 декабря 2017 г. 16:24

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

Введение

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

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


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

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

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

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

fpsmonitorbackend.h

  1. class FpsMonitorBackend : public Qt3DCore::QBackendNode
  2. {
  3. public:
  4. FpsMonitorBackend()
  5. : Qt3DCore::QBackendNode(Qt3DCore::QBackendNode::ReadWrite)
  6. , m_rollingMeanFrameCount(5)
  7. {}
  8.  
  9. private:
  10. void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) override
  11. {
  12. // TODO: Implement me!
  13. }
  14.  
  15. int m_rollingMeanFrameCount;
  16. };

Декларация класса очень проста. Мы наследуем 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

  1. class CustomAspect : public Qt3DCore::QAbstractAspect
  2. {
  3. Q_OBJECT
  4. public:
  5. ...
  6. void addFpsMonitor(Qt3DCore::QNodeId id, FpsMonitorBackend *fpsMonitor)
  7. {
  8. m_fpsMonitors.insert(id, fpsMonitor);
  9. }
  10.  
  11. FpsMonitorBackend *fpsMonitor(Qt3DCore::QNodeId id)
  12. {
  13. return m_fpsMonitors.value(id, nullptr);
  14. }
  15.  
  16. FpsMonitorBackend *takeFpsMonitor(Qt3DCore::QNodeId id)
  17. {
  18. return m_fpsMonitors.take(id);
  19. }
  20. ...
  21.  
  22. private:
  23. QHash<Qt3DCore::QNodeId, FpsMonitorBackend *> m_fpsMonitors;
  24. };

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

fpsmonitorbackend.h

  1. class FpsMonitorMapper : public Qt3DCore::QBackendNodeMapper
  2. {
  3. public:
  4. explicit FpsMonitorMapper(CustomAspect *aspect);
  5.  
  6. Qt3DCore::QBackendNode *create(const Qt3DCore::QNodeCreatedChangeBasePtr &change) const override
  7. {
  8. auto fpsMonitor = new FpsMonitorBackend;
  9. m_aspect->addFpsMonitor(change->subjectId(), fpsMonitor);
  10. return fpsMonitor;
  11. }
  12.  
  13. Qt3DCore::QBackendNode *get(Qt3DCore::QNodeId id) const override
  14. {
  15. return m_aspect->fpsMonitor(id);
  16. }
  17.  
  18. void destroy(Qt3DCore::QNodeId id) const override
  19. {
  20. auto fpsMonitor = m_aspect->takeFpsMonitor(id);
  21. delete fpsMonitor;
  22. }
  23.  
  24. private:
  25. CustomAspect *m_aspect;
  26. };

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

customaspect.cpp

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

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

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

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

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

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

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

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

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

fpsmonitor.h

  1. struct FpsMonitorData
  2. {
  3. int rollingMeanFrameCount;
  4. };

fpsmonitor.cpp

  1. Qt3DCore::QNodeCreatedChangeBasePtr FpsMonitor::createNodeCreationChange() const
  2. {
  3. auto creationChange = Qt3DCore::QNodeCreatedChangePtr<FpsMonitorData>::create(this);
  4. auto &data = creationChange->data;
  5. data.rollingMeanFrameCount = m_rollingMeanFrameCount;
  6. return creationChange;
  7. }

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

fpsmonitorbackend.cpp

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

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

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

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

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

fpsmonitorbackend.cpp

  1. void FpsMonitorBackend::sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e)
  2. {
  3. if (e->type() == Qt3DCore::PropertyUpdated) {
  4. const auto change = qSharedPointerCast<Qt3DCore::QPropertyUpdatedChange>(e);
  5. if (change->propertyName() == QByteArrayLiteral("rollingMeanFrameCount")) {
  6. const auto newValue = change->value().toInt();
  7. if (newValue != m_rollingMeanFrameCount) {
  8. m_rollingMeanFrameCount = newValue;
  9. // TODO: Update fps calculations
  10. }
  11. return;
  12. }
  13. }
  14. QBackendNode::sceneChangeEvent(e);
  15. }

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

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

Заключение

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

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

Рекомендуемые статьи по этой тематике

По статье задано1вопрос(ов)

2

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь