Skip to content

Writing a Custom Qt 3D Aspect – part 1 Extending Qt 3D via Aspects

Introduction

Qt 3D has a flexible and extensible architecture that allows us to easily add our own new functionality to it without disrupting the existing features. The functionality of Qt 3D is divided among so-called aspects, each of which encapsulates a particular subject domain such as rendering, input, or animation.

This short series of articles will walk you through the process of adding a new aspect that provides component types and behaviour for a new domain not covered by Qt 3D out of the box. For this example we have chosen to implement an aspect that allows calculating running means of the frame rate. Of course this could legitimately be added to the renderer, but it’s simple enough that it makes a nice example for our purposes today. The full source code for the example is available for download.

Overview

The application that we will build looks like this. It’s a very simple Qt 3D scene and the window shows the current mean frame rate.

Custom Aspect Screenshot

There are a few parts that we need to consider when adding such new functionality (see the diagram):

  • Aspect — Responsible for orchestrating any jobs that need executing each frame and for managing the storage of any backend objects.
  • Components — Provided by your library/application are what an Entity will aggregate to give it new behaviour. The components are the main API you will need to create.
  • Nodes — Just as for components except that subclasses of QNode typically provide supporting data required by a component. For example QAnimationClipLoader is a node that provides animation key frame data to a QClipAnimator component.
  • Backend Nodes — The backend counterparts to any frontend components or nodes provided by your library/application. These are the objects typically processed by jobs running on the threadpool as dictated by the aspect itself.
  • Mapper — Custom mappers are registered with the aspect and are responsible for creating, fetching, and destroying backend nodes on demand. The mapper is used by QAbstractAspect and QAspectEngine to synchronise lifetimes of the frontend and backend objects.
  • Jobs — Created and scheduled by the aspect and which process the backend nodes. Jobs may also send events to the frontend nodes and components if properties change.
  • Change Arbiter — Responsible for delivering events between and among the frontend and backend objects. No need to do anything with this but be aware of its existence.

Adding the Aspect

Writing the initial aspect itself is really trivial. Just subclass QAbstractAspect and register it with the QAspectEngine:

class CustomAspect : public Qt3DCore::QAbstractAspect
{
    Q_OBJECT
public:
    explicit CustomAspect(QObject *parent = nullptr)
        : Qt3DCore::QAbstractAspect(parent)
    {}

protected:
    QVector<Qt3DCore::QAspectJobPtr> jobsToExecute(qint64 time) override
    {
        qDebug() << Q_FUNC_INFO << "Frame time =" << time;
        return {};
    }
};
int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);
    Qt3DExtras::Quick::Qt3DQuickWindow view;

    view.engine()->aspectEngine()->registerAspect(new CustomAspect);

    view.setSource(QUrl("qrc:/main.qml"));
    view.show();
    return app.exec();
}

QAbstractAspect has a few more virtuals that you can override to do initialisation and cleanup should you need them. However, for this simple example all we need to do is to implement the jobsToExecute() virtual. Later we will use this to schedule a job to execute on the threadpool each frame, but for now we just output some debug text to return an empty vector (no jobs to run). Note that these virtual functions will only be called once you have registered the aspect with the QAspectEngine (see above) so that it is part of the simulation. We will return and complete the aspect a little later.

The FpsMonitor Component

We wish to add functionality to report on the mean frame rate averaged over a given number of frames. With this is mind we might come up with an API something like this:

class FpsMonitor : public Qt3DCore::QComponent
{
    Q_OBJECT
    Q_PROPERTY(int rollingMeanFrameCount READ rollingMeanFrameCount WRITE setRollingMeanFrameCount NOTIFY rollingMeanFrameCountChanged)
    Q_PROPERTY(float framesPerSecond READ framesPerSecond NOTIFY framesPerSecondChanged)

public:
    explicit FpsMonitor(Qt3DCore::QNode *parent = nullptr);

    float framesPerSecond() const;
    int rollingMeanFrameCount() const;

public slots:
    void setRollingMeanFrameCount(int rollingMeanFrameCount);

signals:
    void framesPerSecondChanged(float framesPerSecond);
    void rollingMeanFrameCountChanged(int rollingMeanFrameCount);

private:
    float m_framesPerSecond;
    int m_rollingMeanFrameCount;
};

Note that we use a declarative, property-based API so that this class can easily be used from QML as well as from C++. The property rollingMeanFrameCount is a regular read-write property and the implementation of the setter and getter functions are completely standard. This property will be used to control the number of frames over which we calculate the moving average frame rate. The framesPerSecond property is a read-only property which will later be set from a job that we will write to process FpsMonitor components on the Qt 3D threadpool.

Creating a Small Test Application

Before we can utilise our custom component from QML, we must register the type with the QML type system. This is a simple one-liner that we can add to our main function:

qmlRegisterType<FpsMonitor>("CustomModule", 1, 0, "FpsMonitor");
rootContext->setContextProperty("_window", &view);

where we also took the opportunity to export the window to the QML context too (we’ll need that in a moment).

Once that is done, we can import the CustomModule QML module and make use of the FpsMonitor component just as we would for any of the built in types.

    Entity {
        components: [
            FpsMonitor {
                rollingMeanFrameCount: 20
                onFramesPerSecondChanged: {
                    var fpsString = parseFloat(Math.round(framesPerSecond * 100) / 100).toFixed(2);
                    _window.title = "CustomAspect: FPS = " + fpsString
                            + " (" + rollingMeanFrameCount + " frame average)"
                }
            }
        ]
    }

Of course at this point, the FpsMonitor doesn’t do much except for setting the value of the rollingMeanFrameCount property. Once we complete the backend, the above code will update the window title to show the current mean frame rate and the number of frames used to calculate it.

In the next part we will implement the corresponding backend node for FpsMonitor and make sure it gets created and destroyed on demand and set up communications between the frontend and backend.

is a senior software engineer at KDAB where he heads up our UK office and also leads the 3D R&D team. He has been developing with C++ and Qt since 1998 and is Qt 3D Maintainer and lead developer in the Qt Project. Sean has broad experience and a keen interest in scientific visualization and animation in OpenGL and Qt. He holds a PhD in Astrophysics along with a Masters in Mathematics and Astrophysics.

1 thought on “Writing a Custom Qt 3D Aspect – part 1”

  1. Thanks Sean, this is exactly what I’ve be been waiting for and I look forward to the remaining installments!

Leave a Reply

Your email address will not be published. Required fields are marked *