对于那些简单的图形(四边形,立方体),可以通过代码指定顶点数据的形式绘制出来。但是对于复杂的3D模型(人物,景物)来说,采用这种方式显然是不现实的。因此,Direct3D提供了一种称为网格模型的技术,可以从各种特定的文件格式中读取和绘制3D模型。网格模型是一种将图形的顶点数据,纹理,材质等信息存储在一个文件中。而这些3D模型通常都是有3D建模软件生成的。目前市场上主流的3D建模软件有3DS Max和Maya,两款建模软件都属于AutoDesk公司。3ds max和maya都是高端3D建模软件,两者之间有很多相同的功能,建模,材质和渲染,动画等等。但实际运用过程中,3ds max更加适用于游戏和室内设计,而maya则是专门为影视特效而生的一款软件。也就是说在制作逼真的动画方面来讲,maya会更合适一些,但3ds max更简单易学。ZBrush 是一个数字雕刻和绘画软件,它以强大的功能和直观的工作流程彻底改变了整个三维行业。ZBrush主要用于高模的创建。C4D全名Cinema 4D,德国MAXON出的3D动画软件。C4D是一款易学、易用、高效且拥有电影级视觉表达能力的3D软件,C4D由于其出色的视觉表达能力已成为视觉设计师首选的三维软件。BodyPaint 3D 一经推出立刻成为市场上最佳的 UV 贴图软件,Cinema 4DR10 的版本中将其整合成为 Cinema 4D的核心模块。Blender是一款免费开源三维图形图像软件,提供从建模、动画、材质、渲染、到音频处理、视频剪辑等一系列动画短片制作解决方案。Blender提供了全面的 3D 创作工具,包括建模、UV 映射、贴图、绑定、蒙皮、动画、粒子和其它系统的物理学模拟等等。

3ds max的文件格式是.max,Maya的文件格式为.mb,而.obj是3ds max和maya通用的文件格式。obj格式是由Wavefront公司出品的三维模型文本交换格式,不包含动画、材质特性、贴图路径、动力学、粒子等信息。fbx格式是Autodesk公司出品的支持动画的三维模型交换格式。FBX格式是一种3D通用模型文件。包含动画、材质特性、贴图、骨骼动画、灯光、摄像机等信息。由于该格式包含信息丰富,支持文本和二进制描述,被游戏行业广泛使用。我们后期使用Unity开发游戏的时候,基本都是使用fbx格式的模型文件。虽然我们上面讲述了很多三维模型文件的格式,但是在Direct3D中,最适合的文件格式是.X文件,它是微软定义的3D模型文件格式,其中包括网格纹理,动画等等数据。但是需要注意的是,.X文件通常并不存储纹理图像,它只是包含纹理贴图的文件名,因此一个完整的.X模型文件,还应该包括贴图文件。

使用3ds max制作完模型之后,就可以导出文件,供我们的代码使用了。因为微软默认使用.X文件存储3D模型,因此我们需要在3ds max中将做好的模型导出为.X格式。这就需要一个插件来完成,AutoDesk公司官方中是没有这项功能的。很多人都推荐使用panda,但是panda目前最新支持的也才是3ds max 2012版本。所以,我们这里使用AxeFree这个插件,下载地址是:DirectX Exporter and X Viewer Download  下载之后把插件解压出来,放到3dmax安装目录下面的plugins下面,重启3dmax后,在文件-》导出的文件格式下拉框中,就能看到.X文件格式了。

我们之前的项目中,曾经通过顶点形式绘制过立方体,那种方式非常的繁琐。这里我们使用3ds max快速创建一个立方体,并赋予一个纹理贴图,然后使用DirectX API来加载并绘制它,整个过程非常的简单。以下是我们使用3ds max创建一个立方体,并设置尺寸为5,各分段都是1。这里的尺寸其实就是顶点坐标单位,而分段就是四边形的数量,1就代表立方体每个面只有一个四边形。

创建完立方体之后,还需要将立方体的局部坐标系原点平移到世界坐标系的原点,也就是使用移动工具“W”,将模型的X/Y/Z坐标值归零,如下:

为立方体赋予表面纹理,特别的简单,只需要将纹理图片拖动到立方体上面即可,

接下来,我们就可以导出该模型了,

选择导出的类型为X文件,然后点击保存,

这是文件导出时候的参数设置,基本上保持不变,但是在X File Format选项位置,我们选择 Text格式,也就意味着,我们的模型文件是文本格式,我们可以使用记事本或Notepad++等工具打开文件,看到文件中的明文数据。导出文件成功后,将cube.X文件和纹理贴图文件rocks.jpg一起复制到我们项目中。我们使用VS2019新建一个项目“D3D_08_Mesh”,新建“main.cpp”文件,并复制之前的旧代码,先做全局变量声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 网格对象
LPD3DXMESH D3DMesh = NULL;// 网格的材质数组
D3DMATERIAL9* D3DMaterials = NULL;// 网格的纹理数组
LPDIRECT3DTEXTURE9* D3DTextures = NULL;// 材质的数目
DWORD MaterialsNumber = 0;

这里需要大家注意的是,一个X格式的网格对象可能包括多个材质和纹理贴图,而材质和纹理贴图基本上都是保持一致的。也就是说,有多少个材质,就有多少个纹理贴图。同时,一个网格对象可能包含多个子网格,每个子网格可以拥有自己的材质和纹理贴图。接下来就是我们的initScene函数,它的主要目的就是加载X文件生成网格对象,同时从X文件中解析材质和纹理信息,用于后面渲染。代码如下:

// 声明网格邻近信息(基本不使用)
LPD3DXBUFFER pAdjBuffer = NULL;// 声明网格材质信息(包含材质和纹理)
LPD3DXBUFFER pMtrlBuffer = NULL;// 从X文件中加载网格数据
D3DXLoadMeshFromX(L"cube.X",              // 表示X文件的路径D3DXMESH_MANAGED,        // 表示创建网格对象的附加选项D3DDevice,              // 表示设备对象&pAdjBuffer,           // 表示网格的邻接信息&pMtrlBuffer,           // 表示网格的材质信息NULL,                   // 表示网格的特殊效果&MaterialsNumber,       // 表示网格的材质数量&D3DMesh);              // 表示网格对象// 实例化材质和纹理贴图数组,长度就是材质数量
D3DMaterials = new D3DMATERIAL9[MaterialsNumber];
D3DTextures = new LPDIRECT3DTEXTURE9[MaterialsNumber];// 从 D3DXMATERIAL结构体 读取材质和纹理
D3DXMATERIAL* pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer();
for (DWORD i = 0; i < MaterialsNumber; i++)
{// 获取材质D3DMaterials[i] = pMtrls[i].MatD3D;// 使用漫反射参数去设置环境光反射率D3DMaterials[i].Ambient = D3DMaterials[i].Diffuse;// 获取并创建纹理对象D3DTextures[i] = NULL;if (pMtrls[i].pTextureFilename != "")D3DXCreateTextureFromFileA(D3DDevice, pMtrls[i].pTextureFilename, &D3DTextures[i]);
}// 释放缓冲数据
pAdjBuffer->Release();
pAdjBuffer = NULL;
pMtrlBuffer->Release();
pMtrlBuffer = NULL;// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);// 初始化投影变换
initProjection();// 初始化光照
initLight();

在Direct3D中,微软为我们提供了ID3DXMesh接口表示网格,它实际上就是三维物体的顶点缓存的集合。它将为我们创建顶点缓存,定义灵活顶点格式和绘制顶点缓冲区等功能都封装在一个对象中完成。ID3DXMesh接口中的D3DXLoadMeshFromX方法用于从文件中加载一个网格对象。其中一种重要的参数就是材质信息,它是一个LPD3DXBUFFER类型的数据。LPD3DXBUFFER是一种泛型数据结构,它可以存储顶点位置坐标,材质,纹理等多种类型的数据,可以使用它的GetBufferPointer方法获取里面的数据。我们之前讲过,X文件的材质和纹理是被分组在一起的。因此获取到材质信息pMtrlBuffer之后,就能通过一个材质数量的循环,将材质和纹理分别存储到我们提前声明的数组里面。同时,渲染的时候,也是按照材质进行循环并渲染每个子网格。可以这样认为,每个材质对应的网格就是子网格。那么,renderScene函数代码如下:

// 按照材质进行子网格渲染
for (DWORD i = 0; i < MaterialsNumber; i++)
{// 设置材质和纹理后进行渲染D3DDevice->SetMaterial(&D3DMaterials[i]);D3DDevice->SetTexture(0, D3DTextures[i]);D3DMesh->DrawSubset(i);
}

一个网格(mesh)由一个或数个子集组成。一个子集(subset)是网格中一组可用相同属性渲染的一组三角形。这里的属性就是材质和纹理。为了区分不同的子集, 每个子集指定一个唯一的非负整数值。网格中的每个三角形单元都被赋予一个属性ID指定三角形势单元所属的子集。我们就是根据子网格索引来循环绘制每个子网格。运行代码,效果如下:

由于3D模型经常使用,因此,我们将其封装一个类,方便以后的使用。同时,我们使用VS2019新建一个项目“D3D_08_MeshClass”。首先,我们创建“main.h”用于公共头文件的引入,代码如下:

#pragma once
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <time.h>
#include <math.h>
#include <iostream>
#include <fstream>
#include <string>// 引入依赖的库文件
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")
#pragma comment(lib,"winmm.lib")#define WINDOW_LEFT       200             // 窗口位置
#define WINDOW_TOP      100             // 窗口位置
#define WINDOW_WIDTH    800             // 窗口宽度
#define WINDOW_HEIGHT   600             // 窗口高度
#define WINDOW_TITLE    L"D3D游戏开发"    // 窗口标题
#define CLASS_NAME      L"D3D游戏开发"    // 窗口类名// wchar_t* 转 char*
char* convert(const wchar_t* wstr);

在这里,我们定义了一个wchar_t* 转 char*的函数,具体实现如下:

// wchar_t* 转 char*
char* convert(const wchar_t* wstr) {size_t len = wcslen(wstr) + 1;size_t converted = 0;char* cstr;cstr = (char*)malloc(len * sizeof(char));wcstombs_s(&converted, cstr, len, wstr, _TRUNCATE);return cstr;
};

该方法会在很多地方使用到,这里不在详细介绍。接下来,我们创建“MeshClass.h”和“MeshClass.cpp”两个文件,其中“MeshClass.h”代码如下:

#pragma once
#include "main.h"// 自定义网格类
class MeshClass {public:const wchar_t* fileDir;                     // 网格文件目录const wchar_t* fileName;                   // 网格文件名称LPD3DXMESH meshObj = NULL;                    // 网格对象D3DMATERIAL9* materialsArray = NULL;        // 网格的材质数组LPDIRECT3DTEXTURE9* texturesArray = NULL;    // 网格的纹理数组DWORD materialsNumber = 0;                   // 网格材质的数量LPDIRECT3DDEVICE9 D3DDevice = NULL;          // Direct3D设备指针对象public:// 构造方法MeshClass() {}MeshClass(LPDIRECT3DDEVICE9 device, const wchar_t* dir, const wchar_t* file) : fileDir(dir), fileName(file), D3DDevice(device) {};// 初始化网格void init();// 渲染网格void render();// 析构方法~MeshClass();};

该类的实现,基本上和我们上面讲的类似,只不过在这里,我们将模型文件和贴图文件放入到了asset目录下,这样我们就必须将目录名称作为参数传递给该对象了。“MeshClass.cpp”代码如下:

#include "MeshClass.h";// 自定义网格类:初始化网格
void MeshClass::init() {// 拼接文件路径wchar_t file[100] = { 0 };wcscat_s(file, 100, fileDir);wcscat_s(file, 100, fileName);// 声明网格邻近信息(基本不使用)LPD3DXBUFFER adjacencyBuffer = NULL;// 声明网格材质信息(包含材质和纹理)LPD3DXBUFFER materialsBuffer = NULL;// 从X文件中加载网格数据D3DXLoadMeshFromX(file,                        // 表示X文件的路径D3DXMESH_MANAGED,            // 表示创建网格对象的附加选项D3DDevice,                  // 表示设备对象&adjacencyBuffer,          // 表示网格的邻接信息 &materialsBuffer,          // 表示网格的材质信息NULL,                       // 表示网格的特殊效果&materialsNumber,           // 表示网格的材质数量&meshObj);                  // 表示网格对象// 实例化材质和纹理贴图数组,长度就是材质数量materialsArray = new D3DMATERIAL9[materialsNumber];texturesArray = new LPDIRECT3DTEXTURE9[materialsNumber];// 从 D3DXMATERIAL结构体 读取材质和纹理D3DXMATERIAL* materials = (D3DXMATERIAL*)materialsBuffer->GetBufferPointer();for (DWORD i = 0; i < materialsNumber; i++){// 获取材质,使用漫反射光反射率来补充环境光反射率materialsArray[i] = materials[i].MatD3D;materialsArray[i].Ambient = materialsArray[i].Diffuse;// 加载贴图文件创建纹理对象texturesArray[i] = NULL;if (materials[i].pTextureFilename != "") {char file2[100] = { 0 };LPSTR fname = materials[i].pTextureFilename;wchar_t fdir[20] = { 0 };wcscpy_s(fdir, fileDir);strcat_s(file2, 100, convert(fdir));strcat_s(file2, 100, fname);D3DXCreateTextureFromFileA(D3DDevice, file2, &texturesArray[i]);}}// 释放网格数据adjacencyBuffer->Release();adjacencyBuffer = NULL;materialsBuffer->Release();materialsBuffer = NULL;
}// 自定义网格类:渲染网格
void MeshClass::render() {// 按照材质进行子网格渲染for (DWORD i = 0; i < materialsNumber; i++){// 设置材质和纹理后进行渲染D3DDevice->SetMaterial(&materialsArray[i]);if (texturesArray[i] != NULL) D3DDevice->SetTexture(0, texturesArray[i]);meshObj->DrawSubset(i);}
}// 自定义网格类:析构方法
MeshClass::~MeshClass() {for (DWORD i = 0; i < materialsNumber; i++) {texturesArray[i]->Release();texturesArray[i] = NULL;}delete[] texturesArray;texturesArray = NULL;delete[] materialsArray;materialsArray = NULL;meshObj->Release();meshObj = NULL;
}

和我们上一个项目的代码区别在于,我们加载模型文件和贴图文件的时候,使用目录进行了拼接。模型基本类完成后,我们就开始“main.cpp”文件代码,首先全局变量声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 网格对象数组
MeshClass* mesh;

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

// 从X文件中加载网格数据
mesh = new MeshClass(D3DDevice, L"asset/", L"jiansheng.X");
mesh->init();// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);// 初始化投影变换
initProjection();// 初始化光照
initLight();

由于该模型比较大,因此,我们的摄像机位置做了调整,如下:

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(50.0f, 150.0f, -300.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);

其他代码,都是之前的,没有改变。最后就是renderScene函数了,代码如下:

D3DXMATRIX worldMatrix;
D3DXMatrixTranslation(&worldMatrix, 0, 0, 0);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix);
mesh->render();

代码运行效果如下:

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

workspace.zip

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

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

  1. 第八章 DirectX 3D模型加载和骨骼动画(下)

    接下来,我们介绍一些骨骼动画.我们之前大致讲过骨骼动画,存储骨骼动画的网格文件要比普通的文件复杂一下.主要是增加了骨骼信息,蒙皮信息以及动画帧信息.骨骼动画的实现原理是仿照人体运动学,将3D模型由一种 ...

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

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

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

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

  4. 浅谈DirectX的模型加载

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

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

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

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

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

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

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

  8. OpenGL OBJ模型加载.

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

  9. OpenGL Assimp模型加载库

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

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

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

最新文章

  1. 天梯赛 L2-005 集合相似度 (set容器)
  2. 华为架构师谈如何理解运用模块与微服务
  3. 前端学习(1714):前端系列javascript之目录结构
  4. python 裁判文书网_python - 用selenium模拟登陆裁判文书网,系统报错找不到元素。...
  5. java多线程交替打印_java实现多线程交替打印
  6. 如何假装自己读懂了《时间简史》
  7. ds6708 symbol 驱动_Symbol DS6708扫描器
  8. TED如何掌控你的时间(第一天)
  9. Unity UGUI —— 鼠标穿透UI问题(Unity官方的解决方法)
  10. 强引用、软引用、弱引用、虚引用、终结器引用
  11. 学校校园无盘教学系统
  12. 【线性系统理论】0.线性系统基本概念(1)
  13. 如何为word文档增加脚注
  14. 架构师说低代码:走出半生,归来仍是“毒瘤”!
  15. java写一个文件浏览器_【Java】 实现一个简单文件浏览器(2)
  16. Android 进程间通信方式
  17. 如何通过平台注册到过期域名?
  18. 打开回收站提示“回收站已损坏是否清空该驱动器上的回收站“解决方法
  19. DSP CCS 12.00 芯片:TMS320F28335 建立工程 ,使LED 灯闪烁
  20. [转] Bias-Variance Tradeoff

热门文章

  1. 程序员的自我修养之数学基础10:超定方程的求解
  2. matlab中欠定方程组超定方程组_大规模线性方程组解法简介
  3. java冒泡排序图解_[图解] 冒泡排序
  4. OFDM转向FBMC
  5. ei会议论文录用但不参加会议_ei会议论文还能在报纸杂志发表吗?
  6. 信息计算机课评课,关于信息技术的评课稿
  7. 计算机网络课程设计(ftp)
  8. mysql查询每个月入职人数_按年份统计入职人数sql语句
  9. 适用于高密度或高精度应用的高度可配置和可扩展的螺旋电容器设计
  10. 免费开源平台 CESIUM GIS、Worldwind、skyline、mapgis、mapinfo、ARCGIS、OSGEARTH、UNIGINE、unity3d、ossimplant