Discovering Qt3D

The example project of this chapter will rely on 3D rendering. For this, we will use Qt3D. This part of the framework is divided into various Qt modules that enable the application to have a near-real time simulation of a 3D environment. Built on OpenGL, Qt3D offers a high-level API to describe complex scenes without having to resort to writing low-level OpenGL instructions. Qt3D supports the following basic features:

  • 2D and 3D rendering for C++ and Qt Quick
  • Meshes
  • Materials
  • GLSL shaders
  • Shadow mapping
  • Deferred rendering
  • Instance rendering
  • Uniform Buffer Object

All these features are implemented in the ECS (entity component system) architecture. Each mesh, material, or shader that you define is a component. The aggregation of these components makes an entity. If you wanted to draw a 3D red apple, you would need the following components:

  • A mesh component, holding the vertices of your apple
  • A material component, applying a texture on the mesh or coloring it

These two components will then be regrouped to define the entity Apple. You see here the two parts of the ECS: entities and components. The overall architecture looks like this:

Each of these components can be regrouped in aspects. An aspect is a "slice" of multiple components working on the same part (rendering, audio, logic, and physics). When the graph of all your entities is processed by the Qt3D engine, each layer of aspects is processed sequentially.

The underlying approach is to favor composition over inheritance. In a game, an entity (an apple, a player, an enemy) can have various states during its life cycle: spawning, animating for a given state, dying animation, and so on. Using inheritance to describe these states will lead to a nerve-wracking tree: AppleSpawnAppleAnimationShinyAppleDeath, and so on. It would become quickly unmaintainable. Any modification to a class could have huge impact on many other classes and the number of possible combinations of states would get out of hand. Saying that a state is simply a component for a given entity, gives the flexibility to easily swap components and still keep the entity abstraction; an apple Entity element is still an apple, even though it is using the AnimationShinyComponent instead of the AnimationSpawnComponent.

Let's see how to define a basic Entity element in QML. Imagine that this is the apple we have been talking about. The Apple.qml file would look like this:

import Qt3D.Core 2.0 
import Qt3D.Render 2.0 
import Qt3D.Extras 2.0 
 
Entity { 
 
    property alias position: transform.translation 
    PhongMaterial { 
        id: material 
        diffuse: "red" 
    } 
 
    SphereMesh { 
        id: mesh 
    } 
 
    Transform { 
        id: transform 
    } 
 
    components: [material, mesh, transform] 
} 

In a very few lines, you describe every aspect of the Entity element:

  • Entity: This is the root object of the file; it follows the same QML pattern we studied in Chapter 13Dominating the Mobile UI.
  • PhongMaterial: This defines how the surface will be rendered. Here, it uses the Phong shading technique to achieve smooth surfaces. It inherits QMaterial, which is the base class for all the material classes.
  • CuboidMesh: This defines what type of mesh will be used. It inherits QGeometryRenderer, which also gives the ability to load custom models (exported from 3D modeling software).
  • Transform: This defines the transformation matrix of the component. It can customize the translation, scale, and position of the Entity element.
  • Position: This is a property to expose transform.translation for a given caller/parent. This might quickly become handy if we want to move the apple around.
  • Components: This is the array containing all the IDs of all the components for the Entity element.

If we want to make this Apple a child of another Entity, it is simply a matter of defining the Apple inside this new Entity element. Let's call it World.qml:

import Qt3D.Core 2.0 
import Qt3D.Render 2.0 
import Qt3D.Extras 2.0 
 
Entity { 
    id: sceneRoot 
     RenderSettings { 
        id: frameFraph 
        activeFrameGraph: ForwardRenderer { 
            clearColor: Qt.rgba(0, 0, 0, 1) 
        } 
    } 
 
    Apple { 
        id: apple 
        position: Qt.vector3d(3.0, 0.0, 2.0) 
    } 
 
    components: [frameGraph] 
} 

Here, the World Entity has no visual representation; we want it to be the root of our 3D scene. It only contains the Apple we defined earlier. The xyz coordinates of the apple are relative to the parent. When the parent makes a translation, the same translation will be applied to the apple.

This is how the hierarchy of entities/components is defined. If you write your Qt3D code in C++, the same logic applies to the equivalent C++ classes (QEntityQComponent, and so on).

Because we decided to use the World.qml file as the root of our scene, it has to define how the scene will be rendered. The Qt3D rendering algorithm is data-driven. In other words, there is a clear separation between what should be rendered (the tree of entities and components) and how it should be rendered.

The how relies on a similar tree structure using framegraph. In Qt Quick, a single method of rendering is used and it covers the 2D drawing. On the other hand, in 3D, the need for flexible rendering makes it necessary to decouple the rendering techniques.

Consider this example: you play a game where you control your avatar and you encounter a mirror. The same 3D scene must be rendered from multiple viewports. If the rendering technique is fixed, this poses multiple problems: which viewport should be drawn first? Is it possible to parallelize the rendering of the viewports in the GPU? What if we need to make multiple passes for the rendering?

In this code snippet, we use the traditional OpenGL rendering technique with the ForwardRenderer tree, where each object is rendered directly on the back buffer, one at a time. Qt3D offers the possibility to choose the renderer (ForwardRendererDeferredRenderer, and so on) and configure how the scene should be rendered.

OpenGL typically uses the double-buffering technique to render its content. The front-buffer is what is displayed on the screen and the back-buffer is where the scene is being rendered. When the back-buffer is ready, the two buffers are swapped.

One last thing to notice at the top of each Entity element is that we specified the following:

import Qt3D.Core 2.0 
import Qt3D.Render 2.0 
import Qt3D.Extras 2.0 

There are only Qt3D modules in the import section. Qt3D classes do not inherit Item so cannot be directly mixed with QML components. This inheritance tree of the basic Qt3D building blocks is:

The QNode class is the base class of all Qt3D node classes. It relies on the QObject class to define the parenting relationship. Each QNode class instance also adds a unique id variable, which allows it to be recognized from other instances.

Even though QNode cannot be mixed with Qt Quick types, they can be added to a Q3DScene element (or Scene3D in QML), which serves as the canvas for Qt3D content and can be added to a Qt Quick Item. Adding World.qml to a scene is as simple as this:

Rectangle { 
 
    Scene3D { 
        id: scene 
        anchors.fill: parent 
        focus: true 
 
        World { } 
    } 
} 

The Scene3D element includes a World instance and defines common Qt Quick properties (anchorsfocus).