接下来,我们介绍一些骨骼动画。我们之前大致讲过骨骼动画,存储骨骼动画的网格文件要比普通的文件复杂一下。主要是增加了骨骼信息,蒙皮信息以及动画帧信息。骨骼动画的实现原理是仿照人体运动学,将3D模型由一种称作“蒙皮(skin mesh)”的网格模型和按照一定层次组织起来的“骨骼(bone)”组成。

骨骼层次(骨架)仿照关节动画的组织结构将3D模型组织成一个层次结构,而相邻骨骼之间通过做相对运动来实现特定的动作效果,从而就实现了不同的模型动画效果。

蒙皮(皮肤网格)与骨骼相关联,用于提供绘制动画所有需要的几何模型,还有纹理和材质等一些信息。组成皮肤网格的每个顶点都会受到一个或者多个骨骼的影响,而每个顶点受到多个骨骼影响的程度通过权值(weight)确定。通过计算每个顶点受到不同骨骼对他们影响的加权和,就可以得到这个顶点在运动过程中所处的实际位置。

骨骼蒙皮动画通过“关键帧”记录每个关键时间点的骨骼的位置、朝向等等一些信息。一个完整的动作包含了一系列的“关键帧”,我们称之为一个动画集(Animation Set)。每个动画集中相邻的两个关键帧之间进行插值运算,就可以确定某一时刻各骨骼所处的新位置和新朝向等一些额外信息。每个模型都有多个不同的动画集组成。

X文件有文本和二进制两种存储形式。当我们使用文本存储X文件的时候,我们就可以打开文件查看里面的明文数据。X文件定义了很多关键词来标示不同的数据,例如:

Mesh 定义一个网格,里面包含顶点数量和顶点坐标,三角面数量和构成三角面的顶点索引。MeshMaterialList 定义材质列表,包括纹理坐标的个数和纹理坐标。

MeshNormals 定义Mesh的法向量。普通的静态网格对象基本就包含这些信息了。

Frame 用来定义骨骼,可以包含多个Mesh对象,也可以包含子骨骼。

SkinWeights 定义骨骼对顶点的影响权重。

Animation用来保存动画信息,蒙皮网格包含的信息相对来说就复杂一些。

如下图就是一个X文件的一部分:

X文件可以存储正常的普通的静态mesh,也同样支持skin mesh。蒙皮网格和普通网格的唯一不同点就是看XSkinMeshHeader和SkinWeights模版是否存在。以上是一个独立的Frame(骨骼),它没有层次结构。但是大多数Frame都是有层次结构,各种父子,兄弟关系。网格(Mesh)中包含顶点,索引,法向量,纹理坐标,材质链表,SkinMeshHeader和SkinWeights。其中SkinWeights里面包含顶点权重信息以及骨骼偏移矩阵。DirectX3D使用D3DXFRAME来存储骨骼数据,使用D3DXMESHCONTAINER来存储蒙皮网格数据,使用Animation Set记录动画集。AnimationSet 包括一个或者多个Animation。Animation 描述一个动画,包含一个或几个AnimationKey。AnimationKey 动画关键帧,定义具体的动作数据,本质就是骨骼在该时间点的变换矩阵。骨骼本质就是变换矩阵。

在3D游戏开发中,骨骼动画基本上都有频繁的使用。骨骼动画的难点在于,不仅在制作过程中比较耗时耗力,在游戏开发中对骨骼动画的数据调整也非常困难。我们只所有要了解骨骼动画的存储细节,主要目的就是深入掌握骨骼动画是如何实现的。骨骼动画的播放,基本上就是按照时间来获取骨骼的变换矩阵,然后根据变换矩阵来渲染模型网格。这样,就会形成模型动画效果了。当然,这个过程描述起来很简单,但是使用DriectX实现起来就比较繁琐。坦白讲,DirectX对于骨骼动画的支持,感觉还是不够啊。DirectX中使用D3DXLoadMeshHierarchyFromX来加载一个骨骼动画模型文件。该方法中需要一个ID3DXAllocateHierarchy类型的参数,该类主要用来创建骨骼和蒙皮网格。骨骼动画播放就是根据时间来调整骨骼变换矩阵,最后绘制蒙皮网格。这里,我们只是简单的来做一个骨骼动画的案例,使用VS2019新建一个项目“D3D_08_Bone”,首先我们创建上面的ID3DXAllocateHierarchy类,它是一个接口,需要继承并实现里面的函数。为此我们创建“CAllocateHierarchy.h”和“CAllocateHierarchy.cpp”两个文件,文件的代码基本上来源于DirectX安装目录的SkinnedMesh案例中,当然需要做一些调整。这两个代码由于代码庞大且晦涩难懂,就不发出来了。大家可以下载后,依据注释去了解一下。接下来,我们需要创建“SkinMesh.h”和“SkinMesh.cpp”两个文件,这两个文件主要用来绘制播放骨骼动画。首先是“SkinMesh.h”,代码如下:

#pragma once
#include "CAllocateHierarchy.h"// 骨骼动画网格对象
class SkinMesh {public:// 自定义CAllocateHierarchy类CAllocateHierarchy* hierarchy = NULL;// 网格根骨骼LPD3DXFRAME root = NULL;// 动画控制器LPD3DXANIMATIONCONTROLLER controller = NULL;// 动画开始时间float startTime = 0;// 动画集ID,不同的动画ID是不一样的int current = 0;// 是否播放动画bool isPlay = false;// Direct3D设备指针对象LPDIRECT3DDEVICE9 D3DDevice = NULL;public:// 构造方法SkinMesh() {};SkinMesh(LPDIRECT3DDEVICE9 device, const wchar_t* dir, const wchar_t* file);// 渲染void render(D3DXMATRIX matrix);// 析构方法~SkinMesh();// 绘制某一个骨骼下的蒙皮网格void DrawMeshContainer(IDirect3DDevice9* pd3dDevice, LPD3DXMESHCONTAINER pMeshContainerBase, LPD3DXFRAME pFrameBase);// 按照骨骼层次绘制蒙皮网格,调用上面的 DrawMeshContainer 函数void DrawFrame(IDirect3DDevice9* pd3dDevice, LPD3DXFRAME pFrame);// 更新骨骼层次中的组合变换矩阵,void UpdateFrameMatrices(LPD3DXFRAME pFrameBase, LPD3DXMATRIX pParentMatrix);// 记录骨骼的组合变换矩阵,初始化工作而已HRESULT SetupBoneMatrixPointersOnMesh(LPD3DXMESHCONTAINER pMeshContainerBase, LPD3DXFRAME g_pFrameRoot);// 根据骨骼层次来来保存骨骼的组合变换矩阵,调用上面的 SetupBoneMatrixPointersOnMesh 函数,初始化工作而已HRESULT SetupBoneMatrixPointers(LPD3DXFRAME pFrame, LPD3DXFRAME g_pFrameRoot);};

所有的动画数据都已经加载进来,动画的播放需要一个LPD3DXANIMATIONCONTROLLER控制器,它的作用根据时间来设置动画关键帧。这个关键帧里面保存了该时间点的变换矩阵,紧接着我们就调用UpdateFrameMatrices函数来更新整个骨骼层次的最终变换矩阵,然后调用DrawFrame函数,来根据骨骼层次逐一绘制模型的蒙皮网格。这样,我们不停的设置动画时间点,不停的绘制蒙皮网格,就形成动画了。由于“SkinMesh.cpp”文件代码庞大且晦涩难度,我们就不发出来了。大家可以直接下载,按照注释去了解一下。这里我们只介绍构造函数和render渲染方法内容:

// 构造方法
SkinMesh::SkinMesh(LPDIRECT3DDEVICE9 device, const wchar_t* dir, const wchar_t* file) {// Direct3D设备指针对象D3DDevice = device;// 拼接文件路径wchar_t xfile[100] = { 0 };wcscat_s(xfile, 100, dir);wcscat_s(xfile, 100, file);// 创建骨骼动画hierarchy = new CAllocateHierarchy(device, convert(dir));D3DXLoadMeshHierarchyFromX(xfile, D3DXMESH_MANAGED, D3DDevice, hierarchy, NULL, &root, &controller);// 使用一个数组记录所有骨骼的组合变换矩阵,这个数组在网格容器对象中SetupBoneMatrixPointers(root, root);// 动画开始时间startTime = 0;// 动画集ID,默认第一个动画int current = 0;// 是否播放动画bool isPlay = false;// 使用索引实例化第一个动画集,并将动画集应用于指定轨迹(1.0),接下来才能播放该动画集LPD3DXANIMATIONSET pAnimationSet = NULL;controller->GetAnimationSet(current, &pAnimationSet);controller->SetTrackAnimationSet((UINT)1.0, pAnimationSet);
};

在构造方法中,我们加载骨骼动画文件,同时初始化骨骼的组合变换矩阵。然后就是实例化动画控制器。我们上文介绍过,一个骨骼动画可以包含多个动画集(例如行走动画,攻击动画,死亡动画等等)。每个动画集都可以使用索引或名称表示。本案例中,我们使用索引0来表示模型的第一个动画(行走动画)。播放不同的动画,就设置不同的索引即可。接下来就是render方法,如下:

// 渲染
void SkinMesh::render(D3DXMATRIX matrix) {// 动画时间差float fTimeDelta = 0;// 计算动画时间差if (isPlay) {fTimeDelta = (float)timeGetTime();fTimeDelta = (fTimeDelta - startTime) * 0.001f;startTime = (float)timeGetTime();}// 设置骨骼动画的时间差controller->AdvanceTime(fTimeDelta, NULL);// 更新骨骼层次中的组合变换矩阵// matWorld是一个世界矩阵,所有骨骼矩阵变换的起点就是它UpdateFrameMatrices(root, &matrix);// 绘制骨骼动画DrawFrame(D3DDevice, root);
};

这个渲染的过程,就是之前讲述过的。先设置时间差,然后更新变换矩阵,最后根据骨骼层次来渲染蒙皮网格。设置时间差,其实就是获取骨骼“关键帧”的变换矩阵,然后根据这个变换矩阵更新整个骨架的骨骼变换矩阵,最后在绘制蒙皮网格。时间差不断的递增调整,变换矩阵不断改变,模型也就随之变换运动,形成动画。两个类都封装完毕后,我们就开始“main.cpp”文件的内容,首先是全局变量的声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 骨骼动画网格对象
SkinMesh* mesh = NULL;

紧接着,就是initScene函数,代码如下:

// 实例化骨骼动画网格对象
mesh = new SkinMesh(D3DDevice, L"tiny/", L"tiny.x");
//mesh = new SkinMesh(D3DDevice, L"asset/", L"jianling.X");// 开始播放动画
mesh->current = 0;
mesh->isPlay = true;
mesh->startTime = (float)timeGetTime();// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);// 初始化投影变换
initProjection();// 初始化光照
initLight();

在这里,我们提供了两个骨骼动画模型文件,一个是来源于DirectX的案例(tiny.x),一个来源于网络下载(jianling.X)。由于模型比较大,因此我们的摄像机位置也调整了,

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(300.0f, 100.0f, -700.0f);
D3DXVECTOR3 viewLookAt(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

由于很多模型都不支持全局环境光,因此,本案例我们新增加一个平行光,代码如下:

// 设置一下环境光
D3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(255, 255, 255));// 设置一个平行光照
D3DLIGHT9 light;
::ZeroMemory(&light, sizeof(light));
light.Type = D3DLIGHT_DIRECTIONAL;
light.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
light.Specular = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
light.Direction = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
D3DDevice->SetLight(0, &light);
D3DDevice->LightEnable(0, true);// 开启光照
D3DDevice->SetRenderState(D3DRS_LIGHTING, true);// 设置默认材质
D3DMATERIAL9 defaultMaterial;
::ZeroMemory(&defaultMaterial, sizeof(defaultMaterial));
defaultMaterial.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);   // 100%反射环境光
defaultMaterial.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);   // 100%反射漫反射光
defaultMaterial.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);  // 不反射高光
defaultMaterial.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);  // 不自发光
defaultMaterial.Power = 0.0f;                                  // 没有高光区
D3DDevice->SetMaterial(&defaultMaterial);

最后是我们的renderScene函数,代码如下:

// 世界变换矩阵
D3DXMATRIX worldMatrix, scalingMatrix;
D3DXMatrixTranslation(&worldMatrix, 0.0f, 0.0f, 0.0f);
//D3DXMatrixScaling(&scalingMatrix, 5.0f, 5.0f, 5.0f);
//D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &scalingMatrix);
mesh->render(worldMatrix);

运行代码,就会看到模型行走动画了。

控制动画播放的代码,我们放到了initScene函数中,也就是如下:

// 开始播放动画
mesh->current = 0;
mesh->isPlay = true;
mesh->startTime = (float)timeGetTime();

在上面的代码中,起作用的就是时间的设置。最后,我们添加一个GUI按钮,来手动控制动画的播放和暂停。为此,我们创建“GuiClass.h”和“GuiClass.cpp”两个文件,由于之前我们就对此进行过封装,这里不在介绍这个两个文件,如果有细微的区别的话,以GuiClass为准吧。有了GUI的加入,我们还是先声明两个全局变量:

// 两个GUI按钮
GuiClass* start, * stop;

然后就是initScene函数的初始化工作:

// 开始播放动画
//mesh->current = 0;
//mesh->isPlay = true;
//mesh->startTime = (float)timeGetTime();// 开始和停止按钮
start = new GuiClass(D3DDevice, 300.0f, 430.0f, 200.0f, 50.0f, L"开始播放", L"btn.jpg");
start->isShow = true;
stop = new GuiClass(D3DDevice, 300.0f, 500.0f, 200.0f, 50.0f, L"停止播放", L"btn.jpg");
stop->isShow = true;

接着就是renderScene函数绘制部分:

// GUI按钮渲染
start->render();
stop->render();

最后就是我们的update函数,

// 只接收鼠标点击事件
if (type != 3) return;// 是否单击"开始播放"按钮
if (mx > start->posX && mx < start->posX + start->width && my > start->posY && my < start->posY + start->height) {mesh->current = 0;mesh->isPlay = true;mesh->startTime = (float)timeGetTime();
}// 是否点击"停止播放"按钮
if (mx > stop->posX && mx < stop->posX + stop->width && my > stop->posY && my < stop->posY + stop->height) {mesh->isPlay = false;mesh->startTime = 0;
}

运行效果如下:

本课程的所有代码案例下载地址:

workspace.zip

备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

第八章 DirectX 3D模型加载和骨骼动画(下)相关推荐

  1. 安卓上的 3D 模型加载 和骨骼动画 库 SceneView

    如果你要加载3D 模型,比如Maya 3D max 生成的 3d 模型文件,你会发现基本没有好用的快捷的库, github上是有一个比较出名的3d 库 , https://github.com/the ...

  2. Android OpenGLES2.0(十四)——Obj格式3D模型加载

    转自:http://blog.csdn.net/junzia/article/details/54300202 在博主<OpenGLES系列>文章中,最开始的几篇讲的就是OpenGL世界中 ...

  3. 浅谈DirectX的模型加载

    浅谈DirectX的模型加载 xanxus - 2010年10月3日 - DirectX - 0 Comments 喜欢这篇文章吗?分享给你的朋友吧~  基于DirectX的游戏开发中,人物和模型由针 ...

  4. 优化Flash中的3D模型加载

    2019独角兽企业重金招聘Python工程师标准>>> 来自:Kid's Zone 最近在做一个公司的Flash3D页游项目,遇到了这个问题,前前后后断断续续也优化了一段时间,觉得还 ...

  5. Qt Quick 3D学习:模型加载

    (注意,开源版的 Qt Quick 3D 是狗都不用的 GPL 协议) Qt Quick 3D 模块提供了 Model 类型用于 3D 模型加载,通过设置 source 的资源路径来加载对应的 3D ...

  6. DirectX的OBJ模型加载与渲染

    在之前的DirectX例子里我用的模型是.x文件,DirectX有一个方法D3DXLoadMeshFromX可以加载.x模型,但是这里有个问题,.x文件是没法用文本编辑器打开查看结构的,这里我来演示一 ...

  7. OpenGL OBJ模型加载.

    在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的3D模型文件有OBJ,FBX,da ...

  8. OpenGL Assimp模型加载库

    OpenGL Assimp库 前言 模型加载库 构建Assimp 前言 到目前为止的所有场景中,我们一直都在滥用我们的箱子朋友,但时间久了甚至是我们最好的朋友也会感到无聊.在日常的图形程序中,通常都会 ...

  9. 用Assimp模型加载库加载一个Crytek的游戏孤岛危机(Crysis)中的原版纳米装(Nanosuit)

    用这个例子来对GitHub上的LearnOpenGL教程前四个单元用到的所有自定义或者引入的各种头文件和资源进行一个总结,不得不说这个教程简直太美妙了. 这个模型是来自对GitHub上的LearnOp ...

最新文章

  1. [css] 说说你对css盒子模型的理解
  2. Linux父进程如何发信号给子进程,在父进程和子进程之间发送信号
  3. 最课程学员启示录:一份有诚意的检讨书
  4. 【原】修改shm,oracle11g需要扩大共享内存
  5. G7在实时计算的探索与实践
  6. Day2-运算符和编码
  7. web 前端签名插件_10款前端开发神器,助你成前端高手?
  8. 安卓软件安装包后缀名_安卓手机安装包是什么格式?
  9. unity使用BMFont制作位图字体
  10. mdx 医学词典_Mdict的mdx词典文件如何解析,有相关的开源代码可以参考吗?
  11. 情人节程序员用HTML网页表白【粉色烂漫的七夕情人节专题页面】 HTML5七夕情人节表白网页源码 HTML+CSS+JavaScript
  12. 100种网站推广方法全集
  13. 一种特殊的魔方阵解法
  14. 分子动力学(二)-成键相互作用
  15. IDEA双击不报错打不开解决方法
  16. 你应该知道的requestIdleCallback
  17. GOOGEL翻译软件测试,测试一把google的在线翻译工具
  18. 机器学习岗位的面试准备——总结1
  19. Unity 灯光与渲染 (一)
  20. 临床宏基因组学的应用

热门文章

  1. centos7使用yum安装mysql5.7(官网标准安装方式)
  2. 关于华硕M2N-VM DVI主板前置音频不能正常工作的问题的解决办法
  3. 初识OpenGL (2)编译着色器
  4. Android11 Wifi连接流程之IP地址分配
  5. TST5927无线索力测试分析系统
  6. linux系统分辨率文件,linux系统自定义分辨率
  7. MATLAB-单元数组
  8. python网站迁移_在Python中利用Into包整洁地进行数据迁移的教程
  9. Postman接口测试基础和实战(1)
  10. 支付宝支付-扫码支付详解