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

前言

上一章咱們主要講述了魔方的構造和初始化、紋理的準備工做。目前我尚未打算講Direct3D 11關於底層繪圖的實現,所以接下來這一章的重點是魔方的旋轉。由於咱們要的是能玩的魔方遊戲,而不是一個觀賞品。因此對旋轉這一步的處理就顯得尤爲重要,甚至能夠展開很大的篇幅來說述。如今光是爲了實現旋轉的這個動畫就弄了我大概500行代碼。html

這個旋轉包含了單層旋轉、雙層旋轉、整個魔方旋轉以及魔方的自動旋轉動畫。git

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

Github項目--魔方github

平常安利一波本人正在編寫的DX11教程。數組

DirectX11 With Windows SDK完整目錄函數

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

一個立方體繞魔方的旋轉

回顧一下立方體結構體Cube的定義:動畫

struct Cube
{
    // 獲取當前立方體的世界矩陣
    DirectX::XMMATRIX GetWorldMatrix() const;

    RubikFaceColor faceColors[6];   // 六個面的顏色,索引0-5分別對應+X, -X, +Y, -Y, +Z, -Z面
    DirectX::XMFLOAT3 pos;          // 旋轉結束後中心所處位置
    DirectX::XMFLOAT3 rotation;     // 僅容許存在單軸旋轉,記錄當前分別繞x軸, y軸, z軸旋轉的弧度

};

這裏能夠經過修改rotaion份量的值來指定魔方繞中心點以什麼軸旋轉,好比說rotation.x = XM_PIDIV2是指當前立方體須要繞中心點以X軸按順時針旋轉90度(從座標軸正方向朝中心點看)。spa

以前提到魔方的正中心位於世界座標系的原點,這樣方便咱們進行旋轉操做以節省沒必要要的平移。如今咱們只討論魔方的其中一個立方體的旋轉狀況,它須要繞Z軸順時針旋轉θ度。
3d

這整個過程能夠拆分紅旋轉和平移。其中立方體的旋轉能夠理解爲移到中心按順時針旋轉θ度,而後再平移到目標位置。code

變換過程能夠用下面的公式表示,其中p爲旋轉前立方體的中心位置(即成員pos),p' 爲旋轉後立方體的中心位置,Rz(θ) 爲繞z軸順時針旋轉θ度(即成員rotation.z),Tp'則是平移矩陣,vv'分別爲變換先後的立方體頂點:

\[ \mathbf{p'} = \mathbf{p} \times \mathbf{R_{z}(θ)} \]
\[ \mathbf{v'} = \mathbf{v} \times \mathbf{R_{z}(θ)} \times \mathbf{T_{p'}}\]

如今咱們來考慮這樣一個場景,假如rotation容許其x,y,z值任意,當這個魔方處於已經被徹底打亂的狀態時,這個魔方的物理(內存索引)位置和邏輯(遊戲中)的位置僅能憑藉posrotation聯繫起來。那麼,我如今要順時針轉動如今這個魔方的右面,我怎麼知道這9個邏輯上的立方體原來所處的物理位置在哪裏?顯然要找到它們對應所處的索引是困難的,這麼作還不如保證魔方的物理位置和邏輯位置是一致的,這樣才能方便我直接根據索引來指定哪些立方體須要旋轉。

此外,在實際遊玩魔方的時候始終只會對其中一層或整個魔方進行旋轉,不可能會同時出現諸如正面順時針和頂面順時針旋轉的狀況,即全部的立方體在同一時間段毫不可能會出現相似rotation.yrotation.z都是非0的狀況。所以最終Cube::GetWorldMatrix的代碼能夠表示成:

DirectX::XMMATRIX Cube::GetWorldMatrix() const
{
    XMVECTOR posVec = XMLoadFloat3(&pos);
    // rotation必然最多隻有一個份量是非0,保證其只會繞其中一個軸進行旋轉
    XMMATRIX R = XMMatrixRotationRollPitchYaw(rotation.x, rotation.y, rotation.z);
    posVec = XMVector3TransformCoord(posVec, R);
    // 立方體轉動後最終的位置
    XMFLOAT3 finalPos;
    XMStoreFloat3(&finalPos, posVec);

    return XMMatrixRotationRollPitchYaw(rotation.x, rotation.y, rotation.z) *
        XMMatrixTranslation(finalPos.x, finalPos.y, finalPos.z);
}

XMMatrixRotationRollPitchYaw函數是先按Z軸順時針旋轉,再按X軸順時針旋轉,最後按Y軸順時針旋轉。它實際上只會根據rotation來按其中一個軸旋轉。

如今咱們嘗試給魔方的頂面繞Y軸順時針旋轉,在Rubik::Update方法內部用下述代碼嘗試一下

void Rubik::Update(float dt)
{
    for (int i = 0; i < 3; ++i)
        for (int k = 0; k < 3; ++k)
            mCubes[i][2][k].rotation.y += XM_PI * dt;
}

而後在GameApp::UpdateScene調用Rubik::Update

void GameApp::UpdateScene(float dt)
{
    mRubik.Update(dt);
}

你看,它轉起來啦!

魔方的旋轉保護

以前的旋轉都是基於rotation最多隻能有一個份量是非0的理想狀況,可是若是上面的旋轉不作防禦的話,不免會致使用戶在操做魔方的時候出現異常。如今Rubik類的變更以下:

class Rubik
{
public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    Rubik();

    // 初始化資源
    void InitResources(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext);
    // 當即復原魔方
    void Reset();
    // 更新魔方狀態
    void Update(float dt);
    // 繪製魔方
    void Draw(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect);
    // 當前是否在進行動畫中
    bool IsLocked() const;


    // pos的取值爲0-2時,繞X軸旋轉魔方指定層 
    // pos的取值爲-1時,繞X軸旋轉魔方pos爲0和1的兩層
    // pos的取值爲-2時,繞X軸旋轉魔方pos爲1和2的兩層
    // pos的取值爲3時,繞X軸旋轉整個魔方
    void RotateX(int pos, float dTheta, bool isPressed = false);

    // pos的取值爲3時,繞Y軸旋轉魔方指定層 
    // pos的取值爲-1時,繞Y軸旋轉魔方pos爲0和1的兩層
    // pos的取值爲-2時,繞Y軸旋轉魔方pos爲1和2的兩層
    // pos的取值爲3時,繞Y軸旋轉整個魔方
    void RotateY(int pos, float dTheta, bool isPressed = false);

    // pos的取值爲0-2時,繞Z軸旋轉魔方指定層 
    // pos的取值爲-1時,繞Z軸旋轉魔方pos爲0和1的兩層
    // pos的取值爲-2時,繞Z軸旋轉魔方pos爲1和2的兩層
    // pos的取值爲3時,繞Z軸旋轉整個魔方
    void RotateZ(int pos, float dTheta, bool isPressed = false);
    
    
    

    // 設置旋轉速度(rad/s)
    void SetRotationSpeed(float rad);

    // 獲取紋理數組
    ComPtr<ID3D11ShaderResourceView> GetTexArray() const;

private:
    // 繞X軸的預旋轉
    void PreRotateX(bool isKeyOp);
    // 繞Y軸的預旋轉
    void PreRotateY(bool isKeyOp);
    // 繞Z軸的預旋轉
    void PreRotateZ(bool isKeyOp);

    // 獲取須要與當前索引的值進行交換的索引,用於模擬旋轉
    // outArr1 { [X1][Y1] [X2][Y2] ... }
    //              ||       ||
    // outArr2 { [X1][Y1] [X2][Y2] ... }
    void GetSwapIndexArray(int times, std::vector<DirectX::XMINT2>& outArr1, 
        std::vector<DirectX::XMINT2>& outArr2) const;

    // 獲取繞X軸旋轉的狀況下須要與目標索引塊交換的面,用於模擬旋轉
    // cube[][Y][Z].face1 <--> cube[][Y][Z].face2
    RubikFace GetTargetSwapFaceRotationX(RubikFace face, int times) const;
    // 獲取繞Y軸旋轉的狀況下須要與目標索引塊交換的面,用於模擬旋轉
    // cube[X][][Z].face1 <--> cube[X][][Z].face2
    RubikFace GetTargetSwapFaceRotationY(RubikFace face, int times) const;
    // 獲取繞Z軸旋轉的狀況下須要與目標索引塊交換的面,用於模擬旋轉
    // cube[X][Y][].face1 <--> cube[X][Y][].face2
    RubikFace GetTargetSwapFaceRotationZ(RubikFace face, int times) const;

private:
    // 魔方 [X][Y][Z]
    Cube mCubes[3][3][3];

    // 當前是否鼠標正在拖動
    bool mIsPressed;
    // 當前是否有動畫在播放
    bool mIsLocked;
    // 當前自動旋轉的速度
    float mRotationSpeed;

    // 頂點緩衝區,包含6個面的24個頂點
    // 索引0-3對應+X面
    // 索引4-7對應-X面
    // 索引8-11對應+Y面
    // 索引12-15對應-Y面
    // 索引16-19對應+Z面
    // 索引20-23對應-Z面
    ComPtr<ID3D11Buffer> mVertexBuffer; 

    // 索引緩衝區,僅6個索引
    ComPtr<ID3D11Buffer> mIndexBuffer;
    
    // 紋理數組,包含7張紋理
    ComPtr<ID3D11ShaderResourceView> mTexArray;
};

其中mIsPressedmIsLocked兩個成員用於保護控制。考慮到魔方項目須要同時支持鍵盤和鼠標的操做,可是鍵盤和鼠標的操做特性是不同的,鍵盤是按鍵後就會響應旋轉動畫,而鼠標則是在拖動的時候就在旋轉魔方,而且放開後魔方還要歸位。

下面是關於旋轉保護的狀態圖:

mIsLockedtrue時,此時將會拒絕鍵盤或鼠標的響應,也就是說這個時候的旋轉函數應該是不進行任何的操做。

好比說如今咱們魔方旋轉的方法是這樣的:

// pos的取值爲0-2時,繞X軸旋轉魔方指定層 
// pos的取值爲-1時,繞X軸旋轉魔方pos爲0和1的兩層
// pos的取值爲-2時,繞X軸旋轉魔方pos爲1和2的兩層
// pos的取值爲3時,繞X軸旋轉整個魔方
void RotateX(int pos, float dTheta, bool isPressed = false);

其中isPressedtrue的時候會告訴魔方如今正在用鼠標拖動,反之則爲鍵盤操做或者鼠標完成了拖動。

這裏還有一個潛藏的問題要解決。當mIsLockedfalse的時候,可能這時鼠標正在拖動魔方,而後忽然來了個鍵盤的響應,這時候致使的結果就很嚴重了。要想讓鍵盤和鼠標的操做互斥,就必須嚴格按照狀態圖的流程來執行。(寫到這裏含淚修改本身的代碼)

因爲鍵盤按下後會致使在這一幀產生一個90度的瞬時響應,而讓鼠標在一幀內拖動出90度是幾乎不可能的,咱們能夠把它用做判斷此時執行的是鍵盤操做。若是mIsPressedtrue,說明如今同時發生了鍵盤和鼠標的操做,須要把來自鍵盤的操做給拒絕掉。

此外咱們能夠推廣到180度, 270度等狀況。雖說鍵盤只能產生90度旋轉,可是若是咱們要用棧來記錄玩家的操做的話,鼠標拖動產生的180度旋轉若是也能被標記爲所謂的鍵盤輸入,這樣就能夠一個調用讓魔方自動產生180度的旋轉了。

如今排除全部旋轉相關的實現,加上保護後的代碼以下:

void Rubik::RotateX(int pos, float dTheta, bool isPressed)
{
    if (!mIsLocked)
    {
        // 檢驗當前是否爲鍵盤操做
        // 能夠認爲僅當鍵盤操做時纔會產生絕對值爲pi/2的倍數(不包括0)的瞬時值
        bool isKeyOp =  static_cast<int>(round(dTheta / XM_PIDIV2)) != 0 &&
            (fabs(fmod(dTheta, XM_PIDIV2) < 1e-5f));
        // 鍵盤輸入和鼠標操做互斥,拒絕鍵盤的操做
        if (mIsPressed && isKeyOp)
        {
            return;
        }

        mIsPressed = isPressed;

        // ...

        // 鼠標或鍵盤操做完成
        if (!isPressed)
        {
            
            // 開始動畫演示狀態
            mIsLocked = true;
            
            // ...
        }
    }
}

魔方的旋轉動畫

旋轉動畫能夠說是本篇文章的核心部分了。能夠說這個旋轉自己包含了不少的tricks,不是給rotation加個值這麼簡單的事情,還須要考慮鍵鼠操做的可連續性。

首先,鍵盤操做的話必然只會順(逆)時針旋轉90度,而且只會產生一次有效的Rotation操做。

鼠標操做的隨意性比鍵盤會大的多,在釋放的時候旋轉的角度均可能會是任意的,它會產生連續的Rotation操做,在拖動的時候傳遞mIsPressed = true,僅在最後釋放的時候傳遞mIsPressed = false

如今讓咱們給Rubik::RotateX加上初步的更新操做:

void Rubik::RotateX(int pos, float dTheta, bool isPressed)
{
    if (!mIsLocked)
    {
        // 檢驗當前是否爲鍵盤操做
        // 能夠認爲僅當鍵盤操做時纔會產生絕對值爲pi/2的倍數(不包括0)的瞬時值
        bool isKeyOp =  static_cast<int>(round(dTheta / XM_PIDIV2)) != 0 &&
            (fabs(fmod(dTheta, XM_PIDIV2) < 1e-5f));
        // 鍵盤輸入和鼠標操做互斥,拒絕鍵盤的操做
        if (mIsPressed && isKeyOp)
        {
            return;
        }

        mIsPressed = isPressed;

        // 更新旋轉狀態
        for (int j = 0; j < 3; ++j)
            for (int k = 0; k < 3; ++k)
            {
                switch (pos)
                {
                case 3: mCubes[0][j][k].rotation.x += dTheta;
                case -2: mCubes[1][j][k].rotation.x += dTheta;
                    mCubes[2][j][k].rotation.x += dTheta;
                    break;
                case -1: mCubes[0][j][k].rotation.x += dTheta; 
                    mCubes[1][j][k].rotation.x += dTheta; 
                    break;
                
                default: mCubes[pos][j][k].rotation.x += dTheta;
                }
                
            }

        // 鼠標或鍵盤操做完成
        if (!isPressed)
        {
            
            // 開始動畫演示狀態
            mIsLocked = true;
            
            // 進行預旋轉
            PreRotateX(isKeyOp);
        }
    }
}

而後要討論的就是怎麼實現這個自動旋轉的動畫了(即整個PreRotateX函數的實現)。以前提到爲了方便後續操做,必須保持魔方的邏輯位置(遊戲中的座標)與物理位置(內存索引)一致,這意味所謂的旋轉是經過將被旋轉立方體的數據所有按規則轉移到目標立方體中。其中旋轉角度對於旋轉中的全部立方體都是一致的,因此理論上咱們只須要修改魔方的6個面顏色。

不過在此以前,還須要解決一個鼠標/鍵盤釋放後歸位的問題。

魔方的預旋轉

操做完成後魔方按區間歸位的問題

使用鍵盤操做的話,若是我對頂層順時針旋轉90度,那理論要播放這個動畫的話就是讓魔方的旋轉角度值從0度一路增長到90度。

可是使用鼠標操做的話,若是我拖到順時針30度後釋放(這個操做因爲拖動的角度不夠大,最終會歸回到0度),而後這個動畫就是要讓魔方的旋轉角度值從順時針30度變回0度,只有當鼠標拖動到順時針在45度到接近90度的範圍後釋放的時候,旋轉動畫纔會一路增長到90度。這裏進行一個總結:

釋放時旋轉角度落在[-45°, 45°)時,旋轉動畫結束後會歸位到0度,釋放時旋轉角度落在[45°, 135°)時,旋轉動畫結束後會歸位到90度,以此類推...

從上面的需求咱們能夠看出一些須要解決的問題,一是終止條件不惟一,不利於咱們作判斷;二是魔方在旋轉完成後可能會出現有的立方體rotation存在份量非0的狀況,而後違背了魔方的邏輯位置(遊戲中的座標)與物理位置(內存索引)一致的要求,對後續操做產生影響。

所以,這裏有兩個tricks:

  1. 把全部的終止條件都變爲歸位到0度,這樣意味着只要rotation存在份量的值大於0,就須要讓它逐漸減少到0;rotation存在份量的值小於0,就須要讓它逐漸增長到0.
  2. 咱們能夠在鍵盤按下,或者鼠標釋放後動畫即將開始的瞬間,當即對換全部準備旋轉的立方體的表面,進行預旋轉。這樣正在執行的動畫就只涉及普通的旋轉操做了。

舉個例子,我鼠標拖動某一層到順時針60度的位置釋放,這時候我可讓這一層的貼圖先進行一次90度順時針旋轉,而後把rotation的值減90度,來到-30度,而後一路加回0度。這樣就至關於從60度過渡到90度了。

同理,我鼠標拖動某一層到逆時針160度的位置(超過135度)釋放,這時候我可讓這一層的貼圖先進行一次180度逆時針旋轉,而後把rotation的值加180度,來到20度,而後一路減回0度。這樣就至關於從-160度過渡到-180度了。

而對於鍵盤操做的處理稍微有點特別,按下順時針旋轉的按鍵後會產生一個90度的變化值,這時候我可讓這一層的貼圖先進行一次90度順時針旋轉,而後把rotation的值取反變成-90度,而後一路加回0度。這樣就至關於從0度過渡到90度了。

一個小小的旋轉,裏面竟藏着這麼大的玄機!

緊接着就是要進行代碼分析了,咱們須要先計算出當前開始旋轉的角度須要預先進行幾回90度的順時針旋轉(可能爲負)。再看看這個映射關係:

區間 次數
... ...
(-135°, 45°] -1
(-45°, 45°) 0
[45°, 135°) 1
... ...

咱們能夠推導出:

\[ times = round(\frac{2θ}{\pi}) \]

而後每4次90度順時針旋轉爲一個循環,而且1次90度逆時針旋轉等價於3次90度順時針旋轉。首先咱們進行一次模4運算,這樣結果就映射到區間[-3, 3]內,爲了把times再映射到範圍[0, 4),能夠對結果加4,再進行一次模4運算。

這兩部分代碼能夠寫成:

// 因爲此時被旋轉面的全部方塊旋轉角度都是同樣的,能夠從中取一個來計算。
// 計算歸位回[-pi/4, pi/4)區間須要順時針旋轉90度的次數
int times = static_cast<int>(round(mCubes[pos][0][0].rotation.x / XM_PIDIV2));
// 將歸位次數映射到[0, 3],以計算最小所需順時針旋轉90度的次數
int minTimes = (times % 4 + 4) % 4;

而後若是是鼠標操做的話,咱們能夠利用times作區間歸位:

// 歸位回[-pi/4, pi/4)的區間
mCubes[pos][j][k].rotation.x -= times * XM_PIDIV2;

若是是鍵盤操做的話,則能夠直接作值反轉:

// 順時針旋轉90度--->實際演算從-90度加到0度
// 逆時針旋轉90度--->實際演算從90度減到0度
mCubes[pos][j][k].rotation.x *= -1.0f;

如今咱們將整個預旋轉的操做放到了Rubic::PreRotateX方法中,部分代碼以下(未包含面的對換):

void Rubik::PreRotateX(bool isKeyOp)
{
    for (int i = 0; i < 3; ++i)
    {
        // 當前層沒有旋轉則直接跳過
        if (fabs(mCubes[i][0][0].rotation.x) < 10e-5f)
            continue;
        // 因爲此時被旋轉面的全部方塊旋轉角度都是同樣的,能夠從中取一個來計算。
        // 計算歸位回[-pi/4, pi/4)區間須要順時針旋轉90度的次數
        int times = static_cast<int>(round(mCubes[i][0][0].rotation.x / XM_PIDIV2));
        // 將歸位次數映射到[0, 3],以計算最小所需順時針旋轉90度的次數
        int minTimes = (times % 4 + 4) % 4;

        // 調整全部被旋轉方塊的初始角度
        for (int j = 0; j < 3; ++j)
        {
            for (int k = 0; k < 3; ++k)
            {
                // 鍵盤按下後的變化
                if (isKeyOp)
                {
                    // 順時針旋轉90度--->實際演算從-90度加到0度
                    // 逆時針旋轉90度--->實際演算從90度減到0度
                    mCubes[i][j][k].rotation.x *= -1.0f;
                }
                // 鼠標釋放後的變化
                else
                {
                    // 歸位回[-pi/4, pi/4)的區間
                    mCubes[i][j][k].rotation.x -= times * XM_PIDIV2;
                }
            }
        }

        // ...
    }
}

實際的預旋轉操做

有兩種方式能夠完成魔方的預旋轉:

  1. 開啓一個3x3的立方體臨時數據,而後從源數據按旋轉規則傳遞給臨時數據,再複製回來。
  2. 經過交換的方式完成就址旋轉。

從實現難度來看明顯是2比1難的多,可是從DX9的魔方項目我都是用第2種方式來解決旋轉問題的。我也仍是接着這個思路來繼續談。

如今我依然要面臨兩個難題:

  1. 怎麼的交換順序才能產生最終相似旋轉的效果
  2. 交換時兩個立方體的六個面應該按怎樣的規則來交換

交換實現旋轉的原理

以前提到,全部的旋轉最終均可以化爲0次到3次順時針旋轉的問題,咱們爲此要分3種狀況來討論。爲此我作了一幅圖來講明一切:

可見順時針旋轉90度和270度的狀況下須要交換6次,而旋轉180度的狀況下只須要交換4次。

全部的交換規則能夠用下面的函數來獲取:

void Rubik::GetSwapIndexArray(int minTimes, std::vector<DirectX::XMINT2>& outArr1, std::vector<DirectX::XMINT2>& outArr2) const
{
    // 進行一次順時針90度旋轉至關逆時針交換6次(頂角和棱各3次)
    // 1   2   4   2   4   2   4   1
    //   *   ->  *   ->  *   ->  *
    // 4   3   1   3   3   1   3   2
    if (minTimes == 1)
    {
        outArr1 = { XMINT2(0, 0), XMINT2(0, 1), XMINT2(0, 2), XMINT2(1, 2), XMINT2(2, 2), XMINT2(2, 1) };
        outArr2 = { XMINT2(0, 2), XMINT2(1, 2), XMINT2(2, 2), XMINT2(2, 1), XMINT2(2, 0), XMINT2(1, 0) };
    }
    // 進行一次順時針90度旋轉至關逆時針交換4次(頂角和棱各2次)
    // 1   2   3   2   3   4
    //   *   ->  *   ->  *  
    // 4   3   4   1   2   1
    else if (minTimes == 2)
    {
        outArr1 = { XMINT2(0, 0), XMINT2(0, 1), XMINT2(0, 2), XMINT2(1, 2) };
        outArr2 = { XMINT2(2, 2), XMINT2(2, 1), XMINT2(2, 0), XMINT2(1, 0) };
    }
    // 進行一次順時針90度旋轉至關逆時針交換6次(頂角和棱各3次)
    // 1   2   4   2   4   2   4   1
    //   *   ->  *   ->  *   ->  *
    // 4   3   1   3   3   1   3   2
    else if (minTimes == 3)
    {
        outArr1 = { XMINT2(0, 0), XMINT2(1, 0), XMINT2(2, 0), XMINT2(2, 1), XMINT2(2, 2), XMINT2(1, 2) };
        outArr2 = { XMINT2(2, 0), XMINT2(2, 1), XMINT2(2, 2), XMINT2(1, 2), XMINT2(0, 2), XMINT2(0, 1) };
    }
    // 0次順時針旋轉不變,其他異常數值也不變
    else
    {
        outArr1.clear();
        outArr2.clear();
    }
    
}

交換兩個立方體表面時的規則

這又是一個須要畫圖來理解的問題,經過下圖應該就能夠理解一個立方體旋轉先後六個面的變化了:

而後咱們能夠轉換成下面的代碼:

RubikFace Rubik::GetTargetSwapFaceRotationX(RubikFace face, int times) const
{
    if (face == RubikFace_PosX || face == RubikFace_NegX)
        return face;
    while (times--)
    {
        switch (face)
        {
        case RubikFace_PosY: face = RubikFace_NegZ; break;
        case RubikFace_PosZ: face = RubikFace_PosY; break;
        case RubikFace_NegY: face = RubikFace_PosZ; break;
        case RubikFace_NegZ: face = RubikFace_NegY; break;
        }
    }
    return face;
}

RubikFace Rubik::GetTargetSwapFaceRotationY(RubikFace face, int times) const
{
    if (face == RubikFace_PosY || face == RubikFace_NegY)
        return face;
    while (times--)
    {
        switch (face)
        {
        case RubikFace_PosZ: face = RubikFace_NegX; break;
        case RubikFace_PosX: face = RubikFace_PosZ; break;
        case RubikFace_NegZ: face = RubikFace_PosX; break;
        case RubikFace_NegX: face = RubikFace_NegZ; break;
        }
    }
    return face;
}

RubikFace Rubik::GetTargetSwapFaceRotationZ(RubikFace face, int times) const
{
    if (face == RubikFace_PosZ || face == RubikFace_NegZ)
        return face;
    while (times--)
    {
        switch (face)
        {
        case RubikFace_PosX: face = RubikFace_NegY; break;
        case RubikFace_PosY: face = RubikFace_PosX; break;
        case RubikFace_NegX: face = RubikFace_PosY; break;
        case RubikFace_NegY: face = RubikFace_NegX; break;
        }
    }
    return face;
}

最終完整的預旋轉方法Rubik::PreRotateX實現以下:

void Rubik::PreRotateX(bool isKeyOp)
{
    for (int i = 0; i < 3; ++i)
    {
        // 當前層沒有旋轉則直接跳過
        if (fabs(mCubes[i][0][0].rotation.x) < 10e-5f)
            continue;
        // 因爲此時被旋轉面的全部方塊旋轉角度都是同樣的,能夠從中取一個來計算。
        // 計算歸位回[-pi/4, pi/4)區間須要順時針旋轉90度的次數
        int times = static_cast<int>(round(mCubes[i][0][0].rotation.x / XM_PIDIV2));
        // 將歸位次數映射到[0, 3],以計算最小所需順時針旋轉90度的次數
        int minTimes = (times % 4 + 4) % 4;

        // 調整全部被旋轉方塊的初始角度
        for (int j = 0; j < 3; ++j)
        {
            for (int k = 0; k < 3; ++k)
            {
                // 鍵盤按下後的變化
                if (isKeyOp)
                {
                    // 順時針旋轉90度--->實際演算從-90度加到0度
                    // 逆時針旋轉90度--->實際演算從90度減到0度
                    mCubes[i][j][k].rotation.x *= -1.0f;
                }
                // 鼠標釋放後的變化
                else
                {
                    // 歸位回[-pi/4, pi/4)的區間
                    mCubes[i][j][k].rotation.x -= times * XM_PIDIV2;
                }
            }
        }

        std::vector<XMINT2> indices1, indices2;
        GetSwapIndexArray(minTimes, indices1, indices2);
        size_t swapTimes = indices1.size();
        for (size_t idx = 0; idx < swapTimes; ++idx)
        {
            // 對這兩個立方體按規則進行面的交換
            XMINT2 srcIndex = indices1[idx];
            XMINT2 targetIndex = indices2[idx];
            // 若爲2次順時針旋轉,則只需4次對角調換
            // 不然,須要6次鄰角(棱)對換
            for (int face = 0; face < 6; ++face)
            {
                std::swap(mCubes[i][srcIndex.x][srcIndex.y].faceColors[face],
                    mCubes[i][targetIndex.x][targetIndex.y].faceColors[
                        GetTargetSwapFaceRotationX(static_cast<RubikFace>(face), minTimes)]);
            }
        }
    }
}

Rubik::RotateYRubik::RotateZ的實現這裏忽略。

而後Rubik::Update完成旋轉動畫的部分

void Rubik::Update(float dt)
{
    if (mIsLocked)
    {
        int finishCount = 0;
        for (int i = 0; i < 3; ++i)
        {
            for (int j = 0; j < 3; ++j)
            {
                for (int k = 0; k < 3; ++k)
                {
                    // 令x,y, z軸向旋轉角度逐漸歸0
                    // x軸
                    float dTheta = (signbit(mCubes[i][j][k].rotation.x) ? -1.0f : 1.0f) * dt * mRotationSpeed;
                    if (fabs(mCubes[i][j][k].rotation.x) < fabs(dTheta))
                    {
                        mCubes[i][j][k].rotation.x = 0.0f;
                        finishCount++;
                    }
                    else
                    {
                        mCubes[i][j][k].rotation.x -= dTheta;
                    }
                    // y軸
                    dTheta = (signbit(mCubes[i][j][k].rotation.y) ? -1.0f : 1.0f) * dt * mRotationSpeed;
                    if (fabs(mCubes[i][j][k].rotation.y) < fabs(dTheta))
                    {
                        mCubes[i][j][k].rotation.y = 0.0f;
                        finishCount++;
                    }
                    else
                    {
                        mCubes[i][j][k].rotation.y -= dTheta;
                    }
                    // z軸
                    dTheta = (signbit(mCubes[i][j][k].rotation.z) ? -1.0f : 1.0f) * dt * mRotationSpeed;
                    if (fabs(mCubes[i][j][k].rotation.z) < fabs(dTheta))
                    {
                        mCubes[i][j][k].rotation.z = 0.0f;
                        finishCount++;
                    }
                    else
                    {
                        mCubes[i][j][k].rotation.z -= dTheta;
                    }
                }
            }
        }

        // 全部方塊都結束動畫才能解鎖
        if (finishCount == 81)
            mIsLocked = false;
    }
}

最後GameApp::UpdateScene測試一下效果:

void GameApp::UpdateScene(float dt)
{
    // 反覆旋轉
    static float theta = XM_PIDIV2;
    if (!mRubik.IsLocked())
    {
        theta *= -1.0f;
    }
    // 就算擺出來也不會有問題(只有未上鎖的幀纔會生效該調用)
    mRubik.RotateY(0, theta);
    // 下面的也不會被調用
    mRubik.RotateX(0, theta);
    mRubik.RotateZ(0, theta);
    // 更新魔方
    mRubik.Update(dt);
}

上面的代碼會反覆旋轉底層。

來個鬼畜的動圖:

細思恐極,我竟然花了那麼大篇幅來將一個魔方的旋轉,寫這部分實現的代碼只是用了半天,而後寫這篇博客差很少一天又過去了。。。這個系列目前尚未結束,下一章主要講的是鍵鼠操做。

Github項目--魔方

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

相關文章
相關標籤/搜索