Kuesa Runtime

Kuesa Many Ducks C++ Example

Demonstrates the use of the Kuesa C++ API to import a glTF2 file and extract assets from it.

Setting up a scene

Creating a window

The easiest way to get started is to subclass the Qt3DExtras::Qt3DWindow

 class Window : public Qt3DExtras::Qt3DWindow
Creating a SceneEntity

The SceneEntity will hold the assets we will load later. It will also act as our root entity.

 m_scene = new Kuesa::SceneEntity();
 m_scene->addComponent(new DefaultEnvMap(m_scene));
Creating a Camera and its controller

We can reuse the default camera provided by Qt3DExtras::Qt3DWindow

 camera()->setPosition(QVector3D(5, 1.5, 5));
 camera()->setViewCenter(QVector3D(0, 0.5, 0));
 camera()->setUpVector(QVector3D(0, 1, 0));
 camera()->setAspectRatio(16.f / 9.f);

 auto camController = new Qt3DExtras::QOrbitCameraController(m_scene);
 camController->setCamera(camera());
Setting up the FrameGraph

We can use the pre made ForwardRenderer FrameGraph.

 auto fg = new Kuesa::ForwardRenderer();
 fg->setCamera(camera());
 fg->setGamma(2.2f);
 fg->setExposure(1.f);
 fg->setClearColor("white");
 fg->setSkinning(true);
 // We disable frustum culling if using instancing
 fg->setFrustumCulling(!usesInstancing);
 setActiveFrameGraph(fg);
Importing a glTF2 File

In order to load a glTF2 file, Kuesa provides the GLTF2Importer element. If the sceneEntity property is set to a valid SceneEntity instance, Qt 3D assets generated while parsing the file will be automatically added to the various asset collections.

 auto gltfImporter = new Kuesa::GLTF2Importer(m_scene);
 gltfImporter->setSceneEntity(m_scene);
 gltfImporter->setSource(QUrl{ "qrc:///assets/models/duck/Duck.glb" });

 connect(gltfImporter, &Kuesa::GLTF2Importer::statusChanged,
         this, &Window::on_sceneLoaded);
Adding a Skybox

Kuesa provides Skybox. It expects a patch and an extension.

 // Skybox creation
 auto skybox = new Kuesa::Skybox(m_scene);
 skybox->setBaseName(envmap("radiance"));
 skybox->setExtension(".dds");
Adding a PostProcessing Effect

We can make use of Kuesa's pre made post processing effects such as DepthOfFieldEffect.

 // Depth-of-field
 auto dof = new Kuesa::DepthOfFieldEffect();
 dof->setRadius(15.0f);
 dof->setFocusRange(2.0f);
 dof->setFocusDistance(6.5f);
 fg->addPostProcessingEffect(dof);

Extracting Assets from Collections

Usually, you will want to interact with some elements of your scene.

You can use the Kuesa Studio gltfInspector to introspect a glTF2 scene files and find the names of the various assets it contains.

Upon successful loading of our glTF2 file, we will be able to proceed with asset retrieval.

 void on_sceneLoaded(Kuesa::GLTF2Importer::Status status)
 {
     if (status == Kuesa::GLTF2Importer::Ready) {
Manually Instancing Meshes
Retrieving Geometries and Material

Using the gltfInspector we know our scene files contains a Duck Entity name "KuesaEntity_0"..

We can therefore retrieve it with:

 // First let's take the components that we are going to use to create our clones
 // Gotten from gammaray
 auto parent = m_scene->entity("KuesaEntity_0");

In turn, using Qt3D Component API, we can retrieve the QGeometry, Qt3DRender::QMaterial components of the previously retrieved entity.

 auto *orig_entity = qobject_cast<Qt3DCore::QEntity *>(m_scene->entity("KuesaEntity_2")->childNodes()[1]);
 auto *orig_geometry = componentFromEntity<Qt3DRender::QGeometryRenderer>(orig_entity);
 auto *orig_material = componentFromEntity<Qt3DRender::QMaterial>(orig_entity);

Then, we can create several entities referencing the same material and geometry.

 // Then create clones by giving them a custom transform, and the same components than before

 QRandomGenerator *rand = QRandomGenerator::global();
 for (int i = 0; i < Ducks; i++) {
     auto new_entity = new Qt3DCore::QEntity{ parent };
     auto new_transform = new Qt3DCore::QTransform;
     new_transform->setScale(0.2f);
     new_transform->setTranslation(QVector3D(rand->generate() % r - r / 2, rand->generate() % r - r / 2, rand->generate() % r - r / 2));

     new_transform->setRotationX(rand->generate() % 360);
     new_transform->setRotationY(rand->generate() % 360);
     new_transform->setRotationZ(rand->generate() % 360);

     new_entity->addComponent(new_transform);
     new_entity->addComponent(orig_geometry);
     new_entity->addComponent(orig_material);

     m_transforms[i] = new_transform;
 }
Relying on GPU based instancing

A more efficient approach to drawing multiple instances of the same geometry is to rely on GPU based instancing. In Kuesa, assuming the GPU supports that feature, this can easily be leveraged by using the Kuesa::MeshInstantiator class.

We need to generate a transformation matrix for each of the instances.

 // Build base transformation matrices for each instance

 QRandomGenerator *rand = QRandomGenerator::global();
 m_matrices.reserve(Ducks + 1);
 for (int i = 0; i < Ducks; i++) {
     QMatrix4x4 m;

     const int extent = r / 20;
     m.translate(QVector3D(rand->generate() % extent - extent * 0.5, rand->generate() % extent - extent * 0.5, rand->generate() % extent - extent * 0.5));
     m.rotate(rand->generate() % 360, QVector3D(1.0f, 0.0f, 0.0f));
     m.rotate(rand->generate() % 360, QVector3D(0.0f, 1.0f, 0.0f));
     m.rotate(rand->generate() % 360, QVector3D(0.0f, 0.0f, 1.0f));

     m_matrices.push_back(m);
 }
 // Add an extra matrices to have one instance placed at the origin
 m_matrices.push_back({});

Then we can create our MeshInstantiator, specify the name of the Entity to instantiate and set the transformation matrices on it.

 // Create MeshInstantiator
 m_meshInstantiator = new Kuesa::MeshInstantiator(m_scene);
 // Specify which Entity with a Mesh needs to be instanced
 m_meshInstantiator->setEntityName(QStringLiteral("KuesaEntity_2"));
 // Set transformation matrices
 m_meshInstantiator->setTransformationMatrices(m_matrices);

This approach greatly reduces CPU usage compared to the manual approach.

Animating our Scene

Subclassing the timerEvent function on Qt3DExtras::Qt3DWindow allows us to add some logic to be executed at every frame.

 void timerEvent(QTimerEvent *event) override
 {
     Q_UNUSED(event)
     if (!m_usesInstancing) {
         for (auto transform : m_transforms) {
             transform->setRotationX(transform->rotationX() + 0.1f);
             transform->setRotationY(transform->rotationY() + 0.1f);
             transform->setRotationZ(transform->rotationZ() + 0.1f);
         }
     } else {
         static bool wasInitialized = false;
         static QMatrix4x4 rotationIncrementMatrix;

         if (!wasInitialized) {
             rotationIncrementMatrix.rotate(0.1f, QVector3D(1.0f, 0.0f, 0.0f));
             rotationIncrementMatrix.rotate(0.1f, QVector3D(0.0f, 1.0f, 0.0f));
             rotationIncrementMatrix.rotate(0.1f, QVector3D(0.0f, 0.0f, 1.0f));
             wasInitialized = true;
         }

         // Apply rotation transform to all matrices
         // This accumulates over time
         for (QMatrix4x4 &m : m_matrices)
             m *= rotationIncrementMatrix;

         m_meshInstantiator->setTransformationMatrices(m_matrices);
     }
 }

Please note that glTF2 offers way to embedded animations in the glTF files directly. This should be preferred unless you want to manually do animations like illustrated above.

Files:

Images: