АК
Александр КузьминыхЖел. 18, 2017, 5:24 Т.Ж.

Теңшелетін Qt 3D аспектісін жазу - 2 бөлім

Введение

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

В качестве напоминания о том, что мы имеем в виду, вот диаграмма архитектуры из части 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г.

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
OI
  • Ora Iro
  • Жел. 24, 2024, 6:38 Т.Ж.

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

  • Нәтиже:40ұпай,
  • Бағалау ұпайлары-8
AD

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

  • Нәтиже:50ұпай,
  • Бағалау ұпайлары-4
m
  • molni99
  • Қаз. 26, 2024, 1:37 Т.Ж.

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

  • Нәтиже:80ұпай,
  • Бағалау ұпайлары4
Соңғы пікірлер
ИМ
Игорь МаксимовҚар. 22, 2024, 11:51 Т.Ж.
Django - Оқулық 017. Теңшелген Django кіру беті Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiҚаз. 31, 2024, 2:37 Т.Қ.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEҚаз. 19, 2024, 8:19 Т.Ж.
Qt Creator көмегімен fb3 файл оқу құралы Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовҚаз. 5, 2024, 7:51 Т.Ж.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Қар. 15, 2024, 6:04 Т.Ж.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectМаусым 4, 2022, 3:49 Т.Ж.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimҚаз. 25, 2024, 9:10 Т.Ж.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Бізді әлеуметтік желілерде бақылаңыз