OGL(教程15)——摄像机控制2

原文地址:http://ogldev.atspace.co.uk/www/tutorial15/tutorial15.html

背景知识:
本节我们将会完成实现鼠标控制摄像机方向。在涉及摄像机的时候有很多不能层级的自由性。我们将以第一视角游戏的方式控制相机。这就意味着我们能够改变摄像机的角度,沿着+y轴能够360改动。就是绕着自己转一周。除此之外,我们还能颠倒摄像机的向上的方向,来得到更好的上面或者下面的视觉。我们不会颠倒相机,除非我们全视角。这些高度自由的控制摄像机不是本教程涉及的。我们只是简单的控制摄像机。

下图使我们要照这个制作的一个相机:
在这里插入图片描述

枪有两个控制轴:

  1. 可以绕着(0,1,0)选择360度。这个角度叫做水平角度,这个向量叫做垂直轴。
  2. 可以沿和平行于地面的轴上下调整视角。这个移动是有限制的,这个枪不能画一个完整的圆。这个角度叫做垂直角度,而轴叫做水平轴。注意到垂直轴是常量(0,1,0),水平轴是和枪一同旋转的,而且是和枪的目标是垂直的。这个对于能够正确的数学计算很重要。

计划是跟着鼠标的移动,能够改变水平角度,当鼠标左右移动,然后也能根据鼠标的上下移动来改变。给定这两个角度,我们想要计算目标和向上的向量。

通过水平角度改变目标向量很直观。使用基本的三角函数,我们可以看到,目标向量的z分量,是水平向量的sine值,而x分量是水平角度的cosine。

通过垂直角度改变目标向量要更复杂,由于水平轴沿着摄像机。水平轴可以通过垂直轴和目标向量叉乘得到,然后目标向量在沿着水平轴旋转一定角度,但是这个有点复杂。

幸运的是,我们有一个有用的工具就是四元数。四元数在1843年被Willilam Rowan Hamilton发现,它是irish数学家,四元数的基础是复数系统。四元数用Q表示,被定义为:

在这里插入图片描述

当ij和k是复数,下面等式成立:
在这里插入图片描述

实际上,我们定义一个四元数4-vector(x,y,z,w)。四元数的逆定义为:
在这里插入图片描述

标准化一个四元数和标准化一个向量类似。我将介绍如何使用四元数使一个向量绕着任意一个向量旋转。更多的数学证明可以在网上找到。

通用的方式是,计算一个四元数w,他能够代表旋转向量V,旋转角度为a:

在这里插入图片描述

这里的Q是旋转四元数,定义为:
在这里插入图片描述

在计算出W之后,旋转向量就为(W.x, W.y, W.z)。重要的一点是,我们要先将Q乘以V,这个是四元数乘以向量,结果还是一个四元数。接着,我们需要做一个四元数和四元数的乘法。两个类型相乘是不一样的。math_3d.cpp包含了这个乘法的实现过程。

我们需要在鼠标移动的时候更新水平和垂直角度,我们还需要决定怎样初始化他们。逻辑上的选择是根据摄像机提供的目标向量初始化它。我们从水平角度开始,看下图:
在这里插入图片描述

目标向量是(x,z),我们想要找到水平角度,这个是后面使用的alpha值(y分量只和垂直角度相关)。由于圆的半径为1,很容易得到sine alpha就是z。因此,计算asine(z)就得到了alpha。我们完成了吗?没有,由于z在[-1,1]之间,asine的角度在-90到90内。但是水平角度在0到360度内。除此之外,四元数是顺时针旋转。这就意味着,我们使用四元数旋转90度,我们得到了z为-1,这个好和sine(90)=1,正好相反。很简单的变为正确的方式为,直接+z的asine值,然后和圆的四分之一结合起来。比如,我们的目标向量是(0,1),我们计算asine(1)=90度,然后用360减去它得到270度。0到1的asine范围是0到90度。把它结合圆的四分之一就能得到正确的水平角度。

计算垂直角度简单的多了。我们限制角度在-90度到90度之间。这就意味着我们只需要对asine(y)取反即可。当y=1,就是向上看,asine就是90度,然后我们取反变为-90即可。当y=-1,朝下看,asine就是-90度,取反得到90度。

代码注释:

(camera.cpp:38)

Camera::Camera(int WindowWidth, int WindowHeight, const Vector3f& Pos, const Vector3f& Target, const Vector3f& Up)
{
    m_windowWidth = WindowWidth;
    m_windowHeight = WindowHeight;
    m_pos = Pos;

    m_target = Target;
    m_target.Normalize();

    m_up = Up;
    m_up.Normalize();

    Init();
}

摄像机的构造函数,传递了窗口的维度。我们需要它,是为了能够把鼠标移动到屏幕的中心。除此之外,我们调用Init()函数设置内部的摄像机的属性。

(camera.cpp:54)

void Camera::Init()
{
    Vector3f HTarget(m_target.x, 0.0, m_target.z);
    HTarget.Normalize();

    if (HTarget.z >= 0.0f)
    {
        if (HTarget.x >= 0.0f)
        {
            m_AngleH = 360.0f - ToDegree(asin(HTarget.z));
        }
        else
        {
            m_AngleH = 180.0f + ToDegree(asin(HTarget.z));
        }
    }
    else
    {
        if (HTarget.x >= 0.0f)
        {
            m_AngleH = ToDegree(asin(-HTarget.z));
        }
        else
        {
            m_AngleH = 180.0f - ToDegree(asin(-HTarget.z));
        }
    }

    m_AngleV = -ToDegree(asin(m_target.y));

    m_OnUpperEdge = false;
    m_OnLowerEdge = false;
    m_OnLeftEdge = false;
    m_OnRightEdge = false;
    m_mousePos.x = m_windowWidth / 2;
    m_mousePos.y = m_windowHeight / 2;

    glutWarpPointer(m_mousePos.x, m_mousePos.y);
}

在Init()函数中我们从计算水平角度开始。我们创建了一个新的目标向量叫做HTarget(horizontal target)。这个是原始目标想在xz平面的投影。紧接着我们对其进行标准化。紧接着我们检测目标向量属于第几象限。然后根据z分量的正负计算最终的角度。然后我们计算垂直角度。

摄像机有4个新的标记来揭示鼠标在屏幕中的位置。我们将会实现自动的转换,根据相对应的方向变化。这个允许我们旋转360度。我们初始化这个四个标记为false,因为鼠标从屏幕的中心开始。下面的两行计算出屏幕的中心位置。函数glutWarpPointer 事实上用来移动鼠标。把鼠标从屏幕的中心开始会大大简化问题。

(camera.cpp:140)

void Camera::OnMouse(int x, int y)
{
    const int DeltaX = x - m_mousePos.x;
    const int DeltaY = y - m_mousePos.y;

    m_mousePos.x = x;
    m_mousePos.y = y;

    m_AngleH += (float)DeltaX / 20.0f;
    m_AngleV += (float)DeltaY / 20.0f;

    if (DeltaX == 0) {
        if (x <= MARGIN) {
            m_OnLeftEdge = true;
        }
        else if (x >= (m_windowWidth - MARGIN)) {
            m_OnRightEdge = true;
        }
    }
    else {
        m_OnLeftEdge = false;
        m_OnRightEdge = false;
    }

    if (DeltaY == 0) {
        if (y <= MARGIN) {
            m_OnUpperEdge = true;
        }
        else if (y >= (m_windowHeight - MARGIN)) {
            m_OnLowerEdge = true;
        }
    }
    else {
        m_OnUpperEdge = false;
        m_OnLowerEdge = false;
    }

    Update();
}

这个函数用来通知摄像机鼠标移动了。参数是鼠标的新的屏幕位置。我们开始计算和之前位置的差值delta。然后我们再把这个新的位置记录下来。我们通过缩放这delta来更新目前的水平和垂直的角度。我使用这个缩放值可以正确显示,但是针对不同的电脑,要改动不同的缩放值。我们会在之后的章节中优化这个问题。

下一步是测试更新m_On*Edge标记,根据鼠标的位置。有一个默认的差距默认是10像素,它会触发边缘行为,当鼠标达到屏幕的边缘的时候。最终,我们调用Upate()函数来重新计算目标和向上向量,根据的是新的水平和垂直角度。

(camera.cpp:183)

void Camera::OnRender()
{
    bool ShouldUpdate = false;

    if (m_OnLeftEdge) {
        m_AngleH -= 0.1f;
        ShouldUpdate = true;
    }
    else if (m_OnRightEdge) {
        m_AngleH += 0.1f;
        ShouldUpdate = true;
    }

    if (m_OnUpperEdge) {
        if (m_AngleV > -90.0f) {
            m_AngleV -= 0.1f;
            ShouldUpdate = true;
        }
    }
    else if (m_OnLowerEdge) {
        if (m_AngleV < 90.0f) {
            m_AngleV += 0.1f;
            ShouldUpdate = true;
        }
    }

    if (ShouldUpdate) {
        Update();
    }
}

这个函数在主渲染循环中调用。我们需要这个函数,是因为要在当到达边缘的时候不要再移动了。这种情况下,不会偶鼠标事件,但是我们依然希望摄像机能够持续移动,除非鼠标移动到了边缘之外。我们检测四个边界标记,然后更新对应的角度。如果有一个变换的话,我们调用Update()方法,来更新目标和向上向量。当鼠标移到屏幕之外,我们侦测到鼠标事件,然后清楚标记。注意到垂直角度在-90到90之间。这个阻止了绘制完整的圆形。

(camera.cpp:214)

void Camera::Update()
{
    const Vector3f Vaxis(0.0f, 1.0f, 0.0f);

    // Rotate the view vector by the horizontal angle around the vertical axis
    Vector3f View(1.0f, 0.0f, 0.0f);
    View.Rotate(m_AngleH, Vaxis);
    View.Normalize();

    // Rotate the view vector by the vertical angle around the horizontal axis
    Vector3f Haxis = Vaxis.Cross(View);
    Haxis.Normalize();
    View.Rotate(m_AngleV, Haxis);
    View.Normalize();

    m_target = View;
    m_target.Normalize();

    m_up = m_target.Cross(Haxis);
    m_up.Normalize();
}

这个函数更新目标和向上向量,根据水平和垂直角度。我们从视觉向量开始,首先重置它。此时意味着它和地面平行。垂直角度为0,水平角度为0.我们把垂直指向上方,然后旋转视觉向量,这个旋转的角度就是水平角度。结果是向量指向了通常的方向,指向了目标。把这个方向和垂直向量做叉乘,我们得到了另外一个向量,此向量在xz屏幕上,这个平面和视觉向量和垂直向量构成的平面垂直。这就是新的水平轴,现在是旋转向上向量,向上或者向下,根据的是垂直的角度。结果是最终的目标向量。我们把它设置到对应的成员属性中。现在我们需要修改向上的向量。比如,如果摄像机是向上看的,它必须要向后旋转,因为它必须和目标向量垂直。这个和把头向后仰看天空类似。新的向上的向量,可以通过最终向量和水平轴叉乘得到。如果垂直角度依然为0,然后目标向量保持在xz屏幕,向上向量依然为(0,1,0)。

(tutorial15.cpp:209)

glutGameModeString("1920x1200@32");
glutEnterGameMode();

这两句代码,可以使我们的程序高性能的运行全屏应用。它使得摄像机旋转360度变得。因为你需要的是拉着鼠标到屏幕的一边。分辨率和每个像素的位数是通过string配置的。32位每像素,提供了最大的数量的渲染颜色。

(tutorial15.cpp:214)

pGameCamera = new Camera(WINDOW_WIDTH, WINDOW_HEIGHT);

我们注册两个glut回调函数。一个是用于鼠标,另外一个是键盘点击。

(tutorial15.cpp:81)

static void KeyboardCB(unsigned char Key, int x, int y)
{
    switch (Key) {
        case 'q':
            exit(0);
    }
}

static void PassiveMouseCB(int x, int y)
{
    pGameCamera->OnMouse(x, y);
}

目前是使用全屏的方式运行程序,所以很难退出应用。键盘检测q键,用于退出。鼠标的回调函数仅仅是用来传递鼠标的坐标给摄像机。

(tutorial15.cpp:44)

static void RenderSceneCB()
{
    pGameCamera->OnRender();

我们在主渲染循环的时候,要通知摄像机。这样就能保证,摄像机能够根据鼠标的移动来变化。

猜你喜欢

转载自blog.csdn.net/wodownload2/article/details/83032545