DirectX3D游戏制作之---3D场景的渲染及人物动画的显示

前言:

            无所谓好或不好,人生一场虚空大梦,韶华白首,不够转瞬。

                                                                                                         ----慕容紫英《仙剑奇侠传四》

PS:为了方便大家阅读,个人认为比较重要的内容-------红色字体显示

                                         个人认为可以了解的内容-------紫色字体显示

---------------------------------------------------------------------------------------------------------------------------------------------

       不知不觉就已进入八月了,由于之前没什么时间,所以好久没写博客了,这两个比较空,因此就来写一写关于3D的文章,当然要得到最终的效果需要所用到DirectX库,我们可以下载一个开发工具包DirectX SDK(Microsoft DirectX SDK 是DirectX编程的软件。包含了开发尖端多媒体应用软件不可或缺的开发工具,以及runtime、headers及程序库、范例执行文件、文件、DirectX工具、并且同时支援C++ 以及 Visual Basic开发软件),虽然说已经更新了好多个版本,但是最经典的还是DirectX 9.0版本,其实主要是这个版本可参考的资料比较多,如果你学习最近版本的,那么很有可能你需要看一些外国文章,这对于不喜欢读英文的人来说太痛苦了,因此我们不要一味的赶时髦学习一些最新的技术,从基础开始,循序渐进。而且最重要的是DirectX 9.0版本有最经典的红龙书可以参考!

       如果需要下载这个开发工具包,以下是链接:传送门

       好了废话不多说了,我们开始正式的内容,这次要说的内容有两个,一个是3D场景的渲染,另一个就是骨骼动画的显示。(PS:代码较长,文章之中只给出了实现思路,如果需要完整的代码,可以留下邮箱或者私信我!

首先我们来看一下实际的效果吧:

先来看一一个场景大体的轮廓:


      

       再来看一看地形以及天空(这里说明一下,为了观察方便,我这里选取的天空盒边长比较小,所以很容易看出是方形的天空盒,不过边长设置大些的话,就很难看出了)DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第1张图片

          

          最后在来看一下跳舞的妹子吧!(如果配上Noboby歌曲,是不是很配呢?)



看完之后我们就来分析一下,按顺序来吧。

                                                            3D场景的渲染

        虽然说3D只是在2D的基础之上多加了一个维度,但是难度大的不是一点点。而在3D编程的学习之中最重要的就是3D场景的渲染,当然这也是也是最基本的。如果说2D的编程是贴图的艺术,那么3D编程肯定是渲染的艺术,所以说渲染是3D编程之中最重要的也是最基本的技能。关于3D的渲染,首先你会想要给那个生成的窗口添加什么内容,首先是天空,然后是地形,当然还有必不可少的具体场景画面,这三者一般来说是每个场景都有。好了既然决定了要做什么,那么我们就可以着手去实现它,对于这几者我们都可以将其封装在一个类之中,因为场景的实现这几个必不可少,所以封装在一个类之中后我们可以很方便的调用,我们一个一个来看:

                                                      渲染之地形的实现

       关于这部分,我们先从最简单的地形入手,最简单的地形当然是类似于那种一望无际,可以想象成一个大平原,周围很空旷,那么这样的地形只需要绘制一个大的三角形网格组成的大的矩形区域就可以了,其实这就是一个二维的平面,那么怎么能让二维的平面转化为三维的平面?

       其实简单一点可以想象为原本是一个大的网格平面,如果你把其中的某一些点往上提一下,或者把某些点往里压一些,那么此时做出来的效果就有一些立体感了,但是还不够明显。不过很明显的感受就是,原本在第一种方式下,我们只需要用(x,y)坐标描述每一个坐标即可,但是在我们在这种方式下如果我们需要描述每一个点的时候就要使用(x,y,z)三维坐标,z坐标代表高度,这样写出来的效果虽然不明显,但是至少比原来的好多了,至少有了立体感。这种方式实现三维地形图的过程之中还涉及到一个关于高位图的概念,什么是高度图呢?其实说白了就是一组连续的数组,在这个数组之中的元素与地形网格中的顶点一一对应,而且每一个元素都指定了地形网格的某个顶点的高度值。

        顺便提一点,在高度图中,高度值表现为0 ~ 255之间的明暗值。高度图技巧的使用方法就是:首先,将想要制作的三维地形制作为只包含二维高度信息的高度图,然后,利用高度图信息重新制作为三维地形(terrain)。

        当然简单的说一下高度图的制作原理,高度图的主要原理就是将二维信息转变为三维顶点信息,一般情况下,二维xy平面空间在三维图形中对应的是xz平面。其核心原理是:将二维(x ,z)坐标所对应的像素亮度值换算为三维(x, y, z)坐标中的y值,因此,大部分情况下,高度图只包含256色亮度值的黑白图片文件。【二维图形中的亮度值可用来表示三维空间中的高度值】

       高位图有很多实现的方式,其中最常用的一种就是灰度图。地形之中某一点的海拔越高的话,相应地该点对应的灰度图中的亮度就越大。上面也说了地形之中最低点将使用0表示,而最高点使用255表示(不过要是遇到一个地形之中大部分的区域的高度都差不多情况下,只有少数地方高度差别很大,这时候可能会有一些小问题

       接下来还是说一说用什么方式可以制作一张高度图,一般来说有两种方式:

方式一:采用某种算法为基础,写个程序生成,拿出一个伪代码吧,这部分伪代码对应的是InitVB()函数。利用纹理对象读取高度图信息之后,调用LockRect()函数获取存储器的固定指针进行访问。该方法就是光影贴(lightmapping)技术中所使用的必须技术。调用LockRect()函数后,将纹理的长度和存储器的指针存储在名D3DLOCKED_RECT的结构体中,通过结构体的成员参数中的pBits参数可以访问纹理的像素单位。

        pBits= 读取高度图文件  
        cx= 高度图的长度  
        cz= 高度图的宽度  
        vb= 创建顶点缓冲  
        p= vb->lock()  
        for(z= 0; z < cz; z++)  
        {  
             for(x = 0; x < cx; x++)  
             {  
                    v.p.x= x ? cx / 2.0  
                    v.p.z= -(z ? cz / 2.0)  
                    v.p.y= pBits + x + y * cx  
                    v.n= Normalize(v.p)  
                    v.tu= x / (cx – 1.0)  
                    v.tv= z / (cz – 1.0)  
                    *p++= v  
             }  
        }  
        vb->unlock()  
当然还有比较有名的算法,比如Fault·Formation 和 Midpoint Displacement这两种算法。有兴趣的可以了解一下。

方式二:通过图像编辑软件、三维建模软件或者专业制作地形的软件来制作,通常的我们可以使用Photoshop(不得不说这款软件的强大,平时游戏之中使用的很多素材我都是通过Photoshop加工完成),当然三维建模软件的话首推3Ds Max和Maya这两款软件。专业制作地形的软件的话,可是看看Terragen这一款软件。

可以通过以下图片简单看一下地址制作效果:(图片源自网上)

DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第2张图片

当然我们也可以使用3Ds Max软件制作地形图,这样做出来的效果就会很不错,来看一下:

DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第3张图片

DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第4张图片

下面给出地形类的设计:

//地形类
class TerrainClass
{
public:
	TerrainClass(IDirect3DDevice9 *pd3dDevice);					//构造函数
	virtual ~TerrainClass();							//析构函数
public:
	//从文件之中加载高度图和纹理图的函数
	BOOL LoadTerrainFromFile(wchar_t * pRawFileName, wchar_t * pTextureFile);
	//地形初始化函数
	BOOL InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);
	//地形渲染函数
	BOOL RenderTerrain(D3DXMATRIX * pMatWorld, BOOL bDrawFrame = FALSE);
private:
	LPDIRECT3DDEVICE9					m_pd3dDevice;		//D3D设备对象
	LPDIRECT3DTEXTURE9					m_Texture;		//纹理
	LPDIRECT3DVERTEXBUFFER9					m_pVertexBuffer;	//顶点缓存
	LPDIRECT3DINDEXBUFFER9					m_pIndexBuffer;		//索引缓存

	int m_nCellsPerRow;							//每行的单元个数
	int m_nCellsPerCol;							//每行的单元个数
	int m_nVertsPerRow;							//每行的顶点数
	int m_nVertsPerCol;							//每列的顶点数
	int m_nNumVertices;							//顶点总数
	FLOAT m_fTerrainWidth;							//地形的宽度
	FLOAT m_fTereainDepth;							//地形的深度
	FLOAT m_fCellSpacing;							//单元格的间距
	FLOAT m_fHeightScale;							//高度缩放系数
	std::vector m_vHeightInfo;			                //用于存放高度信息
	//定义一个地形的FVF顶点格式
	struct TERRAINVERTEX
	{
		FLOAT _x, _y, _z;
		FLOAT _u, _v;
		TERRAINVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v)
			:_x(x), _y(y), _z(z), _u(u), _v(v)
		{}
		static const DWORD FVF = D3DFVF_XYZ | D3DFVF_TEX1;
	};
};


                                                       渲染之天空的实现

            说完了地形,那么天空自然也不能少,怎么样制作天空呢?在这里不得不说,其实游戏设计师都是一些视觉“骗子”,他们用自己的技术巧妙地欺骗了我们,如果说的高大上一点,那么可以说游戏设计师是一些视觉“魔术师“

其实三维场影里的天空并不是“真正”的天空,而是用图片拼起来的,欺骗我们眼睛。通常把大家所在的场景用一个几何体包裹起来,再在里面贴上从各个角度的风景图,就好像一个真正的环境一样。想想游戏之中的天空,是不是有点印象?

通常来说,或者说目前来说三维天空的实现主要包括三种类型:

类型一:平面型天空(Sky Plane),这种方式很简单,主要就是用一个平面放在玩家的头顶,这种方式很容易被玩家看出来,真实感太低了,不过对于某些很简单的游戏,这样做也未尝不可,不过要是对于那种强调真实感或者玩家带入感的游戏肯定不会这样做了。

类型二:天空穹庐(Sky Dome),这与上一种方式不同,这也是最难实现的,这种方式放在玩家头顶的是一个曲面,通常是一个半球。虽然这种方式真实性很强,但是这种方式不是目前最流行的,主要也是因为衔接处素材的匮乏,所以不是主流。如以下图片所示:

DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第5张图片

类型三:天空盒(Sky Box),这一种方式名为天空盒,顾名思义这一种方式是与立方体有关的。同时这也是目前使用最广泛的模拟三维天空的技术,你用谷歌或者百度搜索一下天空盒,你会发现网上关于天空盒的制作层出不穷,如以下的天空盒:

DirectX3D游戏制作之---3D场景的渲染及人物动画的显示_第6张图片


        虽然这样以盒子出现你可能会觉得真有这么简单,难道就不怕被看出来,其实如果制作的好一点,玩家完全看不出来(如果不是有意的观察的话),没错,我们就是在一个盒子里“乐不思蜀”的!

        好了,既然说完了概念,那么我们接下来说一说制作天空盒子的原理吧!

原理:(PS:可能以后的游戏可能会半球用的较多吧?因为这样做出来的比较逼真),我们这里以立方体为例。最简单的方法,莫过于画6个正方形(当然5个面应该也就够了,最底下一个面是地形 ),分别为它们贴上纹理(这样做出来的效果肯定不好),我们选择纹理的时候可以多比较一下,选择一个自己最喜欢的。
        立方体环境贴图(Cubic Environment Mapping),也叫立方体贴图,就是一个纹理包含了包围物体场景的图像数据, 就像一个物体在立方体中心一样.每个面包含横竖各90度的视野,每个立方体贴图共6个面. 说到底,就是把6张图片拼到一块儿去了而已,不过我们在制作的过程之中,其实只要5个面就够了,最底下的面只要使用我们的刚刚制作出来的地形就行了。(不过有些情况下,立方体贴图是映射到一个曲面上,而且,它跟本不使用UV坐标,代替它的是一个3D向量,在DirectX中是D3DFVF_TEXCOORDSIZE3类型。这样就很易的根据表面法向量将周围环境映射到物体表面,实现反射效果(激动人心呢))。

       当然在实际的制作过程之中还可以直接导入.X文件(关于什么是.X文件在下文人物模型的时候再提),不过关于制作么,如果不想制作的话,我们可以直接去网上下载资源,毕竟每个人的精力都有限,不可能什么都会,选择一个自己感兴趣的深究下去就行了。不过对这些知识有些了解还是不错的。

       好了,我们来看一下游戏之中天空盒的设计,不过在这之前还是有两个关键点需要注意一下:

       1. 天空理论上应该位于无限远处,场景中任何物品都位于天空盒前部,而不会被其遮住;

             2. 当在场景中移动时,场景中的物体会相对角色移动,但天空盒与自身相对位置保持静止不变(当然我们也可以设计让其绕着Y轴转动,这样就会有云彩移动的效果了,不过建议别设定的速度太快);

       针对1, 可以通过让天空盒的变换后的z坐标位于可视范围内的最大值。这样场景中任何物品都能被正确渲染,并能遮挡远处的天空。因而符合实际情况。

       针对2, 为了实现让天空盒相对自己静止的效果,有两种方法:一是针对任一时刻自己的世界坐标,让天空盒具有相同的平移,这样自己将永远位于天空盒中心,从而相对静止;另一种方法是在渲染天空盒时,不对天空盒进行世界变换,并且让其视角矩阵中平移部分变为[0, 0, 0],这样天空盒可以保持在原点,由于视角空间中相机永远位于原点,因此自己相对天空盒位置总是保持相对静止。(针对一个点,我说一下,我自己的程序之中没有这样做,其实这样也挺好,可以飞上天,与太阳肩并肩,哈哈,不过实际做游戏之中肯定要考虑这一个,否则影响玩家体验性,如果只是练习的话,可以不用考虑)

下面贴出天空盒的设计:

//天空盒类
class SkyBoxClass
{
public:
	//为天空盒类定义一个FVF灵活顶点格式
	struct SKYBOXVERTEX
	{
		float _x, _y, _z;
		float _u, _v;
	};
#define D3DFVF_SKYBOX  D3DFVF_XYZ|D3DFVF_TEX1
public:
	SkyBoxClass(LPDIRECT3DDEVICE9    pDevice);				 //构造函数
	virtual ~SkyBoxClass();							 //析构函数

	BOOL InitSkyBox(float Length);					         //初始化天空盒
	//从文件加载天空盒五个方向上的纹理
	BOOL LoadSkyTextureFromFile(wchar_t * pFrontTextureFile, wchar_t *pBackTextureFile, 
           wchar_t *pLeftTextureFile, wchar_t *pRightTextureFile, wchar_t *pTopTextureFile);
	      //渲染天空盒,根据第一个参数设定天空盒世界矩阵,第二个参数选择是否渲染出线框
        VOID RenderSkyBox(D3DXMATRIX *pMatWorld, BOOL bRenderFrame);
private:
       LPDIRECT3DDEVICE9 m_pd3dDevice; //D3D设备对象
       LPDIRECT3DVERTEXBUFFER9 m_pVertexBuffer;//顶点缓存对象
       LPDIRECT3DTEXTURE9 m_pTexture[5]; //5个接口纹理对象
       float m_Length; //天空盒边长
};


                                                                场景的加载

        好了,开始进入场景的加载,不得不说游戏之中场景是一个很重要的元素,没有美的场景,玩家的体验性就会不好,想一想我们玩过的国内外的3A级大作,不管是哪个,在场景上的绘制都是下足了功夫。一般来说3D场景的制作都是在3DS  Max或者Maya上制作的。简单说一下这两者的区别吧,3DS Max这一款软件更加适合于游戏、建筑学、室内设计等等,不过Maya可以说是一款专门为影视特效而生的软件,因此我们会发现利用Maya制作出来的模型效果更逼真一些,因此Maya软件主要是用于动画片的制作、电影制作、游戏动画制作等等。

        总之一句话就是:3DS Max在建筑动画效果上要强于Maya,但是Maya在影视动画上又有独到的优势,所以怎么选择看情况吧(与从事的行业也有一定的关系)。不过有条件的话(有Maya学习资料的话)建议学Maya,如果没有的话,那还是学3DS Max吧,毕竟这个更加普遍,资料也更好找。

        这里场景的加载主要是加载.X文件。主要是由于Direct3D自带.X模型文件的载入和使用,适合DirectX学习者使用。使用起来也挺方便。

       什么是.X文件呢?.X文件就是微软定义的3D模式文件格式,也是Direct3D的默认三维模型的格式,Direct3D自然对他有非常好的支持度,里面有大量的函数可以供我们使用。

       不过怎么样在程序之中加载呢?首先你得先获得.X文件,由于传统的3DS Max软件是不支持.X文件的导出的,而网上的素材一般也是.max格式居多,所以需要我们将.max格式转化为.X文件,那么具体应该转化呢。这里需要下载一个Panda插件,不过貌似Panda插件2012版的有,2013版的可能没有,所以如果是2013版的话,可能需要使用别的插件,关于Panda插件的下载,只需要早csdn之中搜索一下一下这个资源就可以了,我记得我就是在csdn上下载的,而且是免积分的。不过下载的时候还是注意一下版本与位数(64位与32位)。

       做完了准备工作之后,我们就可以着手进行我们的模型载入了,把我们的模型载入到游戏程序之中,要在程序之中载入模型的话,最主要的就是把.X文件之中的各种数据分别加载到内存之中,当然这些数据包括顶点数据、材质数据以及纹理数据

        简单来说,要将一个.X格式的模型加载到程序之中可以分为以下三步:

第一步:通过.X文件加载网格模型,代码如下:

        LPD3DXBUFFER	pAdjacencyBuffer = NULL;		//网格模型的邻接信息
	LPD3DXBUFFER	pD3DXMtrlBuffer = NULL;			//网格模型的材质信息
	//从磁盘之中加载网格模型
	D3DXLoadMeshFromX(strFilename, D3DXMESH_MANAGED, m_pd3dDevice, &pAdjacencyBuffer, 
                          &pD3DXMtrlBuffer, NULL, &m_dwNumMaterials, &m_pMesh);

第二步:读取材质和纹理数据,我们可以从刚刚上面创建的pD3DXMtrlBuffer变量之中去读,代码如下:

//读取材质和纹理信息
D3DXMATERIAL * d3dxMaterials = (D3DXMATERIAL *)pD3DXMtrlBuffer->GetBufferPointer();
m_pMaterials = new D3DMATERIAL9[m_dwNumMaterials];
m_pTextures = new LPDIRECT3DTEXTURE9[m_dwNumMaterials];
//逐子集提取材质属性和纹理文件
for (DWORD i = 0; i < m_dwNumMaterials; i++)
{
	//获取材质,并设置一下环境光的颜色值
	m_pMaterials[i] = d3dxMaterials[i].MatD3D;
	m_pMaterials[i].Ambient = m_pMaterials[i].Diffuse;
	//创建一下纹理对象
	m_pTextures[i] = NULL;
	if (d3dxMaterials[i].pTextureFilename != NULL && (strlen(d3dxMaterials[i].pTextureFilename) > 0))
	{
		//纹理的创建
		if (FAILED(D3DXCreateTextureFromFileA(m_pd3dDevice, d3dxMaterials[i].pTextureFilename, &m_pTextures[i])))
		{
			MessageBox(NULL, L"Sorry!没有找到对应的纹理文件", L"XFileModeClass类读取文件错误", MB_OK);
		}
	}
}
//优化网格模型
m_pMesh->OptimizeInplace(D3DXMESHOPT_COMPACT | D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_STRIPREORDER, 
       (DWORD *)pAdjacencyBuffer->GetBufferPointer(), NULL, NULL, NULL);
return S_OK;

第三步:也是最后一步,绘制模型,代码如下:简单地用一个for循环绘制就行了

	for (DWORD i = 0; i < m_dwNumMaterials; i++)
	{
		m_pd3dDevice->SetMaterial(&m_pMaterials[i]);
		m_pd3dDevice->SetTexture(0, m_pTextures[i]);
		m_pMesh->DrawSubset(i);
	}

                                                   人物的载入即动画的显示

       好了,如果说你载入的是一个没有绑定动画的.X文件,那么载入之后,就算大功告成了,不过如果你载入的是一个有动画绑定的,或者说一个有骨骼动画的模型的话,自然而然,你想要让这个模型动起来,那么接下来要做的就是骨骼动画的显示了。

       其实微软在DirectX库里面有一个关于骨骼动画的例子,我们可以打开看一看大师的代码,路径的话是先找到你的库位置,然后在Microsoft DirectX SDK (June 2010)\Samples\C++\Direct3D\SkinnedMesh文件下。

       不过骨骼动画是D3D的一个重要应用。尽管微软DXSDK提供了示例Skinned Mesh,但由于涉及众多概念和技术细节,还是很难读懂,我们可以简单来看一下,也不用着急着完全看懂,先学会用,用多了之后,可能你就会发现,你对这个又有了更深的理解。

        我们先来简单说一下骨骼动画的原理和实现方法吧:

        总体上,绝大部分动画实现原理一致,就是“提供一种机制,描述各顶点位置随时间的变化”。有三种方法:
第一种: 关节动画:由 于大部分运动,都是皮肤随骨骼在动,皮肤相对于它的骨骼本身并没有发生运动,所以只要描述清楚骨骼的运动就行了。用矩阵描述各个骨骼的相对于父骨骼运动。 (大多运动都是旋转型) 易知,从子骨骼用矩阵乘法累积到最顶层根骨骼,就可以得到每个子骨骼相对于世界坐标系的转换矩阵
       这种动画,只须用普通Mesh保存最初始的各顶点坐标,以及一系列后续时刻所对应的各骨骼的运动矩阵。不用保存每时刻的顶点数据,节省了大量存储空间。而且比较灵活,可以利用关键帧插值运算,便于通过运算调节动作。缺点是在两段骨骼交接处,容易产生裂缝,影响效果

第二种: 渐变动画:通过保存一系列时刻的顶点坐标来完成动画。虽然比较逼真,但占用大量空间,灵活性也不高。

第三种: 骨骼蒙皮动画(skinned Mesh),相当于上面两方法的折中。现在比较流行。
       在关节动画的基础上,利用顶点混合(Vertex Blend)技术,对于关节附近的顶点,由影响这些顶点的两段(或多段)骨骼运动,分别赋以权值,共同决定顶点位置。相当于在骨骼关节上动态蒙皮,有效解决了裂缝问题。

       这里,引入一个D3D技术概念:“Vertex Blending”---顶点混合技术。比如说,你肯定用过SetTransform(D3DTS_WORLD,....),但SetTransform(D3DTS_WORLDMATRIX(i),....)是不是很奇怪? 你可以在微软的DXSDK的帮助文件中搜索“Geometry Blending”主题,有裂缝及其解决办法图示。

       其实我们如果知道.X文件如何储存骨骼动画,有助于我们使用骨骼动画。理解X文件格式,对用好相关的DX函数是非常重要的。不含动画的普通X文件,有一个Mesh单元,保存了各顶点信息各三角面的索引信息材质种类及定义等。动画X文件,则在这个单元中增加了“各骨骼蒙皮信息”、“骨骼层次及结构信息”、“各时刻骨骼矩阵信息”等。

我们都知道每一个.X文件都有Mesh单元,储存网格信息,当然动画模型肯定也有,在Mesh{}单元中,在原有的普通网格顶点数据基础上,新增了XSkinMeshHeader{}结构,以及多个SkinWeights{}结构。用以描述各个骨骼的蒙皮信息。其中,XSkinMeshHeader是总括,举一实例,如下:

XSkinMeshHeader
{
   //一个顶点可以受到骨骼影响的最大骨骼数,可用于计算共同作用时减少遍历次数
   //一个三角面可以受到骨骼影响的最大骨骼数。这个数字对硬件顶点混合计算提出了基本要求。
   //当前Mesh的骨骼总数。
}

       由于每个骨骼的蒙皮信息都需要用SkinWeights结构去描述,所以有多少块骨骼,在Mesh中就有多少个SkinWeights对象。
注意,一般把SkinWeights视作Mesh的一部分。这种Mesh又称Skinned Mesh (蒙皮网格)

SkinWeights //结构如下:
{
   STRING       transformNodeName;       //骨骼名
   DWORD       nWeights;               //权重数组的元素个数,即该骨骼相关的顶点个数
   array DWORD vertexIndices[nWeights];//受该骨骼控制的顶点索引,实际上定义了该骨骼的蒙皮
   array float weights[nWeights];       //蒙皮各顶点的受本骨骼影响的权值
   Matrix4x4   matrixOffset; //骨骼偏移矩阵,用来从初始Mesh坐标,反向计算顶点在子骨骼坐标系中的初始坐标。
}
在有的书中,把上面的matrixOffset叫骨骼权重矩阵,是不恰当的。应该称为骨骼偏移矩阵比较合适。

       说完了.X文件如何储存骨骼动画,接下来就说一说骨骼层次信息,在这里就不得不提一个Frame,框架的概念    在X文件中,Frame是基本的组成单元。又称框架Frame。 一个.x可以有多个Frame。(PS:此处的Frame不是帧,与帧没什么关系,Web之中也有一个Frame的概念,和这个也不同).   框架Frame允许嵌套,这样就存在父子框架了。而并列的框架,称为兄弟框架。这两种关系组合在一起,即可以纵深,又可以并列,形成一种层次结构。这种结构,可用二叉树描述。

        每个框架结构的最前面,有一个FrameTransformMatrix矩阵数据,描述了该框架相对于父框架的变换矩阵。也就是说,该框架中的坐标,与该矩阵相乘,可转换为父框架坐标系的坐标。这种层次结构,使得X文件能描述许多复杂的物体。如地形场景。在骨骼动画文件中,框架结构可直接拿来描述人物骨骼的层次结构。框架的名字通常为对应的骨骼名。如“左上臂->左前臂->手掌->手指”就形成一个父子骨骼链。而左上臂与右上臂是并行关系。

        接下来说一下动画信息

由一系列AnimatonKey组成,数据示例如下:

  AnimationKey {
   4;              //--动画类型 4表示矩阵
   2118;           //--动画帧数,即下面矩阵个数,由于这是一个连续的动画,所以矩阵比较多
   0;16;0.000000,-0.000099,-1.000000,0.000000,0.000000,1.000000,-0.000099,0.000000,1.000000,0.000000,0.000000,
     0.000000,0.000002,206.074957,-1.012819,1.000000;;,

   160;16;0.000000,-0.000099,-1.000000,0.000000,-0.000045,1.000000,-0.000099,0.000000,1.000000,0.000045,0.000000,
     0.000000,-0.004233,206.073178,-1.015581,1.000000;;,
   ...其它各时刻矩阵...
   { Bip01_R_Calf }--对应的骨骼对象引用
   }

这里需要注意以下几点:
第一点:在上面数据结构中,主要保存了各典型时刻的该骨骼相对于父的变换矩阵
第二点:不要一味只有一个AnimationKey{},其实每块骨骼都有一个AnimationKey{}

第三点: 在0时刻的矩阵,与该骨骼对应的前面的Frame所对应的矩阵是相同的。如Frame Bip01_R_Calf{}中的变换矩阵,与Bip01_R_Calf所对应的AnimationKey 的第0时刻矩阵是一样的。这说明,在以后动画运行时,DX会提供一种功能,用AnimatonKey中的对应数据刷新初始的变换矩阵(也可能启用关键帧插值算法)。这个功能对应于示例中的m_pAnimController->SetTime(...)语句。    

       接下来说一说怎么从.X文件之中加载骨骼动画信息吧!

       在此以SDK中的示例为准,叙述一种标准加载方式,利用函数D3DXLoadMeshHierarchyFromX(),从字面上看是读取Mesh层次信息的意思,当然肯定不止这一种加载方式,有兴趣的可以去多了解一下:

         D3DXLoadMeshHierarchyFromX(
         LPCSTR Filename,                 //.x文件名
         DWORD MeshOptions,               //Mesh选项,一般选D3DXMESH_MANAGED
         LPDIRECT3DDEVICE9 pD3DDevice,     //指向D3D设备Device
         LPD3DXALLOCATEHIERARCHY pAlloc,   //自定义数据容器
         LPD3DXLOADUSERDATA pUserDataLoader,   //一般选NULL
         LPD3DXFRAME *ppFrameHierarchy,       //返回根Frame指针,指向代表整个骨架的Frame层次结构
         LPD3DXANIMATIONCONTROLLER *ppAnimController //返回相应的动画控制器
);

        需要注意的是两个输出参数很重要,不过也很好理解。

         什么是自定义数据容器呢?相信有人会有这样的疑问,原来,鉴于动画数据的复杂性,需要你配合完成加载过程。比如你是否用到自定义扩展结构,Mesh等数据保存在哪里,怎样使用户自己创建容器,自己决定卸载等等。
DX提供了ID3DXALLOCATEHIERARCHY接口,提供了这个自定义的机会,你重载这个接口的虚函数,在加载过程中,它就像回调函数那样运作。代码如下:

//-----------------------------------------------------------------------------
// Name: class CAllocateHierarchy
// Desc: 来自微软官方DirectX SDK Samples中的骨骼动画类,这个类用来从.X文件加载框架层次和网格模型数据
//-----------------------------------------------------------------------------
class CAllocateHierarchy: public ID3DXAllocateHierarchy
{
public:
	STDMETHOD(CreateFrame)(THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame);
	STDMETHOD(CreateMeshContainer)( THIS_ LPCSTR              Name, 
		CONST D3DXMESHDATA*       pMeshData,
		CONST D3DXMATERIAL*       pMaterials, 
		CONST D3DXEFFECTINSTANCE* pEffectInstances, 
		DWORD                     NumMaterials, 
		CONST DWORD *             pAdjacency, 
		LPD3DXSKININFO pSkinInfo, 
		LPD3DXMESHCONTAINER *ppNewMeshContainer);    
	STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree);
	STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerBase);
};
         你需要像上面给出的代码那样建立一个自定义数据容器类STDMETHOD是什么意思?有人肯定有这样的疑问,简单来说相当于virtual   HRESULT   __stdcall 的宏。 因为这种类要与D3D的COM接口打交道,不仅仅在C++内部使用,所以,所有类方法必须做成stdcall的,可对外开放的
#define   STDMETHOD(method)               virtual   HRESULT   STDMETHODCALLTYPE   method  
#define   STDMETHODCALLTYPE               __stdcall  
             有了信息之后,我们需要做的就是读取了,怎么读取呢?

       根据.X文件,在加载过程中,主要有两方面数据需要保存,一个是骨架Frame信息,一个是网格蒙皮Mesh信息。这两个信息保存在如下结构中。

框架信息(对应于骨骼)

typedef struct _D3DXFRAME
{
     LPSTR                   Name;
     D3DXMATRIX               TransformationMatrix; //本骨骼的转换矩阵

     LPD3DXMESHCONTAINER     pMeshContainer;       //本骨骼所对应Mesh数据

     struct _D3DXFRAME       *pFrameSibling;       //兄弟骨骼
     struct _D3DXFRAME       *pFrameFirstChild;     //子骨骼
} D3DXFRAME, *LPD3DXFRAME;

自定义数据容器,其数据来源由上面接口的CreateMeshContainer()函数提供
typedef struct _D3DXMESHCONTAINER
{
     LPSTR                              Name;       //容器名
     D3DXMESHDATA             MeshData;   //Mesh数据,可创建SkinMesh取代这个Mesh
     LPD3DXMATERIAL           pMaterials; //材质数组
     LPD3DXEFFECTINSTANCE     pEffects;  
     DWORD                          NumMaterials;//材质数
     DWORD*                         pAdjacency;   //邻接三角形数组
     LPD3DXSKININFO            pSkinInfo;   //蒙皮信息,其中含.x中的各个skinweight蒙皮顶点索引及各骨骼偏移矩阵等。
     struct _D3DXMESHCONTAINER *pNextMeshContainer;
} D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;

            读取完信息之后我们就可以进行绘制了(当然具体不是这么简单,这里由于篇幅的原因,简单的说一下,具体可以在我代码之中看),怎么样显示动画呢?
       DrawFrame()用来绘制整个X框架。它遍历各个框架,找到Mesh不为空的进行绘制。DrawMeshContainer()是绘制函数。下面给出我的设计代码:

#pragma once
#include 
#include 

// 继承自DXDXFRAME结构的结构
struct D3DXFRAME_DERIVED: public D3DXFRAME
{
	D3DXMATRIXA16 CombinedTransformationMatrix;
};


// 继承自D3DXMESHCONTAINER结构的结构
struct D3DXMESHCONTAINER_DERIVED: public D3DXMESHCONTAINER
{
	LPDIRECT3DTEXTURE9*  ppTextures;            //纹理数组
	LPD3DXMESH           pOrigMesh;             //原始网格
	LPD3DXATTRIBUTERANGE pAttributeTable;
	DWORD                NumAttributeGroups;    //属性组数量,即子网格数量
	DWORD                NumInfl;               //每个顶点最多受多少骨骼的影响
	LPD3DXBUFFER         pBoneCombinationBuf;   //骨骼结合表
	D3DXMATRIX**         ppBoneMatrixPtrs;      //存放骨骼的组合变换矩阵
	D3DXMATRIX*          pBoneOffsetMatrices;   //存放骨骼的初始变换矩阵
	DWORD                NumPaletteEntries;     //骨骼数量上限
	bool                 UseSoftwareVP;         //标识是否使用软件顶点处理
};

//来自微软官方DirectX SDK Samples中的骨骼动画类,这个类用来从.X文件加载框架层次和网格模型数据
class CAllocateHierarchy: public ID3DXAllocateHierarchy
{
public:
	STDMETHOD(CreateFrame)(THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame);
	STDMETHOD(CreateMeshContainer)( THIS_ LPCSTR              Name, 
		CONST D3DXMESHDATA*       pMeshData,
		CONST D3DXMATERIAL*       pMaterials, 
		CONST D3DXEFFECTINSTANCE* pEffectInstances, 
		DWORD                     NumMaterials, 
		CONST DWORD *             pAdjacency, 
		LPD3DXSKININFO pSkinInfo, 
		LPD3DXMESHCONTAINER *ppNewMeshContainer);    
	STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree);
	STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerBase);
};

// 来自微软官方DirectX SDK Samples中的骨骼动画全局函数
void DrawFrame( IDirect3DDevice9* pd3dDevice, LPD3DXFRAME pFrame );
void DrawMeshContainer( IDirect3DDevice9* pd3dDevice, LPD3DXMESHCONTAINER pMeshContainerBase, LPD3DXFRAME pFrameBase );
HRESULT SetupBoneMatrixPointers( LPD3DXFRAME pFrameBase, LPD3DXFRAME pFrameRoot );
void UpdateFrameMatrices( LPD3DXFRAME pFrameBase, LPD3DXMATRIX pParentMatrix );
         其实关于骨骼动画还有很多的要说,这里简单地介绍一下,如果有机会的话,准备专门写一篇关于骨骼动画的博客!
        编写完之后,看到自己的人物可以跳动,还是挺开心的!

    

你可能感兴趣的:(directx,动画,3d,游戏制作,DirectX3D游戏学习)