DirectX11--實現一個3D魔方(3)

前言

(2019/1/9 09:23)上一章咱們主要講述了魔方的旋轉,這個旋轉真是有毒啊,搞完這個部分搭鍵鼠操做不到半天應該就能夠搭完了吧...html

(2019/1/9 21:25)啊,真香git


有人發這張圖片問我寫魔方的目的是否是這個。。。噗github

如今光是鍵鼠相關的代碼也搭了400行左右。。其中鍵盤相關的調用真的是毫無技術可言,重點實現基本上都被鼠標給耽擱了。ide

回來看一眼發現閱讀量竟然比前面兩篇都還高了= =話說以前沒看過這個教程的。。。或許大家應該先看看前面兩章講了什麼內容?函數

本章將魔方應用層的剩餘實現補全。動畫

章節
實現一個3D魔方(1)
實現一個3D魔方(2)
實現一個3D魔方(3)

Github項目--魔方spa

最後平常安利一波本人正在編寫的DX11教程。code

DirectX11 With Windows SDK完整目錄orm

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。htm

我就簡單提一下鍵盤的邏輯

鍵盤操做使用的是DXTK通過修改的Keyboard庫。

由於以前說過,Rubik::RotateX函數在響應了來自鍵盤的輸入後,就會進入自動旋轉模式,此時的鍵盤輸入將不會響應。但後續還須要考慮作棧操做記錄,若是此時魔方正在旋轉,仍是要提早結束這個函數:

void GameApp::KeyInput()
{
    Keyboard::State keyState = mKeyboard->GetState();
    mKeyboardTracker.Update(keyState);

    //
    // 整個魔方旋轉
    //

    // 此時正在旋轉的話則提早結束
    if (mRubik.IsLocked())
        return;

    // 公式x
    if (mKeyboardTracker.IsKeyPressed(Keyboard::Up))
    {
        mRubik.RotateX(3, XM_PIDIV2);
        return;
    }
    
    // ...

    //
    // 雙層旋轉
    //

    // 公式r
    if (keyState.IsKeyDown(Keyboard::LeftControl) && mKeyboardTracker.IsKeyPressed(Keyboard::I))
    {
        mRubik.RotateX(-2, XM_PIDIV2);
        return;
    }
    
    // ...


    //
    // 單層旋轉
    //

    // 公式R
    if (mKeyboardTracker.IsKeyPressed(Keyboard::I))
    {
        mRubik.RotateX(2, XM_PIDIV2);
        return;
    }

    // ...
}

我列個表格來描述鍵盤的36種操做,就當作說明書來看吧:

鍵位 對應公式 描述 鍵位 對應公式 描述
Up x 整個魔方按x軸順時針旋轉 I R 右面兩層按x軸順時針旋轉
Down x' 整個魔方按x軸逆時針旋轉 K R' 右面兩層按x軸逆時針旋轉
Left y 整個魔方按y軸順時針旋轉 J U 頂面兩層按y軸順時針旋轉
Right y' 整個魔方按y軸逆時針旋轉 L U' 頂面兩層按y軸逆時針旋轉
Pg Up z' 整個魔方按z軸逆時針旋轉 U F' 正面兩層按z軸逆時針旋轉
Pg Down z 整個魔方按z軸順時針旋轉 O F 正面兩層按z軸順時針旋轉
-------- ---- ------------------------ -------- ---- ------------------------
LCtrl+I r 右面兩層按x軸順時針旋轉 T M 右面兩層按x軸順時針旋轉
LCtrl+K r' 右面兩層按x軸逆時針旋轉 G M' 右面兩層按x軸逆時針旋轉
LCtrl+J u 頂面兩層按y軸順時針旋轉 F E 頂面兩層按y軸順時針旋轉
LCtrl+L u' 頂面兩層按y軸逆時針旋轉 H E' 頂面兩層按y軸逆時針旋轉
LCtrl+U f' 正面兩層按z軸逆時針旋轉 R S' 正面兩層按z軸逆時針旋轉
LCtrl+O f 正面兩層按z軸順時針旋轉 Y S 正面兩層按z軸順時針旋轉
-------- ---- ------------------------ -------- ---- ------------------------
LCtrl+W l' 左面兩層按x軸逆時針旋轉 W L' 右面兩層按x軸順時針旋轉
LCtrl+S l 左面兩層按x軸順時針旋轉 S L 右面兩層按x軸逆時針旋轉
LCtrl+A d' 底面兩層按y軸逆時針旋轉 A D' 頂面兩層按y軸順時針旋轉
LCtrl+D d 底面兩層按y軸順時針旋轉 D D 頂面兩層按y軸逆時針旋轉
LCtrl+Q b 背面兩層按z軸順時針旋轉 Q B 正面兩層按z軸逆時針旋轉
LCtrl+E b' 背面兩層按z軸逆時針旋轉 E B' 正面兩層按z軸順時針旋轉

鼠標邏輯相關的實現

鼠標操做用的是DXTK通過修改的Mouse

鼠標相關的實現難度遠比鍵盤複雜多了,我主要分三個部分來說:

  1. 立方體的拾取與判斷拾取到的立方體表面
  2. 根據拖動方向判斷旋轉軸
  3. 鼠標在不一樣的操做階段對應的處理

在此以前,我先講講在這個項目加的一點點私貨

鼠標的輕微抖動效果

首先來看效果

這個效果的實現比較簡單,如今我使用的是第三人稱攝像機。現規定以遊戲窗口中心爲0偏移點,那麼偏離中心作左右移動會產生繞中心以Y軸旋轉,而作上下移動產生繞中心以X軸旋轉。

相關代碼的實現以下:

void GameApp::MouseInput(float dt)
{
    Mouse::State mouseState = mMouse->GetState();
    // ...

    // 獲取子類
    auto cam3rd = dynamic_cast<ThirdPersonCamera*>(mCamera.get());

    // ******************
    // 第三人稱攝像機的操做
    //

    // 繞物體旋轉,添加輕微抖動
    cam3rd->SetRotationX(XM_PIDIV2 * 0.6f + (mouseState.y - mClientHeight / 2) *  0.0001f);
    cam3rd->SetRotationY(-XM_PIDIV4 + (mouseState.x - mClientWidth / 2) * 0.0001f);
    cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);

    // 更新觀察矩陣
    mCamera->UpdateViewMatrix();
    mBasicEffect.SetViewMatrix(mCamera->GetViewXM());

    // 重置滾輪值
    mMouse->ResetScrollWheelValue();
    
    // ...
}

立方體的拾取與判斷拾取到的立方體表面

如今要先判斷鼠標點擊拾取到哪一個立方體,考慮到咱們能拾取到的立方體都是能夠看到的,這也說明它們的深度值確定是最小的。所以,咱們的Rubik::HitCube函數實現以下:

DirectX::XMINT3 Rubik::HitCube(Ray ray, float * pDist) const
{
    BoundingOrientedBox box(XMFLOAT3(), XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f));
    BoundingOrientedBox transformedBox;
    XMINT3 res = XMINT3(-1, -1, -1);
    float dist, minDist = FLT_MAX;

    // 優先拾取暴露在外的立方體(同時也是距離攝像機最近的)
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            for (int k = 0; k < 3; ++k)
            {
                box.Transform(transformedBox, mCubes[i][j][k].GetWorldMatrix());
                if (ray.Hit(transformedBox, &dist) && dist < minDist)
                {
                    minDist = dist;
                    res = XMINT3(i, j, k);
                }
            }
        }
    }
    if (pDist)
        *pDist = (minDist == FLT_MAX ? 0.0f : minDist);
        
    return res;
}

上面的函數會遍歷全部的立方體,找出深度最小且拾取到的立方體的索引值,經過pDist能夠返回射線起始點到目標立方體表面的最小距離。這個信息很是有用,稍後咱們會提到。

對了,若是沒有拾取到立方體呢?咱們能夠利用屏幕空白的地方,在拖動這些地方的時候會帶動整個魔方的旋轉。

根據拖動方向判斷旋轉軸

首先給出魔方旋轉軸的枚舉:

enum RubikRotationAxis {
    RubikRotationAxis_X,    // 繞X軸旋轉
    RubikRotationAxis_Y,    // 繞Y軸旋轉
    RubikRotationAxis_Z,    // 繞Z軸旋轉
};

如今讓咱們再看一眼魔方:

界面中能夠看到魔方的面有+X面,+Y面和-Z面。

在咱們拾取到立方體後,咱們還要根據這兩個信息來肯定旋轉軸:

  1. 當前具體是拾取到立方體的哪一個面
  2. 當前鼠標的拖動方向

這又是一個十分細的問題。其中-X面和-Z面在屏幕上是對稱關係,代碼實現能夠作鏡像處理,可是+Y面的操做跟其它兩個面又有一些差異。

鼠標落在立方體的-Z面

如今咱們只討論拾取到立方體索引[2][2][0]的狀況,鼠標落在了該立方體白色的表面上。咱們只是知道鼠標拾取到當前立方體上,那怎麼作才能知道它如今拾取的是其中的-Z面呢?

Rubik::HitCube函數不只返回了拾取到的立方體索引,還有射線擊中立方體表面的最短距離。咱們知道-Z面的全部頂點的z值在不產生旋轉的狀況下都會爲-3,所以咱們只須要將獲得的 \(t\) 值帶入射線方程 \(\mathbf{p}=\mathbf{e}+t\mathbf{d}\) 中,判斷求得的 \(\mathbf{p}\) 其中的z份量是否爲3,若是是,那說明當前鼠標拾取的是該立方體的-Z面。

接下來就是要討論用鼠標拖動魔方會產生怎麼樣的旋轉問題了。咱們還須要肯定當前的拖動會讓哪一層魔方旋轉(或者說繞什麼軸旋轉)。如下圖爲例:

上圖的X軸和Y軸對應的是屏幕座標系,座標軸的原點爲我鼠標剛點擊時的落點,經過兩條虛線,能夠將鼠標的拖動方向劃分爲四個部分,對應魔方旋轉的四種狀況。其中屏幕座標系的主+X(-X)拖動方向會使得魔方的+Y面作逆(順)時針旋轉,而屏幕座標系的主+Y(-Y)拖動方向會使得魔方的+X面作逆(順)時針旋轉。

咱們能夠將這些狀況進行簡單歸類,即當X方向的瞬時位移量比Y方向的大時,魔方的+Y面就會繞Y軸進行旋轉,反之則是魔方的+X面繞X軸進行旋轉。

如今新增了用於記錄魔方操做的RubikRotationRecord類:

struct RubikRotationRecord
{
    RubikRotationAxis axis; // 當前旋轉軸
    int pos;                // 當前旋轉層的索引
    float dTheta;           // 當前旋轉的弧度
};

這裏先把GameApp中全部與鼠標操做相關的新增成員先列出來,後面我就再也不重複:

//
// 鼠標操做控制
//
    
int mClickPosX, mClickPosY;                 // 初次點擊時鼠標位置
float mSlideDelay;                          // 拖動延遲響應時間 
float mCurrDelay;                           // 當前延遲時間
bool mDirectionLocked;                      // 方向鎖

RubikRotationRecord mCurrRotationRecord;    // 當前旋轉記錄

核心判斷方法以下:

// 判斷當前主要是垂直操做仍是水平操做
bool isVertical = abs(dx) < abs(dy);
// 當前鼠標操縱的是-Z面,根據操做類型決定旋轉軸
if (pos.z == 0 && fabs((ray.origin.z + dist * ray.direction.z) - (-3.0f)) < 1e-5f)
{
    mCurrRotationRecord.pos = isVertical ? pos.x : pos.y;
    mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_X : RubikRotationAxis_Y;
}

pos爲鼠標拾取到的立方體索引。

鼠標落在立方體的+X面

如今咱們拾取到了索引爲[2][2][0]立方體的+X面,該表面全部頂點的x值在不旋轉的狀況下爲3。當鼠標拖動時的X偏移量比Y的大時,會使得魔方的+Y面繞Y軸作旋轉,反之則使得魔方的-X面繞X軸作旋轉。

這部分的判斷以下:

// 當前鼠標操縱的是+X面,根據操做類型決定旋轉軸
if (pos.x == 2 && fabs((ray.origin.x + dist * ray.direction.x) - 3.0f) < 1e-5f)
{
    mCurrRotationRecord.pos = isVertical ? pos.z : pos.y;
    mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_Z : RubikRotationAxis_Y;
}

鼠標落在立方體的+Y面

以前+X面和-Z面在屏幕中是對稱的,處理過程基本上差很少。可是處理+Y面的狀況又不同了,先看下圖:

如今的虛線按垂直和水平方向劃分紅四個拖動區域。當鼠標在屏幕座標系拖動時,若是X的瞬時偏移量和Y的符號是一致的(劃分虛線的右下區域和左上區域), 魔方的-Z面會繞Z軸旋轉;若是異號(劃分虛線的左下區域和右上區域),魔方的+X面會繞X軸旋轉。

而後就是魔方+Y面的頂點在不產生旋轉的狀況下y值恆爲3,所以這部分的判斷邏輯以下:

// 當前鼠標操縱的是+Y面,要判斷平移變化量dx和dy的符號來決定旋轉方向
if (pos.y == 2 && fabs((ray.origin.y + dist * ray.direction.y) - 3.0f) < 1e-5f)
{
    // 判斷異號
    bool diffSign = ((dx & 0x80000000) != (dy & 0x80000000));
    mCurrRotationRecord.pos = diffSign ? pos.x : pos.z;
    mCurrRotationRecord.axis = diffSign ? RubikRotationAxis_X : RubikRotationAxis_Z;
}

鼠標沒有拾取到魔方

前面咱們一直都是在討論鼠標拾取到魔方的立方體產生了單層旋轉的狀況。如今咱們還想讓整個魔方進行旋轉,能夠依靠拖動遊戲界面的空白區域來實現,按下圖的方式劃分紅兩片區域:

只要在魔方區域外拖動,且水平偏移量比垂直的大,就會產生繞Y軸的旋轉。在窗口左(右)半部分產生了主垂直拖動則會繞X(Z)軸旋轉。

整個拾取部分的判斷以下:

// 找到當前鼠標點擊的方塊索引
Ray ray = Ray::ScreenToRay(*mCamera, (float)mouseState.x, (float)mouseState.y);
float dist;
XMINT3 pos = mRubik.HitCube(ray, &dist);

// 判斷當前主要是垂直操做仍是水平操做
bool isVertical = abs(dx) < abs(dy);
// 當前鼠標操縱的是-Z面,根據操做類型決定旋轉軸
if (pos.z == 0 && fabs((ray.origin.z + dist * ray.direction.z) - (-3.0f)) < 1e-5f)
{
    mCurrRotationRecord.pos = isVertical ? pos.x : pos.y;
    mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_X : RubikRotationAxis_Y;
}
// 當前鼠標操縱的是+X面,根據操做類型決定旋轉軸
else if (pos.x == 2 && fabs((ray.origin.x + dist * ray.direction.x) - 3.0f) < 1e-5f)
{
    mCurrRotationRecord.pos = isVertical ? pos.z : pos.y;
    mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_Z : RubikRotationAxis_Y;
}
// 當前鼠標操縱的是+Y面,要判斷平移變化量dx和dy的符號來決定旋轉方向
else if (pos.y == 2 && fabs((ray.origin.y + dist * ray.direction.y) - 3.0f) < 1e-5f)
{
    // 判斷異號
    bool diffSign = ((dx & 0x80000000) != (dy & 0x80000000));
    mCurrRotationRecord.pos = diffSign ? pos.x : pos.z;
    mCurrRotationRecord.axis = diffSign ? RubikRotationAxis_X : RubikRotationAxis_Z;
}
// 當前鼠標操縱的是空白地區,則對整個魔方旋轉
else
{
    mCurrRotationRecord.pos = 3;
    // 水平操做是Y軸旋轉
    if (!isVertical)
    {
        mCurrRotationRecord.axis = RubikRotationAxis_Y;
    }
    // 屏幕左半部分的垂直操做是X軸旋轉
    else if (mouseState.x < mClientWidth / 2)
    {
        mCurrRotationRecord.axis = RubikRotationAxis_X;
    }
    // 屏幕右半部分的垂直操做是Z軸旋轉
    else
    {
        mCurrRotationRecord.axis = RubikRotationAxis_Z;
    }
}

鼠標在不一樣的操做階段對應的處理

鼠標拖動魔方旋轉能夠分爲三個階段:鼠標初次點擊、鼠標產生拖動、鼠標剛釋放。

肯定拖動方向

在鼠標初次點擊的時候不必定會產生偏移量,但咱們必需要在這個時候判斷鼠標是在作垂直拖動仍是豎直拖動來肯定當前的旋轉軸,以限制魔方的旋轉。

如今要考慮這樣一個狀況,我鼠標在初次點擊魔方時可能會由於手抖或者鼠標不穩產生了一個如下方向爲主的瞬時移動,而後程序判斷我如今在作向下的拖動,但實際狀況倒是我須要向右方向拖動鼠標,程序卻只容許我上下拖動。這就十分尷尬了。

因爲鼠標的拖動過程相對程序的運行會比較緩慢,咱們能夠給程序加上一個延遲判斷。好比說我如今能夠根據鼠標初次點擊後的0.05s內產生的累計垂直/水平偏移量來判斷此時是水平拖動仍是豎直拖動。

此外,一旦肯定這段時間內產生了偏移值,必需要加上方向鎖,防止後續又從新判斷旋轉方向。

這部分代碼實現以下:

// 此時未肯定旋轉方向
if (!mDirectionLocked)
{
    // 此時未記錄點擊位置
    if (mClickPosX == -1 && mClickPosY == -1)
    {
        // 初次點擊
        if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::PRESSED)
        {
            // 記錄點擊位置
            mClickPosX = mouseState.x;
            mClickPosY = mouseState.y;
        }
    }
            
    // 僅當記錄了點擊位置才進行更新
    if (mClickPosX != -1 && mClickPosY != -1)
        mCurrDelay += dt;
    // 未到達滑動延遲時間則結束
    if (mCurrDelay < mSlideDelay)
        return;

    // 未產生運動則不上鎖
    if (abs(dx) == abs(dy))
        return;

    // 開始上方向鎖
    mDirectionLocked = true;
    // 更新累積的位移變化量
    dx = mouseState.x - mClickPosX;
    dy = mouseState.y - mClickPosY;

    // 找到當前鼠標點擊的方塊索引
    Ray ray = Ray::ScreenToRay(*mCamera, (float)mouseState.x, (float)mouseState.y);
    // ...剩餘部分就是上面的代碼
}

拖動時更新魔方狀態

這部分實現就比較簡單了。只要鼠標左鍵按下,且確認方向鎖,就能夠進行魔方的旋轉。

若是是繞X軸的旋轉,鼠標向右移動和向上移動都會產生順時針旋轉。
若是是繞Y軸的旋轉,只有鼠標向左移動纔會產生順時針旋轉。
若是是繞Z軸的旋轉,鼠標向左移動和向上移動都會產生順時針旋轉。

這裏的Rotate函數最後一個參數必需要傳遞true以告訴內部不要進行預旋轉操做。

// 上了方向鎖才能進行旋轉
if (mDirectionLocked)
{
    // 進行旋轉
    switch (mCurrRotationRecord.axis)
    {
    case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, (dx - dy) * 0.008f, true); break;
    case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, -dx * 0.008f, true); break;
    case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, (-dx - dy) * 0.008f, true); break;
    }
}

拖動完成後的操做

完成拖動後,須要恢復方向鎖和滑動延遲,而且鼠標剛釋放時產生的偏移咱們直接丟掉。如今Rotate函數僅用於發送進行預旋轉的命令:

// 鼠標左鍵是否點擊
if (mouseState.leftButton)
{
    // ...
}
// 鼠標剛釋放
else if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::RELEASED)
{
    // 釋放方向鎖
    mDirectionLocked = false;
    // 滑動延遲歸零
    mCurrDelay = 0.0f;
    // 座標移出屏幕
    mClickPosX = mClickPosY = -1;
    // 發送完成指令,進行預旋轉
    switch (mCurrRotationAxis)
    {
    case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, 0.0f); break;
    case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, 0.0f); break;
    case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, 0.0f); break;
    }
}

最終鼠標拖動的效果以下:

鍵盤的效果以下:

撤銷操做的支持

回顧一下RubikRotationRecord類的定義:

struct RubikRotationRecord
{
    RubikRotationAxis axis; // 當前旋轉軸
    int pos;                // 當前旋轉層的索引
    float dTheta;           // 當前旋轉的弧度
};

pos爲0-2時,均爲單層魔方的旋轉,-1和-2爲雙層魔方的旋轉,3則爲整個魔方的旋轉。

咱們使用一個棧來記錄用戶的操做,它放在了GameApp類中:

std::stack<RubikRotationRecord> mRotationRecordStack;

對於鍵盤操做來講特別簡單,只須要在每次操做後記錄便可:

// 公式x
if (mKeyboardTracker.IsKeyPressed(Keyboard::Up))
{
    mRubik.RotateX(3, XM_PIDIV2);
    // 此處新增
    mRotationRecordStack.push(RubikRotationRecord{ RubikRotationAxis_X, 3, XM_PIDIV2 });
    return;
}

而鼠標操做是一個連續的過程,而且記錄要點以下:

  1. 鼠標正在拖動的過程不記錄,只記錄釋放鼠標的瞬間
  2. 對於鼠標旋轉角度(-pi/2 + 2kpi, pi/2 + 2kpi)的操做都不要記錄,這些操做至關於沒有實質性旋轉
  3. 對於實質性的旋轉最終角度都按pi/2的倍數來記錄

鼠標釋放部分通過修改後:

// 鼠標剛釋放
if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::RELEASED)
{
    // 釋放方向鎖
    mDirectionLocked = false;
    // 滑動延遲歸零
    mCurrDelay = 0.0f;
    // 座標移出屏幕
    mClickPosX = mClickPosY = -1;

    // 發送完成指令,進行預旋轉
    switch (mCurrRotationRecord.axis)
    {
    case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, 0.0f); break;
    case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, 0.0f); break;
    case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, 0.0f); break;
    }

    // 此處新增
    // 若此次旋轉有意義,記錄到棧中
    int times = static_cast<int>(round(mCurrRotationRecord.dTheta / XM_PIDIV2)) % 4;
    if (times != 0)
    {
        mCurrRotationRecord.dTheta = times * XM_PIDIV2;
        mRotationRecordStack.push(mCurrRotationRecord);
    }
    // 旋轉值歸零
    mCurrRotationRecord.dTheta = 0.0f;
}

開局打亂魔方

上面的那個棧不只能夠用來記錄用戶操做記錄,還能夠用來存儲打亂魔方的操做。即遊戲剛開始先給這個棧塞入一堆隨機操做,而後每執行一個操做就退棧一次,直到棧空時打亂操做完成,用戶能夠開始對魔方進行操做,同時這個棧也開始記錄用戶操做。

GameApp::Shuffle的操做以下:

void GameApp::Shuffle()
{
    // 清棧
    while (!mRotationRecordStack.empty())
        mRotationRecordStack.pop();
    // 往棧上塞30個隨機旋轉操做用於打亂
    RubikRotationRecord record;
    srand(static_cast<unsigned>(time(nullptr)));
    for (int i = 0; i < 30; ++i)
    {
        record.axis = static_cast<RubikRotationAxis>(rand() % 3);
        record.pos = rand() % 4;
        record.dTheta = XM_PIDIV2 * (rand() % 2 ? 1 : -1);
        mRotationRecordStack.push(record);
    }
}

添加開場攝像機旋轉動畫

這是一個簡單的攝像機移動過程,包含的繞Y軸的旋轉和鏡頭的推動。這個動畫過程須要根據幀時間間隔作更新。總體動畫時間爲5s,在沒有結束前GameApp::PlayCameraAnimation會返回false,完成動畫後則返回true

bool GameApp::PlayCameraAnimation(float dt)
{
    // 獲取子類
    auto cam3rd = dynamic_cast<ThirdPersonCamera*>(mCamera.get());

    // ******************
    // 第三人稱攝像機的操做
    //
    mAnimationTime += dt;
    float theta, dist;

    theta = -XM_PIDIV2 + XM_PIDIV4 * mAnimationTime * 0.2f;
    dist = 20.0f - mAnimationTime * 2.0f;
    if (theta > -XM_PIDIV4)
        theta = -XM_PIDIV4;
    if (dist < 10.0f)
        dist = 10.0f;

    cam3rd->SetRotationY(theta);
    cam3rd->SetDistance(dist);

    // 更新觀察矩陣
    mCamera->UpdateViewMatrix();
    mBasicEffect.SetViewMatrix(mCamera->GetViewXM());

    if (fabs(theta + XM_PIDIV4) < 1e-5f && fabs(dist - 10.0f) < 1e-5f)
        return true;
    return false;
}

注意GameApp::PlayCameraAnimation絕對不能同GameApp::MouseInput或者GameApp::KeyInput共存!

開場動畫+打亂效果以下:

至此魔方的應用層就講述到這裏,剩下的邏輯部分實現能夠參考源碼,本系列教程到這裏就結束了。該DX11實現的魔方的功能跟DX9比起來有多的地方,也有少的地方,我的感受不必再增長新的東西。畢竟做爲一個遊戲來講,它算是一個合格的做品了。

此外我以爲沒有必要展開大的篇幅再來說底層的實現,我更但願的是你能跟着個人DX11教程把底層好好的過一遍,裏面有些部分的內容是在龍書裏面沒有涉及到的。

Github項目--魔方

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索