基于四元数的相机
Quaternion based camera
我尝试基于四元数数学实现 FPS 相机。
我存储一个名为 _quat
的旋转四元数变量,并在需要时将其乘以另一个四元数。这是一些代码:
void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat = q;
}
void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat *= q;
}
应用程序可以通过 GetOrientation
请求方向矩阵,这只是将四元数转换为矩阵。
glm::mat4 Camera::GetOrientation() const
{
return glm::mat4_cast(_quat);
}
应用程序按以下方式更改方向:
int diffX = ...;//some computations based on mouse movement
int diffY = ...;
camera.OffsetOrientation(g_mouseSensitivity * diffX, g_mouseSensitivity * diffY);
这会导致围绕几乎所有轴的不良混合旋转。我做错了什么?
问题在于您累积旋转的方式。无论您使用四元数还是矩阵,这都是一样的。将表示俯仰和偏航的旋转与另一个旋转相结合将引入滚动。
到目前为止,实现 FPS 相机的最简单方法是简单地累积航向和俯仰的变化,然后在需要时转换为四元数(或矩阵)。我会将相机中的方法 class 更改为:
void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
_rightAngle = rightAngle;
_upAngle = upAngle;
}
void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
_rightAngle += rightAngle;
_upAngle += upAngle;
}
glm::mat4 Camera::GetOrientation() const
{
glm::quat q = glm::angleAxis(glm::radians(-_upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(_rightAngle), glm::vec3(0,1,0));
return glm::mat4_cast(q);
}
问题
GuyRT已经指出,你积累的方式不好。从理论上讲,它会那样工作。但是,浮点数学远非完全精确,而且您执行的操作越多,错误就会越积越多。组合两个四元数旋转是 28 次操作,而不是一次将值添加到角度的操作(另外,四元数乘法中的每个操作都会以非常不明显的方式影响 3D space 中产生的旋转)。
此外,用于旋转的四元数对归一化相当敏感,旋转它们会使它们稍微去归一化(旋转它们多次会使它们去归一化很多,并用另一个已经去归一化的四元数旋转它们会放大效果)。
反思
为什么我们首先要使用四元数?
四元数常用的原因如下:
- 避免可怕的万向节锁定(虽然很多人不明白这个问题,用三个四元数替换三个角度不会神奇地消除一个结合三个旋转的事实围绕单位向量——必须正确使用四元数才能避免这个问题)
- 许多旋转的有效组合,例如蒙皮(28 次操作对比使用矩阵时的 45 次操作),节省 ALU。
- 更少的值(因此更少的自由度),更少的操作,因此与组合许多转换时使用矩阵相比,出现不良影响的机会更少。
- 要上传的值更少,例如当蒙皮模型有几百块骨头或绘制一万个对象实例时。较小的顶点流或统一块。
- 四元数很酷,使用它们的人也很酷。
这些都不能真正解决您的问题。
解决方案
将两个旋转累加为角度(通常不需要,但对于这种情况完全可以接受),并在需要时创建旋转矩阵。这可以通过组合两个四元数并转换为矩阵来完成,如 GuyRT 的答案,或者通过直接生成旋转矩阵(这可能更有效,无论如何 OpenGL 想要看到的只是一个矩阵)。
据我所知,glm::rotate
只能绕任意轴旋转。您 可以 当然可以使用(但是您更愿意组合两个四元数!)。幸运的是,围绕 x、y、z 旋转的矩阵公式众所周知且简单明了,您会发现它 for example in the second paragraph of (3) here.
你不想围绕 z 旋转,所以 cos(gamma) = 1
和 sin(gamma) = 0
,这大大简化了公式(写在一张纸上)。
使用旋转角度会让很多人对你大喊大叫(通常并非完全不应该)。
一个更简洁的替代方法是跟踪您注视的方向,或者使用从您的眼睛指向您希望注视的方向的矢量,或者通过记住您注视的 space 中的点(这是结合与第三人称游戏中的物理学也很好)。如果你想允许任意旋转,那也需要一个 "up" 向量——从那时起 "up" 并不总是世界 space "up" —— 所以你可能需要 两个向量。这更好更灵活,但也更复杂。
对于您的示例中所需的 FPS,您唯一的选择是左右和上下查看,我发现旋转角度——仅适用于相机——完全可以接受。
我没有使用过 GLM,所以您可能不会喜欢这个答案。不过四元数旋转也不错
假设您的相机有一个初始保存的方向 'vecOriginalDirection'(标准化的 vec3)。假设您希望它跟随另一个 'vecDirection'(也已标准化)。这样我们就可以采用类似轨迹球的方法,并将 vecDirection 视为相机默认焦点的偏转。
在现实世界中进行四元数旋转的通常首选方法是使用 NLERP。让我们看看我是否能记住:在伪代码中(假设是浮点数)我认为是这样的:
quat = normalize([ cross(vecDirection, vecOriginalDirection),
1. + dot(vecDirection, vecOriginalDirection)]);
(不要忘记'1.+';我忘了它为什么在那里,但它曾经是有道理的。我想我拉了几天头发才找到它。它基本上是单位四元数,IIRC,它正在被平均化,从而使双角像角一样......也许:))
重新归一化,如上所示 'normalize()',是必不可少的(它是 NLERP 中的 'N')。当然,归一化 quat (x,y,z,w) 只是:
quat /= sqrt(x*x+y*y+z*z+w*w);
然后,如果您想使用自己的函数从 quat 生成 3x3 方向矩阵:
xx=2.*x*x,
yy=2.*y*y,
zz=2.*z*z,
xy=2.*x*y,
xz=2.*x*z,
yz=2.*y*z,
wx=2.*w*x,
wy=2.*w*y,
wz=2.*w*z;
m[0]=1.-(yy+zz);
m[1]=xy+wz;
m[2]=xz-wy;
m[3]=xy-wz;
m[4]=1.-(xx+zz);
m[5]=yz+wx;
m[6]=xz+wy;
m[7]=yz-wx;
m[8]=1.-(xx+yy);
要实际实现轨迹球,您需要在按住手指时计算 vecDirection,并在第一次按下时将其保存到 vecOriginalDirection(假设是触摸界面)。
您可能还想根据分段 half-sphere/hyperboloid 函数计算这些值(如果您还没有这样做的话)。我认为 @minorlogic 是在尝试节省一些修补工作,因为听起来您可能只需要使用一个嵌入式虚拟轨迹球即可。
向上角度旋转应该预乘,post乘将世界围绕原点旋转通过(1,0,0),预乘将旋转相机。
glm::quat q_up = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q_right = glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat *= q_right;
_quat = q_up * _quat;
我尝试基于四元数数学实现 FPS 相机。
我存储一个名为 _quat
的旋转四元数变量,并在需要时将其乘以另一个四元数。这是一些代码:
void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat = q;
}
void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat *= q;
}
应用程序可以通过 GetOrientation
请求方向矩阵,这只是将四元数转换为矩阵。
glm::mat4 Camera::GetOrientation() const
{
return glm::mat4_cast(_quat);
}
应用程序按以下方式更改方向:
int diffX = ...;//some computations based on mouse movement
int diffY = ...;
camera.OffsetOrientation(g_mouseSensitivity * diffX, g_mouseSensitivity * diffY);
这会导致围绕几乎所有轴的不良混合旋转。我做错了什么?
问题在于您累积旋转的方式。无论您使用四元数还是矩阵,这都是一样的。将表示俯仰和偏航的旋转与另一个旋转相结合将引入滚动。
到目前为止,实现 FPS 相机的最简单方法是简单地累积航向和俯仰的变化,然后在需要时转换为四元数(或矩阵)。我会将相机中的方法 class 更改为:
void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
_rightAngle = rightAngle;
_upAngle = upAngle;
}
void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
_rightAngle += rightAngle;
_upAngle += upAngle;
}
glm::mat4 Camera::GetOrientation() const
{
glm::quat q = glm::angleAxis(glm::radians(-_upAngle), glm::vec3(1,0,0));
q*= glm::angleAxis(glm::radians(_rightAngle), glm::vec3(0,1,0));
return glm::mat4_cast(q);
}
问题
GuyRT已经指出,你积累的方式不好。从理论上讲,它会那样工作。但是,浮点数学远非完全精确,而且您执行的操作越多,错误就会越积越多。组合两个四元数旋转是 28 次操作,而不是一次将值添加到角度的操作(另外,四元数乘法中的每个操作都会以非常不明显的方式影响 3D space 中产生的旋转)。
此外,用于旋转的四元数对归一化相当敏感,旋转它们会使它们稍微去归一化(旋转它们多次会使它们去归一化很多,并用另一个已经去归一化的四元数旋转它们会放大效果)。
反思
为什么我们首先要使用四元数?
四元数常用的原因如下:
- 避免可怕的万向节锁定(虽然很多人不明白这个问题,用三个四元数替换三个角度不会神奇地消除一个结合三个旋转的事实围绕单位向量——必须正确使用四元数才能避免这个问题)
- 许多旋转的有效组合,例如蒙皮(28 次操作对比使用矩阵时的 45 次操作),节省 ALU。
- 更少的值(因此更少的自由度),更少的操作,因此与组合许多转换时使用矩阵相比,出现不良影响的机会更少。
- 要上传的值更少,例如当蒙皮模型有几百块骨头或绘制一万个对象实例时。较小的顶点流或统一块。
- 四元数很酷,使用它们的人也很酷。
这些都不能真正解决您的问题。
解决方案
将两个旋转累加为角度(通常不需要,但对于这种情况完全可以接受),并在需要时创建旋转矩阵。这可以通过组合两个四元数并转换为矩阵来完成,如 GuyRT 的答案,或者通过直接生成旋转矩阵(这可能更有效,无论如何 OpenGL 想要看到的只是一个矩阵)。
据我所知,glm::rotate
只能绕任意轴旋转。您 可以 当然可以使用(但是您更愿意组合两个四元数!)。幸运的是,围绕 x、y、z 旋转的矩阵公式众所周知且简单明了,您会发现它 for example in the second paragraph of (3) here.
你不想围绕 z 旋转,所以 cos(gamma) = 1
和 sin(gamma) = 0
,这大大简化了公式(写在一张纸上)。
使用旋转角度会让很多人对你大喊大叫(通常并非完全不应该)。
一个更简洁的替代方法是跟踪您注视的方向,或者使用从您的眼睛指向您希望注视的方向的矢量,或者通过记住您注视的 space 中的点(这是结合与第三人称游戏中的物理学也很好)。如果你想允许任意旋转,那也需要一个 "up" 向量——从那时起 "up" 并不总是世界 space "up" —— 所以你可能需要 两个向量。这更好更灵活,但也更复杂。
对于您的示例中所需的 FPS,您唯一的选择是左右和上下查看,我发现旋转角度——仅适用于相机——完全可以接受。
我没有使用过 GLM,所以您可能不会喜欢这个答案。不过四元数旋转也不错
假设您的相机有一个初始保存的方向 'vecOriginalDirection'(标准化的 vec3)。假设您希望它跟随另一个 'vecDirection'(也已标准化)。这样我们就可以采用类似轨迹球的方法,并将 vecDirection 视为相机默认焦点的偏转。
在现实世界中进行四元数旋转的通常首选方法是使用 NLERP。让我们看看我是否能记住:在伪代码中(假设是浮点数)我认为是这样的:
quat = normalize([ cross(vecDirection, vecOriginalDirection),
1. + dot(vecDirection, vecOriginalDirection)]);
(不要忘记'1.+';我忘了它为什么在那里,但它曾经是有道理的。我想我拉了几天头发才找到它。它基本上是单位四元数,IIRC,它正在被平均化,从而使双角像角一样......也许:))
重新归一化,如上所示 'normalize()',是必不可少的(它是 NLERP 中的 'N')。当然,归一化 quat (x,y,z,w) 只是:
quat /= sqrt(x*x+y*y+z*z+w*w);
然后,如果您想使用自己的函数从 quat 生成 3x3 方向矩阵:
xx=2.*x*x,
yy=2.*y*y,
zz=2.*z*z,
xy=2.*x*y,
xz=2.*x*z,
yz=2.*y*z,
wx=2.*w*x,
wy=2.*w*y,
wz=2.*w*z;
m[0]=1.-(yy+zz);
m[1]=xy+wz;
m[2]=xz-wy;
m[3]=xy-wz;
m[4]=1.-(xx+zz);
m[5]=yz+wx;
m[6]=xz+wy;
m[7]=yz-wx;
m[8]=1.-(xx+yy);
要实际实现轨迹球,您需要在按住手指时计算 vecDirection,并在第一次按下时将其保存到 vecOriginalDirection(假设是触摸界面)。
您可能还想根据分段 half-sphere/hyperboloid 函数计算这些值(如果您还没有这样做的话)。我认为 @minorlogic 是在尝试节省一些修补工作,因为听起来您可能只需要使用一个嵌入式虚拟轨迹球即可。
向上角度旋转应该预乘,post乘将世界围绕原点旋转通过(1,0,0),预乘将旋转相机。
glm::quat q_up = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
q_right = glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));
_quat *= q_right;
_quat = q_up * _quat;