使用DirectX进行3D透视'抓取'平移

时间:2013-02-11 20:30:36

标签: math 3d directx direct3d

我正在我们软件的3D视图中实现一个平移工具,它应该像Photoshop或Acrobat Reader的抓取工具一样工作。也就是说,当鼠标移动时,用户用鼠标抓住的点(点击并保持,然后移动鼠标)停留在鼠标光标下。

这是一个常见的范例,之前被问过best answer being to this question about the technique in OpenGL。有another that also has some hints,我一直在阅读very informative CodeProject article。 (它没有解释它的许多代码示例的变量等,但是从阅读文本我认为我理解该技术。)但是,我有一些实现问题,因为我的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是一种已知的ok方法,可以在鼠标下选择一个3D坐标。 CalculateRayFromPixel是一种已知的ok方法,它接收屏幕空间鼠标坐标并投射光线,并使用近平面(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 panning sketch

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

问题

但是,正如你可能已经猜到的那样,这并不像我希望的那样有效。我想看到的是点击的对象随着鼠标光标移动。我实际看到的是对象在鼠标方向上移动,但还不够,即它没有保持光标下的点击点。其次,运动会闪烁并跳跃,有时会抖动多达二十或三十个像素,然后闪烁回来。如果我用常量替换oDiff,则不会发生这种情况。

任何有关如何使用DirectX(D3DX,DX矩阵顺序等)实现此功能的想法或代码示例都将非常感激。

修改

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

2 个答案:

答案 0 :(得分: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平面的线。

试一试,看看是否有帮助。

答案 1 :(得分:1)

以下是我如何解决这个问题。

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".

注意这适用于视场apsect比率为1的情况,我认为如果垂直视野不同于水平视野,则必须将“比例”分解为单独的x和y分量

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