前言

上一章我们主要讲述了魔方的构造和初始化、纹理的准备工作。目前我还没有打算讲Direct3D 11关于底层绘图的实现,因此接下来这一章的重点是魔方的旋转。因为我们要的是能玩的魔方游戏,而不是一个观赏品。所以对旋转这一步的处理就显得尤其重要,甚至可以展开很大的篇幅来讲述。现在光是为了实现旋转的这个动画就弄了我大概500行代码。

这个旋转包含了单层旋转、双层旋转、整个魔方旋转以及魔方的自动旋转动画。

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

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度(从坐标轴正方向朝中心点看)。

之前提到魔方的正中心位于世界坐标系的原点,这样方便我们进行旋转操作以节省不必要的平移。现在我们只讨论魔方的其中一个立方体的旋转情况,它需要绕Z轴顺时针旋转θ度。

这整个过程可以拆分成旋转和平移。其中立方体的旋转可以理解为移到中心按顺时针旋转θ度,然后再平移到目标位置。

变换过程可以用下面的公式表示,其中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,以及有什么问题也可以在这里汇报。

DirectX11--实现一个3D魔方(2)的更多相关文章

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

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

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

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

  3. css3之3D魔方动画(小白版)

      在这里分享一下3D魔方动画,html5+CSS3即可完成~无图无真相,先上效果图 第一步非常简单,就是先将魔方的结构画出来.大家都玩过魔方,知道魔方是一个有六个面的正方体.这里我们先写一个大的di ...

  4. canvas实现3D魔方

    摘要:使用canvas实现可交互的3D魔方 一.简单分析 魔方物理性质: 1.中心块(6个):中心块与中心轴连接在一起,但可以顺着轴的方向自由的转动. 2.棱块(12个):棱块的表面是两个正方形,结构 ...

  5. 如何用CSS3画出一个立体魔方?

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

  6. 如何用CSS3画出懂你的3D魔方?

    作者:首席填坑官∙苏南公众号:honeyBadger8,群:912594095,本文原创,著作权归作者所有,转载请注明原链接及出处. 前言 最近在写<每周动画点点系列>文章,上一期分享了& ...

  7. 使用CSS3实现一个3D相册

    CSS3系列我已经写过两篇文章,感兴趣的同学可以先看一下CSS3初体验之奇技淫巧,CSS3 3D立方体效果-transform也不过如此 第一篇主要列出了一些常用或经典的CSS3技巧和方法:第二篇是一 ...

  8. 第一章 用three.js创建你的第一个3D场景

    第一章 用three.js创建你的第一个3D场景 到官网下载three.js的源码和示例. 创建HTML框架界面 第一个示例的代码如下: 01-basic-skeleton.html 位于 Learn ...

  9. 如何用webgl(three.js)搭建一个3D库房-第一课

    今天我们来讨论一下如何使用当前流行的WebGL技术搭建一个库房并且实现实时有效交互 第一步.搭建一个3D库房首先你得知道库房长啥样,我们先来瞅瞅库房长啥样(这是我在网上找的一个库房图片,百度了“库房” ...

随机推荐

  1. eclipse 使用Git教程

    做一夜搬运工: https://www.cnblogs.com/heal/p/6427402.html https://blog.csdn.net/fan510988896/article/detai ...

  2. 如何查看linux中文件打开情况

    前言 我们都知道,在linux下,“一切皆文件”,因此有时候查看文件的打开情况,就显得格外重要,而这里有一个命令能够在这件事上很好的帮助我们-它就是lsof. linux下有哪些文件 在介绍lsof命 ...

  3. 1 Openwrt无线中继设置并访问外网

    https://www.cnblogs.com/wsine/p/5238465.html 配置目标 主路由器使用AP模式发射Wifi 从路由器使用Client模式接受Wifi 从路由器使用Master ...

  4. cordova 自定义 plugin

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_30879415/article/details/81265455准备工作安装cordovanp ...

  5. Vue.Draggable 文档总结

    本文章转自https://blog.csdn.net/zjiang1994/article/details/79809687 Vue.Draggable学习总结 Draggable为基于Sortabl ...

  6. Activiti开发案例之activiti-app工作流导出图片

    前言 自从 Activiti 和 JBPM4 分家以后,Activiti 目前已经发展到了版本7,本着稳定性原则我们最终选择了6,之前还有一个版本5. 问题 在开发使用的过程中发现 Activiti ...

  7. 吐血总结|史上最全的MySQL学习资料!!

    在日常工作与学习中,无论是开发.运维.还是测试,对于数据库的学习是不可避免的,同时也是日常工作的必备技术之一.在互联网公司,开源产品线比较多,互联网企业所用的数据库占比较重的还是MySQL. 在刚刚出 ...

  8. Cookie Session和自定义分页

    cookie Cookie的由来 大家都知道HTTP协议是无状态的. 无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不 ...

  9. nextInt()和nextLine()一起使用时的注意点

    问题原因:nextLine()会把nextInt(),next(),nextDouble(),nextFloat()的结束换行符作为字符串读入,进而不需要从键盘输入字符串nextLine便已经转向了下 ...

  10. 简单解析nestJS目录

    使用Nest CLI设置新项目非常简单 .只需确保 安装了npm,然后在OS终端中使用以下命令: $ npm i -g @nestjs/cli $ nest new project-name $ cd ...