3D visualisation using Qt3D: part 2

3D visualisation using Qt3D: part 2

Introduction

In the second part of the tutorial we will dig deeper into the structure used by Qt3D to represent scenes, and we will see how we can manipulate this structure to perform simple animations.

Have you checked out part 1 ? If not, please do so:

In part1 we have seen how to set up the application, the rendering apparatus, and everything needed to import a 3D model from Blender and show it into a window. We will now see in more detail what has been imported, and how it can be manipulated.

Enter graphviz

The Qt3D scene graph is a tree, therefore we are going to represent it as such to understand what we are dealing with.

There are cases in which the scene graph is actually a graph, not a tree. This can for example happen when we share components among various entities. But for the example we are dealing with, it's truly a tree.

We could write some code to display a tree in our application, but that's beyond the scope of this article. We will use a ready made tool called graphviz:

Graphviz can be used to visualise anything which is graph-related, and it has tons of options. It can be used as a library, which can be integrated into an application, or as a stand-alone tool which reads and processes text files describing graphs. We will use it in the simpler stand-alone version: our application will produce a text file which graphviz will then process.

A simple graphviz file

We will use graphviz in a very primitive form, so let's just see a simple example of a file describing a tree consisting of a root and two leaves:

graph
{
    label = "a simple tree"
    n01 [label="Parent"];
    n02 [label="A child"];
    n03 [label="Another child"];
    n01 -- n02;
    n01 -- n03;
}

In this snippet we see 3 nodes (n01, n02, n03), each of which with a label. We then see two connections: "n01 -- n02" and "n01 -- n03".

We need to process this file using "dot", a program included with graphviz, which processes graph configuration files and produces graphical output in various formats. We will use the PDF format and pipe it in to okular (a Linux PDF viewer). So, assuming that the graph decription file above is called "tree.txt", we invoke dot as follows:

dot -Tpdf test.txt | okular -

"-T" is followed by the output type we want to produce. Invoking okular with "-" means we want it to read its input from stdin.

The following graph is displayed:

No alt text provided for this image

We will now use graphviz to represent the scene graph.

Creating the tree for the scene graph

As we have seen from part 1 of the tutorial, the scene graph is represented by a tree of Qt3DCore::QEntity instances. The root of the tree is represented by a variable we have called rootEntity. We therefore need to navigate this tree recursively and generate its graphviz representation.

QSceneLoader status

We can't generate the tree immediately after importing the scene. In fact, Qt3DRender::QSceneLoader works asynchronously and if we try to generate the tree immediately after invoking the setSource function we will get an empty tree.

The scene loader will go through a series of status changes, represented by the Qt3DRender::QSceneLoader::Status enum:

    enum Status {
        None = 0,
        Loading,
        Ready,
        Error
    };

Each time the status changes, the scene loader will emit a Qt signal (statusChanged). We therefore add a matching slot in MainWindow.h:

protected slots:
    void OnLoaderStatusChanged(Qt3DRender::QSceneLoader::Status status);

And we connect the scene loader's signal to it in MainWindow's constructor, before importing the scene:

QObject::connect(loader, &Qt3DRender::QSceneLoader::statusChanged, 
                 this, &MainWindow::OnLoaderStatusChanged);

loader->setSource(QUrl(QString("file:../Qt3DTests/Models/object1.dae")));

in the slot we can check if the status has become "Ready", and we can then generate the tree:

void MainWindow::OnLoaderStatusChanged(Qt3DRender::QSceneLoader::Status 
                                       status)
{
    if(status == Qt3DRender::QSceneLoader::Ready)
    {
        printf("graph \"\"\n");
        printf("{\n");
        printf("label=\"Entity tree\"\n");
        int rootNodeNumber=0;
        Tree(rootEntity, rootNodeNumber);
        printf("}\n");
    }
}

Here we are generating the opening and closing braces, the graph label, and in between we call "Tree", which is our recursive tree navigation function, on rootEntity.

I'm using good old "printf" instead of std::cout, because I find it much more compact then the bloated cout equivalent.

Generating the tree

The tree is generated in the "Tree" function. I did not pay much attention on writing this function so there are certainly more elegant ways to do it, especially I'm not very proud of having two indices. The purpose of which is to insert the current node in the file before its children, to make the file more readable for me when debugging code.

Anyway, here's Tree's implementation:

int Tree(Qt3DCore::QEntity *e, int &nextNodeNumber)
{
    int myNodeNumber = nextNodeNumber++;
    int childNodeNumber;

    // Insert myself in the tree
    printf("n%03d [label=\"%s\n%s\"] ;\n",
           myNodeNumber,
           e->metaObject()->className(),
           e->objectName().toStdString().c_str());

    // Enumerate components
    for(Qt3DCore::QComponent *comp: e->components())
    {
        printf("n%03d [shape=box,label=\"%s\n%s\"] ;\n",
               nextNodeNumber,
               comp->metaObject()->className(),
               comp->objectName().toStdString().c_str());
        printf("n%03d -- n%03d [style=dotted];\n", 
               myNodeNumber, nextNodeNumber);
        nextNodeNumber++;
    }


    // Build tree for children
    for (auto childNode : e->childNodes())
    {
        auto childEntity = qobject_cast<Qt3DCore::QEntity *>(childNode);
        if (childEntity)
        {
            childNodeNumber = Tree(childEntity, nextNodeNumber);
            printf("n%03d -- n%03d ;\n", myNodeNumber, childNodeNumber);
        }
    }

    return myNodeNumber;
}

In step 1 of the tutorial we have seen that the scene graph is composed of a tree of Qt3DCore::QEntity instances.

Each QEntity has:

  • 0 to N children QEntity pointers
  • 0 to N QComponent pointers

QComponent, as we have briefly seen in part 1, is an abstract class which represents various "features" of the entity. More on this later.

So, in Tree function we generate the tree by scanning the component list for the current entity, and then navigating recursively child entities. Entities are represented by ellipses connected by solid lines, components by rectangles connected by dotted lines to the entity owning them.

For each entity or component, the node in the graph shows:

  • The object's class name
  • The object's name

For each class derived from QObject, the basis of any Qt class, we can retrieve run-time information by using the metaObject( ) function. This is similar to C++ RTTI but, according to Qt documentation at least, it's more lightweight.

Interpreting the tree

We run our program redirecting its output in a file, then we run dot:

./Qt3DTests > tree.txt
dot -Tpdf tree.txt | okular -

And here is what we get:

No alt text provided for this image

For each node we print the class name (retrieved using metaObject( )) and the object name (retrieved with objectName( )). Please notice that the object name has nothing to do with the name of the variable used in C++ code to store that variable: it's a run-time information which can be assigned and read using setObjectName( ) and objectName( ).

For instance, we can set the root object name after creating it:

    rootEntity = new Qt3DCore::QEntity();
    rootEntity->setObjectName("This is the root");

And this is reflected in the graph we print:

No alt text provided for this image

The Collada importer assigns the objectName to the entity name it found in the Blender-exported file, as we will see soon.

Looking into the tree we can identify some familiar portions.

This part, in the left area, is the camera controller which we created in C++ and which allows us to move spin the camera around the scene:

No alt text provided for this image

This portion is the point light we created in C++:

No alt text provided for this image

Notice that there are two components, a QPointLight which defines the entity its capability to emit light (including details about colour and strength), and a QTransform which defines where the object is located and how it's oriented (although orientation has no meaning for point lights).

Finally we have the scene loader and the scene it imported:

No alt text provided for this image

The top entity is the QEntity that we created to hold the scene loader (QSceneLoader) which is a component:

sceneLoaderEntity = new Qt3DCore::QEntity(rootEntity);
loader = new Qt3DRender::QSceneLoader(sceneLoaderEntity);
loader->setObjectName("object1.dae");
sceneLoaderEntity->addComponent(loader);
loader->setSource(QUrl(QString("file:../Qt3DTests/Models/object1.dae")));

Then underneath we can see a QEntity with name "Scene", which represents the scene we imported from Blender. Within we can see two objects named "Light" and "Camera" which have been imported too, but we are not going to use them. We only care about a QEntity named "TorusObject", which obviously represents the torus.

TorusObject has three components:

  • Qt3DExtras::QPhongMaterial (Material.001) : this is the azure material we assigned to the torus
  • Qt3DRender::QGeometryRenderer: this is the mesh representing the torus' geometry
  • Qt3DCore::QTransform: this represents the torus' position and orientation in space

We can compare this portion of the tree with Blender's outliner:

No alt text provided for this image

One very important difference we can see is that the material in Blender is an attribute of the mesh, whereas in Qt3D it's a component of the QEntity (TorusObject). This has an implication on how objects with multiple materials are imported, as we will see in future steps of this tutorial.

Moving the torus around

We will now write a small animation example in which our torus will move around in a circular movement around the world's origin.

To achieve this result we must:

  • Find the QEntity which represents the TorusObject inside the scene graph
  • Find the QComponent within the TorusObject which represents its position / rotation (QTransform) and store it as a member of MainWindow
  • Start a QTimer in MainWindow. In the timer handler we will use the stored QTransform to move the torus around. We will use a simple circular motion with sin / cos around the centre.

Finding the torus' QEntity

We can use many ways to identify the torus in the tree, such as navigating the tree recursively until we find the right node. As the Collada importer kindly assigns the Blender object name to the QEntity's objectName, we can use a utility function called findChild provided by QObject (the ancestor of any Qt-related class):

auto *torusEntity = rootEntity->findChild<Qt3DCore::QEntity *>
                    ("TorusObject");
if(torusEntity)
{
    // found it... do something
}
       

The findChild function will navigate the object tree within rootEntity, look for a child with objectName( ) == "TorusObject" and, if found, will try to cast it dynamically to <Qt3DCore::QEntity *> (the template paramenter of the function).

If no object with that name is found, or the object can't be cast to the specified type, findChild returns nullptr.

Finding the transform

Now that we have identified the QEntity which represents our torus, we need to access its QTransform so we can move it around. A QEntity contains a vector of QComponent instances, each of which represents a different feature of the entity.

We need to scan the torus components and locate the one which represents its transform, i.e. try to cast each component to QTransform and see if casting succeeds. We will store the transform into a member variable of MainWindow:

    Qt3DCore::QTransform *transform = nullptr;

Then, in the OnLoaderStatusChanged slot we will scan the component list and look for the right instance. Here is the complete code (the recursive creation of the graphviz tree, which is also done here, is omitted for the sake of clarity:

void MainWindow::OnLoaderStatusChanged(Qt3DRender::QSceneLoader::Status 
                                       status)
{
    if(status == Qt3DRender::QSceneLoader::Ready)
    {
        auto torusEntity = rootEntity->findChild<Qt3DCore::QEntity *>
                            ("TorusObject");
        if(torusEntity)
        {
            for(auto comp: torusEntity->components())
            {
                transform = qobject_cast<Qt3DCore::QTransform *>(comp);
                if(transform)
                    break;
            }
        }
    }
}

Moving the torus around

Having found the transform, we can move the torus around. For this purpose we create a timer member in MainWindow. The timer emits a timeout signal when it expires, so we also create a corresponding slot to catch it:

protected:
    QTimer timer;

protected slots:
    void OnTimer();

In MainWindow's constructor we connect the timer's signal to our slot, and we start the timer with a 10 ms timeout:

    connect(&timer, SIGNAL(timeout()), this, SLOT(OnTimer()));
    timer.start(10);

In the OnTimer slot we can then move the torus around:

void MainWindow::OnTimer()
{
    if(transform)
    {
        alpha += 0.01;
        transform->setTranslation(QVector3D(cos(alpha), sin(alpha), 0));
    }
}

Alpha is just a float member variable inside MainWindow storing the angle for the current frame.

Running the program we can now see the torus moving around (sorry about the low quality video capture)

When running the application, you may notice that the animation "stutters" a bit. This is because the update of the object's position is performed in the timer slot, which is not synchronised with rendering. In future articles we will see how to make the animation smoother.

Next steps

In the next episode of this tutorial we will see how to manage objects which have more than one material, how to deal with textures, and how to move 3D models into resource files.

Where to get source code

This series of tutorials is stored on GitHub:

https://github.com/gpiasenza/Qt3DTests

If you want to get the exact version used in this article, please checkout the tag STEP_002

https://github.com/gpiasenza/Qt3DTests/releases/tag/STEP_002










要查看或添加评论,请登录

Guido Piasenza的更多文章

  • Realtime audio processing with Linux: part 3

    Realtime audio processing with Linux: part 3

    Introduction In the previous articles we have introduced basic concepts about audio processing and got ourselves…

    3 条评论
  • Realtime audio processing with Linux: part 2

    Realtime audio processing with Linux: part 2

    Introduction In the previous article we have introduced basic concepts about audio processing, and begun to map them to…

    4 条评论
  • Realtime Audio processing with Linux

    Realtime Audio processing with Linux

    Introduction Do you drive a recent car ? Chances are your infotainment system, i.e.

    4 条评论
  • 3D visualisation using Qt3D: part 3

    3D visualisation using Qt3D: part 3

    Introduction In this third installement of my Qt3D tutorials we will dig a bit deeper into materials and the impact…

    1 条评论
  • 3D visualisation using Qt3D: part 1

    3D visualisation using Qt3D: part 1

    Introduction In my company we are developing a machine vision + AI based assistant which can provide visual hints about…

    6 条评论
  • C++: detailed analysis of the language performance (Part 3)

    C++: detailed analysis of the language performance (Part 3)

    This is the third of a series of articles about C++ perfomance. Did you read part 2 already ? If not, please do so: In…

    2 条评论
  • C++: detailed analysis of the language performance (Part 2)

    C++: detailed analysis of the language performance (Part 2)

    This is the second in a series of articles about C++ performance. Did you read part 1 already ? If not, please do so:…

    3 条评论
  • C++: detailed analysis of the language performance (Part 1)

    C++: detailed analysis of the language performance (Part 1)

    As an avid C++ supporter I frequently had to face several objections to the language, mostly based on its (supposed)…

    16 条评论

社区洞察

其他会员也浏览了