我正在实施用于空间可视化的3D引擎,并且正在编写具有以下导航功能的摄像头:
相机不应该滚动 - 也就是说,' up'还在继续因此,我代表具有位置和两个角度的摄像机,围绕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档案,因为到目前为止,我所阅读的大多数相机答案都是代码。
感谢您的帮助:)
一些澄清
感谢您的评论和答案到目前为止!我会尝试澄清一些关于这个问题的事情:
只要其中一个变化,就会从摄像机位置和两个角度重新计算视图矩阵。矩阵本身永远不会累积(即更新) - 它会重新重新计算。然而,相机位置和两个角度变量被累积(例如,每当鼠标移动时,基于鼠标上下移动的像素数量,一个或两个角度将增加或减少一小部分和/或或左右在屏幕上。)
评论者JCooper说我受到万向节锁定的困扰,我需要:
在旋转eyePos的变换上添加另一个旋转 在应用转换之前完全在y-z平面中,并且 然后是另一个旋转,然后将其移回。绕着旋转 在施加之前和之后,y轴由以下角度 偏航 - 俯仰 - 滚动矩阵(其中一个角度需要被否定; 尝试一下是决定哪个的最快方法。
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;
}
RotateXAxis
和RotateYAxis
方法非常简单:
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);
}
答案 0 :(得分:8)
在我看来," Roll"鉴于您构建视图矩阵的方式,不应该是可能的。无论所有其他代码(其中一些看起来确实有点滑稽),调用D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector);
应该创建一个不滚动的矩阵,当[0,1,0]
作为' Up'向量,除非oTarget-oEyePos
恰好与向上向量平行。这种情况似乎并非如此,因为您将m_dRotationY
限制在(-.49pi,+。49pi)之内。
也许你可以澄清一下你是如何知道这一点的。正在发生。你有地平面,地平面的地平线是否偏离水平?
另外,在UpdateView
中,D3DXQuaternionRotationYawPitchRoll
似乎完全没必要,因为您立即转身并将其更改为矩阵。只需像在鼠标事件中那样使用D3DXMatrixRotationYawPitchRoll
。四元数用于相机,因为它们是累积眼睛坐标中发生的旋转的便捷方式。由于您只按严格的顺序使用两个旋转轴,因此积累角度的方法应该没问题。 (0,0,1)的向量变换也不是必需的。 oRotationMatrix
条目中的(_31,_32,_33)
应该已经包含这些值。
<强>更新强>
鉴于它没有滚动,这就是问题所在:你创建一个旋转矩阵来移动 world 坐标中的眼睛,但你想要 pitch < / em>发生在相机坐标中。由于不允许滚动并且最后执行偏航,因此在世界和相机参考帧中偏航总是相同的。考虑下面的图片:
您的代码适用于局部俯仰和偏航,因为这些是在摄像机坐标中完成的。
但是当您围绕参考点旋转时,您将创建一个旋转矩阵,该矩阵位于世界坐标中并使用它旋转摄像机中心。如果相机的坐标系恰好与世界对齐,这种方法也可以。但是,如果您在旋转相机位置之前没有检查是否符合音高限制,那么当您达到该限制时,您将会遇到疯狂的行为。相机会突然开始在世界各地滑冰 - 仍然在旋转&#39;围绕参考点,但不再改变方向。
如果相机的斧头不与世界对齐,那么奇怪的事情就会发生。在极端情况下,相机根本不会移动,因为你正试图让它滚动。
以上是通常会发生的情况,但由于您单独处理相机方向,因此相机实际上并没有滚动。
相反,它保持直立,但你会得到奇怪的翻译。
处理这种情况的一种方法是(1)始终将相机放入相对于参考点的规范位置和方向,(2)进行旋转,然后(3)将光线放回到“#”; re done(例如,类似于将参考点平移到原点的方式,应用Yaw-Pitch旋转,然后平移回来)。然而,想一想它,这可能不是最佳方式。
更新2
我认为 Generic Human的答案可能是最好的。问题仍然是如果旋转是离轴应该应用多少音高,但是现在,我们将忽略它。也许它会给你可接受的结果。
答案的本质是:在鼠标移动之前,您的相机处于 c 1 = m_oEyePos
并且以 M <为导向sub> 1 = D3DXMatrixRotationYawPitchRoll(&M_1,m_dRotationX,m_dRotationY,0)
。考虑参考点 a = m_oRotateAroundPos
。从相机的角度来看,这一点是 a&#39; = M 1 (a-c 1 )。
您想要将相机的方向更改为 M 2 = D3DXMatrixRotationYawPitchRoll(&M_2,m_dRotationX+dX,m_dRotationY+dY,0)
。 [重要提示:由于您不允许m_dRotationY
超出特定范围,因此您应确保dY不违反该约束。]作为相机更改方向,您还希望其位置围绕 a 旋转到新点 c 2 。这意味着 a 不会从相机的角度改变。即, M 1 (ac 1 )== M 2 (ac 2 )
所以我们求解 c 2 (记住旋转矩阵的转置与反转相同):
<强>中号<子> 2 子> Ť中号<子> 1 子>(AC <子> 1 子>)==(AC <子> 2 ) =&gt;
<强> -M <子> 2 子> Ť中号<子> 1 子>(AC <子> 1 子>)+ A ==ç<子> 2 子> 强>
现在,如果我们将此视为应用于 c 1 的转换,那么我们可以看到它首先被否定,然后由 a <翻译/ strong>,然后通过 M 1 旋转,然后旋转 M 2 T ,再次否定,然后再由 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;
}
答案 1 :(得分:4)
通过重复应用小旋转矩阵绕点旋转,这可能会导致漂移(小精度误差加起来),我打赌你不会在一段时间后真正完成一个完美的圆圈。由于视角的角度使用简单的一维双,因此它们的漂移要小得多。
可能的解决方法是在您进入该视图模式时存储专用的偏航/俯仰和相对位置,并使用它们进行数学运算。这需要更多的簿记,因为您需要在移动相机时更新它们。请注意,如果点移动,它也会使相机移动,我认为这是一个改进。
答案 2 :(得分:4)
我认为有一个更简单的解决方案可以让你回避所有轮换问题。
符号: A 是我们想要旋转的点, C 是原始相机位置, M 是原始相机旋转矩阵将全局坐标映射到摄像机的本地视口。
作为额外的奖励,旋转行为现在在“眼睛旋转”和“点旋转”模式之间保持一致。
答案 3 :(得分:2)
如果我理解正确,你对最终矩阵中的旋转分量感到满意(除了问题#3中的反向旋转控制),但是对于翻译部分不是这样吗?
问题似乎来自于你以不同的方式对待它们:你每次都是从头开始重新计算旋转部分,但是累积了翻译部分(m_oEyePos)。其他评论提到精度问题,但它实际上比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是相当奇怪的(这是偏航,而不是俯仰,对吗?)并且可能与上述有关。