实现一个复杂的基于旋转的相机

Posted

技术标签:

【中文标题】实现一个复杂的基于旋转的相机【英文标题】:Implementing a complex rotation-based camera 【发布时间】:2012-07-13 07:59:56 【问题描述】:

我正在实现一个用于空间可视化的 3D 引擎,并且正在编写一个具有以下导航功能的相机:

旋转相机(即类似于旋转头部) 围绕任意 3D 点(空间中的一个点,可能不在屏幕中心;相机需要围绕该点旋转,保持相同的相对观察方向,即观察方向也会发生变化。这不会直接看选择的旋转点) 在相机平面中平移(因此在垂直于相机观察矢量的平面中向上/向下或向左/向右移动)

相机不应该滚动 - 也就是说,“向上”仍然是向上的。因此,我用一个位置和两个角度表示相机,围绕 X 和 Y 轴旋转(Z 将是滚动的。)然后使用相机位置和这两个角度重新计算视图矩阵。这对于平移和旋转眼睛非常有用,但对于围绕任意点旋转。相反,我得到以下行为:

眼睛本身明显地向上或向下移动而不是应该的 m_dRotationX 为 0 或 pi 时,眼睛根本不会上下移动。 (云台锁?如何避免这种情况?) 当m_dRotationX 介于 pi 和 2pi 之间时,眼睛的旋转会反转(更改旋转会使它在应该往下看时往上看,在往上看时往下看)。

(a) 是什么导致了这种旋转“漂移”?

这可能是gimbal lock。如果是这样,对此的标准答案是“使用四元数表示旋转”,在 SO 上多次说过(1、2、3 例如),但不幸的是 没有具体细节 (example。这是the best answer 到目前为止我发现的;它很少见。)我一直在努力使用结合上述两种旋转类型的四元数来实现相机。事实上,我正在使用两个旋转构建一个四元数,但下面的评论者说没有理由 - 立即构建矩阵就可以了。

当围绕一个点旋转时更改 X 和 Y 旋转(表示相机的观察方向)时会发生这种情况,但不会仅在直接更改旋转时发生,即围绕自身旋转相机。对我来说,这没有意义。这是相同的值。

(b) 不同的方法(例如四元数)是否更适合这款相机?如果是这样,我如何实现上述所有三个相机导航功能?

如果另一种方法会更好,那么请考虑提供该方法的具体实施示例。 (我使用的是 DirectX9 和 C++,以及 SDK 提供的 D3DX* 库。)在第二种情况下,我将在几天内添加并奖励赏金,届时我可以在问题中添加一个。这听起来像是我在抢先一步,但我的时间很短,需要快速实施或解决这个问题(这是一个截止日期很紧的商业项目。)详细的答案也将改进 SO 档案,因为大多数到目前为止我读过的相机答案对代码很简单。

感谢您的帮助:)


一些说明

感谢 cmets 到目前为止的回答!我将尝试澄清有关该问题的一些事项:

只要其中之一发生变化,就会根据相机位置和两个角度重新计算视图矩阵。矩阵本身永远不会累积(即更新) - 它会重新计算。但是,相机位置和两个角度变量是累加的(例如,每当鼠标移动时,一个或两个角度都会有少量添加或减去,具体取决于鼠标上下移动的像素数和/或在屏幕上左右显示。)

评论者JCooper 表示我遇到了云台锁定问题,我需要:

在您的变换上添加另一个旋转,将 eyePos 旋转为 在应用转换之前完全在 y-z 平面中,并且 然后另一个旋转将其向后移动。绕着旋转 y 轴在应用之前和之后的以下角度 yaw-pitch-roll 矩阵(其中一个角度需要取反; 尝试是决定哪种方法的最快方法)。 double fixAngle = atan2(oEyeTranslated.z,oEyeTranslated.x);

不幸的是,当按照描述执行此操作时,由于其中一个旋转,我的眼睛以非常快的速度从场景上方射出。我确信我的代码只是这个描述的一个糟糕的实现,但我仍然需要更具体的东西。一般来说,我发现算法的非特定文本描述不如注释的、解释的实现有用。 我正在为与以下代码集成的具体工作示例添加赏金(即也与其他导航方法)。这是因为我想理解解决方案,还有一些可行的东西,因为我需要快速实施一些可行的东西,因为我的截止日期很紧。

如果您用算法的文字描述回答,请确保它足够详细以实现(“围绕 Y 旋转,然后变换,然后旋转回来”可能对您有意义,但缺乏了解您的详细信息意思是。Good answers are clear, signposted, will allow others to understand even with a different basis, are 'solid weatherproof information boards.')

反过来,我试图清楚地描述问题,如果我可以更清楚地告诉我。


我当前的代码

要实现上述三个导航功能,在鼠标移动事件中根据光标移动的像素移动:

// Adjust this to change rotation speed when dragging (units are radians per pixel mouse moves)
// This is both rotating the eye, and rotating around a point
static const double dRotatePixelScale = 0.001;
// Adjust this to change pan speed (units are meters per pixel mouse moves)
static const double dPanPixelScale = 0.15;

switch (m_eCurrentNavigation) 
    case ENavigation::eRotatePoint: 
        // Rotating around m_oRotateAroundPos
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;

        // To rotate around the point, translate so the point is at (0,0,0) (this makes the point
        // the origin so the eye rotates around the origin), rotate, translate back
        // However, the camera is represented as an eye plus two (X and Y) rotation angles
        // This needs to keep the same relative rotation.

        // Rotate the eye around the point
        const D3DXVECTOR3 oEyeTranslated = m_oEyePos - m_oRotateAroundPos;
        D3DXMATRIX oRotationMatrix;
        D3DXMatrixRotationYawPitchRoll(&oRotationMatrix, dX, dY, 0.0);
        D3DXVECTOR4 oEyeRotated;
        D3DXVec3Transform(&oEyeRotated, &oEyeTranslated, &oRotationMatrix);
        m_oEyePos = D3DXVECTOR3(oEyeRotated.x, oEyeRotated.y, oEyeRotated.z) + m_oRotateAroundPos;

        // Increment rotation to keep the same relative look angles
        RotateXAxis(dX);
        RotateYAxis(dY);
        break;
    
    case ENavigation::ePanPlane: 
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dPanPixelScale;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dPanPixelScale;
        m_oEyePos += GetXAxis() * dX; // GetX/YAxis reads from the view matrix, so increments correctly
        m_oEyePos += GetYAxis() * -dY; // Inverted compared to screen coords
        break;
    
    case ENavigation::eRotateEye: 
        // Rotate in radians around local (camera not scene space) X and Y axes
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
        RotateXAxis(dX);
        RotateYAxis(dY);
        break;
    

RotateXAxisRotateYAxis 方法非常简单:

void Camera::RotateXAxis(const double dRadians) 
    m_dRotationX += dRadians;
    m_dRotationX = fmod(m_dRotationX, 2 * D3DX_PI); // Keep in valid circular range


void Camera::RotateYAxis(const double dRadians) 
    m_dRotationY += dRadians;

    // Limit it so you don't rotate around when looking up and down
    m_dRotationY = std::min(m_dRotationY, D3DX_PI * 0.49); // Almost fully up
    m_dRotationY = std::max(m_dRotationY, D3DX_PI * -0.49); // Almost fully down

并由此生成视图矩阵:

void Camera::UpdateView() const 
    const D3DXVECTOR3 oEyePos(GetEyePos());
    const D3DXVECTOR3 oUpVector(0.0f, 1.0f, 0.0f); // Keep up "up", always.

    // Generate a rotation matrix via a quaternion
    D3DXQUATERNION oRotationQuat;
    D3DXQuaternionRotationYawPitchRoll(&oRotationQuat, m_dRotationX, m_dRotationY, 0.0);
    D3DXMATRIX oRotationMatrix;
    D3DXMatrixRotationQuaternion(&oRotationMatrix, &oRotationQuat);

    // Generate view matrix by looking at a point 1 unit ahead of the eye (transformed by the above
    // rotation)
    D3DXVECTOR3 oForward(0.0, 0.0, 1.0);
    D3DXVECTOR4 oForward4;
    D3DXVec3Transform(&oForward4, &oForward, &oRotationMatrix);
    D3DXVECTOR3 oTarget = oEyePos + D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z); // eye pos + look vector = look target position
    D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector);

【问题讨论】:

我认为这个错误来自舍入/截断错误。如果你增加值的范围,那么如果我是对的,你就会使漂移变小 你应该使用比你需要的更多的有效数字 最后一位是舍入/截断的来源 @tuğrulbüyükışık:浮点错误是可能的,但我不确定你的意思是什么?角度存储为双精度并且是“整个”旋转(每次鼠标移动增加/调整矩阵会导致更多错误;这里每次都从头开始重新计算。)我也不确定这将如何导致角度-依赖漂移。 【参考方案1】:

在我看来,考虑到您形成视图矩阵的方式,“滚动”是不可能的。不管所有其他代码(其中一些看起来有点滑稽),调用D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector); 应该在将[0,1,0] 作为“向上”向量给出时创建一个没有滚动的矩阵,除非oTarget-oEyePos 恰好与向上平行向量。情况似乎并非如此,因为您将 m_dRotationY 限制在 (-.49pi,+.49pi) 范围内。

也许您可以澄清一下您是如何知道“滚动”正在发生的。您是否有地平面并且该地平面的水平线偏离水平线?

顺便说一句,在UpdateView 中,D3DXQuaternionRotationYawPitchRoll 似乎完全没有必要,因为您立即转身将其更改为矩阵。只需像在鼠标事件中一样使用D3DXMatrixRotationYawPitchRoll。四元数用于相机,因为它们是一种方便的方式来累积眼睛坐标中发生的旋转。由于您仅以严格的顺序使用两个旋转轴,因此累积角度的方式应该没问题。 (0,0,1) 的向量变换也不是必需的。 oRotationMatrix 应该已经在 (_31,_32,_33) 条目中包含这些值。


更新

鉴于它不是滚动,这就是问题所在:您创建一个旋转矩阵以在 world 坐标中移动眼睛,但您希望 pitch 中发生>相机坐标。由于不允许滚动并且最后执行偏航,因此在世界和相机参考系中偏航始终相同。考虑下面的图片:

您的代码适用于局部俯仰和偏航,因为它们是在相机坐标中完成的。

但是当您围绕参考点旋转时,您会创建一个位于世界坐标中的旋转矩阵,并使用它来旋转相机中心。如果相机的坐标系恰好与世界坐标系对齐,这可以正常工作。但是,如果您在旋转相机位置之前不检查是否达到俯仰限制,那么当您达到该限制时,您会出现疯狂的行为。相机会突然开始在世界上滑行——仍然围绕参考点“旋转”,但不再改变方向。

如果相机的轴与世界的轴不一致,就会发生奇怪的事情。在极端情况下,相机根本不会移动,因为您要让它滚动。

以上是通常会发生的情况,但是由于您单独处理相机方向,相机实际上并没有滚动。

相反,它保持直立,但你会得到奇怪的翻译。

处理此问题的一种方法是 (1) 始终将相机置于相对于参考点的规范位置和方向,(2) 进行旋转,然后 (3) 完成后将其放回原处(例如,类似于将参考点平移到原点的方式,应用 Yaw-Pitch 旋转,然后平移回来)。然而,仔细想想,这可能不是最好的方法。


更新 2

我认为 Generic Human 的 答案可能是最好的。如果旋转是离轴的,问题仍然是应该应用多少俯仰,但现在,我们将忽略它。也许它会给你可接受的结果。

答案的本质是:在鼠标移动之前,您的相机位于 c1 = m_oEyePos 并以 M 为方向1 = D3DXMatrixRotationYawPitchRoll(&M_1,m_dRotationX,m_dRotationY,0)。考虑参考点 a = m_oRotateAroundPos。从相机的角度来看,这个点是a'=M1(a-c1)

您想将相机的方向更改为 M2 = D3DXMatrixRotationYawPitchRoll(&M_2,m_dRotationX+dX,m_dRotationY+dY,0)。 [重要提示:由于您不允许 m_dRotationY 超出特定范围,您应该确保 dY 不违反该约束。] 随着相机改变方向,您还需要其位置围绕 a 旋转到新点 c2。这意味着 a 不会从相机的角度发生变化。即M1(ac1)==M2(ac2).

所以我们求解c2(记住旋转矩阵的转置与逆矩阵相同):

M2TM1(ac1)==(ac 2) =>

-M2TM1(ac1)+a==c2

现在,如果我们将其视为应用于 c1 的转换,那么我们可以看到它首先被否定,然后被 a,然后旋转 M1,然后旋转 M2T >,再次否定,然后再次被a翻译。这些是图形库擅长的变换,它们都可以压缩成一个变换矩阵。

@Generic Human 的答案值得称赞,但这里是它的代码。当然,您需要在应用之前实现该函数来验证音高的变化,但这很简单。这段代码可能有几个错别字,因为我没有尝试编译:

case ENavigation::eRotatePoint: 
    const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
    double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
    dY = validatePitch(dY); // dY needs to be kept within bounds so that m_dRotationY is within bounds

    D3DXMATRIX oRotationMatrix1; // The camera orientation before mouse-change
    D3DXMatrixRotationYawPitchRoll(&oRotationMatrix1, m_dRotationX, m_dRotationY, 0.0);

    D3DXMATRIX oRotationMatrix2; // The camera orientation after mouse-change
    D3DXMatrixRotationYawPitchRoll(&oRotationMatrix2, m_dRotationX + dX, m_dRotationY + dY, 0.0);

    D3DXMATRIX oRotationMatrix2Inv; // The inverse of the orientation
    D3DXMatrixTranspose(&oRotationMatrix2Inv,&oRotationMatrix2); // Transpose is the same in this case

    D3DXMATRIX oScaleMatrix; // Negative scaling matrix for negating the translation
    D3DXMatrixScaling(&oScaleMatrix,-1,-1,-1);

    D3DXMATRIX oTranslationMatrix; // Translation by the reference point
    D3DXMatrixTranslation(&oTranslationMatrix,
         m_oRotateAroundPos.x,m_oRotateAroundPos.y,m_oRotateAroundPos.z);

    D3DXMATRIX oTransformMatrix; // The full transform for the eyePos.
    // We assume the matrix multiply protects against variable aliasing
    D3DXMatrixMultiply(&oTransformMatrix,&oScaleMatrix,&oTranslationMatrix);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix1);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix2Inv);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oScaleMatrix);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oTranslationMatrix);

    D3DXVECTOR4 oEyeFinal;
    D3DXVec3Transform(&oEyeFinal, &m_oEyePos, &oTransformMatrix);

    m_oEyePos = D3DXVECTOR3(oEyeFinal.x, oEyeFinal.y, oEyeFinal.z) 

    // Increment rotation to keep the same relative look angles
    RotateXAxis(dX);
    RotateYAxis(dY);
    break;

【讨论】:

感谢 JCooper。它不是滚动的-我在上面添加了一些说明。相反,当m_dRotationX approaches 0 或 pi 时,它似乎变得“疯狂”。 这听起来很有帮助 - 谢谢!画质也不错。因此,平移和旋转眼睛:将眼睛平移到 YZ 平面(通过 X);按上述fixAngle 旋转(可能被否定);绕X轴旋转;再次旋转固定角度(可能被否定);翻译回来;绕 Y 轴旋转?唔。如果这是错误的,您介意添加一些伪代码来帮助我弄清楚吗? 我已经为代码示例添加了赏金(有关详细信息,请参阅问题。)谢谢您的文字描述,但我无法将其翻译成可行的东西。毫无疑问这是我自己的错/问题,你的回答是对的!但我不太了解您的描述,无法编写有效的代码。 @DavidM 我不太喜欢 Direct3d,但我至少可以整理一些伪代码。不过请告诉我:如果相机围绕向右偏离 90° 的参考点“俯仰”,期望的行为是什么?考虑我照片中绿点处的相机,但面向左侧。在这种情况下,围绕参考点的“俯仰”将是相机的“滚动”。你不允许它滚动,但你确实改变了音高,即使它围绕一个完全不同的轴。在这种情况下改变look vector的方向似乎很奇怪。 用户不能点击向右偏离 90 度的点。我不完全确定我理解这个问题。通过避免滚动,我的意思是保持地平线水平。【参考方案2】:

我认为有一个更简单的解决方案可以让您回避所有轮换问题。

表示法:A是我们要旋转的点,C是原始相机位置,M是原始相机旋转矩阵将全局坐标映射到相机的本地视口。

    记下A的局部坐标,等于A' = M × (A - C)。 像在正常的“眼睛旋转”模式下一样旋转相机。更新视图矩阵M,使其修改为M2,而C保持不变。 现在我们想找到 C2 使得 A' = M2 × (A - C2)。 这很容易通过等式 C2 = A - M2-1 × A' 完成。李> 瞧,相机已旋转,因为 A 的本地坐标没有改变,A 保持在相同的位置、相同的比例和距离。

作为额外的奖励,“眼睛旋转”和“点旋转”模式之间的旋转行为现在是一致的。

【讨论】:

好电话。这是一个优雅的解决方案。【参考方案3】:

您通过重复应用小的旋转矩阵围绕该点旋转,这可能会导致漂移(小的精度误差加起来),我敢打赌,一段时间后您不会真正做一个完美的圆。由于视图的角度使用简单的一维双精度,因此它们的漂移要小得多。

一种可能的解决方法是在您进入该视图模式时存储一个专用的偏航/俯仰角和相对位置,并使用它们进行数学计算。这需要更多的簿记,因为您需要在移动相机时更新它们。请注意,如果点移动,它也会使相机移动,我认为这是一个改进。

【讨论】:

它不是重复应用矩阵:“然后使用相机位置和这两个角度重新计算视图矩阵。”这是重新计算,而不是将现有矩阵与新矩阵合并。唯一积累的东西是两个角度(单独)和相机位置本身,它们用于重新创建矩阵。 除非我完全误解了你的意思......看看RotateXAxis()(和Y)和Update()方法 - 你可以看到更新忽略了m_oViewMatrix的现有值和完全计算一个新值。 是的,我的意思是在 10 次旋转后,m_oEyePos 将是对初始位置应用 10 次旋转矩阵的结果,但对于 m_dRotationX,您只需添加 10 次 dY。第一个错误的较大累积使我说“没有在一个完美的圆圈中移动”(顺便说一句,应该很容易测试)。我认为位置漂移是对您所描述的唯一可能的解释(例如旋转点消失)。【参考方案4】:

如果我理解正确,您对最终矩阵中的旋转分量感到满意(除了问题#3 中的反向旋转控件),但对平移部分不满意,是这样吗?

问题似乎来自您对它们的不同处理:您每次都从头开始重新计算旋转部分,但累积了平移部分(m_oEyePos)。其他 cmets 提到了精度问题,但它实际上比 FP 精度更重要:从小偏航/俯仰值累积旋转与从累积的偏航/俯仰值进行一次大旋转完全不同——在数学上。因此旋转/平移差异。要解决此问题,请尝试在旋转部分同时从头开始重新计算眼睛位置,类似于您如何找到“oTarget = oEyePos + ...”:

oEyePos = m_oRotateAroundPos - dist * D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z)

dist 可以固定或从旧的眼睛位置计算。这将使旋转点保持在屏幕中心;在更一般的情况下(您感兴趣),此处的-dist * oForward 应替换为旧的/初始的m_oEyePos - m_oRotateAroundPos 乘以旧的/初始相机旋转以将其带到相机空间(在相机的坐标系),然后乘以反转的新相机旋转得到世界的新方向。

当然,当俯仰垂直向上或向下时,这会受到云台锁定的影响。您需要准确定义在这些情况下您期望的行为来解决这部分问题。另一方面,锁定在 m_dRotationX=0 或 =pi 是相当奇怪的(这是偏航,而不是俯仰,对吗?)并且可能与上述有关。

【讨论】:

以上是关于实现一个复杂的基于旋转的相机的主要内容,如果未能解决你的问题,请参考以下文章

基于opencv的相机标定

使用基于相机朝向的操纵杆的玩家运动

基于HT for Web矢量实现3D叶轮旋转

Unity:使用鼠标位置为我的相机制作角度以围绕对象旋转

基于Three.js中的父/其他对象坐标系保持对象旋转?

将四元数旋转转换为旋转矩阵?