如何在OpenGL中实现轨迹球?


15

在深入了解转换之后,是时候为我的应用实现跟踪球了。我知道我必须创建一个从起点到单击鼠标的向量,然后再从起点到释放鼠标的另一个向量。

我的问题是,我是否必须将(x,y)像素坐标转换为世界坐标,还是应该只在图像空间中做所有事情(考虑图像空间是以像素为单位测量场景的2D投影)?

编辑

Richie Sams的答案是一个很好的答案。但是,我认为我采用的方法略有不同,如果我错了或者我误会了一些东西,请纠正我。

在我的应用程序有一个SimplePerspectiveCamera接收类position相机中,position of the target我们正在寻找的up载体中,fovyaspectRationearfar距离。

通过这些,我构建了视图和投影矩阵。现在,如果要放大/缩小,请更新视野并更新投影矩阵。如果要平移,请移动摄像机的位置,并按鼠标产生的增量查看。

最后,对于旋转,我可以使用角度轴变换或四元数。为此,我将像素坐标保存在按下鼠标的位置,然后当鼠标移动时,我还将保存像素坐标。

对于每一对COORDS的我可以计算Z值给出的公式为球状,即,SQRT(1-X ^ 2-Y ^ 2),然后计算与从去矢量targetPointMousePressed和从targetPointMouseMoved,做叉积获取旋转轴并使用任何方法计算新的相机位置。

但是,我最大的疑问是(x,y,z)值是以像素坐标给出的,在计算我使用的矢量时,target这是世界坐标中的一个点。坐标系的这种混合不影响我尝试做的旋转结果吗?


1
“轨迹球”是否意味着像在3D建模应用程序中一样围绕物体旋转的摄像机?如果是这样,我认为通常可以通过跟踪2D鼠标坐标并为相机旋转映射x = yaw,y = pitch来完成。
内森·里德

1
@NathanReed另一个选项是基于轴角度的,您将2个鼠标点投影到(虚拟)球体上,然后找到从一个到另一个的旋转。
棘手怪胎

@NathanReed是的,这就是我所说的轨迹球,我认为它是CG社区中的通用名称。
BRabbit27年

@ratchetfreak是的,我的方法考虑了基于轴角度的旋转。我的疑问是是否需要将2D鼠标坐标映射到世界坐标。我知道我可以使用(x,y)来计算z一个半径球体的值r,但是我不确定该球体是否生活在世界空间或图像空间中,其含义是什么。也许我想得太多了。
BRabbit27年

2
编辑时:是。您将需要使用“视图”矩阵将(x,y,z)值转换为世界空间。
RichieSams 2015年

Answers:


16

假设您指的是根据鼠标移动旋转的相机:

一种实现方法是跟踪摄像机的位置及其在空间中的旋转。由于可以直接表示角度,因此球面坐标恰好方便。

球坐标图像

float m_theta;
float m_phi;
float m_radius;

float3 m_target;

摄像机位于由m_theta,m_phi和m_radius定义的P处。通过更改这三个值,我们可以随意旋转和移动。但是,我们总是查看并旋转m_target。m_target是球体的本地原点。但是,我们可以将这个原点自由移动到世界空间中的任何地方。

相机具有三种主要功能:

void Rotate(float dTheta, float dPhi);
void Zoom(float distance);
void Pan(float dx, float dy);

在最简单的形式中,Rotate()和Zoom()很简单。分别修改m_theta,m_phi和m_radius即可:

void Camera::Rotate(float dTheta, float dPhi) {
    m_theta += dTheta;
    m_phi += dPhi;
}

void Camera::Zoom(float distance) {
    m_radius -= distance;
}

平移有点复杂。摄像机平移定义为分别将摄像机移动到当前摄像机视图的左/右和/或上/下。实现此目的最简单的方法是将当前的摄影机视图从球面坐标转换为笛卡尔坐标。这将为我们提供一个向上向右向量。

void Camera::Pan(float dx, float dy) {
    float3 look = normalize(ToCartesian());
    float3 worldUp = float3(0.0f, 1.0f, 0.0f, 0.0f);

    float3 right = cross(look, worldUp);
    float3 up = cross(look, right);

    m_target = m_target + (right * dx) + (up * dy);
}

inline float3 ToCartesian() {
    float x = m_radius * sinf(m_phi) * sinf(m_theta);
    float y = m_radius * cosf(m_phi);
    float z = m_radius * sinf(m_phi) * cosf(m_theta);
    float w = 1.0f;

    return float3(x, y, z, w);
}

因此,首先,我们将球坐标系转换为笛卡尔坐标,以获得外观向量。接下来,我们将向量与世界上的向量做叉积,以获得正确的向量。这是直接指向摄像机视图右侧的向量。最后,我们进行另一个矢量叉积运算,以使相机达到矢量。

为了完成平移,我们沿着向上向右向量移动m_target 。

您可能会问的一个问题是:为什么总是在笛卡尔和球面之间进行转换(您还必须进行转换才能创建View矩阵)。

好问题。我也有这个问题,并试图专门使用笛卡尔。您最终会遇到旋转问题。由于浮点操作不完全精确,因此多次旋转最终会积累误差,这会缓慢地对应于相机,并且会意外滚动。

在此处输入图片说明

因此,最后,我坚持使用球坐标。为了抵消额外的计算量,我最终缓存了视图矩阵,并且仅在摄像机移动时对其进行计算。

最后一步是使用此Camera类。只需在应用程序的MouseDown / Up / Scroll函数中调用适当的成员函数即可:

void MouseDown(WPARAM buttonState, int x, int y) {
    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;

    SetCapture(m_hwnd);
}

void MouseUp(WPARAM buttonState, int x, int y) {
    ReleaseCapture();
}

void MouseMove(WPARAM buttonState, int x, int y) {
    if ((buttonState & MK_LBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            // Calculate the new phi and theta based on mouse position relative to where the user clicked
            float dPhi = ((float)(m_mouseLastPos.y - y) / 300);
            float dTheta = ((float)(m_mouseLastPos.x - x) / 300);

            m_camera.Rotate(-dTheta, dPhi);
        }
    } else if ((buttonState & MK_MBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            float dx = ((float)(m_mouseLastPos.x - x));
            float dy = ((float)(m_mouseLastPos.y - y));

            m_camera.Pan(-dx * m_cameraPanFactor, dy * m_cameraPanFactor);
        }
    }

    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;
}

void MouseWheel(int zDelta) {
    // Make each wheel dedent correspond to a size based on the scene
    m_camera.Zoom((float)zDelta * m_cameraScrollFactor);
}

m_camera * Factor变量只是比例因子,可改变相机旋转/平移/滚动的速度

我上面的代码是我为辅助项目制作的相机系统的简化伪代码版本:camera.hcamera.cpp。相机尝试模仿Maya相机系统。该代码是免费和开源的,因此可以在您自己的项目中随意使用它。


1
我想除以300只是考虑到鼠标位移的旋转灵敏度参数?
BRabbit27年

正确。这正是我当时的决议所能奏效的事情。
RichieSams,2015年

-2

如果您想看看现成的解决方案,我有一个 C ++和C#的THREE.JS TrackBall控件端口


我倾向于这样做,他可以从跟踪球内部的代码中学习。
Michael IV

1
@MichaelIV不过,艾伦·沃尔夫(Alan Wolfe)有一点。您可以通过在答案本身中包含相关代码,使其自成体系,并保证将来有一天不会出现链接失效,从而大大改善您的答案。
马丁·恩德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.