You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 03:01:08