Qt Quick 3D - XR Advanced Touch
Présente les interactions tactiles sur des écrans incurvés et la manipulation tactile de modèles 3D.

Cet exemple s'appuie sur l'exemple « XR Simple Touch » pour présenter des techniques d'interaction tactile plus avancées dans Qt Quick 3D Xr :
- Interaction tactile avec des éléments d’interface utilisateur 2D affichés sur une surface 3D incurvée.
- Manipulation tactile directe de modèles 3D à l'aide de coordonnées UV.
- Saisie et manipulation d’objets 3D à l’aide de gestes de pincement.
- Création de maillages courbes procéduraux pour les écrans immersifs.
- Création de textures personnalisées pouvant être peintes avec C++.
Affichage courbe avec élément source
L’une des principales fonctionnalités de cet exemple est l’affichage d’une interface utilisateur 2D sur une surface courbe. Nous créons un composant de maillage procédural personnalisé, CurvedMesh, qui génère un plan courbe :
// 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 hérite de ProceduralMesh et génère des sommets, des normales et des coordonnées UV pour une surface courbe. La courbure est contrôlée par la propriété radius, qui détermine l’amplitude de la courbure de la surface. Le maillage est divisé en segments partitions afin de créer une courbe lisse.
Nous appliquons ensuite cette géométrie courbe à un « Model » doté d’un « PrincipledMaterial » qui utilise un « sourceItem » pour afficher du contenu 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
}La propriété « sourceItem » contient un « Qt Quick » standard « Rectangle » doté d’une interface utilisateur complexe comprenant des nuanciers, des curseurs et un canevas de dessin. Ce contenu 2D est automatiquement projeté sur la surface 3D courbe. Les événements tactiles sur la surface courbe sont reconvertis en coordonnées 2D et transmis aux éléments « Qt Quick » appropriés.
Remarque : les propriétés ` emissiveMap ` et ` emissiveFactor ` sont utilisées pour donner à l'écran un aspect « auto-éclairé », améliorant ainsi la visibilité dans différentes conditions d’éclairage. La légère transparence (opacité : 0,99) permet le mélange alpha.
Texture personnalisable
Le canevas de dessin de l’interface utilisateur utilise une classe C++ personnalisée, TextureItem, qui étend QQuickPaintedItem afin de fournir une texture personnalisable pouvant être utilisée dans les matériaux 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;
}
};La classe ` TextureItem ` gère une instance interne de ` QImage ` sur laquelle il est possible de dessiner à l’aide de ` QPainter`. Les données d’image sont ensuite exposées sous la forme d’un ` QQuick3DTextureData ` pouvant être utilisé dans les textures de matériaux. Cela nous permet de créer des textures dynamiques, modifiables par l’utilisateur, qui s’actualisent en temps réel.
La méthode principale est ` setUv()`, qui gère les entrées tactiles dans l'espace de coordonnées 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;
}Cette méthode met en œuvre plusieurs fonctionnalités sophistiquées :
- Lissage temporel avec filtrage adaptatif pour réduire les saccades.
- Détection et gestion du bouclage des coordonnées UV (lorsque le tracé dépasse les limites de la texture).
- Suivi de l'état par point de contact pour la prise en charge du multitouch.
Gestion des interactions tactiles sur les modèles 3D
Cet exemple illustre l'interaction tactile directe avec des modèles 3D grâce au composant amélioré « XrGadget ». Lorsqu'une main touche un modèle 3D, la position du toucher est convertie en coordonnées UV puis transmise au modèle :
const touchState = view.touchpointState(root.touchId)
const gadget = touchState.model as XrGadget
if (gadget) {
gadget.handleTouch(touchState.uvPosition, root.touchId, touchState.pressed)
}Le composant ` XrGadget ` reçoit ces événements tactiles via sa fonction ` handleTouch ` :
onTouched: (uvPosition, touchID, pressed) => {
if (!isGrabbing)
painterItem.setUv(uvPosition.x, uvPosition.y, touchID, pressed);
}Lorsque l’utilisateur ne saisit pas le modèle, les événements tactiles sont transmis à la fonction ` TextureItem`, qui dessine sur la texture du modèle. Cela permet aux utilisateurs de peindre directement sur la surface de l’objet 3D.
Saisie et manipulation d’objets 3D
L’exemple implémente également une fonctionnalité de saisie et de déplacement à l’aide du geste de pincement :
XrInputAction {
id: grabAction
controller: XrInputAction.RightHand
actionId: [ XrInputAction.IndexFingerPinch ]
onPressedChanged: {
if (pressed)
grabController.startGrab()
}
}Lorsque l’utilisateur effectue un geste de pincement avec sa main droite, le système capture le décalage et la rotation entre la main et l’objet sélectionné :
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)
}
}
}À mesure que la main se déplace, l’objet la suit tout en conservant le décalage et la rotation relative d’origine. Cela offre un moyen naturel et intuitif de positionner des objets 3D dans l’espace.
Sélection des couleurs et multitouch
La palette de couleurs affichée sur l’écran incurvé illustre l’interaction multi-touch. Chaque main peut sélectionner une couleur de manière indépendante en tapant sur un échantillon de couleur :
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)
}
}
}L’ TapHandler e réagit aux tapotements provenant de différents points de contact (identifiés par pt.id), ce qui permet à chaque main d’avoir sa propre couleur. Lorsqu’une couleur est sélectionnée, elle met à jour à la fois la couleur de la main et la couleur de dessin pour cet identifiant tactile via le signal colorChange:
y: xrView.referenceSpace === XrView.ReferenceSpaceLocalFloor ? 130 : 0
onColorChange: (col, id) => {
if (id === 1)
rightHand.color = col
else
leftHand.color = col
}Ceci illustre comment créer des interfaces multi-utilisateurs ou multi-mains où différents points de contact peuvent avoir un état et un comportement indépendants.
Contrôles de l’interface utilisateur et personnalisation du modèle
L’exemple comprend un panneau de contrôle complet qui permet aux utilisateurs de :
- Sélectionner différentes primitives géométriques (tore, cube, sphère, cylindre et cône).
- Ajuster l’échelle du modèle 3D.
- Modifier l'épaisseur du trait de dessin.
- Effacer la zone de dessin en appuyant longuement sur un échantillon de couleur.
Ces commandes montrent comment créer des interfaces spatiales complètes qui combinent les paradigmes d'interface utilisateur 2D avec l'interaction 3D dans un environnement XR.
© 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.