3D visualisation using Qt3D: part 3
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:
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:
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:
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:
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)
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):
We then go to face select mode (the icon with the cube and one face highlighted)
We can then select some faces by dragging LMB around an area we like. I'm selecting the rightmost faces:
And this is what we get:
Because we had X-Ray mode on, we have selected also the bottom faces which were hidden by the top ones:
And we can now switch X-Ray mode off:
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.
After this, the object will be neatly split in two:
Notice that in the outliner we still have one object, one mesh, two materials:
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:
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:
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:
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:
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).
Let's now create a texture to place on the cube. I saved it as Models/f.png in the source tree.
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:
Now, having the new material selected, we click on the small circle close to "Base color" and we select "Image Texture":
Then the material panel changes and shows an "open" button beneath "Base color", we click it and select f.png
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:
Before we conclude this episode, let's have a look at the tree which has been imported:
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
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?