Skip to content

Qt 3D Changes in Qt 6 to the public API

Overview

Qt 6 is nearly upon us. While this has not been addressed by other publications, Qt 3D is also introducing a number of changes with this major release. This includes changes in the public API that will bring a number of new features and many internal changes to improve performance and leverage new, low-level graphics features introduced in QtBase. I will focus on API changes now, while my colleague, Paul Lemire, will cover other changes in a follow up post.

Distribution of Qt 3D for Qt 6

Before looking at what has changed in the API, the first big change concerns how Qt 3D is distributed. Qt 3D has been one of the core modules that ship with every Qt release. Starting with Qt 6, however, Qt 3D will be distributed as a separate source-only package. Qt 6 will ship with a number of such modules, and will use conan to make it easy for those modules to be built. This means that users interested in using Qt 3D will need to compile once for every relevant platform.

Since it ships independently, Qt 3D will also most likely be on a different release cycle than the main Qt releases. We will be able to release more, frequent minor releases with new features and bug fixes.

Another consequence of this is that Qt 3D will not be bound by the same binary compatibility constraints as the rest of Qt. We do however, aim to preserve source compatibility for the foreseeable future.

Basic Geometry Types

The first API change is minimal, but, unfortunately, it is source-incompatible. You will need to change your code in order to compile against these changes.

In order to make developing new aspects that access geometry data more straight forward, we have moved a number of classes that relate to that from the Qt3DRender aspect to the Qt3DCore aspect. These include QBuffer, QAttribute and QGeometry.

When using the QML API, impact should be minimal, the Buffer element still exists, and importing the Render module implicitly imports the Core module anyway. You may have to change your code if you’ve been using module aliases, though.

In C++, this affects which namespace these classes live in, which is potentially more disruptive. So if you were using Qt3DRender::QBuffer (often required to avoid clash with QBuffer class in QtCore), you would now need to use Qt3DCore::QBuffer, and so on…

If you need to write code that targets both Qt5 and Qt6, one trick you can use to ease the porting is to use namespace aliases, like this:

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <Qt3DCore/QBuffer>
namespace Qt3DGeometry = Qt3DCore;
#else
#include <Qt3DRender/QBuffer>
namespace Qt3DGeometry = Qt3DRender;
#endif

void prepareBuffer(Qt3DGeometry::QBuffer *buffer) {
    ...
}

The main reason this was done is so that all aspects could have access to the complete description of a mesh. Potential collision detection or physics simulation aspects don’t need to have their own representation of a mesh, separate from the one used for rendering.

So QBuffer, QAttribute and QGeometry are now in Qt3DCore. But this is not enough to completely describe a mesh.

Changes in Creating Geometry

A mesh is typically made of a collection of vertices. Each vertex will have several properties (positions, normal, texture coordinates, etc.) associated to it. The data for those properties is stored somewhere in memory. So in order to register a mesh with Qt 3D, you need:

  • A QGeometry instance that is simply a collection of QAttribute instances
  • Each QAttribute instance to define the details of a vertex attribute. For example, for the position, it would include the number of components (usually 3), the type of the component (usually floats), the name of the attribute as it will be exposed to the shaders (usually “position” or QAttribute::defaultNormalAttributeName(), if you are using Qt3D built-in materials), etc.
  • Each QAttribute to also point to a QBuffer instance. This may be the same for all attributes, or it may be different, especially for attribute data that needs to be updated often.

But this is still incomplete. We are missing details such as how many points make up the mesh and what type of primitives these points make up (triangles, strips, lines, etc) and more.

Prior to Qt 6, these details were stored on a Qt3DRender::QGeometryRenderer class. The name is obviously very rendering-related (understatement), so we couldn’t just move that class.

For these reasons, Qt 6 introduces a new class, Qt3DCore::QGeometryView. It includes a pointer to a QGeometry and completely defines a mesh. It just doesn’t render it. This is useful as the core representation of a mesh that can then be used for rendering, bounding volume specifications, picking, and much more.

Bounding Volume Handling

One of the very first things Qt 3D needs to do before rendering is compute the bounding volume of the mesh. This is needed for view frustum culling and picking. Internally, the render aspect builds a bounding volume hierarchy to quickly find objects in space. To compute the bounding volume, it needs to go through all the active vertices of a mesh. Although this is cached, it can take time the first time the object is rendered or any of its details change.

Furthermore, up to now, this was completely internal to Qt 3D’s rendering backend and the results were not available to the user for using in the rest of the application.

So Qt 6 introduces a QBoundingVolume component which serves two purposes:

  • it has implicit minimum point and maximum point properties that contain the result of the bounding volume computations done by the backend. This can be used by the application.
  • it has explicit minimum point and maximum point properties which the user can set. This will prevent the backend from having to calculate the bounds in order to build the bounding volume hierarchy.

The minimum and maximum extents points are the corners of the axis-aligned box that fits around the geometry.

But how does QBoundingVolume know which mesh to work on? Easy — it has a view property which points to a QGeometryView instance!

Reading bounding volume extents

So if you need to query the extents of a mesh, you can use the implicit values:

Entity {
   components: [
       Mesh {
           source: "..."
           onImplicitMinPointChanged: console.log(implicitMinPoint)
       },
       PhongMaterial { diffuse: "green" },
       Transform { ... }
    ]
}

Note that if the backend needs to compute the bounding volume, this is done at the next frame using the thread pool. So the implicit properties might not be immediately available when updating the mesh.

If you need the extents immediately after setting or modifying the mesh, you can call QBoundingVolume::updateImplicitBounds() method, which will do the computations and update the implicit properties.

Setting bounding volume extents

But you know the extents. You can set them explicitly to stop Qt 3D from computing them:

Entity {
   components: [
       Mesh {
           source: "..."
           minPoint: Qt.vector3d(-.5, -.5, -.5)
           maxPoint: Qt.vector3d(.5, .5, .5)
       },
       PhongMaterial { diffuse: "green" },
       Transform { ... }
    ]
}

Note that, since setting the explicit bounds disables the computation of the bounding volume in the backend, the implicit properties will NOT be updated in this case.

Mesh Rendering

Now, before everyone goes and adds QBoundingVolume components to all their entities, one other thing: QGeometryRenderer, in the Qt3DRender module, now derives from QBoundingVolume. So, it will also have all the extents properties.

It also means you can provide it with a geometry view to tell it what to draw, rather than providing a QGeometry and all the other details.

It still, however, has all the old properties that are now taken care of by the QGeometryView instance. If that is defined, all the legacy properties will be ignored. We will deprecate them soon and remove them in Qt 7.

So what happens if you provide both a QBoundingVolume and a QGeometryRenderer component to an entity? In this case, the actual bounding volume component takes precedence over the geometry renderer. If it specifies explicit bounds, those will be used for the entity.

The main use case for that is to specify a simpler geometry for the purpose of bounding volume computation. If you don’t know the extents of a mesh but you know that a simpler mesh (with much fewer vertices) completely wraps the object you want to render, using that simpler mesh can be a good way of speeding up the computations that Qt 3D needs to do.

New Core Aspect

Most of these computations took place previously in the Render aspect. Since this is now in core, and in order to fit in with Qt 3D’s general architecture, we introduce a new Core aspect. This aspect will be started automatically if you are using Scene3D or Qt3DWindow. In cases where you are creating your own aspect engine, it should also automatically be started as long as the render aspect is in use via the new aspect dependency API (see below).

The core aspect will take care of the all the bounding volume updates for the entities that use the new geometry view-based API (legacy scenes using a QGeometryRenderer instances without using views will continue to be updated by the rendering aspect).

The Core aspect also introduces a new QCoreSettings component. Like the QRenderSettings component, a single instance can be created. It is, by convention, attached to the root entity.

Currently, its only purpose is to be able to completely disable bounding volume updating. If you are not using picking and have disabled view frustum culling, bounding volumes are actually of no use to Qt 3D. You can disable all the jobs which are related to bounding volume updates by setting QCoreSettings::boundingVolumesEnabled to false. Note that implicit extent vertices on QBoundingVolume component will then not be updated.

New Aspect and AspectJob API

 

The base class for aspects, QAbstractAspect, has gained a few useful virtual methods:

  • QAbstractAspect::dependencies() should return the list of aspect names that should be started automatically if an instance of this aspect is registered.
  • QAbstractAspect::jobsDone() is called on the main thread when all the jobs that the aspect has scheduled for a given frame have completed. Each aspect has the opportunity to take the results of the jobs and act upon them. It is called every frame.
  • QAbstractAspect::frameDone() is called when all the aspects have completed the jobs AND the job post processing. In the case of the render aspect, this is when rendering actually starts for that frame.

 

Similarly, jobs have gained a number of virtual methods on QAspectJob:

  • QAspectJob::isRequired() is called before a job is submitted. When building jobs, aspects will build graphs of jobs with various dependencies. It’s often easier to build the same graph every frame, but not all jobs might have something to do on a given frame. For example, the picking job has nothing to do if there are no object pickers or the mouse has not moved. The run method can test for this and return early, but this still causes the job to be scheduled onto a thread in the pool, with all the associated, sometime expensive locking. If QAspectJob::isRequired() returns false, the job will not be submitted to the thread pool and processing will continue with its dependent jobs.
  • QAspectJob::postFrame() is called on the main thread once all the jobs are completed. This is place where most jobs can safely update the foreground classes with the results of the backend computations (such as bounding volume sizes, picking hits, etc).

Picking Optimization

We have introduced optimization for picking. A QPickingProxy component has been introduced, deriving from QBoundingVolume. If it has an associated geometry view, that mesh will be used instead of the rendered mesh for picking tests. This applies to QObjectPicker and the QRayCaster and QScreenRayCaster classes. Since precise picking (when QPickingSettings is not set to use bounding volume picking) needs to look at every primitive (triangle, line, vertex), it can be very slow. Using QPickingProxy makes it possible to provide a much simpler mesh for the purpose of picking.

Entity {
    components: [
        GeometryRenderer { view: actualView },
        PickingProxy { view: simpleView }
        ...
]
...

So for example, you can provide a down sampled mesh, such as the bunny on the right (which only includes 5% of the total of primitives in the original mesh), to get fast picking results.

Of course, the picking results (the local coordinate, the index of the picked primitive, etc) will all be defined relative to the picking proxy mesh (not the rendered mesh).

 

Finally, QRayCaster and QScreenRayCaster now have pick() methods, which do a ray casting test synchronously, whereas the pre-existing trigger() methods would schedule a test for the next frame. This will block the caller until completed and return the list of hits. Thus, it’s possible for an application to implement event delegation. For example, if the user right clicks, the application can decide to do something different depending on the type of the closest object, or display a context menu if nothing was hit.

Conclusion

As you can see, Qt 3D for Qt 6 has quite a few changes. My colleague, Paul Lemire, will go through many more internal changes in a later post. We hope this ensures an on-going successful future for Qt 3D in the Qt 6 series.

KDAB provides a number of services around Qt 3D, including mentoring your team and embedding Qt 3D code into your application, among others. You can find out more about these services here.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

FacebookTwitterLinkedInEmail

Categories: KDAB Blogs / KDAB on Qt / Qt / Qt3D / QtDevelopment / Technical

Tags: /

2 thoughts on “Qt 3D Changes in Qt 6”

  1. I like the changes and the decoupling from Qt! Are there any plans to develop Qt3d on a modern code platform like Gitlab or Github? The current Qt coding platform with the old jira, gerrit and awful cgit is really a not easy to learn for new developer…

    1. Hi, Qt3D remains a Qt module. Everything is mirrored to GitHub I believe. But issue tracking etc remains on Qt’s infrastructure.

Leave a Reply

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