能夠說,魔方跟個人人生也有必定的聯繫。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,以及有什麼問題也能夠在這裏彙報。