相关文章推荐
斯文的佛珠  ·  Alternate to ...·  11 月前    · 
憨厚的烈马  ·  Web 框架 mojo.js 1.0 ...·  1 年前    · 

对于那些简单的图形(四边形,立方体),可以通过代码指定顶点数据的形式绘制出来。但是对于复杂的 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来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

我自己的D3DX开发经验,计划做一个3D游戏地图编辑器。目前功能要求 :读取资源文件MeshLPDIRECT3D9             g_pD3D           = NULL; // Used to create the D3DDeviceLPDIRECT3DDEVICE9       g_pd3dDevice     = NULL; // Our rendering d 基于DirectX游戏开发中,人物和模型由针对每个对象的成千上万个多边形组成的,对纹理映射而言这是非常复杂的,假设不加索引的几何图形,而只是用三角形(DirectX模型基础是三角形)。那将会是一场恐怖的噩梦。所以现在 Direcx一般都是用VC++开发环境,而c#相关的资料比较少,最近做的一个项目中需要用到一个3D模型,而开发环境是visual c#,没办法,就四处搜集资料,查找相关的资料,然后找到了几篇相关的博文,不过这些博文说的都不太仔细,有些内容又丢失了一些关键代码,这个时候就需要我们自己搜集资料,自己学习来补全这些关键代码了。 废话少说,我们直接说说步骤吧。 一.首先要准备一个3D模型,现在3D模型  要想从3DMAX中导出设计好的模型,通常需要自己用3DS Max SDK来创建自己的导出器, 而微软提供了一种导出格式.X,这种格式主要用于DirectX9, Directx10及以后都不支持这种格式。 第一步: 导出模型首先我们需要将模型3DMAX中导出来,这里推荐Pandasoft  - 将下载的插件放到3DS plugins目录下。这样你就可以在3DMAX中将模型保存成X类型 今天来聊一下程序的孤独感,孤独是每一个走向成功的程序员必将经历的过程,也是磨练的必修课。这是跟程序员这个职业,紧密相关的,这个职业注定了,我们大多时候都是和极强打交道多于人。我们善于思考,大部分的时候,就是在看资料,规格书,想问题,所以跟人交流就少了很多。我觉得,大部分的优秀程序员都是孤独的,我们应该享受这份特有的孤独。我想这也就是大多程序员喜欢开夜车的原因。因为寂静和孤独,多了一份宁静,可以,让 下面这图是我在3ds max中按照 tutorial 中创建的,多了木板纹理,3Ds max中给物体加纹理可以参考3Ds max tutorial中的Materi 前言:在这里,我们将继续研究 D3DX 库提供的与网格相关的接口,结构和函数,以 网格(一) 为基础,我们现在将研究一些更有趣的技术,例如从 X文件 中加载一个复杂的 3D 模型并将该模型绘制出来,或者利用渐进网格接口来控制网格的细节层次。 学习目标: 了解如何加载 X 文件到 ID3DXMesh 中 理解使用渐进网格的优点以及如何使用渐进网格接口 ID3DXPMesh 了解使用外接体的作用以及... 日常工作中经常要拿到3dmax bip骨去UE里和小白人骨架校对,因此学习骨架绑定可以自己绑定一些测试骨架便于调试。第六步.添加skin修改器,点击add按钮添加骨骼,添加骨骼时注意要选中所有骨骼。第四步.勾选这几项,即可对骨架位置进行修改,此时需要手动移动、旋转骨架匹配至模型。第三步.修改操控模式到local,这样才能缩放骨架。第五步.将模型转换为可编辑多边形。这样即完成骨骼绑定。 模型主要是物理组成,数据传输和固定图形管道模型;概念主要包括IDirect3D, Adapter, Device, swap chain,surface后台缓存、前台缓存、深度和模板缓存,资源,资源类型_D3DRESOURCETYPE,格式_D3DFORMAT,资源存放的内存区域Pool,资源的使用标识Usage。 0.图形模型 1)    显卡物理组成和数据传输: 基本物理组成模型