这篇说一下如何构造魔方,主要包括魔方几何体的构造及纹理贴图。以下论述皆以三阶魔方为例,三阶魔方共有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}, //
{ x, y + length_, z, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f}, //
{x + length_, y + length_, z, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f}, //
{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}, //
{x + length_, y + length_, z + length_, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f}, //
{ x, y + length_, z + length_, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f}, //
{ 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}, //
{ x, y + length_, z + length_, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f}, //
{ x, y + length_, z, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f}, //
{ 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}, //
{x + length_, y + length_, z, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f}, //
{x + length_, y + length_, z + length_, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f}, //
{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}, //
{ x, y + length_, z + length_, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f}, //
{x + length_, y + length_, z + length_, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f}, //
{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}, //
{x + length_, y, z + length_, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}, //
{ x, y, z + length_, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f}, //
{ x, y, z, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f}, //
};

层的编号

层的编号主要用来确定旋转那一层,层的编号依如下顺序进行。

  • 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,
,
,
D3DFMT_A8R8G8B8, // 4 bytes for a pixel
D3DPOOL_MANAGED,
&pTexture); if (FAILED(hr))
{
MessageBox(NULL, L"Create texture failed", L"Error", );
} // Lock the texture and fill in color
D3DLOCKED_RECT lockedRect;
hr = pTexture->LockRect(, &lockedRect, NULL, );
if (FAILED(hr))
{
MessageBox(NULL, L"Lock texture failed!", L"Error", );
} DWORD sideColor = 0xff000000; // the side line color int side_width = ; // Calculate number of rows in the locked Rect
int rowCount = (texWidth * texHeight * ) / lockedRect.Pitch; for (int i = ; i < texWidth; ++i)
{
for (int j = ; 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, );
}
else
{
memcpy(&pData[index], &color, );
}
}
} pTexture->UnlockRect(); 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 = ; 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[].GetLength();
float cube_length = cubes[].GetLength();
float gap = gap_between_layers_; // Calculate half face length
float half_face_length = face_length_ / ;
for (int i = ; i < kNumLayers; ++i)
{
for (int j = ; j < kNumLayers; ++j)
{
for (int k = ; 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 = ; i < kNumFaces_; ++i)
{
if(textureId[i] >= )
{
d3d_device_->SetTexture(, pTextures[textureId[i]]);
}
else
{
d3d_device_->SetTexture(, inner_texture_);
} d3d_device_->SetStreamSource(, vertex_buffer_, , sizeof(Vertex));
d3d_device_->SetIndices(pIB[i]) ;
d3d_device_->SetFVF(VERTEX_FVF); d3d_device_->DrawIndexedPrimitive(D3DPT_TRIANGLESTRIP, , , , , );
}
}

绘制整个魔方,kNumCubes是一个魔方中小立方体的总数,对于三阶魔方来说是3 x 3 x 3 = 27。

//draw all unit cubes to build the Rubik cube
for(int i = ; i < kNumCubes; i++)
{
cubes[i].Draw();
}

程序下载

RubikCube

上次发布的时候有一个严重的bug,在旋转的时候会出现丢失某一层的情况,现已修复,欢迎各位继续捉虫。

Haypp coding!

用DirectX实现魔方(二)的更多相关文章

  1. 用DirectX实现魔方(三)视角变换及缩放(附源码)

    在本系列第一篇介绍过鼠标按键的功能,如下. 左键拖拽 - 旋转魔方 右键拖拽 - 变换视角 滚轮 - 缩放魔方 今天研究一下如何实现后面两个功能,用到的技术主要是Arcball,Arcball是实现M ...

  2. 用DirectX实现魔方(一)

    关于魔方 魔方英文名字叫做Rubik's Cube,是由匈牙利建筑学教授和雕塑家Ernő Rubik于1974年发明,最初叫做Magic Cube(这大概也是中文名字的来历吧),1980年Ideal ...

  3. 整理了一下浅墨大神的Visual C++/DirectX 9.0c的游戏开发手记

    还是非常棒的博客,只是没有一个文件夹.所以自己做了一个山寨文件夹在这里.便于随时查找. 前面31期从略. [Visual C++]游戏开发笔记三十二 浅墨DirectX提高班之中的一个 DirectX ...

  4. 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——载入三维模型&Alpha混合技术&深度测试与Z缓存

    第17章 三维游戏模型的载入 主要是如何从3ds max中导出.X文件,以及如何从X文件加载三维模型到DirextX游戏程序里.因为复杂的3D物体,要用代码去实现,那太反人类了,所以我们需要一些建模软 ...

  5. 【Visual C++】游戏开发五十六 浅墨DirectX教程二十三 打造游戏GUI界面(一)

    本系列文章由zhmxy555(毛星云)编写,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/16384009 作者:毛星云 ...

  6. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS) 学习目标 回顾复数,以及 ...

  7. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader) 代码工 ...

  8. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二) 代码工程地址: https:/ ...

  9. Directx11学习笔记【二十】 使用DirectX Tool Kit加载mesh

    本文由zhangbaochong原创,转载请注明出处:http://www.cnblogs.com/zhangbaochong/p/5788482.html 现在directx已经不再支持.x文件了, ...

随机推荐

  1. python第三方库学习(2):requests

    Make a Request r = requests.get('https://github.com/timeline.json') Passing Parameters In URLspayloa ...

  2. hdoj 2039 三角形

    Problem Description 给定三条边,请你判断一下能不能组成一个三角形.   Input 输入数据第一行包含一个数M,接下有M行,每行一个实例,包含三个正数A,B,C.其中A,B,C & ...

  3. spring的beans.xml中classpath

    classpath就是代表 /WEB-INF /classes/ 这个路径(如果不理解该路径,就把一个web工程发布为war包,然后用winrar查看其包内路径就理解啦) 常用的场景: 在SSH架构中 ...

  4. Java 第8章 循环结构进阶

    循环结构进阶 什么是二重循环? 二重循环的执行顺序是什么?

  5. ERDAS文件格式:IGE、IMG、RRD、AUX

    ERDAS如果需要打开大于2GB的文件,ERDAS需要把文件转换成IMG格式.这时候,ERDAS自动生成三个文件,分别是IMG.IGE和RRD文件,其中:1.IGE:是数据文件,实际用来存储栅格数据: ...

  6. DWG2SHP DXF2SHP 如何把AutoCAD的DWG,DXF文件转换为Esri ArcGIS的Shape文件

    dwg是AutoCAD创立的一种图纸保存格式,已经成为二维CAD的标准格式,很多其他CAD为了兼容AutoCAD,也直接使用dwg作为默认工作文件. 地图shape文件由ESRI开发,一个ESRI的s ...

  7. nginx简易安装

    yum -y install perl-ExtUtils-Embed ./configure --prefix=/usr/local/nginx --user=nginx --group=nginx ...

  8. Beta版本的贡献率百分比

    我真的是服了..刚刚写完最后一次作业,还感叹了一下终于完成了最后的工作,一看群还得发一篇. 贡献率这种东西不是应该默认是100%除以团队人数的吗,有没有搞错啊,这样很容易引起团队不融洽的啊. 0313 ...

  9. DB_oracle学习笔记_概念分析

    概念分析: 1.       数据库(Database): 数据库是一个文件集合,包括数据文件,临时文件,重做日志文件和控制文件.也可以说数据库是物理操作系统文件或磁盘集合.数据库可以由多个实例(sc ...

  10. [java基础]java跨平台的基础知识

    1.Javac编译器 Javac编译器读取Java源代码,并将其编译成字节代码(.class格式),调用Javac的命令行示例如下: C:>javac options filename.java ...