Auf dieser Seite

Qt Quick 3D - XR Advanced Touch

Demonstriert die Berührung auf gekrümmten Displays und die Handhabung von 3D-Modellberührungen.

Beispiel für ein gebogenes UI-Panel und malbare 3D-Objekte

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.

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.