前言

(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;// 公式xif (mKeyboardTracker.IsKeyPressed(Keyboard::Up)){mRubik.RotateX(3, XM_PIDIV2);return;}// ...//// 双层旋转//// 公式rif (keyState.IsKeyDown(Keyboard::LeftControl) && mKeyboardTracker.IsKeyPressed(Keyboard::I)){mRubik.RotateX(-2, XM_PIDIV2);return;}// ...//// 单层旋转//// 公式Rif (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,因此我们只需要将得到的 ttt 值带入射线方程 p=e+td\mathbf{p}=\mathbf{e}+t\mathbf{d}p=e+td 中,判断求得的 p\mathbf{p}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魔方(3)相关推荐

  1. DirectX11--实现一个3D魔方(1)

    前言 可以说,魔方跟我的人生也有一定的联系. 在高中的学校接触到了魔方社,那时候的我虽然也能够还原魔方,可看到大神们总是可以非常快地还原,为此我也走上了学习高级公式CFOP的坑.当初学习的网站是在魔方 ...

  2. DirectX11--实现一个3D魔方(2)

    前言 上一章我们主要讲述了魔方的构造和初始化.纹理的准备工作.目前我还没有打算讲Direct3D 11关于底层绘图的实现,因此接下来这一章的重点是魔方的旋转.因为我们要的是能玩的魔方游戏,而不是一个观 ...

  3. Unity教程 | 手把手教你拼一个3D“魔方”

    本教程将为刚接触unity的初学者讲解如何在Unity中实现3D立方体的堆叠,利用一个或多个预制件动态创建立方体进行拼接,为这些立方体赋予不同的颜色. 下载本文的示例工程,示例工程使用Unity 5. ...

  4. html js3d魔方,使用CSS3一个3D魔方详解~

    前言 最近在写<动画点点系列>文章,上一期分享了< 手把手教你如何绘制一辆会跑车 >,本期给大家带来是结合CSS3画出来的一个立体3d魔方,结合了js让你随心所欲想怎么转,就怎 ...

  5. android程序设计魔方矩阵,【图片】【记录】零基础用c4droid写一个3D魔方软件_c4droid吧_百度贴吧...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 9.从前述教程中整理出来的native activity框架 /*请忽视注释中提到的楼层,或结合叜駣的原教程阅读*/ #include #include ...

  6. CSS 实现一个3d魔方

    前言

  7. 3d在调试区输出坐标_CSS3如何实现一个 3D 效果的魔方

    作者:山月行 转发链接:https://mp.weixin.qq.com/s/B5hlPmpvLXHdOvNWSeln8g 前言 ❝ 合抱之木,生于毫末:九层之台,起于累土. ❞ 当我们遇到一个较难问 ...

  8. android 3d魔方 代码,css实现3d立体魔方的示例代码

    今天来做一个简单的3d魔方 先看效果图吧!把这个看会了,一些网上的3d的相册你就都会了 一.我们先准备好们的html代码 3d立体魔方 好了我们html代码就准备完成了,首先我们要有一个3d的思维,在 ...

  9. Android 3D 魔方游戏的设计与开发

    Android 3D 魔方游戏的设计与开发 5.1 Feature 定义 魔方是一个有趣的益智游戏,相信很多人都玩过.本次毕业设计,欲完成的主要的功能如下: (1) 开始游戏:开始一个新的游戏 (2) ...

最新文章

  1. 一款很好用的页面滚动元素动画插件-AOS.JS
  2. AutoHotkey 使用笔记
  3. postgres-#和postgres=#
  4. QT使用SQLite数据库实现登录功能
  5. jtoken判断是否包含键_Redis 数据库、键过期的实现
  6. Java-计算程序运行时间
  7. 华为应用锁退出立即锁_华为P40系列三大安全锁,教你锁住你的隐私 - 企业资讯...
  8. springboot maven打包pom配置
  9. k8s pod之间不能通信_Kubernetes的工作由两个pod组成(必须在不同的节点上运行并相互通信)...
  10. 二十三、Oracle学习笔记:综合案例
  11. 多变量微积分(4)——多重积分之三重积分
  12. 干货分享:PDF分割合并工具免费哪个好用?
  13. VLAN,GRE,VXLAN
  14. Cannot commit, transaction is already closed
  15. 基于51单片机智能温度控制器温控系统(毕设课设)
  16. 长虹应用商店服务器连接异常,长虹电视为什么应用商店打不开,上面显示服务器或网络异常,可是网络没问题,还显示sn异常,求解……...
  17. C++ Standard Library Style Guidelines
  18. Ubuntu 16.04 安装思维导图freemind
  19. 分享63个投票调查PHP源码,总有一款适合你
  20. vue父页面实时给子页面传值

热门文章

  1. java将字符转换成拼音_java中将汉字转换成拼音的实现代码
  2. MAC系统字体库存放目录
  3. RFC系列协议--rfc2461--Neighbor Discovery for IP Version 6 (IPv6)
  4. 入网许可证_入网许可证
  5. 电脑浏览器一直显示邮箱服务器失败,各种PC客户端都无法连接Outlook.com邮箱
  6. 【paper笔记】Personalized Top-N Sequential Recommendation via Convolutional Sequence Embedding
  7. ps导出内容快捷键 快速导出png
  8. oracle批量替换保留字,Oracle中的关键字保留字
  9. java 红外光谱数据库_免费的谱图数据库20个 - 晶体 - 小木虫 - 学术 科研 互动社区...
  10. 回首2019,瞻仰2020