Writing a custom Qt 3D aspect - part 2

Introduction

In the previous article we reviewed the custom aspect creation process and showed how to create (most of) the front-end functionality. In this article, we will continue to build our user facet by implementing the appropriate backend types, registering the types, and setting up the relationship between frontend objects and backend objects. This will take up most of this article. In the next article, we'll look at how to implement jobs to process our aspect's components.

As a reminder of what we mean, here is the architecture diagram from part 1:


Create a backend

One of the nice things about Qt 3D is that it is capable of very high throughput. This is achieved through the use of jobs running on a thread pool in the backend. To be able to do this without introducing an intricate network of synchronization points (which would limit concurrency), we recreate the classic computer situation of trade-offs and sacrificing memory in the interest of speed. By having each aspect work on its own copy of the data, it can schedule jobs safely, knowing that nothing else will touch its data.

It's not as expensive as it sounds. Backend nodes are not derived from QObject . The base class for backend nodes is Qt3DCore::QBackendNode which is a fairly lightweight class . Also, note that aspects only store the data they need in the backend. For example, the animation aspect doesn't care which Material component has Entity , so there is no need to store any of that component's data. In contrast, the rendering aspect does not concern animation clips or Animator components.

In our little custom aspect, we only have one type of frontend component, FpsMonitor . Logically, we will only have one corresponding backend type, which we will figuratively call 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;
};

The class declaration is very simple. We inherit from Qt3DCore::QBackendNode as you would expect; add a data element to reflect information from the front-end component FpsMonitor ; and override the virtual function initializeFromPeer() . This function will be called right after Qt 3D instantiates our backend type. The argument allows us to get the data sent from the corresponding frontend object, as we'll see shortly.

Registration of types

We now have simple frontend and backend component implementations. The next step is to register them with the aspect so that it knows how to instantiate the backend node whenever a frontend node is created. Likewise for destruction. We do this with an intermediate helper known as a node matcher.

To create a node mapper, we inherit from Qt3DCore::QNodeMapper and override virtual functions to create, find and destroy backend objects on demand. How you create, store, find, and destroy objects is entirely up to you as a developer. Qt 3D does not force any particular control scheme on you. The rendering aspect does some pretty fancy stuff with managed memory managers and memory alignment for SIMD types, but we can do something much simpler here.

We will store pointers to backend nodes in QHash inside CustomAspect and index them using the Qt3DCore::QNodeId node. The node ID is used to uniquely identify a given node, even between the frontend and all available backends. In Qt3DCore::QNode the identifier is available via the id() function, while for QBackendNode you access it via the peerId() function. For two corresponding objects representing a component, the id() and peerId() functions return the same QNodeId value.

Let's go ahead and add some storage for the backend nodes in CustomAspect along with some helper functions:

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;
};

We can now implement a simple node matcher as:

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;
};

To complete this piece of the puzzle, we need to talk about how these types and matcher relate to each other. We do this by calling the template function QAbstractAspect::registerBackendType() , passing a shared pointer to the matcher that will create, find, and destroy the corresponding backend nodes. The template argument is the type of the frontend node on which this matcher is to be called. A convenient place to do this is in the CustomAspect constructor. In our case, it looks like this:

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);
}

That's all! With this in-place registration, when the FpsMonitor bean is added to the frontend object tree (scene), the aspect will look for a node mapper for that object type. Here it will find our registered FpsMonitorMapper object and call its create() function to create the backend node and manage its storage. The story is similar with the destruction (technically, this is the removal from the scene) of the front-end node. The resolver's get() function is used internally to be able to call the backend node's virtual functions at appropriate times (for example, when properties report that they have been changed).

Front-back communication

Now that we can create, access, and destroy the backend node of any frontend node, let's see how we can let them talk to each other. There are three main points when front-end and back-end nodes communicate with each other:

  1. Initialization - When our backend node is created, we get a chance to initialize it with the data sent from the frontend node.
  2. From Frontend to Backend - Usually, when properties on the frontend node change, we want to send the new value of the property to the backend node so that it works with up-to-date information.
  3. From backend to frontend - When our jobs process data stored in backend nodes, it can happen that this results in updated values that need to be sent to the frontend node.

Here we consider the first two cases. The third case will be postponed until the next article, when we present the assignments.

Initialize the backend node

All communication between frontend and backend objects is done by sending a subclass of Qt3DCore::QSceneChanges . They are similar in nature and concept to QEvent , but the change arbiter that handles the changes has the ability to manipulate them in case of conflicts from multiple aspects, refocus them on priority, or any other manipulation that may be needed in the future.

To initialize the backend node on creation, we use Qt3DCore::QNodeCreatedChange , which is a template that we can use to wrap data of a specific type. When Qt 3D wants to notify the backend of the initial state of your frontend node, it calls the private virtual function QNode::createNodeCreationChange() . This function returns a node-created change containing whatever information we want to access in the backend node. We have to do this by copying the data rather than simply dereference the pointer to the frontend object, because by the time the backend processes the request, the frontend object may have been deleted - a classic data race. For our simple component, our implementation looks like this:

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;
}

The change created by our frontend node is passed to the backend node (via the change arbiter) and processed by the initializeFromPeer() virtual function:

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;
}

Communication frontend with backend

At this point, the backend node reflects the initial state of the frontend node. But what if the user changes a property in the frontend node? When this happens, our backend node will store stale data.

The good news is that this is easy to deal with. The Qt3DCore::QNode implementation takes care of the first half of the problem for us. Internally, it listens for Q_PROPERTY notification signals, and when it sees that a property has changed, it creates a [QPropertyUpdatedChange] for us (http://code.qt.io/cgit/qt/qt3d.git/tree/src/core/changes/ qpropertyupdatedchange.h#n51) and sends it to the change arbiter, which in turn delivers it to the sceneChangeEvent() function in the backend node.

So all we have to do as authors of the backend node is override this function, retrieve the data from the change object, and update our internal state. You will often want to tag the backend node in some way so that the aspect knows that it needs to be processed in the next frame. Here, however, we'll simply update the state to display the latest value from the frontend:

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);
}

If you don't want to use Qt3DCore::QNode 's built-in automatic property change dispatch, you can disable it by wrapping emitting a property notification signal when [QNode::blockNotifications()] is called (https://doc.qt.io/ qt-5/qt3dcore-qnode.html#blockNotifications) . This works exactly like QObject::blockSignals() , except that it only blocks notifications from being sent to the underlying node, not the signal itself. This means that other connections or property bindings that rely on your signals will still work.

If you block notifications by default in this way, then you need to send them to make sure the underlying node has updated information. Feel free to inherit from any class in the Qt3DCore::QSceneChange hierarchy and customize it to suit your needs. The general approach is to inherit Qt3DCore::QStaticPropertyUpdatedChangeBase , which handles the property name and in a subclass adds a strongly typed class member to the payload value property. The advantage of this over the built-in mechanism is that it avoids the use of QVariant , which suffers a bit from highly threaded contexts in terms of performance. Usually, frontend properties don't change too often, and that's fine by default.

Conclusion

In this article, we have shown how to implement most of the backend node; how to register a node mapper to create, find, and destroy backend nodes; how to securely initialize a backend node from a frontend node, and how to synchronize its data with the frontend.
In the next article, we'll finally do our user side, actually do some real work, and learn how to get the backend node to send updates to the frontend node (average fps). We will ensure that the heavy parts are executed in the context of the Qt 3D thread pool so you understand how it can scale. See you later.

Article written by: Sean Harmer | Wednesday, December 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.

Do you like it? Share on social networks!

Comments

Only authorized users can post comments.
Please, Log in or Sign up
SH

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

  • Result:33points,
  • Rating points-10
г
  • ги
  • April 23, 2024, 3:51 p.m.

C++ - Test 005. Structures and Classes

  • Result:41points,
  • Rating points-8
l
  • laei
  • April 23, 2024, 9:19 a.m.

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:10points,
  • Rating points-10
Last comments
k
kmssrFeb. 8, 2024, 6:43 p.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lesson 007. Working with ICMP Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVADec. 25, 2023, 10:30 a.m.
Boost - static linking in CMake project under Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJoDec. 25, 2023, 8:38 a.m.
Boost - static linking in CMake project under Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
GvozdikDec. 18, 2023, 9:01 p.m.
Qt/C++ - Lesson 056. Connecting the Boost library in Qt for MinGW and MSVC compilers Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Now discuss on the forum
G
GarApril 22, 2024, 5:46 a.m.
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil AcademicsApril 20, 2024, 7:45 a.m.
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasovApril 14, 2024, 6:41 a.m.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел ДорофеевApril 14, 2024, 2:35 a.m.
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrexApril 4, 2024, 4:47 a.m.
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Follow us in social networks