能够说,魔方跟个人人生也有必定的联系。html
在高中的学校接触到了魔方社,那时候的我虽然也可以还原魔方,可看到大神们老是能够很是快地还原,为此我也走上了学习高级公式CFOP的坑。当初学习的网站是在魔方小站,不过因为公式太多了,那一年主要也就学会了顶层公式PLL和底二层公式F2L,最好的时候大概30s可以复原一个魔方,不事后来仍是退坑了。git
而后到了大学,参加考核的时候被要求用DirectX9来实现考题规定的游戏,我选择了魔方。而后在仅有12天的时间狂肝Direct3D 9,虽然那时候写的代码还比较生涩,不过至少实现的效果仍是比较满意的,至少在可玩性上我感受还不错,甚至能够用来竞速。github
这个是DX9魔方的游玩过程。碍于图片最大只能上传10M,将就一下。
数组
嗯,如今距离这个Demo都已通过去快两年了,而后电脑应为一些不可抗因素把系统升到了Win10。而后如今,我竟然运行不了全部的DirectX 9游戏,包括我以前写的demo也翻车了。不过目前我学DirectX 11断断续续也是差不过有两年了,而后重构的念头一直在我脑海中回响。写了大半年的教程,中间也积累了很多的代码,用现有的代码框架应该也能够很快搭建出来吧。数据结构
截止目前,完成这个项目用了2天半,写下这套博客用了2天半app
注意:本教程会主要是讲述一个3D魔方游戏的实现原理,即使不是用DirectX来进行开发,你也能够根据这里面的原理在OpenGL,WebGL,Unity3D等地方实现出来。框架
章节 |
---|
实现一个3D魔方(1) |
实现一个3D魔方(2) |
实现一个3D魔方(3) |
顺便下面安利一波本人正在编写的DX11教程。ide
DirectX11 With Windows SDK完整目录函数
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。
该项目的Direct3D 11源自Windows SDK,注意不是DirectX SDK!这意味着只要你有Visual Studio 2015/2017,只要安装了C++相关的组件,打开本项目你就能够直接生成出来并运行了。
说实话,即使是个看起来比较简单的魔方,内部的实现也是比较硬核的。并且由于是使用DirectX 11写的,对于正在学习或者想要学习DirectX 11的人来讲,你必需要把不少底层的原理给弄懂,因此大多数人可能会偏向于先造一个本身的软引擎。
如下是对学习DX11的人的基本要求:
而如下则是对只是想要了解魔方实现的人的基本要求:
对了,本项目不打算使用光照。
为了尽量简化开发流程,我把以前写教程实现的大部分模块都搬过来这里用了,这样能够尽量屏蔽底层实现而让我更专一于魔方自己的实现。若是要理解这些模块的功能你仍须要花费大量的时间来学习。
首先列出项目的超长文件结构图(先不要被吓跑)。。。
其中从微软那边直接搬运过来的模块以下:
微软提供的模块 | 功能 |
---|---|
DirectXTex/DDSTextureLoader | DDS纹理加载 |
DirectXTex/WICTextureLoader | WIC相关位图加载(估计用不上) |
DirectXTex/ScreenGrab | 截屏保存(估计用不上) |
DXTK/Mouse(源码上有所修改) | 鼠标类 |
DXTK/Keyboard(源码上有所修改) | 键盘类 |
而后是本身以前积累下来的一些模块,也包括龙书的:
我的或龙书曾经编写过的模块 | 功能 |
---|---|
Camera | 简易摄像机 |
d3dUtil | 包含了一些d3d经常使用的头文件和我的以前实现过的一些函数 |
DXTrace | 贡献了HR宏,用于错误追踪 |
GameTimer | 龙书的计时器 |
Vertex | 包含了一些经常使用的顶点类型 |
Collision | 用于鼠标拾取、碰撞检测 |
因为上述代码都是已经实现好的,因此对我来讲里面的实现如今能够忽略。
而下面这些模块则是我须要重点进行修改和编写的
模块 | 功能 |
---|---|
BasicEffect | 特效、常量缓冲区的管理 |
d3dApp | Direct3D和Windows的初始化 |
GameApp | 管理游戏的逻辑实现部分 |
Rubik | 魔方类 |
而后基础游戏框架使用的本人项目13的d3dApp
和GameApp
。对于通常人来讲,你只须要看懂Rubik
类,以及GameApp
类里面的游戏逻辑便可。前面的内容也是重点围绕这里面的代码来展开描述。
本项目的魔方预期实现的功能和当前进度以下:
首先,魔方的6个面可使用下面的枚举值来肯定:
enum RubikFace { RubikFace_PosX, // +X面 RubikFace_NegX, // -X面 RubikFace_PosY, // +Y面 RubikFace_NegY, // -Y面 RubikFace_PosZ, // +Z面 RubikFace_NegZ, // -Z面 };
这和天空盒指定面的枚举值是一致的。所谓的+X面你能够理解为从魔方中心发射一条+X轴的射线所指向的面,注意这是创建在左手坐标系的基础上肯定的。
而后,本项目提供了7种魔方纹理的颜色,由先的枚举值来肯定:
enum RubikFaceColor { RubikFaceColor_Black, // 黑色 RubikFaceColor_Orange, // 橙色 RubikFaceColor_Red, // 红色 RubikFaceColor_Green, // 绿色 RubikFaceColor_Blue, // 蓝色 RubikFaceColor_Yellow, // 黄色 RubikFaceColor_White // 白色 };
所谓的黑色是指藏在魔方内部平时看不到的面,可是在魔方旋转的时候能够看到露出的一部分。
这里我准备了七张魔方表面的纹理贴图:
目前立方体结构体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轴旋转的弧度 };
如今咱们不讨论Cube::GetWorldMatrix
的实现,你能够先默认它返回一个根据pos进行平移的矩阵。
能够看到这个结构体甚至不存放什么顶点和索引数据,它只记录一下关键的信息。这么作是方便我判断魔方是否还原,以及尽量最简化魔方的旋转操做。
而后是魔方类Rubik
的初步定义:
class Rubik { public: template<class T> using ComPtr = Microsoft::WRL::ComPtr<T>; // 初始化资源 void InitResources(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext); // 当即复原魔方 void Reset(); // 更新魔方状态 void Update(); // 绘制魔方 void Draw(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect); private: // 魔方 [X][Y][Z] Cube mCubes[3][3][3]; // 顶点缓冲区,包含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; };
魔方的索引对应的关系知足左手坐标系,一级、二级、三级索引分别对应X轴、Y轴、Z轴方向上的偏移:
注意咱们的魔方中心是始终位于世界坐标系的中心的,这样有利于咱们对魔方进行旋转操做。此外你也能够看到,我将立方体六个正方形表面的24个顶点都同时存放在一个索引缓冲区中,在绘制的时候只须要设置顶点偏移量就能够指定当前绘制哪一个面。全部的27个立方体都是依赖于这两个缓冲区,加上世界矩阵和纹理数组绘制出来的。
固然上面的索引缓冲区实际上也是能够扔掉的,只须要将顶点缓冲区中的顶点次序稍微调整下,而后使用原始拓扑类型D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
便可。正方形面此时顶点按索引的排布以下:
这个类在后续咱们还会进行修改。
根据上面所给的数据结构,如今我须要初始化的数据有:纹理数组、顶点缓冲区、索引缓冲区、每一个立方体的数据。
其中顶点和索引直接在初始化中提供便可。下面是Rubik::InitResources
的实现:
void Rubik::InitResources(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext) { // 初始化纹理数组 mTexArray = CreateDDSTexture2DArrayFromFile( device, deviceContext, std::vector<std::wstring>{ L"Resource/Black.dds", L"Resource/Orange.dds", L"Resource/Red.dds", L"Resource/Green.dds", L"Resource/Blue.dds", L"Resource/Yellow.dds", L"Resource/White.dds", }); // // 初始化立方体网格模型 // VertexPosTex vertices[] = { // +X面 { XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 1.0f) }, // -X面 { XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT2(1.0f, 1.0f) }, // +Y面 { XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT2(1.0f, 1.0f) }, // -Y面 { XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 1.0f) }, // +Z面 { XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT2(1.0f, 1.0f) }, // -Z面 { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT2(1.0f, 0.0f) }, { XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT2(0.0f, 0.0f) }, { XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT2(0.0f, 1.0f) }, { XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT2(1.0f, 1.0f) }, }; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA initData; ZeroMemory(&initData, sizeof(initData)); initData.pSysMem = vertices; HR(device->CreateBuffer(&vbd, &initData, mVertexBuffer.ReleaseAndGetAddressOf())); WORD indices[] = { 0, 1, 2, 2, 3, 0 }; // 设置索引缓冲区描述 D3D11_BUFFER_DESC ibd; ZeroMemory(&ibd, sizeof(ibd)); ibd.Usage = D3D11_USAGE_IMMUTABLE; ibd.ByteWidth = sizeof indices; ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; ibd.CPUAccessFlags = 0; // 新建索引缓冲区 initData.pSysMem = indices; HR(device->CreateBuffer(&ibd, &initData, mIndexBuffer.ReleaseAndGetAddressOf())); // 初始化魔方全部面 Reset(); // 预先绑定顶点/索引缓冲区到渲染管线 UINT strides[1] = { sizeof(VertexPosTex) }; UINT offsets[1] = { 0 }; deviceContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), strides, offsets); deviceContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); }
而Rubik::Reset
用来方便一次性还原魔方,初始化各个立方体的位置:
void Rubik::Reset() { // 初始化魔方中心位置,用六个面默认填充黑色 for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) for (int k = 0; k < 3; ++k) { mCubes[i][j][k].pos = XMFLOAT3(-2.0f + 2.0f * i, -2.0f + 2.0f * j, -2.0f + 2.0f * k); mCubes[i][j][k].rotation = XMFLOAT3(); memset(mCubes[i][j][k].faceColors, 0, sizeof mCubes[i][j][k].faceColors); } // +X面为橙色,-X面为红色 // +Y面为绿色,-Y面为蓝色 // +Z面为黄色,-Z面为白色 for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) { mCubes[2][i][j].faceColors[RubikFace_PosX] = RubikFaceColor_Orange; mCubes[0][i][j].faceColors[RubikFace_NegX] = RubikFaceColor_Red; mCubes[j][2][i].faceColors[RubikFace_PosY] = RubikFaceColor_Green; mCubes[j][0][i].faceColors[RubikFace_NegY] = RubikFaceColor_Blue; mCubes[i][j][2].faceColors[RubikFace_PosZ] = RubikFaceColor_Yellow; mCubes[i][j][0].faceColors[RubikFace_NegZ] = RubikFaceColor_White; } }
在Rubik::InitResources
中用到了我本身以前编写的CreateDDSTexture2DArrayFromFile
函数,里面要求传递的是dds纹理文件,可是我如今所拥有的魔方贴图所有都是从画图工具弄出来的png格式。为此,我还须要对纹理进行格式的转换。
dxtex一般是在你安装了DirectX SDK后能够找到的,位于Microsoft DirectX SDK\Utilities\bin\x86
或Microsoft DirectX SDK\Utilities\bin\x64
中。没有安装该SDK的,你也能够在个人Github中找到:
打开dxtex,载入png位图
而后选择Format-Change Surface Format,将位图格式改成Unsigned 32-bit: A8R8G8B8
紧接着,咱们须要给它生成mipmap,不然可能会致使在用大纹理绘制实际较小的部分时,某些倾斜的条纹会由于采样而产生相似锯齿状条纹:
并且就是开了4倍MSAA都拯救不了这么强烈的锯齿感!
点击Format-Generate Mip Maps,程序自动为其建立Mipmap。在View选项中你能够经过Smaller Mipmap Level来观察生成的mipmap。
最后选择File-Save As,直接另存为.dds文件便可。
该框架的流程图以下:
其中须要我作修改的部分主要落在了GameApp::Init
, GameApp::UpdateScene
和GameApp::DrawScene
上。
该方法随GameApp::Init
调用,用于初始化游戏所需的资源:
bool GameApp::InitResource() { // 初始化魔方 mRubik.InitResources(md3dDevice, md3dImmediateContext); // 初始化特效、着色器资源 mBasicEffect.SetRenderDefault(md3dImmediateContext); mBasicEffect.SetViewMatrix(XMMatrixLookAtLH( XMVectorSet(6.0f, 6.0f, -6.0f, 1.0f), XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f), XMVectorSet(0.0f, 1.0f, 0.0f, 1.0f) )); mBasicEffect.SetProjMatrix(XMMatrixPerspectiveFovLH( XM_PI / 3, AspectRatio(), 1.0f, 1000.0f )); mBasicEffect.SetTextureArray(mRubik.GetTexArray()); return true; }
对于mBasicEffect
,你如今暂时不须要知道它底层原理,能够先把它当作一个相似于ID3DX11Effect
的对象。它能够用于设置默认的渲染模式,以及各项所需的资源给HLSL,包括世界矩阵、观察矩阵、投影矩阵和纹理数组。
着色器的具体实现这里咱们也先不提,咱们把更细节的内容留到后续的章节来说。如今要作的,就是利用现有的框架先把这个魔方给绘制出来。
目前GameApp::UpdateScene
尚未作任何事情,能够无论。GameApp::DrawScene
的实现以下:
void GameApp::DrawScene() { assert(md3dImmediateContext); assert(mSwapChain); // 使用偏紫色的纯色背景 float backgroundColor[4] = { 0.45882352f, 0.42745098f, 0.51372549f, 1.0f }; md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), backgroundColor); md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // 绘制魔方 mRubik.Draw(md3dImmediateContext, mBasicEffect); // 省略目前没有做为的部分... HR(mSwapChain->Present(0, 0)); }
而后Rubik::Draw
的实现目前以下:
void Rubik::Draw(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect) { for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) for (int k = 0; k < 3; ++k) { effect.SetWorldMatrix(mCubes[i][j][k].GetWorldMatrix()); for (int face = 0; face < 6; ++face) { effect.SetTexIndex(mCubes[i][j][k].faceColors[face]); effect.Apply(deviceContext); deviceContext->DrawIndexed(6, 0, 4 * face); } } }
经过BasicEffect::SetTexIndex
咱们能够指定当前绘制的立方体面使用的是纹理数组中的哪个纹理。
每绘制一个立方体中的一个表面,就须要切换一次世界矩阵,并应用全部的变动。
因为我把全部的顶点都放在同一个缓冲区了,只须要在ID3D11DeviceContext::DrawIndexed
指定起始顶点的偏移量便可。
最终的效果以下:
目前的开发进度用了我半天时间,而后还有大半天的时间用来写这篇博客,理论上我稍微爆肝一点可能两天时间就能够弄出来了吧。虽然表面开发了半天,但为了这个教程至少也准备了大半年的时间。如今趁这个机会能够好好理顺一下本身的开发思路,可能要多花3-4天的时间。目前的项目我已经放到Github中了:
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。