zoukankan      html  css  js  c++  java
  • opengl 教程(15) 摄像机控制(2)

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

          本篇教程中,我们将实现鼠标控制摄像机的方位。摄像机可以有多个自由度(所谓自由度,是指沿着某个方向的移动或旋转等操作,通常一个自由度用一个参数表示),不同的应用所需要的摄像机模式不同,所需要的自由度也不同,比如第一人称射击游戏和普通RPG游戏摄像机模式就不同。在第一人称射击游戏摄像机中,我们需要摄像机在原地转360度(沿y轴旋转360度,模拟人头的转动),另外还需要沿着观察方向,拉近,远移摄像机,或者上下倾斜摄像机,以便扩大观察视野 。我们将不会实现在上下方向旋转一圈(绕x轴或z轴旋转),这是飞行游戏中可能的摄像机模式,本篇教程中,我们只实现一个第一人称射击游戏的摄像机模式。 完成本篇教程后,我们将有一个摄像机类,该类中实现键盘和鼠标控制摄像机的操作。

    下图是世界战争2中的防空机枪,本篇教程中我们将实现类似该机枪操作的摄像机。

    aa_gun[1]

    机枪有2个控制轴:

    1. 它能沿着向量 (0,1,0)旋转360度,角度称作水平角,向量称作垂直轴。
    2. 它能够沿着平行地面的向量上下倾斜,但倾斜的角度是有限的,不可能转一圈,这个角度称作垂直角,这个向量称作水平轴。注意:垂直轴是向量 (0,1,0),而水平轴则是个变化的向量,它总是垂直于机枪瞄准的方向向量同时平行于地面(xz平面)。我们要正确理解这些概念,以便得到正确的数学公式。

                  我们的实现是通过鼠标左右移动来改变水平角,上下移动改变垂直角,得到这两个角度后,我们再来计算观察向量和up向量。

          通过水平角转动观察向量是很直观的,此时,观察向量的z分量是水平向量的sine值(如下图所示),x分量是水平向量的cosin值,此时y分量可以看作0。

    h_angle[1]

          通过垂直角来转动观察向量比较复杂,因为水平轴会随着摄像机转动而变化。当沿着水平角转动时,我们可以通过垂直轴观察向量的叉积计算得到水平轴。但当机枪上下移动时候,要得到水平轴,需要一定技巧。幸运的,我们可以通过一个方便的数学工具四元素,来实现这个功能。

         四元素由爱尔兰的数学家Willilam Rowan Hamilto在1843年首先提出,他的想法基于严密的数学逻辑。四元素Q定义如下:

    quaternion[1]

    这儿i,j,k是复数,且有下面的等式成立:

    quaternion1[2]

    我们常用4维向量 (x, y, z, w)来表示一个四元素,四元素 Q 的共轭四元素为:

    conjugate[1]

          归一化四元素和归一化向量的方法是一样的,下面我们将通过四元素来学习一个向量如何绕一个任意向量旋转。我们仅给出公式,关于原理可参考相关数学教程或到网上搜索。

    下面的公式描述了一个向量V绕四元素Q旋转角度a:

    rotation[1]

    旋转四元素Q定义如下:

    rotationq[1]

           旋转后的向量W可以简单表示为 (W.x,W.y,W.z),W计算中,我们首先用Q乘以V,这是四元素乘以向量,结果是四元素,接下来用这个四元素再乘以四元素Q的共轭四元素,结果是一个向量,这两种乘法是不一样的,在math_3d.cpp中实现了这些不同类型的乘法。

          当用户在屏幕上移动鼠标时候,我们需要实时更新水平角和垂直角。我们通过观察向量来初始化垂直角和水平角,上面的圆以及坐标系图是XZ平面图:

         假定观察向量为 (x,z),水平角alpha, (y分量只和垂直角有关),圆的半径是1,则可以看出alpha角的sine值等于z,通过反sine函数就可以求出alpha角,由于z范围是[-1,1],所以alpha角的值范围是-90度到90度,但水平角范围是360度,且四元素按顺时针旋转,这意味着我们通过四元素旋转90度,当z轴是-1时候,再转alpha角会从-90跳到90,这就会出现一些歧义。最简单的解决办法,就是我们只计算z范围[0,1],这时候得到正的alpha角0-90度,然后绑定结果在向量位于的1/4圆,例如当观察向量是(0,1)的时候,asin得到90度,360度减90度等于270度,则该向量的水平角是270度。

          垂直角的计算更加简单,我们首先限定它的范围是-90(看的直上)度到+90度(看的直下),这意味着我们只需要计算观察向量y分量的asin值,然后取反,例如Y=1,则为90度,取反-90度,此时看向上,Y=-1,为-90度,取反,90度,此时看向下。

    主要代码:

    camera.cpp

    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()函数设定摄像机的一些内部属性。

    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 = 90.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函数中,我们会计算水平角,另外我们还会创建一个targe向量称作HTarget(水平target向量),该向量是原始target向量在XZ平面上的投影,对该向量,我们会做归一化操作,接下来,我们会检测target向量在哪一个1/4圆里,并用它的z分量计算得到水平角。接下来,我们计算垂直角,这更简单,只需要一行代码。

          摄像机还有四个参数用来表示鼠标是否位于屏幕(或窗口)范围内,当超出屏幕范围时,我们会自动翻转方向,这将允许我们360度旋转。我们把这四个参数初始化为false,是因为鼠标初始位置在屏幕中心,新的glut函数glutWarpPointer用来移动鼠标。

    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();
    }

          这个函数用来通知摄像机鼠标已经移动,传入的参数x,y是鼠标在屏幕上的位置,delta是当前鼠标位置和上次鼠标位置的差,我们分别计算x和y方向的delta,计算完后,我们会把当前的鼠标位置保存在m_mousePos变量中,以便下次调用。接下来,我们用delta值更新当前摄像机的水平角和垂直角, 注意我们增加了一个缩放系数,可能在不同的计算机中,需要不同的系数,可以自己调整这个值。在后面的教程中,我们将会用帧率做为缩放系数,这样就能保证在不同计算机上应用程序执行效果一致。

          接下来通过鼠标位置更新 m_On*Edge变量,这儿还有一个margin变量,缺省值是10,表示鼠标靠近边10个像素时,触发靠近边界的事件。最后,我们调用Update函数基于新的水平角和垂直角重新计算target向量和up向量.

    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();
    }
    }

          这个摄像机渲染函数在main函数渲染循环中调用。当鼠标移动到窗口边界时,这时候将不会有鼠标事件,但我们仍需要调用这个函数进行检测,直到鼠标离开窗口边界。鼠标停留窗口边界时候,Render函数会根据相应的边界条件来更新相应的水平角和垂直角。当鼠标离开窗口范围时候,我们会检测到相应事件,并清除这些边界条件标志变量。

    (camera.cpp)

    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();
    }

          这个函数根据水平角和垂直角更新target和up向量。view向量初始化时候,平行于地面,这意味着垂直角为0,看向右边(x正轴方向),这意味着水平角为0,我们同时设定垂直轴指向上方,通过水平角来旋转view向量,得到结果总是指向要观察的物体,但摄像机的高度不一定正确,比如位于xz平面上,等于贴到了地面。我们用垂直轴和view向量做一个叉积,得到一个位于XZ平面上的向量,且该向量垂直于view向量和垂直轴位于的平面,这是我们新的水平轴, 接着我们会用这个新的水平轴旋转垂直角,结果就是最终的target向量。最后我们还要修正up向量,就是通过target向量和新的水平轴叉积得到新的up向量并归一化,这个修正是必要的,当摄像机抬头时候,up向量必须倾斜,就像我们抬头看天空时候,我们的头必须倾斜,此时up向量并不是y轴的方向,而是和垂直于我们的头。当摄像机上下倾斜时候,up向量也会不断变化。

    (tutorial15.cpp)

        //设定分辨率,颜色格式以及屏幕刷新率
        glutGameModeString("1920x1200:32@75");
       //进入游戏模式,即使glutGameModeString的设置生效
        glutEnterGameMode();


          这两个函数使我们的摄像机运行在高分率的游戏模式,也就是全屏模式,这使得360度旋转变得更容易观察。注意:分辨率和像素格式都是通过这个字符串定义的,32表示是32位颜色格式,即每个像素占32位,但这个全屏设置,不一定生效,有可能会失败,此时我们可以通过函数glutGameModeGet(GLenum mode)进行查询。

    tutorial15.cpp

    pGameCamera = new Camera(WINDOW_WIDTH, WINDOW_HEIGHT);

          动态创建摄像机,不能用静态变量,因为摄像机类初始化时候调用了glutWarpPointer,但如果glut没有初始化,则该函数会失败。

    glutPassiveMotionFunc(PassiveMouseCB);
    glutKeyboardFunc(KeyboardCB);

          这2个新的glut回调函数,一个用来处理鼠标事件,一个用来处理普通键盘事件(相对于特殊键),Passive移动意味着鼠标只是移动,并没有任何键(左右中)按下。

    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键退出游戏,鼠标回调函数仅仅简单把鼠标参数传给摄像机。

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

    在渲染函数中调用摄像机渲染函数,使得没有鼠标移动时候或者鼠标在屏幕边缘时候,也可以移动摄像机。

    在我的笔记本上,进入游戏模式失败,所以是一个1920*1200的窗口模式,但点击窗口用alt+prt截屏,却截的下面的界面:

    clipboard

  • 相关阅读:
    ASP.NET Core应用针对静态文件请求的处理[1]: 以Web的形式发布静态文件
    在ASP.NET Core应用中如何设置和获取与执行环境相关的信息?
    如何远程关闭一个ASP.NET Core应用?
    ASP.NET Core应用中如何记录和查看日志
    ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
    ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起
    学习ASP.NET Core, 怎能不了解请求处理管道[6]: 管道是如何随着WebHost的开启被构建出来的?
    学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter
    框架升级后某个类型所在程序集发生转移,应用还能正常运行吗?
    学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup
  • 原文地址:https://www.cnblogs.com/mikewolf2002/p/2865316.html
Copyright © 2011-2022 走看看