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;
}

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);
}

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);
}
What about RHI?
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.