1. 程式人生 > >OGL(教程15)——攝像機控制2

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("[email protected]");
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();

我們在主渲染迴圈的時候,要通知攝像機。這樣就能保證,攝像機能夠根據滑鼠的移動來變化。