Qt Quick 3D - XR Advanced Touch
Demonstriert die Berührung auf gekrümmten Displays und die Handhabung von 3D-Modellberührungen.

Dieses Beispiel baut auf dem XR Simple Touch-Beispiel auf und demonstriert fortgeschrittene Touch-Interaktionstechniken in Qt Quick 3D Xr:
- Berührungsinteraktion mit 2D-UI-Elementen, die auf einer gekrümmten 3D-Oberfläche gerendert werden.
- Direkte Berührungsinteraktion auf 3D-Modellen unter Verwendung von UV-Koordinaten.
- Greifen und Manipulieren von 3D-Objekten mit Pinch-Gesten.
- Erstellen prozeduraler gekrümmter Meshes für immersive Displays.
- Erstellung benutzerdefinierter malbarer Texturen mit C++.
Gekrümmte Anzeige mit Source Item
Eines der Hauptmerkmale dieses Beispiels ist die Anzeige einer 2D-Benutzeroberfläche auf einer gekrümmten Oberfläche. Wir erstellen eine benutzerdefinierte prozedurale Mesh-Komponente, CurvedMesh, die eine gekrümmte Ebene erzeugt:
// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick3D import QtQuick3D.Helpers ProceduralMesh { property real partitions: 50 property real segments: 1 property real radius: 50.0 property real height: 50.0 property real width: 80 property var meshArrays: generateMesh(partitions , width, height, radius) positions: meshArrays.verts normals: meshArrays.normals uv0s: meshArrays.uvs indexes: meshArrays.indices function generateMesh(partitions : real, width: real, height: real, radius: real) : var { let verts = [] let normals = [] let uvs = [] let indices = [] // width = angleSpan * radius const angleSpan = width / radius for (let i = 0; i <= partitions ; ++i) { for (let j = 0; j <= 1; ++j) { const u = i / partitions ; const v = j; const x = u - 0.5 // centered const angle = x * angleSpan const posZ = radius - radius * Math.cos(angle) const posY = height * v const posX = radius * Math.sin(angle) verts.push(Qt.vector3d(posX, posY, posZ)); let normal = Qt.vector3d(0 - posX, 0, radius - posZ).normalized(); normals.push(normal); uvs.push(Qt.vector2d(u, v)); } } for (let i = 0; i < partitions ; ++i) { let a = (segments + 1) * i; let b = (segments + 1) * (i + 1); let c = (segments + 1) * (i + 1) + 1; let d = (segments + 1) * i + 1; // Generate two triangles for each quad in the mesh // Adjust order to be counter-clockwise indices.push(a, b, d); indices.push(b, c, d); } return { verts: verts, normals: normals, uvs: uvs, indices: indices } } }
CurvedMesh erbt von ProceduralMesh und erzeugt Eckpunkte, Normalen und UV-Koordinaten für eine gekrümmte Oberfläche. Die Krümmung wird durch die Eigenschaft radius gesteuert, die bestimmt, wie stark sich die Oberfläche krümmt. Das Netz wird in partitions Segmente unterteilt, um eine glatte Kurve zu erzeugen.
Diese gekrümmte Geometrie wird dann auf ein Model mit einem PrincipledMaterial angewendet, das ein sourceItem zum Rendern von 2D-Inhalten verwendet:
Model { objectName: "curved screen" x: 0 y: -40 z: -50 // mesh origin is at bottom of screen; scene origin is at eye height geometry: CurvedMesh { width: height * 32 / 9 height: 40 radius: 80 } visible: true pickable: true materials: PrincipledMaterial { baseColorMap: Texture { id: screenTexture sourceItem: Rectangle { id: uiItem width: height * 32 / 9 height: 500 property real dim: Math.min(width, height) color: "#dd000000" ColumnLayout { id: leftItem anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: centralItem.left anchors.margins: 15 spacing: 10 GridLayout { rows: 3 columns: 3 columnSpacing: 5 rowSpacing: 5 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter ColorSwatch { color: "red" } ColorSwatch { color: "orange" } ColorSwatch { color: "yellow" } ColorSwatch { color: "green" } ColorSwatch { color: "blue" } ColorSwatch { color: "purple" } ColorSwatch { color: "white" } ColorSwatch { color: "gray" } ColorSwatch { color: "black" } } RowLayout { Label { text: "Stroke width: " + penWidthSlider.value.toFixed(1) } Slider { id: penWidthSlider Layout.fillWidth: true from: 1 to: 30 value: 10 onValueChanged: painterItem.setPenWidth(value) } } } Rectangle { id: centralItem width: uiItem.dim - 10 height: uiItem.dim - 10 anchors.centerIn: parent color: "gray" TextureItem { id: painterItem anchors.fill: parent anchors.margins: 2 MultiPointTouchArea { anchors.fill: parent onPressed: (list) => { for (const pt of list) { painterItem.setPoint(pt.x, pt.y, pt.pointId, pt.pressed) } } onUpdated: (list) => { for (const pt of list) { painterItem.setPoint(pt.x, pt.y, pt.pointId, pt.pressed) } } onReleased: (list) => { for (const pt of list) { painterItem.setPoint(pt.x, pt.y, pt.pointId, pt.pressed) } } } Component.onCompleted: { // Let initial colors be the same as the hand colors setColor("green", 1) setColor("red", 2) } } } Item { id: rightItem anchors.left: centralItem.right anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right GroupBox { anchors.centerIn: parent title: "3D Model" ColumnLayout { ColumnLayout { id: radioButtons RadioButton { text: "Torus" checked: true } RadioButton { text: "Cube" } RadioButton { text: "Sphere" } RadioButton { text: "Cylinder" } RadioButton { text: "Cone" } } RowLayout { Label { text: "Scale: " + scaleSlider.value.toFixed(2) } Slider { id: scaleSlider Layout.fillWidth: true from: 0.01 to: 2 value: 0.25 } } } ButtonGroup { buttons: radioButtons.children onCheckedButtonChanged: { selectableModel.meshName = checkedButton.text } } } } } } emissiveMap: screenTexture emissiveFactor: Qt.vector3d(0.8, 0.8, 0.8) } opacity: 0.99 // enable alpha blending }
Die Eigenschaft sourceItem enthält eine Standard Qt Quick Rectangle mit einer komplexen Benutzeroberfläche mit Farbfeldern, Schiebereglern und einer Zeichenfläche. Dieser 2D-Inhalt wird automatisch auf die gekrümmte 3D-Oberfläche projiziert. Berührungsereignisse auf der gekrümmten Oberfläche werden in 2D-Koordinaten zurückverwandelt und an die entsprechenden Qt Quick Elemente weitergeleitet.
Hinweis: emissiveMap und emissiveFactor werden verwendet, um den Bildschirm selbst zu beleuchten und die Sichtbarkeit bei unterschiedlichen Lichtverhältnissen zu verbessern. Die leichte Transparenz (Deckkraft: 0,99) ermöglicht Alpha-Blending.
Benutzerdefinierte malbare Textur
Die Zeichenfläche in der Benutzeroberfläche verwendet eine benutzerdefinierte C++-Klasse, TextureItem, die QQuickPaintedItem erweitert, um eine malbare Textur bereitzustellen, die in 3D-Materialien verwendet werden kann:
class TextureItem : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(const QQuick3DTextureData* textureData READ textureData NOTIFY textureDataChanged FINAL) QML_ELEMENT public: TextureItem(QQuickItem *parent = nullptr); void paint(QPainter *painter) override; const QQuick3DTextureData *textureData() const; Q_INVOKABLE void setPoint(float x, float y, int id, bool pressed); Q_INVOKABLE void setUv(float u, float v, int id, bool pressed); Q_INVOKABLE void clear(const QColor &color); Q_INVOKABLE void setColor(const QColor &newColor, int id = 0); Q_INVOKABLE void setPenWidth(qreal newPenWidth); Q_INVOKABLE void resetPoint(int id); public slots: void updateImage(); signals: void textureDataChanged(); void colorChanged(); void penWidthChanged(); private: struct TouchPointState { QColor color = "red"; qreal penWidth = 10; // unused double alpha = 0.5; // smoothing factor (0..1) bool initialized = false; QPointF prevFiltered; QPointF prevRaw; qint64 prevTime = 0; // timestamp in milliseconds void reset() { initialized = false; prevTime = 0; } QPointF filter(const QPointF &unfiltered, qint64 timestamp) { prevRaw = unfiltered; if (!initialized) { prevFiltered = unfiltered; prevTime = timestamp; initialized = true; } else { // Adjust alpha based on time delta qint64 dt = timestamp - prevTime; double adaptiveAlpha = qMin(1.0, alpha * (dt / 16.0)); // 16ms = ~60fps baseline prevFiltered.setX(adaptiveAlpha * unfiltered.x() + (1 - adaptiveAlpha) * prevFiltered.x()); prevFiltered.setY(adaptiveAlpha * unfiltered.y() + (1 - adaptiveAlpha) * prevFiltered.y()); prevTime = timestamp; } return prevFiltered; } };
Die Klasse TextureItem verwaltet ein internes QImage, auf das mit QPainter gezeichnet werden kann. Die Bilddaten werden dann als QQuick3DTextureData offengelegt und können in Materialtexturen verwendet werden. Auf diese Weise können wir dynamische, vom Benutzer geänderte Texturen erstellen, die in Echtzeit aktualisiert werden.
Die wichtigste Methode ist setUv(), die Berührungseingaben im UV-Koordinatenraum verarbeitet:
void TextureItem::setUv(float u, float v, int id, bool pressed) { auto &s = state[id]; bool inRange = u >= 0 && u <= 1 && v >=0 && v <= 1; if (!pressed || !inRange) { s.reset(); return; }
Diese Methode implementiert mehrere anspruchsvolle Funktionen:
- Zeitliche Glättung mit adaptiver Filterung zur Reduzierung von Jitter.
- Erkennung und Behandlung von UV-Koordinaten-Wrapping (beim Zeichnen über Texturgrenzen hinweg).
- Zustandsverfolgung pro Berührungspunkt für Multitouch-Unterstützung.
Handhabung von Berührungen auf 3D-Modellen
Das Beispiel demonstriert die direkte Berührungsinteraktion mit 3D-Modellen über die erweiterte Komponente XrGadget. Wenn eine Hand ein 3D-Modell berührt, wird die Berührungsposition in UV-Koordinaten umgewandelt und an das Modell weitergeleitet:
const touchState = view.touchpointState(root.touchId) const gadget = touchState.model as XrGadget if (gadget) { gadget.handleTouch(touchState.uvPosition, root.touchId, touchState.pressed) }
Die Komponente XrGadget empfängt diese Berührungsereignisse über ihre Funktion handleTouch:
onTouched: (uvPosition, touchID, pressed) => { if (!isGrabbing) painterItem.setUv(uvPosition.x, uvPosition.y, touchID, pressed); }
Wenn der Benutzer das Modell nicht anfasst, werden die Berührungsereignisse an die TextureItem weitergeleitet, die auf die Textur des Modells zeichnet. So kann der Benutzer direkt auf der Oberfläche des 3D-Objekts malen.
Greifen und Manipulieren von 3D-Objekten
Das Beispiel implementiert auch eine Grab-and-Move-Funktionalität mit Hilfe der Pinch-Geste:
XrInputAction { id: grabAction controller: XrInputAction.RightHand actionId: [ XrInputAction.IndexFingerPinch ] onPressedChanged: { if (pressed) grabController.startGrab() } }
Wenn der Benutzer mit der rechten Hand eine Pinch-Geste ausführt, erfasst das System den Versatz und die Drehung zwischen der Hand und dem ausgewählten Objekt:
XrController { id: grabController controller: XrController.RightHand property vector3d grabOffset property quaternion grabRotation function startGrab() { const scenePos = selectableModel.scenePosition const sceneRot = selectableModel.sceneRotation grabOffset = scenePos.minus(scenePosition) grabRotation = rotation.inverted().times(sceneRot) } onRotationChanged: { if (isGrabbing) { let newPos = scenePosition.plus(grabOffset) let newRot = sceneRotation.times(grabRotation) selectableModel.setPosition(newPos) selectableModel.setRotation(newRot) } } }
Wenn sich die Hand bewegt, folgt das Objekt unter Beibehaltung des ursprünglichen Versatzes und der relativen Drehung. Auf diese Weise lassen sich 3D-Objekte auf natürliche und intuitive Weise im Raum positionieren.
Farbauswahl und Multi-Touch
Die Farbpalette auf dem gebogenen Display demonstriert die Multi-Touch-Interaktion. Jede Hand kann unabhängig eine Farbe auswählen, indem sie auf ein Farbfeld tippt:
component ColorSwatch: Rectangle { implicitWidth: 100 implicitHeight: 100 function perceivedBrightness() { return 0.2 * color.r + 0.7 * color.g + 0.1 * color.b } function contrastColor() { if (perceivedBrightness() > 0.6) return Qt.darker(color, 1.1) const h = color.hslHue const s = color.hslSaturation const l = color.hslLightness + 0.2 return Qt.hsla(h, s, l, 1.0) } border.color: contrastColor() border.width: csth.pressed ? 8 : 4 property real penWidth: 5 TapHandler { id: csth dragThreshold: 1000 onTapped: (pt) => { painterItem.setColor(parent.color, pt.id) colorChange(parent.color, pt.id) } onLongPressed: { painterItem.clear(parent.color) } } }
Die TapHandler reagiert auf Berührungen von verschiedenen Berührungspunkten (gekennzeichnet durch pt.id), so dass jede Hand ihre eigene Farbe haben kann. Wenn eine Farbe ausgewählt wird, werden sowohl die Farbe der Hand als auch die Farbe der Zeichnung für diese Touch-ID über das Signal colorChange aktualisiert:
y: xrView.referenceSpace === XrView.ReferenceSpaceLocalFloor ? 130 : 0 onColorChange: (col, id) => { if (id === 1) rightHand.color = col else leftHand.color = col }
Dies zeigt, wie man Multi-User- oder Multi-Hand-Schnittstellen erstellt, bei denen verschiedene Berührungspunkte unabhängige Zustände und Verhaltensweisen haben können.
UI-Steuerelemente und Modellanpassung
Das Beispiel enthält ein komplettes Bedienfeld, mit dem Benutzer Folgendes tun können
- Auswahl verschiedener geometrischer Primitive (Torus, Würfel, Kugel, Zylinder und Kegel).
- Einstellen des Maßstabs des 3D-Modells.
- Ändern Sie die Stiftbreite zum Zeichnen.
- Löschen der Zeichenfläche durch langes Drücken eines Farbfeldes.
Diese Steuerelemente zeigen, wie man räumliche Schnittstellen mit vollem Funktionsumfang erstellen kann, die 2D-UI-Paradigmen mit 3D-Interaktion in einer XR-Umgebung kombinieren.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.