Auf dieser Seite

Qt Quick 3D - XR Advanced Touch

Demonstriert die Touch-Bedienung auf gekrümmten Displays und die Touch-Bedienung von 3D-Modellen.

Beispiel mit einem gekrümmten UI-Panel und bearbeitbaren 3D-Objekten

Dieses Beispiel baut auf dem Beispiel „XR Simple Touch “ auf und demonstriert fortgeschrittenere Touch-Interaktionstechniken in „ Qt Quick 3D Xr“:

  • Touch-Interaktion mit 2D-UI-Elementen, die auf einer gekrümmten 3D-Oberfläche gerendert werden.
  • Direkte Touch-Bedienung von 3D-Modellen mithilfe von UV-Koordinaten.
  • Ergreifen und Bearbeiten von 3D-Objekten mithilfe von Pinch-Gesten.
  • Erstellung prozeduraler gekrümmter Meshes für immersive Displays.
  • Erstellen benutzerdefinierter, übermalbarer Texturen mit C++.

Gekrümmtes Display mit Quellobjekt

Eines der Hauptmerkmale dieses Beispiels ist die Darstellung einer 2D-Benutzeroberfläche auf einer gekrümmten Oberfläche. Wir erstellen eine benutzerdefinierte prozedurale Mesh-Komponente, „ CurvedMesh “, die eine gekrümmte Ebene generiert:

// 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 generiert Scheitelpunkte, 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 wölbt. Das Mesh wird in „ partitions “-Segmente unterteilt, um eine glatte Kurve zu erzeugen.

Anschließend wenden wir diese gekrümmte Geometrie auf einen „ Model “ an, der über einen „ PrincipledMaterial “ verfügt, der eine „ 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 “ mit einem „ Rectangle “ und einer komplexen Benutzeroberfläche, einschließlich 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 auf 2D-Koordinaten abgebildet und an die entsprechenden „ Qt Quick “-Elemente weitergeleitet.

Hinweis: Die Eigenschaften „ emissiveMap “ und „ emissiveFactor “ werden verwendet, um den Bildschirm selbstleuchtend erscheinen zu lassen und so die Sichtbarkeit unter verschiedenen Lichtverhältnissen zu verbessern. Die leichte Transparenz (Opazität: 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 beschreibbare 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 ` TextureItem ` verwaltet eine interne ` QImage `, auf die mit ` QPainter` gezeichnet werden kann. Die Bilddaten werden dann als ` QQuick3DTextureData ` bereitgestellt, das in Materialtexturen verwendet werden kann. Dies ermöglicht es uns, dynamische, vom Benutzer veränderbare Texturen zu 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 ausgefeilte Funktionen:

  • Zeitliche Glättung mit adaptiver Filterung zur Reduzierung von Jitter.
  • Erkennung und Behandlung von UV-Koordinaten-Übergängen (beim Zeichnen über Texturgrenzen hinweg).
  • Zustandsverfolgung pro Berührungspunkt für Multi-Touch-Unterstützung.

Touch-Verarbeitung bei 3D-Modellen

Das Beispiel veranschaulicht die direkte Touch-Interaktion mit 3D-Modellen mithilfe der erweiterten 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 greift, werden die Berührungsereignisse an „ TextureItem “ weitergeleitet, das auf der Textur des Modells zeichnet. So können Benutzer direkt auf die Oberfläche des 3D-Objekts malen.

3D-Objekte greifen und manipulieren

Das Beispiel implementiert außerdem eine Greif- und Verschiebungsfunktion mithilfe 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)
        }
    }
}

Während sich die Hand bewegt, folgt das Objekt ihr und behält dabei den ursprünglichen Versatz und die relative Drehung bei. Dies ermöglicht eine natürliche und intuitive Art, 3D-Objekte im Raum zu positionieren.

Farbauswahl und Multi-Touch

Die Farbpalette auf dem gekrümmten Display demonstriert die Multitouch-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)
        }
    }
}

Das „ TapHandler “ reagiert auf Berührungen an verschiedenen Touch-Punkten (gekennzeichnet durch „ pt.id “), sodass jede Hand ihre eigene Farbe haben kann. Wenn eine Farbe ausgewählt wird, aktualisiert es sowohl die Handfarbe als auch die Zeichenfarbe für diese „Touch ID“ über das „ colorChange “-Signal:

y: xrView.referenceSpace === XrView.ReferenceSpaceLocalFloor ? 130 : 0
onColorChange: (col, id) => {
    if (id === 1)
        rightHand.color = col
    else
        leftHand.color = col
}

Dies veranschaulicht, wie sich Mehrbenutzer- oder Mehrhand-Oberflächen erstellen lassen, bei denen verschiedene Berührungspunkte einen unabhängigen Zustand und ein eigenständiges Verhalten aufweisen können.

UI-Steuerelemente und Modellanpassung

Das Beispiel enthält ein vollständiges Bedienfeld, das es Benutzern ermöglicht:

  • verschiedene geometrische Grundformen (Torus, Würfel, Kugel, Zylinder und Kegel) auswählen;
  • den Maßstab des 3D-Modells anzupassen
  • die Stiftbreite zum Zeichnen ändern
  • die Zeichenfläche durch langes Drücken eines Farbfelds löschen.

Diese Steuerelemente veranschaulichen, wie man voll funktionsfähige räumliche Benutzeroberflächen erstellt, die 2D-UI-Paradigmen mit 3D-Interaktion in einer XR-Umgebung kombinieren.

Beispielprojekt @ code.qt.io

© 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.