這篇說一下如何構造魔方,主要包括魔方几何體的構造及紋理貼圖。以下論述皆以三階魔方為例,三階魔方共有3 x 3 x 3 = 27個小立方體。
構造魔方
在第一篇裡面說過,最初模型用的是微軟的.x檔案格式,由於魔方要實現按層旋轉,所以不能將整個模型做成一個.x檔案,只能分成若干個小立方體,每個立方體對應一個.x檔案。這導致在釋出程式的時候也要釋出這些模型檔案,而且.x檔案已經逐漸為微軟遺棄,所以就乾脆不用了,自己畫吧。魔方由27個小立方體構成,所以只要繪製一個小立方體,並複製27分,再將這個27個小立方體按一定順序堆疊在一起,最後貼上紋理,就可以構成一個完整的魔方了。
一個小立方體包含六個面,由於每個面的紋理可能不同,所以需要逐個面繪製,這樣可以方便的為每個面單獨設定紋理。
一個面由兩個三角形構成,這裡採用TriangleStrip的方式進行繪製,只需要指定四個頂點即可,如果是TriangleList,則需要六個頂點。
頂點結構
下面來分析一下頂點的資料結構,首先要有一個位置座標(位置是一個頂點必須要包含的資訊),其次,為了新增光照效果,還需要一個法向量。最後,為了實現紋理貼圖,需要有紋理座標。所以一個完整的頂點有以下三部分構成:
- 位置
- 法向量
- 紋理座標
用一個結構體來表示頂點,如下:
struct Vertex { float x, y, z; // position float nx, ny, nz; // normal float u, v; // texture };
定義頂點陣列
對於任意一個立方體,它的邊長是固定的,所以只要給定立方體8個頂點中任意一個,就可以推算出其他的頂點座標,這裡使用立方體的左下角頂點來計算其他頂點。假設左下角頂點座標為P(x,y,z),正方形邊長為length,那麼有如下關係成立。
頂點陣列按面定義,順序如下:
- Front face
- Back face
- Left face
- Right face
- Top face
- Bottom face
在定義任意一個面的四個頂點時,從左下角點開始按順時針方向至右下角點結束,如下:
程式碼如下,解釋一下第一行,其他行類似。
- x,y,z是位置座標
- 0.0f, 0.0f, -1.0f是法向量,法向量垂直於該面指向外。
- 0.0f, 0.0f是紋理座標
// Vertex buffer data Vertex vertices[] = { // Front face { x, y, z, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f}, // 0 { x, y + length_, z, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f}, // 1 {x + length_, y + length_, z, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f}, // 2 {x + length_, y, z, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f}, // 3 // Back face {x + length_, y, z + length_, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f}, // 4 {x + length_, y + length_, z + length_, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f}, // 5 { x, y + length_, z + length_, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f}, // 6 { x, y, z + length_, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f}, // 7 // Left face { x, y, z + length_, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, // 8 { x, y + length_, z + length_, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f}, // 9 { x, y + length_, z, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f}, // 10 { x, y, z, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f}, // 11 // Right face {x + length_, y, z, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, // 12 {x + length_, y + length_, z, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f}, // 13 {x + length_, y + length_, z + length_, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f}, // 14 {x + length_, y, z + length_, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}, // 15 // Top face { x, y + length_, z, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, // 16 { x, y + length_, z + length_, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f}, // 17 {x + length_, y + length_, z + length_, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f}, // 18 {x + length_, y + length_, z, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f}, // 19 // Bottom face {x + length_, y, z, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f}, // 20 {x + length_, y, z + length_, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}, // 21 { x, y, z + length_, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f}, // 22 { x, y, z, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f}, // 23 };
層的編號
層的編號主要用來確定旋轉那一層,層的編號依如下順序進行。
- X軸,從左到右,依次為0,1,2層
- Y軸,從下到上,依次為3,4,5層
- Z軸,從後至前,依次為6,7,8層
實際上編號都是由各個座標軸的負方向到正方向依次遞增,因為DirectX使用左手系,所以Z軸垂直螢幕向內為正,這與OpenGL正好相反,如果是OpenGL的話,需要將678層顛倒一下。
小立方體編號
小立方體編號是初始化小立方體陣列時的順序,在本程式中不以立方體編號來確定哪些立方體要旋轉,因為這樣比較麻煩,在旋轉之後需要更新編號,而且擴充套件性不好。小立方體編號按上圖中6,7,8號層依次進行。順序從左到右,從下到上,如下圖所示(注意,這裡只標出了能看見的立方體,看不見的可以按順序計算出來)
面的編號
給面編號的原因是,當滑鼠點選魔方時,需要確定當前拾取的是哪個面,確定了面以後,再根據滑鼠的位置來確定旋轉那一層,後續的篇章有詳細介紹。面的編號按如下規則。下圖中只有三個面可見,看不見的面可以推算出來。
- Front face - 0
- Back face - 1
- Left face - 2
- Right face - 3
- Top face - 4
- Bottom face - 5
紋理貼圖
紋理佈局如下:前白,後黃,左紅,右橙,上綠,下藍。
最初紋理是從圖片生成的,後來發現魔方的顏色都是簡單顏色,可以省去載入圖片的步驟,直接在記憶體中建立紋理即可。函式CreateTexture有三個引數,分別是紋理寬度,高度及顏色,該函式內部呼叫D3DXCreateTexture來建立紋理。紋理建立好以後,呼叫Lock函式鎖定之,然後使用memcpy進行顏色填充。
LPDIRECT3DTEXTURE9 D3D9::CreateTexture(int texWidth, int texHeight, D3DCOLOR color) { LPDIRECT3DTEXTURE9 pTexture; HRESULT hr = D3DXCreateTexture(d3ddevice_, texWidth, texHeight, 0, 0, D3DFMT_A8R8G8B8, // 4 bytes for a pixel D3DPOOL_MANAGED, &pTexture); if (FAILED(hr)) { MessageBox(NULL, L"Create texture failed", L"Error", 0); } // Lock the texture and fill in color D3DLOCKED_RECT lockedRect; hr = pTexture->LockRect(0, &lockedRect, NULL, 0); if (FAILED(hr)) { MessageBox(NULL, L"Lock texture failed!", L"Error", 0); } DWORD sideColor = 0xff000000; // the side line color int side_width = 10; // Calculate number of rows in the locked Rect int rowCount = (texWidth * texHeight * 4 ) / lockedRect.Pitch; for (int i = 0; i < texWidth; ++i) { for (int j = 0; j < texHeight; ++j) { int index = i * rowCount + j; int* pData = (int*)lockedRect.pBits; if (i <= side_width || i >= texWidth - side_width || j <= side_width || j >= texHeight - side_width) { memcpy(&pData[index], &sideColor, 4); } else { memcpy(&pData[index], &color, 4); } } } pTexture->UnlockRect(0); return pTexture; }
呼叫上面的函式依次建立6個面的顏色紋理及魔方內部的紋理(旋轉時可見,白色)。
void RubikCube::InitTextures() { DWORD colors[] = { 0xffffffff, // White, front face 0xffffff00, // Yellow, back face 0xffff0000, // Red, left face 0xffffa500, // Orange, right face 0xff00ff00, // Green, top face 0xff0000ff, // Blue, bottom face }; // Create face textures for(int i = 0; i < kNumFaces; ++i) { face_textures_[i] = d3d9->CreateTexture(texture_width_, texture_height_, colors[i]); } // Create inner texture inner_textures_ = d3d9->CreateInnerTexture(texture_width_, texture_height_, 0xffffffff); Cube::SetFaceTexture(face_textures_, kNumFaces); Cube::SetInnerTexture(inner_textures_); }
繪製
在RubikCube類裡面依次初始化所有的小立方體。
void RubikCube::InitCubes() {// Get unit cube length and gaps between layers float length = cubes[0].GetLength(); float cube_length = cubes[0].GetLength(); float gap = gap_between_layers_; // Calculate half face length float half_face_length = face_length_ / 2;
for (int i = 0; i < kNumLayers; ++i) { for (int j = 0; j < kNumLayers; ++j) { for (int k = 0; k < kNumLayers; ++k) { // calculate the front-bottom-left corner coodinates for current cube // The Rubik Cube's center was the coordinate center, but the calculation assume the front-bottom-left corner // of the Rubik Cube was in the coodinates center, so move half_face_length for each coordinates component. float x = i * (cube_length + gap) - half_face_length; float y = j * (cube_length + gap) - half_face_length; float z = k * (cube_length + gap) - half_face_length; // calculate the unit cube index in inti_pos int n = i + (j * kNumLayers) + (k * kNumLayers * kNumLayers); // Initiliaze cube n cubes[n].Init(D3DXVECTOR3(x, y, z)); } } } }
繪製一個小立方體,pIB是一個index buffer陣列,共有六個元素,每個元素代表一個面的index buffer。常量kNumFaces_=6。在繪製每個面的時候要先設定這個面的紋理。
void Cube::Draw() { // Setup world matrix for current cube d3d_device_->SetTransform(D3DTS_WORLD, &world_matrix_) ; // Draw cube by draw every face of the cube for(int i = 0; i < kNumFaces_; ++i) { if(textureId[i] >= 0) { d3d_device_->SetTexture(0, pTextures[textureId[i]]); } else { d3d_device_->SetTexture(0, inner_texture_); } d3d_device_->SetStreamSource(0, vertex_buffer_, 0, sizeof(Vertex)); d3d_device_->SetIndices(pIB[i]) ; d3d_device_->SetFVF(VERTEX_FVF); d3d_device_->DrawIndexedPrimitive(D3DPT_TRIANGLESTRIP, 0, 0, 24, 0, 2); } }
繪製整個魔方,kNumCubes是一個魔方中小立方體的總數,對於三階魔方來說是3 x 3 x 3 = 27。
//draw all unit cubes to build the Rubik cube for(int i = 0; i < kNumCubes; i++) { cubes[i].Draw(); }
程式下載
上次釋出的時候有一個嚴重的bug,在旋轉的時候會出現丟失某一層的情況,現已修復,歡迎各位繼續捉蟲。
Haypp coding!