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

对于那些简单的图形(四边形,立方体),可以通过代码指定顶点数据的形式绘制出来。但是对于复杂的3D模型(人物,景物)来说,采用这种方式显然是不现实的。因此,Direct3D提供了一种称为网格模型的技术,可以从各种特定的文件格式中读取和绘制3D模型。网格模型是一种将图形的顶点数据,纹理,材质等信息存储在一个文件中。而这些3D模型通常都是有3D建模软件生成的。目前市场上主流的3D建模软件有3DS MaxMaya,两款建模软件都属于AutoDesk公司。3ds maxmaya都是高端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的文件格式是.maxMaya的文件格式为.mb,而.obj3ds maxmaya通用的文件格式。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来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DirectX 3D图形与动画程序设计是一门涉及计算机图形学和动画技术的课程。它教授的是使用DirectX 3D平台编写图形和动画程序的基本原理和方法。 DirectX是一个由微软开发的多媒体API,其中包括DirectX 3D,这是用于创建和呈现3D图形的一个组件。它提供了一系列功能强大的工具和函数,使得开发者可以轻松地创建出逼真的3D图形效果。 在这门课程中,学生将学习到如何使用DirectX 3D创建3D模型,并将其渲染到屏幕上。学生将学习如何使用顶点和像素着色器来操控模型的外观和行为。他们还将学习如何使用贴图来增强模型的真实感。 除了基本的图形渲染,学生还将学习如何使用DirectX 3D创建动画效果。他们将学习如何创建动画骨骼,并将其应用于3D模型上。学生还将学习如何使用关键帧动画和插值技术来创建平滑的动画过渡效果。 在课程的最后,学生将有机会应用他们所学的知识,开发一个自己的项目。他们可以选择创建一个游戏、仿真或其他类型的项目,并应用DirectX 3D技术来实现。 总的来说,DirectX 3D图形与动画程序设计是一门非常实用和有趣的课程。通过学习这门课程,学生将获得开发3D图形和动画程序所需的基本技能,并能够应用这些技能创造出令人印象深刻的效果。这门课程对于计算机图形学和动画技术领域的学生来说,是非常有价值的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咆哮的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值