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 세그먼트로 분할하여 부드러운 곡선을 만듭니다.
그런 다음 이 곡선 지오메트리를 sourceItem 을 사용하여 2D 콘텐츠를 렌더링하는 PrincipledMaterial 을 가진 Model 에 적용합니다:
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 속성에는 색상 견본, 슬라이더, 그리기 캔버스 등 복잡한 UI가 포함된 표준 Qt Quick Rectangle 이 포함됩니다. 이 2D 콘텐츠는 곡면 3D 표면에 자동으로 투사됩니다. 곡면의 터치 이벤트는 다시 2D 좌표로 매핑되어 적절한 Qt Quick 요소로 전달됩니다.
참고: emissiveMap 및 emissiveFactor 은 화면을 자체 조명으로 표시하여 다양한 조명 조건에서 가시성을 향상시키는 데 사용됩니다. 약간의 투명도(불투명도: 0.99)는 알파 블렌딩을 가능하게 합니다.
사용자 지정 페인팅 가능한 텍스처
UI의 드로잉 캔버스는 커스텀 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 은 QPainter 을 사용하여 그릴 수 있는 내부 QImage 을 유지합니다. 그런 다음 이미지 데이터는 머티리얼 텍스처에 사용할 수 있는 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 좌표 래핑 감지 및 처리(드로잉이 텍스처 경계를 넘을 때).
- 멀티터치 지원을 위한 터치 포인트별 상태 추적.
3D 모델에서의 터치 처리
이 예시는 향상된 XrGadget 컴포넌트를 통해 3D 모델과의 직접 터치 상호 작용을 보여줍니다. 손이 3D 모델에 닿으면 터치 위치가 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 객체의 표면에 직접 페인트를 칠할 수 있습니다.
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 }
이는 서로 다른 터치 포인트가 독립적인 상태와 동작을 가질 수 있는 다중 사용자 또는 다중 손 인터페이스를 구축하는 방법을 보여줍니다.
UI 컨트롤 및 모델 사용자 지정
이 예시에는 사용자가 다음을 수행할 수 있는 완전한 제어판이 포함되어 있습니다:
- 다양한 기하학적 기본 요소(원환, 정육면체, 구, 원통, 원뿔)를 선택합니다.
- 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.