Qt

Ich beschäftige mich schon seit einiger Zeit mit dem umfangreichen Framework Qt. Qt ist ein plattformübergreifendes Open Source Software-Framework, entwickelt für grafische Benutzeroberflächen und Anwendungen.
Heutzutage ist eine Applikation, sei es auf einem Android- oder iOS-Smartphone oder auf einem Desktop-PC kaum vorstellbar ohne einem grafischen User Interface (UI).

Qt gibt es seit nunmehr über 30 Jahren und wird für das User Interface von Medizingeräten (Geräte zur Patientenüberwachung), bei Haushaltsgeräten (Thermomix) und z.B. auch in der Automobilindustrie (Infotainment-Systeme) eingesetzt.

Qt hat also schon eine lange Geschichte und kann mit Stabilität und Qualität glänzen, da es über die Jahre kontinuierlich durch die Entwickler als auch durch die Community verbessert wurde. Dadurch, dass das Qt Framework plattformübergreifend ausgerichtet ist, können Qt-Applikationen für verschiedene Betriebssysteme wie Linux, Windows oder macOS gebaut werden. Sogar für Embedded-Plattformen können Qt-Apps entwickelt werden.
Es bietet sich an, für die Programmierung von Qt-GUIs die Programmiersprache C++ zu verwenden, da das Qt Framework mit C++ entwickelt wird. Es ist aber grundsätzlich auch möglich Python über PySide zu verwenden. Zudem gibt es noch Sprachbindungen für Rust oder Java.
Ich habe bereits erwähnt, dass Qt eine Software-Framework ist. Aber was heißt das genau?
Im Grunde kann man sich das Qt-Framework als Werkzeugkasten vorstellen, der unterschiedliche Werkzeuge (Module) bereitstellt, die für die Entwicklung von komplexen User-Interfaces notwendig sind. Und Qt kann noch mehr: Neben schönen Benutzeroberflächen bietet der Qt-Werkzeugkasten auch noch Module zur Implementierung einer Backend-Logik an. Somit ist es recht einfach möglich, Funktionalitäten wie ein Netzwerk- oder Datenbankmodul in ein Qt-Projekt zu implementieren. Man kann aber auch eigene C++-Bibliotheken einbinden.
Neben dem Qt Framework gibt es auch noch die hauseigenen Entwicklertools von Qt. Dazu gehören der bekannte Qt-Creator sowie das Qt Design Studio.
In diesem Beispiel arbeite ich vorwiegend mit dem Qt Creator und verwende nur ein PlugIn von Design Studio.
Kommen wir nun zu den 3D-Werkzeugen, die das Qt Framework zur Verfügung stellt.

3D-Modellierung mit Qt Quick 3D

Qt bietet das Modul Qt Quick 3D an, das eine Erweiterung von Qt Quick ist. Qt Quick ist wiederum die Standardbibliothek zum Erstellen von QML-Applikationen.
QML (Qt Meta Object Language) ist die deklarative Sprache zum Beschreiben von Benutzeroberflächen von Qt-Anwendungen. Qt Quick 3D erweitert daher die Qt Quick 2D-Bibliothek mit 3D-Elementen wie 3D Rendering, Kamera, Szenen, Animationen, Beleuchtung und Materialien.
Der große Vorteil von Qt Quick 3D ist die relativ einfache Entwicklung von 3D-Inhalten mit der High-Level-API und QML als Beschreibungssprache der GUI. Zudem ist Qt Quick 3D als Teil des Qt Frameworks auf unterschiedlichen Plattformen lauffähig.
Mein Interesse gilt aber vor allem der Kombination von 2D- und 3D-Inhalten, um damit interaktive Applikationen erstellen zu können.

Mein kleines 3D-Projekt

Ich wage mich das erste Mal in die 3D-Welt. Bisher habe ich noch nicht viel Erfahrung mit der Entwicklung von 3D-Applikationen.
Das soll sich aber nun ändern. Als erstes mache ich mir daher einen Plan, wie mein 3D-Projekt aussehen und was letztendlich das Ziel dahinter sein soll.

Zielsetzung

Ich möchten mit zwei Qt Quick 3D-Projekten beginnen. Die Qt-Projekte sollen jeweils ein 3D-Objekt enthalten und auf meinem Linux Rechner lauffähig sein.
Die erste 3D-Applikation soll vom Schwierigkeitsgrad geringer sein als das zweite 3D-Projekt. Erstmal klein anfangen…
Das Grande Finale aber soll die Entwicklung einer 2D-Applikation werden, die beide 3D-Objekte laden kann. Diese Applikation soll auf meinem Linux- als auch auf meinem Windows-Rechner sowie auf einem Pixel 7 Pro Android-Gerät ausführbar sein.
Dazu kurz und knapp, eine schematische Darstellung meines zielgerichteten Vorgehens:
workflow

Auf die einzelnen Schritte des gezeigten Vorgehens werde ich im Folgendem nach und nach eingehen. Keine Sorge. Los geht`s!

Los geht`s: Der Purple Cube

Es existieren nicht viele Qt 3D-Beispiele im Netz, einen interessanten Artikel habe ich jedoch bei Scythe Studio gefunden.
Die Kollegen, die den Artikel verfasst haben, haben sich speziell mit Qt Quick 3D ohne Vorerfahrungen auseinander gesetzt und ihre Erkenntnisse geteilt. Ihr Beispiel beginnt mit einem einfachen 3D-Würfel. Im Grunde werde ich in etwa dasselbe ausprobieren.
Ich erstelle daher zunächst mit der Grafiksoftware Blender einen Würfel. Wobei “erstellen” brauche ich den Würfel nicht wirklich, da Blender als Default-Einstellung immer einen Würfel mitbringt. Ich füge dem Würfel nur noch Material, eine Kamera, Lichtquellen (zwei Spots) hinzu und runde die Kanten ab. Als Material wähle ich eine lila Farbe, die leicht glänzt.
In Blender sieht das dann wie folgt aus: Purple Cube in Blender Sobald ich mit dem Würfel in Blender zufrieden bin, exportiere ich diesen als FBX-Datei. Aus diesem Format kann das Tool Balsam auf magische Weise QML- und Mesh-Dateien erzeugen. Zumindest wird das so in der Qt-Dokumentation zu Balsam beschrieben.
Als nächstes wird also Balsam über die Kommandozeile aufgerufen und ich übergebe im Grunde nur die FBX-Datei und drücke auf Enter. Balsam hat übrigens auch eine sehr rudimentär gehaltene GUI, funktioniert dort aber auch genauso wunderbar.
Balsam erstellt wie beschrieben, einen Ordner mit Mesh-Files sowie ein QML-File aus dem FBX ohne Probleme. Als nächstes lege ich ein QtQuick-Projekt mit dem Qt Creator an.
Die Struktur des Projekts sieht wie folgt aus:
qtquick3dProjekt
Wie man aus der Abbildung sehen kann, verwende ich ein CMake Projekt. Das Projekt enthält das Modul appqtquick3D_purpleCube mit den Source Files main.cpp, Main.qml und PurpleCube.qml. Außerdem erstellt Balsam einen Ordner meshes, worin sich die 3D Mesh Modelle befinden.
Ein 3D Mesh Modell ist ein digitales Oberflächenmodell, das aus Eckpunkten (Vertics), Kanten und Flächen (Polygonen) besteht. Die Eckpunkte werden als Koordinaten verwendet und die Kanten verbinden zwei nebeneinander liegende Eckpunkte. Die Polygone schließen die Kanten ein und bilden somit die Oberfläche des Objekts. Polygone sind meistens Dreiecke, können aber auch als Vierecke dargestellt werden. Die Zusammensetzung aus Koordinaten, Kanten und Polygonen beschreibt das 3D Mesh Modell.
Der Ordner meshes enthält hier also das 3D Mesh Modell des Würfels aus Blender.
Was passiert nun, wenn man das Qt Programm ausführt? Anbei ein Blick in den Quellcode:

 1#include <QGuiApplication>
 2#include <QQmlApplicationEngine>
 3
 4int main(int argc, char *argv[])
 5{
 6    QGuiApplication app(argc, argv);
 7
 8    QQmlApplicationEngine engine;
 9    QObject::connect(
10        &engine,
11        &QQmlApplicationEngine::objectCreationFailed,
12        &app,
13        []() { QCoreApplication::exit(-1); },
14        Qt::QueuedConnection);
15    engine.loadFromModule("qtquick3D_purpleCube", "Main");
16
17    return app.exec();
18}

Die Ausführung des Quellcodes beginnt in der main-Funktion. Es werden Instanzen von QGuiApplication als auch QQmlApplicationEngine erstellt. Erst danach können die C++-Objekte aus dem QML-Code generiert werden. Im letzten Schritt wird das QML-Modul in Zeile 15 geladen. qtquick3D_purpleCube bezieht sich auf den jeweiligen Ordner des Projekts in dem das QML-Modul abliegt, im vorliegenden Fall gibt es nur diesen Ordner. Zudem muss der Ordnerpfad mit der URI im qt_add_module()-Befehl aus dem CMake übereinstimmen. In dem Ordner wird dann die Main-QML mit der 3D-View geladen:

 1import QtQuick
 2import QtQuick.Window
 3import QtQuick.Controls
 4import QtQuick3D
 5import QtQuick3D.Helpers
 6
 7
 8
 9Window {
10  id: root
11  width: 900
12  height: 600
13  visible: true
14  title: qsTr("QML 3D")
15
16  View3D {
17    id: view
18    anchors.fill: parent
19
20    environment: SceneEnvironment {
21      clearColor: "#758493"
22      backgroundMode: SceneEnvironment.Color
23    }
24
25    PerspectiveCamera {
26            id: camera
27            z: 700
28            y: 700
29    }
30
31    PurpleCube { }
32
33}

Die main.qml enthält also das Main-Window sowie die View3D.
Die View3D ist Teil der importierten QtQuick3D Library und enthält neben der Kameraposition die Komponente PurpleCube, die in Zeile 31 instanziiert wird.
Diese Komponente enthält die von Balsam autogenerierten Informationen zu dem 3D-Modell in QML. Darin enthalten sind beispielsweise Materialien, Beleuchtung und die Position des Modells. Zudem habe ich eine Animation hinzugefügt. Dadurch soll sich der Würfel um x- und y-Achse drehen:

 1Model {
 2        id: cube
 3        objectName: "Cube"
 4        position: Qt.vector3d(-61.6131, 701.571, -51.2541)
 5        rotation: Qt.quaternion(0.0242635, 0.762168, -0.243607, 0.599306)
 6        scale: Qt.vector3d(146.232, 146.232, 146.232)
 7        source: "meshes/cube_mesh.mesh"
 8        materials: [ PrincipledMaterial { // added materials from PrincipledMaterial, baseColor, metalness and roughness
 9        baseColor: "#ff5002cc";
10        metalness: 0.6
11        roughness: 0.1736842393875122
12            }]
13
14        PropertyAnimation on eulerRotation.x { // added animation
15                    loops: Animation.Infinite
16                    duration: 5000
17                    to: 360
18                    from: 0
19                }
20        PropertyAnimation on eulerRotation.y {
21                    loops: Animation.Infinite
22                    duration: 5000
23                    to: 360
24                    from: 0
25                }
26    }

In Zeile 7 wird mit source das zugehörige Mesh des Modells referenziert.
Interessant ist, dass die Eigenschaften in den autogenerierten QML-Files meist eine hohe Anzahl an Nachkommastellen aufweisen. Grund hierfür sind die genauen Berechnungen aus den Grafik-Tools wie Blender.
Was passiert nun, wenn das PurpleCube Projekt erstellt und ausgeführt wird? Wir erwarten einen rotierenden, lila Würfel… 🤓

Den Cube habe ich vollständig auf meinem Linux System erstellt. Die Erstellung einer Windows Qt-3D-Applikation sowie einer Android App mit 3D-Inhalten folgen dann in der allerletzten Schwierigkeitsstufe. 😀

Next Level: Der Tie Fighter

Als nächstes sehe ich mir ein etwas aufwändigeres Mesh an. Dazu lade ich mir das Objektmodell vom Star Wars Tie Fighter aus dem Netz herunter.
Ich muss nun in Blender noch einige Anpassungen vornehmen, z.B. passe ich die Beleuchtung und das Material dem Modell an. Sobald alles erledigt ist, exportiere ich das Modell als FBX-Datei aus Blender. Genauso wie beim Cube nutze ich Balsam, um das FBX in QML und in ein zugehöriges Mesh zu übersetzen. Das alles klappt ohne Probleme.
Im Qt Creator angekommen, muss ich nur noch die Kameraperspektive ändern, damit der Tie Fighter auch schön mittig im Blickfeld ist.
Genauso wie bei meinem lila Würfel, enthält das Hauptfenster eine View3D, in dem der Tie Fighter gerendert wird. In dieser 3D-Ansicht, wird die QML-Komponente TieFighter instanziiert, die alle notwendigen Informationen zum Modell enthält. Zudem baue ich wieder eine Rotationsanimation nachträglich in dieses QML-File ein. Das Endresultat sieht dann folgendermaßen aus:

Ich bin vom Endergebnis begeistert. Nun möchte ich herausfinden, ob die Darstellung einer 3D View aus einer 2D View auch so unkompliziert funktioniert.

Zum Abschluss: Purple Cube und Tie Fighter in einer 2D-3D-Applikation

Zum Schluss möchte ich eine 2D-Applikation, die die beiden 3D-Views, den Purple Cube als auch den Tie Fighter enthält, für Windows als auch für Android entwickeln.
Wie gehe ich dazu vor? Zunächst überlege ich mir eine einfache 2D-View, die zwei Button enthält. Der erste Button befindet sich in der oberen Hälfte des Fensters und soll die 3D View vom Purple Cube öffnen. Der zweite Button befindet sich in der unteren Hälfte des Fensters und soll den Tie Fighter laden.
Über die beiden Button sollen die 3D-Views per Maus-Klick gestartet werden. Dafür habe ich mehrere Möglichkeiten zur Auswahl. Es gibt z.B. die Option, eine StackView zu verwenden. Damit würde ich eine zweite Seite erstellen, die nur sichtbar wird, wenn eine der beiden 3D-Ansichten geladen wird. In dieser zweiten View sind dann also die beiden 3D-Views des Tie Fighters bzw. des Cubes integriert. Der Vorteil ist, dass so eine StackView einfach zu implementieren ist und die Seiten nicht erst geladen werden müssen, wenn man sie starten möchte, sie stehen schon beim Start der Applikation zur Verfügung. Andererseits wirkt sich dieser Vorteil bei 3D-Ansichten eher nachteilig aus, da 3D-Modelle einen hohen Arbeitsspeicherverbrauch und eine hohe CPU/GPU-Last erzeugen. Daher habe ich mich für eine andere Variante entschieden. Ich verwende einen Loader, um die 3D-Views zu starten.

 1Loader {
 2        id: loadFighter
 3
 4        width: parent.width
 5        height: parent.height
 6
 7
 8        asynchronous: true
 9        active: false
10        source: Qt.resolvedUrl("fighter.qml")
11
12
13        Rectangle {
14            id: fighter_positioner
15            width: parent.width
16            height: rootWindow.height / 2
17            anchors.bottom: parent.bottom
18            color: systemPalette.window
19
20            Button {
21                id: fighter3d
22                height: 50
23                width: 200
24                text: "3D-View FIGHTER öffnen..."
25                anchors.centerIn: parent
26                highlighted: true
27                onClicked: {
28                    loadFighter.active = true
29                }
30
31
32            }
33
34        }
35
36    }

Das Laden des Tie Fighters per Mausklick auf den Button ist in den Code-Zeilen 27 und 28 beschrieben.
Zusätzlich habe ich dem Projekt neben den beiden QML-Dateien aus Balsam, die beiden Meshes hinzugefügt.
Letztendlich ist Struktur und Aufbau identisch mit den beiden Vorgänger-Projekten, ich habe nur zwei Loader in die Main.qml implementiert, die auf Maus-Klicks auf der Button-Oberfläche reagieren.
Der einzige Unterschied besteht im Fenster-Typen. Ich verwende statt eines “normalen” Window ein ApplicatonWindow, bei dem ich noch einen Header und einen Footer integrieren kann. Der Header dient mir dabei als Home-Button, um wieder zurück in das Hauptmenü mit den beiden Loader-Button zu gelangen.
Um mit Qt eine Android APK zu bauen, brauche ich das Android Kit, welches ein vollständiges Android SDK in den Einstellungen voraussetzt. Da ich bereits Android Studio installiert habe, ist bereits ein Großteil der Abhängigkeiten installiert. Der Rest wird von den SDK Managern aus Qt Creator und Android Studio erledigt.

Das war jetzt ziemlich viel Theorie. Daher zeige ich euch im nachfolgendem Video kurz die Projektstruktur, die Main.qml und das Endergebnis mit dem Android-Kit auf einem Pixel 7 Pro Gerät. Das Deployment der App erfolgt direkt auf meinem Pixel 7, ich verwende in diesem Beispiel keinen Emulator.

Falls jemand weiß, ob es eine Bildschirmspiegelung von einem Android Device im Qt Creator gibt, bitte Bescheid geben!
Ich habe dafür alternativ das Screen Mirroring aus Android Studio genutzt. 🤓

Für das Bauen der Windows Executable nutze ich das Tool windeployqt, das mit den Qt Deployment Tools installiert wurde. Windeployqt sammelt alle notwendigen Abhängigkeiten und Libraries des Qt Projekts ein und legt diese zusammen mit der Executable in einem Ordner ab. Damit sollte dann die Executable auf Windows lauffähig sein. Leider fehlten bei meinem Projekt dennoch einige Abhängigkeiten, die ich dann manuell hinzufügen musste. Als dann aber alle notwendigen Libraries an Ort und Stelle waren, konnte ich mein 3D Projekt auch auf Windows ausführen.

Fazit

Abschließend lässt sich sagen, das Qt mit Quick 3D eine super leichtgewichtige und performante Lösung für die Darstellung von 3D-Modellen zur Verfügung gestellt hat. Insbesondere die Integration von 3D-Objekten in eine 2D-UI und die Möglichkeit plattformübergreifend zu entwickeln, hat mich überzeugt.
Wenn man vorher in Blender sein Modell bearbeitet hat, müssen aber dennoch in Qt einige Anpassungen vorgenommen werde, da diese nicht 1:1 übernommen werden. Das hängt vor allem daran, dass Blender und Qt unterschiedliche Render-Engines verwenden. Ich musste bei meinen Modellen Kamera, Material und Beleuchtung nochmals justieren. Da ich aber relativ einfache Modelle verwendet habe, war der Aufwand nicht allzu groß.
Mittlerweile habe ich mich aber an komplexere Modelle herangewagt. Bei solchen 3D-Objekten bietet sich an, das Austauschformat GLB zu nutzen. Insbesondere dann, wenn Texturen vorhanden sind, da diese direkt in das Format eingebettet werden. Mir ist aufgefallen, dass das beim FBX nicht immer der Fall war. Zudem würde ich für die Anpassungen Qt Design Studio verwenden. Hier werden dann die Änderungen (Kamera, Material, Beleuchtung, etc.) direkt sichtbar.
Ich bin mit den Ergebnissen insgesamt sehr zufrieden. Falls ihr Anmerkungen oder Fragen habt, schreibt mir gerne an sandra@fotonote.de.