АК
Александр КузьминыхЖел. 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 хостинг.

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

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
Г

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

  • Нәтиже:66ұпай,
  • Бағалау ұпайлары-1
t

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

  • Нәтиже:33ұпай,
  • Бағалау ұпайлары-10
t

Qt - Тест 001. Сигналы и слоты

  • Нәтиже:52ұпай,
  • Бағалау ұпайлары-4
Соңғы пікірлер
G
GoattRockҚыр. 3, 2024, 1:50 Т.Қ.
Linux жүйесінде файлдарды қалай көшіруге болады Задумывались когда-нибудь о том, как мы привыкли доверять свои вещи службам грузоперевозок? Сейчас такие услуги стали неотъемлемой частью нашей жизни, особенно когда речь идет о переездах между …
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssrАқп. 8, 2024, 6:43 Т.Қ.
Qt Linux - Сабақ 001. Linux астында Autorun Qt қолданбасы как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий КононенкоАқп. 5, 2024, 1:50 Т.Ж.
Qt WinAPI - Сабақ 007. Qt ішінде ICMP Ping арқылы жұмыс істеу Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
F
FynjyШілде 22, 2024, 4:15 Т.Ж.
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …
BlinCT
BlinCTМаусым 25, 2024, 1 Т.Ж.
Нарисовать кривую в qml Всем привет. Имеется Лист листов с тосками, точки получаны интерполяцией Лагранжа. Вопрос, как этими точками нарисовать кривую? ChartView отпадает сразу, в qt6.7 появился новый элемент…
BlinCT
BlinCTМамыр 5, 2024, 5:46 Т.Ж.
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
Evgenii Legotckoi
Evgenii LegotckoiМамыр 2, 2024, 2:07 Т.Қ.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.

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