我在我们软件的 3D 视图中实现了一个平移工具,它应该很像 Photoshop 或 Acrobat Reader 的抓取工具。也就是说,当鼠标移动时,用户用鼠标抓住的点(单击并按住,然后移动鼠标)保持在鼠标光标下方。
这是一种常见的范例,之前在 SO 上被问过,最好的答案是这个关于 OpenGL 中技术的问题。还有另一个也有一些提示,我一直在阅读这篇内容丰富的CodeProject 文章。(它没有解释它的许多代码示例的变量等,但通过阅读文本,我认为我理解了这项技术。)但是,我有一些实现问题,因为我的 3D 环境的导航设置与那些文章完全不同,并且我正在寻求一些指导。
我的技术——这可能存在根本缺陷,所以请这样说——是:
场景“相机”存储为两个
D3DXVECTOR3
点:眼睛位置和注视点。视图矩阵是这样构造的D3DXMatrixLookAtLH
:const D3DXVECTOR3 oUpVector(0.0f, 1.0f, 0.0f); // Keep up "up", always. D3DXMatrixLookAtLH(&m_oViewMatrix, &m_oEyePos, &m_oLook, &oUpVector);
当按下鼠标按钮时,通过该像素射出一条光线并找到: 被点击的像素的坐标(在未投影的场景/世界空间中);该射线与近平面的交点;以及近平面点与物体之间的距离,即这两点之间的长度。存储这个和鼠标位置,以及原始导航(眼睛和外观)。
// Get the clicked-on point in unprojected (normal) world space D3DXVECTOR3 o3DPos; if (Get3DPositionAtMouse(roMousePos, o3DPos)) { // fails if nothing under the mouse // Mouse location when panning started m_oPanMouseStartPos = roMousePos; // Intersection at near plane (z = 0) of the ray from camera to clicked spot D3DXVECTOR3 oRayVector; CalculateRayFromPixel(m_oPanMouseStartPos, m_oPanPlaneZ0StartPos, oRayVector); // Store original eye and look points m_oPanOriginalEyePos = m_oEyePos; m_oPanOriginalLook = m_oLook; // Store the distance between near plane and the object, and the object position m_dPanPlaneZ0ObjectDist = fabs(D3DXVec3Length(&(o3DPos - m_oPanPlaneZ0StartPos))); m_oPanOriginalObjectPos = o3DPos;
Get3DPositionAtMouse
是一种在鼠标下选取 3D 坐标的已知方法。CalculateRayFromPixel
是一种已知可行的方法,它采用屏幕空间鼠标坐标并投射光线,并用近平面 (Z = 0) 处的光线交点和归一化的光线矢量填充其他两个参数。当鼠标移动时,在新位置投射另一条射线,但使用旧的(原始)视图矩阵。(感谢下面的 Nico 指出这一点。)通过从近平面延伸光线来计算对象应该在哪里,计算对象和近平面之间的距离(这样,原始对象和新对象点应该在平行平面上)近平面。)移动眼睛并看坐标这么多。Eye 和 Look 是根据其原始值(开始平移时)设置的,不同之处在于原始鼠标位置和新鼠标位置。这是为了减少随着鼠标移动而通过粒状(整数)像素移动而增加或减少的任何精度损失,即它每次都计算导航的整体差异。
// Set navigation back to original (as it was when started panning) and cast a ray for the mouse m_oEyePos = m_oPanOriginalEyePos; m_oLook = m_oPanOriginalLook; UpdateView(); D3DXVECTOR3 oRayVector; D3DXVECTOR3 oNewPlaneZPos; CalculateRayFromPixel(roMousePos, oNewPlaneZPos, oRayVector); // Now intersect that ray (ray through the mouse pixel, using the original navigation) // to hit the plane the object is in. Function uses a "line", so start at near plane // and the line is of the length of the far plane away D3DXVECTOR3 oNew3DPos; D3DXPlaneIntersectLine(&oNew3DPos, &m_oPanObjectPlane, &oNewPlaneZPos, &(oRayVector * GetScene().GetFarPlane())); // The eye/look difference /should/ be as simple as: // const D3DXVECTOR3 oDiff = (m_oPanOriginalObjectPos - oNew3DPos); // But that lags and is slow, ie the objects trail behind. I don't know why. What does // work is to scale the from-to difference by the distance from the camera relative to // the whole scene distance const double dDist = D3DXVec3Length(&(oNew3DPos - m_oPanOriginalEyePos)); const double dTotalDist = GetScene().GetFarPlane() - GetScene().GetNearPlane(); const D3DXVECTOR3 oDiff = (m_oPanOriginalObjectPos - oNew3DPos) * (1.0 + (dDist / dTotalDist)); // Adjust the eye and look points by the same amount, so orthogonally changed m_oEyePos = m_oPanOriginalEyePos + oDiff; m_oLook = m_oPanOriginalLook + oDiff;
图表
这张图是我实现这个的工作草图:
并希望比文字更简单地解释上述内容。您可以看到一个移动点,以及相机必须移动的位置以使该点保持在相同的相对位置。点击的点(从相机到对象的光线)就在代表中心像素的直线前方光线的右侧。
问题
但是,正如您可能已经猜到的那样,这并不像我希望的那样工作。我想看到的是被点击的对象随着鼠标光标移动。我实际看到的是对象向鼠标的方向移动,但还不够,即它没有将点击的点保持在光标下方。其次,运动会闪烁和跳跃,有时会抖动多达二十或三十个像素,然后会闪烁回来。如果我oDiff
用不变的东西代替,这不会发生。
任何展示如何使用 DirectX(D3DX、DX 矩阵顺序等)实现此功能的想法或代码示例将不胜感激。
编辑
下面的评论者 Nico 指出,在使用鼠标光标移动的位置计算新位置时,我需要使用原始视图矩阵。这样做有很大帮助,并且对象保持在鼠标位置附近。但是,它仍然不准确。我注意到的是,在屏幕的中心,它是精确的;随着鼠标从中心移动得越来越远,它会越来越多地离开。这似乎也根据物体的距离而改变。通过纯粹的“我不知道自己在做什么”的猜测,我将其缩放了近/远平面的一个因子以及物体的距离,这使它非常接近鼠标光标,但仍然有一些像素距离(1 到,例如,屏幕最边缘的 30 像素,这足以让人感觉不对。)