Qt Quick 3D - XR 高级触摸
演示曲面显示屏上的触摸和 3D 模型触摸处理。

本示例以XR 简单触摸示例为基础,在Qt Quick 3D Xr 中演示更高级的触摸交互技术:
- 与渲染在 3D 曲面上的 2D UI 元素进行触摸交互。
- 使用 UV 坐标对 3D 模型进行直接触摸处理。
- 使用捏合手势抓取和操作 3D 物体。
- 为沉浸式显示创建程序化曲面网格。
- 使用 C++ 构建自定义可绘画纹理。
带源项的曲面显示器
本示例的主要功能之一是在曲面上显示 2D 用户界面。我们创建了一个自定义程序网格组件CurvedMesh ,它可以生成一个曲面:
// 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 继承自ProceduralMesh ,并为曲面生成顶点、法线和 UV 坐标。曲率由radius 属性控制,该属性决定了曲面的弯曲程度。网格被划分为partitions 段,以创建平滑的曲线。
然后,我们将这个弯曲的几何体应用到带有PrincipledMaterial 的Model 中,该 使用sourceItem 渲染 2D 内容:
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 }
sourceItem 属性包含一个标准的Qt Quick Rectangle ,该 具有复杂的用户界面,包括色块、滑块和绘图画布。这些 2D 内容会自动投射到 3D 曲面上。曲面上的触摸事件会被映射回 2D 坐标,并传送到相应的Qt Quick 元素。
注: emissiveMap 和emissiveFactor 用于使屏幕自发光,从而提高在不同光线条件下的可视性。轻微的透明度(不透明度:0.99)可实现 alpha 混合。
自定义可绘画纹理
用户界面中的绘图画布使用自定义的 C++ 类TextureItem ,该类扩展了QQuickPaintedItem ,提供了可用于 3D 材质的可上色纹理:
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; } };
TextureItem 维护一个内部QImage ,可使用QPainter 在其上绘制。然后,图像数据会以QQuick3DTextureData 的形式显示,可用于材质纹理中。这样,我们就可以创建实时更新的动态用户修改纹理。
关键方法是setUv() ,它可以处理 UV 坐标空间中的触摸输入:
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; }
该方法实现了几个复杂的功能:
- 通过自适应滤波进行时间平滑处理,以减少抖动。
- 检测和处理 UV 坐标缠绕(当绘图跨越纹理边界时)。
- 支持多点触摸的每个触摸点状态跟踪。
三维模型上的触摸处理
该示例演示了通过增强型XrGadget 组件与三维模型进行直接触摸交互。当手触摸三维模型时,触摸位置会转换为 UV 坐标并转发到模型:
const touchState = view.touchpointState(root.touchId) const gadget = touchState.model as XrGadget if (gadget) { gadget.handleTouch(touchState.uvPosition, root.touchId, touchState.pressed) }
XrGadget 组件通过其handleTouch 函数接收这些触摸事件:
onTouched: (uvPosition, touchID, pressed) => { if (!isGrabbing) painterItem.setUv(uvPosition.x, uvPosition.y, touchID, pressed); }
当用户没有抓取模型时,触摸事件会被转发到TextureItem ,后者会在模型的纹理上作画。这样,用户就可以直接在三维物体表面作画。
抓取和操纵 3D 物体
本示例还使用捏合手势实现了抓取和移动功能:
XrInputAction { id: grabAction controller: XrInputAction.RightHand actionId: [ XrInputAction.IndexFingerPinch ] onPressedChanged: { if (pressed) grabController.startGrab() } }
当用户用右手做出 "捏 "的手势时,系统会捕捉手与所选对象之间的偏移和旋转:
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) } } }
当手移动时,对象也会跟着移动,同时保持原来的偏移和相对旋转。这样就能以自然直观的方式在空间中定位 3D 物体。
颜色选择和多点触摸
曲面显示屏上的调色板展示了多点触控交互功能。每只手都可以通过点击色块来独立选择颜色:
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) } } }
TapHandler 会对来自不同触控点(通过pt.id 标识)的点击做出响应,从而让每只手都能拥有自己的颜色。当选择一种颜色时,它会通过colorChange 信号更新该触摸 ID 的手部颜色和绘图颜色:
y: xrView.referenceSpace === XrView.ReferenceSpaceLocalFloor ? 130 : 0 onColorChange: (col, id) => { if (id === 1) rightHand.color = col else leftHand.color = col }
这演示了如何构建多用户或多手界面,让不同的触摸点拥有独立的状态和行为。
用户界面控件和模型定制
该示例包含一个完整的控制面板,允许用户
- 选择不同的几何基元(环面、立方体、球面、圆柱和圆锥)。
- 调整 3D 模型的比例。
- 更改绘图笔宽度。
- 通过长按色块清除绘图画布。
这些控件演示了如何在 XR 环境中构建将 2D UI 范例与 3D 交互相结合的全功能空间界面。
© 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.