1

我在我们软件的 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;
    

图表

这张图是我实现这个的工作草图:

3D 平移草图

并希望比文字更简单地解释上述内容。您可以看到一个移动点,以及相机必须移动的位置以使该点保持在相同的相对位置。点击的点(从相机到对象的光线)就在代表中心像素的直线前方光线的右侧。

问题

但是,正如您可能已经猜到的那样,这并不像我希望的那样工作。我想看到的是被点击的对象随着鼠标光标移动。我实际看到的是对象向鼠标的方向移动,但还不够,即它没有将点击的点保持在光标下方。其次,运动会闪烁和跳跃,有时会抖动多达二十或三十个像素,然后会闪烁回来。如果我oDiff用不变的东西代替,这不会发生。

任何展示如何使用 DirectX(D3DX、DX 矩阵顺序等)实现此功能的想法或代码示例将不胜感激。

编辑

下面的评论者 Nico 指出,在使用鼠标光标移动的位置计算新位置时,我需要使用原始视图矩阵。这样做有很大帮助,并且对象保持在鼠标位置附近。但是,它仍然不准确。我注意到的是,在屏幕的中心,它是精确的;随着鼠标从中心移动得越来越远,它会越来越多地离开。这似乎也根据物体的距离而改变。通过纯粹的“我不知道自己在做什么”的猜测,我将其缩放了近/远平面的一个因子以及物体的距离,这使它非常接近鼠标光标,但仍然有一些像素距离(1 到,例如,屏幕最边缘的 30 像素,这足以让人感觉不对。)

4

2 回答 2

2

这是我解决这个问题的方法。

float fieldOfView = 45.0f;

float halfFOV = (fieldOfView / 2.0f) * (DEGREES_TO_RADIANS);
float distanceToObject = // compute the world space distance from the camera to the object you want to pan
float projectionToWorldScale = distanceToObject * tan( halfFov );
Vector mouseDeltaInScreenSpace = // the delta mouse in pixels that we want to pan

Vector mouseDeltaInProjectionSpace = Vector( mouseDeltaInScreenSpace.x * 2 / windowPixelSizeX, mouseDeltaInScreenSpace.y * 2 / windowPixelSizeY ); // ( the "*2" is because the projection space is from -1 to 1)

// go from normalized device coordinate space to world space (at origin)
Vector cameraDelta = -mouseDeltaInProjectionSpace * projectionToWorldScale;

// now translate your camera by "cameraDelta".

请注意,这适用于视野纵横比为 1,如果垂直视野与水平视野不同,我认为您必须将“比例”分解为单独的 x 和 y 分量

另外,您提到了“查看”向量。我不确定我的数学需要如何改变,因为我的相机总是直视 z 轴。

于 2013-02-28T19:15:07.657 回答
1

一个问题是您计算新的 3d 位置。我不确定这是否是根本原因,但您可以尝试一下。如果没有帮助,请发表评论。

问题是您的偏移矢量不平行于 znear 平面。这是因为两条光线不平行。因此,如果 znear 后面的长度相同,则端点到 znear 平面的距离不可能相等。

您可以使用相交线定理计算偏移矢量。如果 zNearA 和 zNearB 分别是 znear 平面与射线 A 和射线 B 的交点,则定理表明:

Length(original_position - cam_position) / Length(offset_vector) = Length(zNearA - cam_position) / Length(zNearB - zNearA)

因此

offset_vector = Length(original_position - cam_position) / Length(zNearA - cam_position) * (zNearB - zNearA)

然后,您可以确保在平行于 znear 平面的直线上移动。

试试看它是否有帮助。

于 2013-02-12T23:36:58.040 回答