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,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索