DirectX11--实现一个3D魔方

Posted lonelyxmas

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DirectX11--实现一个3D魔方相关的知识,希望对你有一定的参考价值。

原文:DirectX11--实现一个3D魔方(3)

前言

(2019/1/9 09:23)上一章我们主要讲述了魔方的旋转,这个旋转真是有毒啊,搞完这个部分搭键鼠操作不到半天应该就可以搭完了吧...

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

技术图片
有人发这张图片问我写魔方的目的是不是这个。。。噗

现在光是键鼠相关的代码也搭了400行左右。。其中键盘相关的调用真的是毫无技术可言,重点实现基本上都被鼠标给耽搁了。

回来看一眼发现阅读量居然比前面两篇都还高了= =话说之前没看过这个教程的。。。或许你们应该先看看前面两章讲了什么内容?

本章将魔方应用层的剩余实现补全。

章节
实现一个3D魔方(1)
实现一个3D魔方(2)
实现一个3D魔方(3)

Github项目--魔方

最后日常安利一波本人正在编写的DX11教程。

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

我就简单提一下键盘的逻辑

键盘操作使用的是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,以及有什么问题也可以在这里汇报。

以上是关于DirectX11--实现一个3D魔方的主要内容,如果未能解决你的问题,请参考以下文章

Directx11学习笔记 第一个D3D11程序

OpenGL实现3D魔方游戏源代码

canvas实现3D魔方

3D魔方的思路与实现

HTML+CSS+JS实现 ❤️3D旋转魔方图片相册特效❤️

DirectX11--深入理解Effects11使用着色器反射机制(Shader Reflection)实现一个复杂Effects框架