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 materials have on how we can manage our scene.

Before you read this article, please make sure you have read the second episode where some details about the scene graph are unveiled:

Animating the material

Before we get deeper into our code, let's try animating the material a bit. In the OnTimer slot, where we move the torus object around, we will also tweak its material properties.

First of all we must grab the TorusObject material pointer, like we did for the transform.

By looking at the tree generated after importing, we see that the material is represented by a class named Qt3DExtras::QPhongMaterial:

No alt text provided for this image

QPhongMaterial is deprecated according to Qt documentation, but that's what we get from the importer, so we stick to it.

We then add a MainWindow member variable to track the material which has been imported:

private:
    Qt3DExtras::QPhongMaterial *material = nullptr;

And in the OnLoaderStatusChanged slot we look both for the QTransform and the QPhongMaterial:

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())
            {
                if(!transform)
                    transform = 
                        qobject_cast<Qt3DCore::QTransform *>(comp);

                if(!material)
                    material = 
                        qobject_cast<Qt3DExtras::QPhongMaterial *>(comp);
            }
        }
    }
}


If we located the material we can then use it in our OnTimer slot. We will change its specular colour so that reflections on it change constantly over time, whereas its basic surface colour will stay unchanged. Here I am showing the OnTimer slot, the portion of code which moves the torus around is remove for simplicity's sake:

void MainWindow::OnTimer()
{
    if(material)
    {
        material->setSpecular(QColor(colourValue, 0, 255-colourValue));
        colourValue += colourDir;
        if(colourValue == 0 || colourValue == 255)
            colourDir = - colourDir;
    }
}

This snippet of code will make the specular colour bounce between two values, so we will see out torus reflections changing hue gradually over time, between a bluish and a purplish colour:

No alt text provided for this image

So far, so good. Let's now try to make things a bit more complex by defining two materials in the same object.

An object with two materials

Let's go back to Blender and assign two different materials to the torus. Some faces will have the tranquilising azure colour, some others will have a more evil looking reddish tint.

Again, this is not a Blender tutorial and I'm far from being a Blender expert, so don't expect much under that point of view.

So having TorusObject selected we go to the materials tab and add a new material, which will be called Material.002, and assign a sort of red hue to it:

No alt text provided for this image

Until this point, no change is visible in the object, because the first material has been assigned to all faces, and the second one is just sitting there doing nothing useful:

No alt text provided for this image

Now we switch to Edit Mode (shortcut: 1) and we will soon assign the new material to some of the faces. Let's switch to top view (shortcut: F3)

No alt text provided for this image

We will now select some of the faces with a box selection. We switch X-Ray on to be able to select hidden faces (it's the icon with two squares overlapping each other):

No alt text provided for this image

We then go to face select mode (the icon with the cube and one face highlighted)

No alt text provided for this image

We can then select some faces by dragging LMB around an area we like. I'm selecting the rightmost faces:

No alt text provided for this image

And this is what we get:

No alt text provided for this image

Because we had X-Ray mode on, we have selected also the bottom faces which were hidden by the top ones:

No alt text provided for this image

And we can now switch X-Ray mode off:

No alt text provided for this image

Now that we have a face set selected, we go back to the material tab, make sure we have "Material.002" selected, and press the "Assign" button. This will assign the selected material to the current face set.

No alt text provided for this image

After this, the object will be neatly split in two:

No alt text provided for this image

Notice that in the outliner we still have one object, one mesh, two materials:

No alt text provided for this image

The mapping between materials and which face set they are applied to is kept internally by Blender.

Let's now save our scene, and export it to Collada as object1.dae as usual.

Back to our program

We can now run our program again and see what happens.

With great joy we will notice that the new colour scheme works, and that our torus continues to spin around the origin in the familiar circular motion:

No alt text provided for this image

With much less joy we will also notice that the specular colour animation is not working anymore: specular reflections are white. Why ? Let's go back to the scene graph.

The scene graph, again

If you don't remember how to generate the scene graph, please go back to article #2.

Here is what we are importing:

No alt text provided for this image

We can see that:

  • TorusObject has no material and no mesh
  • TorusObject has a transform
  • TorusObject has two children, called TorusObject_Child0 and TorusObject_Child1
  • Each of the children has a mesh (QGeometryRenderer)
  • Each of the children has a material (QPhongMaterial)
  • Each of the children has a transform (QTransform)

The importer has split our TorusObjects in two parts, one for each material. Each child will include the material and a subset of the mesh (QGeometryRenderer) containing only the faces to which that specific material has been assigned.

The good news is that children's QTransforms are relative to their parent's. So if we move the parent (TorusObject) using its QTransform, children will follow. The bad news is that if we want to do anything else, such as manipulating materials or geometries, we need to do it recursively.

Wrapping QEntity into a helper class

So far we have been managing the Qt3D entities and components directly in our MainWindow:

        auto torusEntity = rootEntity->findChild<Qt3DCore::QEntity *>
                           ("TorusObject");
        if(torusEntity)
        {
            for(auto comp: torusEntity->components())
            {
                if(!transform)
                    transform = qobject_cast<Qt3DCore::QTransform *>
                                (comp);

                if(!material)
                    material = qobject_cast<Qt3DExtras::QPhongMaterial *>
                               (comp);
            }
        }

Now that the entity and all of its components have been split into a tree, it becomes unfeasible to manage the scene in this way.

In any Qt3D program that I have written, I always found it convenient to write a small wrapper class which helps me to deal with these details. Let's see a simple implementation of such a class:

class EntityWrapper
{
public:
    EntityWrapper(Qt3DCore::QEntity *entity);

    void Move(double x, double y, double z);
    void setSpecular(const QColor &col);

protected:
    Qt3DCore::QEntity *entity = nullptr;
    Qt3DCore::QTransform *transform = nullptr;
    Qt3DExtras::QPhongMaterial *mat = nullptr;
    QList<EntityWrapper *> children;
};

An EntityWrapper contains:

  • A QEntity pointer, representing the entity it is wrapping
  • A QTransform pointer, representing the transform of its entity (if any)
  • A QPhongMaterial pointer, representing the material of its entity (if any)
  • A QList of children

The idea is that, given a QEntity, EntityWrapper recursively builds a tree which mimics the QEntity tree, and then provides some convenient recursive functions on the tree.

The constructor takes care about building the tree mimicking the QEntity tree, and also takes care about finding the QTransform and QPhongMaterial components (if present), caching them in member variables:

EntityWrapper::EntityWrapper(Qt3DCore::QEntity *entity)
{
    this->entity = entity;

    for(auto comp: entity->components())
    {
        if(!transform)
            transform = qobject_cast<Qt3DCore::QTransform *>(comp);

        if(!mat)
            mat = qobject_cast<Qt3DExtras::QPhongMaterial *>(comp);
    }

    for(auto childNode: entity->childNodes())
    {
        auto childEntity = qobject_cast<Qt3DCore::QEntity *>(childNode);
        if (childEntity)
            children.push_back(new EntityWrapper(childEntity));
    }
}

This recursive constructor will set up a "replica" of the QEntity tree, in which the tree layout is the same as the QEntity tree layout with some additional cached information.

We can now add some simple functionality in EntityWrapper to simplify our life in MainWindow, the first one is going to be a Move function which allows us to move a QEntity without using the QTransform interface from the outside. Remember that, even if the QEntity has children, their position and orientation is relative to their parent's transform, so we don't need to move children recursively:

void EntityWrapper::Move(double x, double y, double z)
{
    if(transform)
        transform->setTranslation(QVector3D(x, y, z));
}

Let's now add another function to change the specular colour. As we have seen with our split-colour torus, we will need to change the colour recursively to apply that change to all children:

void EntityWrapper::setSpecular(const QColor &col)
{
    if(mat)
        mat->setSpecular(col);
    for(auto child: children)
        child->setSpecular(col);
}

Using EntityWrapper in the MainWindow

Now that we have wrapped the QEntity / QComponent tree into the EntityWrapper class, we can simplify our life in MainWindow. Once we finish loading the scene, we can build a wrapper for TorusObject:

void MainWindow::OnLoaderStatusChanged(Qt3DRender::QSceneLoader::Status 
                                       status)
{
    if(status == Qt3DRender::QSceneLoader::Ready)
    {
        auto torusEntity = rootEntity->findChild<Qt3DCore::QEntity *>
                           ("TorusObject");
        if(torusEntity)
            torusWrapper = new EntityWrapper(torusEntity);
    }
}

We no longer need to scan the QComponent vector inside torusEntity looking for transform and material, as EntityWrapper will do that for us.

In the OnTimer slot we can now rely on EntityWrapper:

void MainWindow::OnTimer()
{
    if(torusWrapper)
    {
        alpha += 0.01;
        torusWrapper->Move(cos(alpha), sin(alpha), 0);
        torusWrapper->setSpecular(QColor(colourValue, 0, 255-colourValue));
        colourValue += colourDir;
        if(colourValue == 0 || colourValue == 255)
            colourDir = - colourDir;
    }
}

After these changes, the purplish hue which changes over time can be seen again on our torus:

No alt text provided for this image

And now some textures

To begin our journey into texture management, let's begin by adding a simple cube to our scene. We scale it down to fit within the torus' hole, rename it to CubeObject/CubeMesh, and add a green material to it:

No alt text provided for this image
No alt text provided for this image

If we run our application now, we will see that the torus is moving in the usual circular path, whereas the cube is standing still in the origin. That's because the two entities are independent from each other (if we wanted the cube to follow the torus, we could make the cube to be a child of the torus in Blender, thus linking its reference system to its parent's).

No alt text provided for this image

Let's now create a texture to place on the cube. I saved it as Models/f.png in the source tree.

No alt text provided for this image

We will now assign the texture to one of the cube's faces. Select the cube, go to edit mode, choose face select mode (all of these should already be familiar as we used them before to assign the second material to a subset of faces).

Then create a new material, select the top face, select the new material, and choose "assign". You should now see all of the cube with its normal green material, and the top face with the white colour which is the default colour when you create a material from scratch:

No alt text provided for this image

Now, having the new material selected, we click on the small circle close to "Base color" and we select "Image Texture":

No alt text provided for this image

Then the material panel changes and shows an "open" button beneath "Base color", we click it and select f.png

No alt text provided for this image

There are many ways in which the texture can be placed, scaled, etc. This is beyond the scope of this tutorial. Let's save the model, export it to Collada, and run our application:

No alt text provided for this image

Before we conclude this episode, let's have a look at the tree which has been imported:

No alt text provided for this image

We focus on the CubeObject only, as the rest of the scene did not change. We see that CubeObject has been split in two sub-entities, CubeObject_Child0 and CubeObject_Child1. This happens, as we have already seen, because CubeObject contains more than one material.

CubeObject_Child0 has a QPhongMaterial component, hence it represents the 5 faces with the green material. CubeObject_Child1 has a QDiffuseMapMaterial component (a class we hadn't previously met), and it represents the top face with the texture.

Next steps

In the next episode of this tutorial we will see how to pack 3D models into resource files.

Where to get source code

This series of tutorials is stored on GitHub. As usual, don't expect anything even remotely resembling production-ready code. To make reading simpler, most best practices have been intentionally not applied: pointers are not checked, error codes are not tested, and so on. The purpose is to make code short and to the point. There are no comments, too: those sources are intended to be a complement to these articles.

https://github.com/gpiasenza/Qt3DTests

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

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





Juliusz Kaczmarek

Pipeline TD at Brown Bag Films

10 个月

Nice articles on the Qt3D! A question - do the child entities of multi-material meshes need Transform component? I know this is a result of using SceneLoader, but if building scene manually, do we need to include it, if we don't expect the parts with different materials to move independently?

回复

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

Guido Piasenza的更多文章

社区洞察

其他会员也浏览了