basysKom Application Development Services

3D Rendering Solutions in Qt – an Overview
Essential Summary
Qt’s 3D offering is changing, so we decided to look at different options for rendering 3D content in Qt.

Qt’s 3D offering is changing, so we decided to look at different options for rendering 3D content in Qt.

Qt itself offers not one, but two 3D rendering engines: Qt 3D from our friends at KDAB and Qt Quick 3D. While the former has been removed as an official part of Qt 6.9 onwards, the latter is only available under GPL and commercial licenses. Additionally, Qt Quick 3D is, as the name suggests, restricted to QML applications. If your application is based on QWidget, you have no choice but to look elsewhere.

So we decided to test different 3D rendering solutions and how they integrate into the Qt world. To give a spoiler: There is no single perfect solution. The choice of framework always depends on the specific use case.

Here, we investigate Coin 3D, Ogre, BGFX and Threepp. All of them are licensed permissively: Coin 3D and BGFX are licensed under BSD, Ogre and Threepp under MIT.

Coin 3D

Coin 3D is a graphics API with the stated goal of being fully compatible with Open Inventor – a toolkit that has its roots in the early 1990s when Silicon Graphics Inc. (SGI) designed it to offer an abstraction layer on top of OpenGL.

Open Inventor, and therefore Coin 3D, manages all scene objects – primitives, but also lights, materials, transforms, the camera – in a scene graph object. The scene descriptions can be loaded from *.iv files. Coin 3D even comes with not one but two Qt integration APIs: SoQt and Quarter, where the latter is more current and the one that the developers recommend to use.

The following is a minimal example that renders a simple coin object inside a Quarter QWidget.

// https://www.coin3d.org/quarter/
 
#include <QApplication>
 
#include <Inventor/nodes/SoCone.h>
#include <Inventor/nodes/SoSphere.h>
#include <Inventor/nodes/SoCube.h>
 
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoGroup.h>
 
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/nodes/SoDirectionalLight.h>
 
#include <Inventor/nodes/SoTranslation.h>
#include <Inventor/nodes/SoTransform.h>
#include <Inventor/nodes/SoRotationXYZ.h>
 
#include <Quarter/Quarter.h>
#include <Quarter/QuarterWidget.h>
 
using namespace Qt::StringLiterals;
using namespace SIM::Coin3D::Quarter;
 
SoNode* createObjects()
{
 
    auto root = new SoSeparator;
    root->ref();
 
    auto camera = new SoPerspectiveCamera;
 
 
    root->ref();
    root->addChild(camera);
    root->addChild(new SoDirectionalLight);
 
    auto objGroup = new SoGroup();
 
    auto coneMaterial = new SoMaterial;
    coneMaterial->diffuseColor.setValue(0.5, 0.5, 0.0);
    objGroup->addChild(coneMaterial);
    objGroup->addChild(new SoCone());
 
    auto sphereMaterial = new SoMaterial;
    sphereMaterial->diffuseColor.setValue(0.7, 0.1, 0.1);
    objGroup->addChild(sphereMaterial);
    auto sphereTranslation = new SoTranslation;
    sphereTranslation->translation.setValue(-3.0, 0.0, 0.0);
    objGroup->addChild(sphereTranslation);
    objGroup->addChild(new SoSphere());
 
    auto boxMaterial = new SoMaterial;
    boxMaterial->diffuseColor.setValue(0.1, 0.1, 0.75);
    objGroup->addChild(boxMaterial);
    auto boxTranslation = new SoTranslation;
    boxTranslation->translation.setValue(3.0 * 2.0, 0.0, 0.0);
    objGroup->addChild(boxTranslation);
    objGroup->addChild(new SoCube());
 
    SoRotationXYZ *yRotation = new SoRotationXYZ;
    yRotation->axis = SoRotationXYZ::Y;
    yRotation->angle = M_PI / 4.0;
 
    SoRotationXYZ *xRotation = new SoRotationXYZ;
    xRotation->axis = SoRotationXYZ::X;
    xRotation->angle = M_PI / 8.0;
 
    root->addChild(yRotation);
    root->addChild(xRotation);
 
    root->addChild(objGroup);
 
    return root;
}
 
int main(int argc, char ** argv)
{
  QApplication app(argc, argv);
 
  // Quater is Coins connection to Qt
  Quarter::init();
 
  auto rootNode = createObjects();
  QuarterWidget * viewer = new QuarterWidget;
  viewer->setSceneGraph(rootNode);
  // Add some basic user interaction
  viewer->setNavigationModeFile(QUrl(u"coin:///scxml/navigation/examiner.xml"_s));
 
  viewer->show();
  app.exec();
 
  // Clean
  rootNode->unref();
  delete viewer;
  
  Quarter::clean();
  
  return 0;
} 
3D Rendering Solutions in Qt - an Overview 1 basysKom, HMI Dienstleistung, Qt, Cloud, Azure

Coin 3D might be the right choice if you already have some knowledge of Open Inventor and if you are able to structure a scene by organizing multiple objects. Unfortunately, the available examples feel a bit outdated and, while possible, there is no clear path to loading meshes or tuning materials to achieve a nice-looking result. Also, rendering is always done via OpenGL, which is not state of the art.

Nevertheless, Coin 3D is used in production: For an example of Coin 3D in use, you may want to have a look at the open source software FreeCAD (which, by the way, also uses Qt for its user interface.

Ogre 3D

Ogre 3D is a rendering engine generally used in games, but there are also examples for robotic visualization or medical applications. It is plugin-driven: You can load backends for different graphics APIs like OpenGL (ES), Direct3D or Vulkan. Like Coin 3D, Ogre 3D relies on a scene graph structure. The actual implementation of the scene graph’s data structure – whether the scene is organized in an Octree, a BSP tree or something else – is also managed through plugins. Ogre was initially released in 2005. Since then the graphics world has changed fundamentally, leading to a rewrite of Ogre 3D called Ogre Next to better fit the needs of modern graphics pipelines (minimize the driver overhead). Both the original Ogre 3D and Ogre Next are actively developed. You can find a comparison here.

The original Ogre 3D has the advantage that, albeit sparsely documented, it comes with Qt integration. We provide an example of how to load and render a mesh in a QWidget using OgreBites::ApplicationContextQt.

The header file:

#pragma once
 
#include <QWidget>
#include <QPaintEvent>
 
#include <Ogre.h>
#include <OgreApplicationContextQt.h>
 
class OgreWidget : public QWidget {
    Q_OBJECT
 
public:
    explicit OgreWidget(QWidget* parent = nullptr);
    ~OgreWidget();
 
protected:
    void paintEvent(QPaintEvent* e) override;
 
private:
    OgreBites::ApplicationContextQt m_ogreCtx;
    Ogre::SceneManager* m_sceneManager;
    Ogre::Viewport* m_viewport;
    Ogre::Camera* m_camera;
    Ogre::SceneNode* m_cameraNode;
 
    void initialize();
 
}; 

And the source file:

#include "OgreWidget.h"
 
#include <QDir>
 
using namespace Qt::StringLiterals;
 
OgreWidget::OgreWidget(QWidget* parent)
    : QWidget(parent)
    , m_ogreCtx("QtOgreExample")
{
    setAttribute(Qt::WA_OpaquePaintEvent);
    setAttribute(Qt::WA_NativeWindow);
}
 
OgreWidget::~OgreWidget()
{
 
}
 
void OgreWidget::paintEvent(QPaintEvent* e)
{
    if(!m_ogreCtx.getRoot()) {
        m_ogreCtx.injectMainWindow(windowHandle());
        m_ogreCtx.useQtEventLoop(true);
        m_ogreCtx.initApp();
 
        initialize();
 
        show();
    }
 
    m_ogreCtx.getRoot()->renderOneFrame();
}
 
void OgreWidget::initialize()
{
    // initialise Ogres resource system
    Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
        QString(u"/%1/assets"_s).arg(QDir::currentPath()).toStdString(), "FileSystem", "General"
    );
    Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
 
    // set Render system to GL
    auto root = m_ogreCtx.getRoot();
    Q_ASSERT(root);
    const auto renderPlugin = QString("%1/%2").arg(OGRE_DIR, u"/lib/OGRE/RenderSystem_GL.so"_s);
    root->loadPlugin(renderPlugin.toStdString());
 
    m_sceneManager = root->createSceneManager(Ogre::SMT_DEFAULT);
    m_camera = m_sceneManager->createCamera("mainCamera");
    m_camera->setNearClipDistance(5.0);
    m_camera->setFarClipDistance(1000.0);
    m_camera->setAutoAspectRatio(true);
 
    m_cameraNode = m_sceneManager->getRootSceneNode()->createChildSceneNode();
    m_cameraNode->attachObject(m_camera);
    m_cameraNode->setPosition(0.0, 0.0, 100.0);
 
    auto shadergen = Ogre::RTShader::ShaderGenerator::getSingletonPtr();
    shadergen->addSceneManager(m_sceneManager);
 
    m_sceneManager->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));
 
    auto light = m_sceneManager->createLight("MainLight");
    auto lightNode = m_sceneManager->getRootSceneNode()->createChildSceneNode();
    lightNode->attachObject(light);
    lightNode->setPosition(20, 80, 50);
 
    auto mesh = m_sceneManager->createEntity("penguin.mesh");
    auto meshNode = m_sceneManager->getRootSceneNode()->createChildSceneNode((Ogre::Vector3(0, 0, 0)));
    meshNode->attachObject(mesh);
 
    m_viewport = m_ogreCtx.getRenderWindow()->addViewport(m_camera);
    m_viewport->setBackgroundColour(Ogre::ColourValue(0.0, 0.0, 0.0));
    m_viewport->setClearEveryFrame(true);
} 
3D Rendering Solutions in Qt - an Overview 2 basysKom, HMI Dienstleistung, Qt, Cloud, Azure

Mesh loading is more straightforward in Ogre than in Coin 3D. Since Ogre comes with its own mesh format (*.mesh), objects must either be converted first or developers have to come up with their own loading routines.  Ogre is a framework that brings its own paradigms, its own resource system and a powerful language to define materials and compositing steps – therefore, it may feel a bit like leaving classical Qt standards (like Qt’s own internal way to load resources) behind. On the plus side, there are many examples and tutorials on the Internet –  at least for the original Ogre 3D. For me it was a bit confusing to navigate having two incompatible Ogre versions. The reimplementation Ogre Next (or Ogre 2) brings modern rendering but omits the default way to integrate Qt.

BGFX

While Coin 3D and Ogre 3D/Ogre Next are scene graph APIs, BGFX is more low-level. It is not a rendering engine per se; its goal is to provide a wrapper around the most important modern graphics APIs like OpenGL (ES), DirectX11/12, Vulkan, Metal and even WebGL via Wasm/Emscripten. You must implement everything yourself: This includes setting up buffers and the graphics pipeline, defining shaders, loading textures etc. Speaking of shaders: Shaders in BGFX are written in a shader language which is based on GLSL and compiled at build time.

With RHI (Rendering Hardware Interface), Qt has a similar concept of wrapping platform-specific graphics APIs. So, you may ask why you should consider BGFX. RHI is even more low-level than BGFX. If you need full control and are not interested in high-level scene graph management but don’t want to wire everything yourself, BGFX might be something to look into.

Threepp

I have to confess that I really like the JavaScript library Three.js which lets you quickly define 3D scenes. The good news is that Threepp makes the API available for C++ developers. Since the rendering backend depends on OpenGL and embedding the api requires C++20, this may unfortunately be a showstopper for some. There is no official integration path with Qt, but it’s easy to render using a QOpenGLWidget as demonstrated in the following example.

#include "ThreeppWidget.h"
 
#include <QOpenGLContext>
 
#include <QDir>
 
using namespace Qt::StringLiterals;
 
ThreeppWidget::ThreeppWidget(QWidget* parent)
    : QOpenGLWidget(parent)
{
 
}
 
ThreeppWidget::~ThreeppWidget()
{
 
}
 
void ThreeppWidget::paintEvent(QPaintEvent* e)
{
    paintThreeppScene();
}
 
void ThreeppWidget::paintGL()
{
    paintThreeppScene();
}
 
 void ThreeppWidget::resizeGL(int w, int h)
{
     if (!m_initialized) {
        return;
     }
 
     auto aspect = w / h;
 
     m_camera->aspect = aspect;
     m_camera->updateProjectionMatrix();
     m_renderer->setSize(std::make_pair( w, h));
}
 
void ThreeppWidget::initializeGL()
{
 
    if (m_initialized) {
        return;
    }
 
    initializeOpenGLFunctions();
 
    auto widgetSize = std::make_pair( width(), height());
    auto aspect = width() / height();
    m_renderer = std::make_shared<threepp::GLRenderer>(widgetSize);
 
    m_scene = threepp::Scene::create();
    m_camera = threepp::PerspectiveCamera::create(75, aspect, 0.1f, 1000);
    m_camera->position.z = 5;
 
    // create box
    const auto boxGeometry = threepp::BoxGeometry::create();
    const auto boxMaterial = threepp::MeshBasicMaterial::create();
    boxMaterial->color.setRGB(1, 1, 0);
    boxMaterial->opacity = 0.1f;
    auto box = threepp::Mesh::create(boxGeometry, boxMaterial);
    m_scene->add(box);
 
    m_initialized = true;
 
}
 
void ThreeppWidget::paintThreeppScene()
{
    if (!m_initialized) {
        qWarning() << "Cannot render scene: Not initialized";
    }
 
    makeCurrent();
    glClearColor(0.5, 0.1, 0.2, 1);
    m_renderer->clear();
    m_renderer->render(*m_scene, *m_camera);
} 
Compared to the other libraries, Threepp is smaller in scope. While in recent years Three.js has grown to be a de-facto standard in the JavaScript world, Threepp has a smaller community. Qt integration has to be done with the help of QOpenGLWidget, as shown above. Still, due to its easy-to-use API and plenty of included examples, it might be a reasonable choice for demo applications.

What about RHI?

If you need full control about the graphic stack and would like to stay in the Qt world, you may want to try RHI. Be warned, RHI has a steep learning curve. You have to write a lot of boilerplate code to even get a triangle on the screen. We have published a blog post how to get started here.

Guidance

So you might ask, which library is the best for your needs. To conclude the blog post, we would like to give you a small guidance:

I only need OpenGL. I have some 3D meshes I have to render in a CAD like setting, and I already used Open Inventor in the past: Coin 3D

I have complex scenes and I would like to script the appearance of my 3D objects in a detailed fashion: Ogre 3D

I have volume data (like in medical application): Ogre Next, (Coin 3D with extensions) or VTK (not covered in this post)

I must load complex animations: Ogre 3D

I want to create a quick demo application: Threepp

I need full control over rendering, but RHI might be too low-level: BGFX

I need full control, want to stay in the Qt ecosystem and I don’t mind about the steep learning curve: RHI

If you have more questions, please feel free to contact us.

Conclusion

In this post, we have shown that there are plenty of 3D libraries outside the Qt world that can be easily integrated into Qt applications. The guidance in the last section gives an idea which library might fit into your application. Have you tried one of these libraries or have you worked with some other 3D rendering library together with Qt? Please let us know in the comments.

Picture of Berthold Krevert

Berthold Krevert

Since over 10 years, Berthold Krevert works as a senior software developer here at basysKom GmbH. He has a strong background in developing user interfaces using technologies like Qt, QtQuick and HTML5 for embedded systems and industrial applications. He holds a diploma of computer science from the University of Paderborn. During his studies he focused - amongst other topics - on medical imaging systems and machine learning. His interests include data visualisation, image processing and modern graphic APIs and how to utilize them to render complex UIs in 2d and 3d.

Leave a Reply

Your email address will not be published. Required fields are marked *

More Blogarticles

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy