QtQuick3D实现任意相机视角下鼠标与世界坐标转换
说明/代码
适配Qt 6.3.0版本编译的Qt Quick 3D视图及对应场景,初始实现代码如下:
import QtQuick import QtQml import QtQuick3D import QtQuick3D.Helpers Window { width: 800 height: 600 visible: true property var selectedItem property bool mousePressed: false function multiply_vectors(vec1, vec2) { return Qt.vector3d(vec1.x * vec2.x, vec1.y * vec2.y, vec1.z * vec2.z); } View3D { renderMode: View3D.Inline camera: camera anchors.fill: parent width: 800 height: 600 x: 0 y: 0 id: view environment: SceneEnvironment { clearColor: "black" backgroundMode: SceneEnvironment.Color depthTestEnabled: false depthPrePassEnabled: true } Model { id: rootEntity pickable: true source: "#Cube" materials: PrincipledMaterial { baseColor: "red" roughness: 0.1 } position: Qt.vector3d(25.0, 15.0, -60.0) scale: Qt.vector3d(1.0, 1.0, 1.0) } PerspectiveCamera { id: camera position.z: 330.0 position.y: 0.75 eulerRotation.x: -12 clipNear: 0.0 clipFar: 1600.0 } MouseArea { acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent id: mouseArea onPressed: function (mouse) { var result = view.pick(mouse.x, mouse.y); if (result.objectHit) { selectedItem = result.objectHit; mousePressed = true; } else { mousePressed = false; } } onMouseXChanged: function(mouse) { if (mousePressed) { var viewCoords = view.mapFromGlobal(mouseArea.mapToGlobal(mouse.x, mouse.y)); var sceneCoords = Qt.vector3d(viewCoords.x, viewCoords.y, 0); var worldCoords = view.mapTo3DScene(sceneCoords); worldCoords.z = selectedItem.z selectedItem.position = multiply_vectors(worldCoords, Qt.vector3d(Math.abs(camera.z - selectedItem.z), Math.abs(camera.z - selectedItem.z), 1.0)) } } onReleased: function (mouse) { mousePressed = false } } Component.onCompleted: { camera.lookAt(rootEntity) } } }
场景概述
实现需求为:当鼠标指向立方体并按下按键时,鼠标移动将带动立方体同步移动到3D场景中对应的位置。
当前实现在相机与物体处于同一Z轴视角时运行效果正常,但当相机移动到沿X轴朝向物体等非Z轴视角时,模型会沿X轴异常偏移,无法正确跟随鼠标位置移动。
问题咨询
需要修改onMouseXChanged中的业务逻辑,通过正确的矩阵变换(或等效坐标变换方式),实现无论相机相对于模型的位置如何,模型位置都能始终与鼠标位置精准匹配。
问题根因
原有拖拽逻辑硬编码了「相机视线沿Z轴方向」的前提,换视角后必然失效:
- 直接给坐标转换结果赋值
worldCoords.z = selectedItem.z,仅在相机面朝Z轴时成立,相机旋转后Z轴不再对应视线深度方向,坐标会出现固定偏移 - 用Z轴坐标差做缩放系数换算屏幕坐标到世界坐标的映射,换视角后这个缩放系数和实际透视关系完全不匹配
- 仅监听
onMouseXChanged事件,鼠标纵向移动时不会触发位置更新,本身逻辑存在缺失
正确实现方案
核心思路是拖拽时构建一个过被选中物体、法线与相机视线方向垂直的虚拟平面,每次鼠标移动时,计算鼠标对应3D射线与该平面的交点,将物体移动到交点位置即可。这个方案和相机位置、朝向完全解耦,任意视角下都能保证物体和鼠标位置精准对齐。
具体修改步骤
- 删除原有自定义的
multiply_vectors函数,不需要硬编码向量缩放逻辑 - 在View3D节点下新增三个临时属性,存储拖拽平面的法线、平面点、偏移量参数
- 鼠标按下拾取到物体时,初始化拖拽平面:平面过被选中物体的位置,法线方向与相机前向(视线方向)保持一致
- 将原有的
onMouseXChanged事件替换为onPositionChanged,同时响应鼠标横向、纵向移动,通过射线与平面求交计算物体新位置,增加射线与平面平行的边界判断,避免除零错误
修改后的完整核心代码
import QtQuick import QtQml import QtQuick3D import QtQuick3D.Helpers Window { width: 800 height: 600 visible: true property var selectedItem property bool mousePressed: false View3D { renderMode: View3D.Inline camera: camera anchors.fill: parent id: view environment: SceneEnvironment { clearColor: "black" backgroundMode: SceneEnvironment.Color depthTestEnabled: false depthPrePassEnabled: true } // 拖拽平面临时参数 property vector3d dragPlaneNormal property vector3d dragPlanePoint property real dragPlaneOffset Model { id: rootEntity pickable: true source: "#Cube" materials: PrincipledMaterial { baseColor: "red" roughness: 0.1 } position: Qt.vector3d(25.0, 15.0, -60.0) scale: Qt.vector3d(1.0, 1.0, 1.0) } PerspectiveCamera { id: camera position.z: 330.0 position.y: 0.75 eulerRotation.x: -12 clipNear: 0.1 clipFar: 1600.0 } MouseArea { acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent id: mouseArea onPressed: function (mouse) { var result = view.pick(mouse.x, mouse.y); if (result.objectHit) { selectedItem = result.objectHit; mousePressed = true; // 初始化拖拽平面:过选中物体,法线为相机视线方向 dragPlanePoint = selectedItem.scenePosition; dragPlaneNormal = camera.forward.normalized(); dragPlaneOffset = -Qt.vector3d.dotProduct(dragPlaneNormal, dragPlanePoint); } else { mousePressed = false; } } onPositionChanged: function(mouse) { if (mousePressed && selectedItem) { // 获取鼠标位置对应的世界空间射线 var rayOrigin = view.rayFromViewportPos(mouse.x, mouse.y, 0); var rayEnd = view.rayFromViewportPos(mouse.x, mouse.y, 1); var rayDir = rayEnd.minus(rayOrigin).normalized(); // 射线与平面求交 var vd = Qt.vector3d.dotProduct(dragPlaneNormal, rayDir); if (Math.abs(vd) < 1e-6) return; // 射线与平面平行,跳过计算 var t = -(Qt.vector3d.dotProduct(dragPlaneNormal, rayOrigin) + dragPlaneOffset) / vd; var hitPos = rayOrigin.plus(rayDir.times(t)); // 转换坐标到物体父节点空间,赋值给物体位置 selectedItem.position = selectedItem.parent.mapFromGlobal(hitPos, selectedItem); } } onReleased: function (mouse) { mousePressed = false selectedItem = null } } Component.onCompleted: { camera.lookAt(rootEntity) } } }
实现说明
- 拖拽过程中物体到相机的距离始终保持和按下鼠标时一致,不会出现透视导致的大小跳变
- 所有坐标计算基于世界空间几何关系,无论相机旋转、移动到任何位置,物体都能严格跟随鼠标移动
- 自动适配物体父节点的坐标变换,即使物体放在其他嵌套节点下也能正常拖拽
内容的提问来源于stack exchange,提问作者mike510a
相关产品推荐
相关产品推荐

