热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

第17章他山之石可攻玉——三维游戏模型的载入

17.1网格模型技术的前生今世网格模型是一种将物体模型的顶点数据、纹理、材质等信息存储在一个外部文件中的3D物体模型。对于那些简单的图元描述的图形,比如点、线、三角形等等,我们可以

17.1 网格模型技术的前生今世

网格模型是一种将物体模型的顶点数据、纹理、材质等信息存储在一个外部文件中的3D 物体模型。对于那些简单的图元描述的图形,比如点、线、三角形等等,我们可以通过写代码指定顶点数据、索引数据、法线向量、纹理和材质等信息。但对于复杂的3D 物体的话, 采用这种方式显然是不现实的。因此,Direct3D 提供了一种称作网格模型的技术,可以从各种特定的文件格式中读取和绘制3D 图形,极大地方便了游戏的开发。

使用网格模型最普遍的方式是从外部的3D 模型文件中加载一个网格。而这些3D 模型通常都是由3D 建模软件生成的,比较复杂的网格数据。目前市面上主流的3D 建模软件有3DS Max 和Maya。而目前流行的3D 模型文件格式有.3ds 、.max 、.obj 以及.mb 。其中.3ds 、. max 为3DS Max常用的格式, .mb 为Maya 常用的格式, 而.obj 为3DSMax 和Maya 通用的文件格式。

17.2 认识三维建模软件3DS Max 和Maya

再不厌其烦地说一遍,我们在通常的三维游戏开发中, 常常要涉及到非常复杂的三维物体数据模型,如果用我们之前讲的知识,顶点缓存索引缓存,通过写代码来构造这些三维模型, 显然是不合实际的。
这些复杂物体的模型通常需要利用专业的三维建模软件来制作。目前市面上主流的3D 建模软件有3DS Max 和Maya。这两款在三维建模行业作为竞争对手的软件,目前同为Autodesk 公司所有, 这倒是有些令人匪夷所思。下面我们先来分别介绍一下当前市场上主流的三维建模软件(当然,不仅仅是用于三维建模这么简单) 3DS Max 和Maya。

1. 3DS Max 软件简介
"D"\Program Files\Autodesk\3ds Max 2012\plugins" 目录中添加dle 插件文件;
然后, 重启一下3DS Max 2012 ,我们就可以发现, 导出选项中有了X 文件导出项了。
第三个要素, 也就是导出X 文件的具体步骤了。
首先, 正所谓巧妇难为无米之炊,我们需要有一个导出的对象,也就是一个三维的模型。模型嘛, 可以自己用3DS Max现场做,也可以自己去网上下载,这里推荐一个论坛, 有大量的3D 模型资源:http://www.cgmodel.com/
最后经过3DS Max 的处理,我们就发现在我们设定的保存路径下多了X 文件以及配套的纹理了。

如果我们想要在我们写的游戏程序中使用这个三维模型,把这两个文件一起放到我们工程中相应的地方就可以了。
另外提一点, Maya 中可以用cvXporter 插件来导出X 文件,对应于3DS Max 中的Panda 插件。
之前讲了那么多元非是在为X 文件的诞生造势,下面就开始隆重介绍如何在Direct3D 程序中载入X 文件的具体知识吧。
想利用X 文件来在游戏程序中载入三维模型的话,首先就需要将X 文件中的各种数据分别加载到内存中,而这些数据主要包括顶点数据、材质数据和纹理数据等等。首先,我们需要介绍一下与网格模型相关的一个重要的接口——ID3DXMESH 。

17.5 网格模型接口ID3DXMESH

在Direct3D 中,微软为我们提供了ID3DXMesh 接口表示网格,这个接口继承自ID3DXBaseMesh 接口。网格模型接口ID3DXMesh 实际上是三维物体的顶点缓存的集合,他将为我们创建顶点缓存、定义灵活顶点格式和绘制顶点缓冲区等功能封装在一个COM 对象中,这样复杂三维物体的绘制就显得非常简便了。
其中, ID3DXMESH 接口中的D3DXCreateMesh()可用于创建一个Direct3D 网格模型对象,我们可以在MSDN 中查到该函数声明是这样的:

 HRESULT  D3DXCreateMesh(
  __in   DWORD NumFaces,
  __in   DWORD NumVertices,
  __in   DWORD Options,
  __in   const LPD3DVERTEXELEMENT9 *pDeclaration,
  __in   LPDIRECT3DDEVICE9 pD3DDevice,
  __out  LPD3DXMESH *ppMesh
);

这个函数的参数说明如下。
  •  第一个参数, DWORD 类型的NumFaces ,表示创建网格模型的多边形数目。
  •  第二个参数, DWORD 类型的NumVertices ,表示创建网格的顶点数目。
  •  第三个参数, DWORD 类型的Options ,表示创建网格时的附加选项,他的取值为D3DXMESH枚举体中的一个或者多个值。这个枚举体定义如下:
typedef enum D3DXMESH {
  D3DXMESH_32BIT                   = 0x001,
  D3DXMESH_DOnOTCLIP= 0x002,
  D3DXMESH_POINTS                  = 0x004,
  D3DXMESH_RTPATCHES               = 0x008,
  D3DXMESH_NPATCHES                = 0x4000,
  D3DXMESH_VB_SYSTEMMEM            = 0x010,
  D3DXMESH_VB_MANAGED              = 0x020,
  D3DXMESH_VB_WRITEOnLY= 0x040,
  D3DXMESH_VB_DYNAMIC              = 0x080,
  D3DXMESH_VB_SOFTWAREPROCESSING   = 0x8000,
  D3DXMESH_IB_SYSTEMMEM            = 0x100,
  D3DXMESH_IB_MANAGED              = 0x200,
  D3DXMESH_IB_WRITEOnLY= 0x400,
  D3DXMESH_IB_DYNAMIC              = 0x800,
  D3DXMESH_IB_SOFTWAREPROCESSING   = 0x10000,
  D3DXMESH_VB_SHARE                = 0x1000,
  D3DXMESH_USEHWOnLY= 0x2000,
  D3DXMESH_SYSTEMMEM               = 0x110,
  D3DXMESH_MANAGED                 = 0x220,
  D3DXMESH_WRITEOnLY= 0x440,
  D3DXMESH_DYNAMIC                 = 0x880,
  D3DXMESH_SOFTWAREPROCESSING      = 0x18000 
} D3DXMESH, *LPD3DXMESH;

一般情况下,我们都把这个Options 取为D3DXMESH_ SYSTEMMEM 或者D3DXMESH_MANAGED , 表示对Direct3D 顶点缓冲区和索引缓冲区使用D3DPOOL_SYSTEMMEM 或者D3DPOOL_MANAGED 内存。
  •  第四个参数, const LPD3DVERTEXELEMENT9 类型的*pDeclaration , 表示顶点包含哪些信息。这个参数的作用类似于我们之前一直在用的灵活顶点格式(FVF ),表示顶点包含了哪些具体数据,但是它却高于灵活顶点格式。它的类型LPD3DVERTEXELEMENT9 表示顶点元素, 主要用于我们来没讲到的可编程渲染流水线之中,在此我们暂且不用去多做考虑。
  • 第五个参数, LPDIRECT3DDEVICE9 类型的pD3DDevice,就是我们的金钥匙, Direct3D设备的指针了。
  •  第六个参数,LPD3DXMESH 类型的*ppMesh,指向我们创建好的网格模型对象指针的地址,用于返回创建好的网格模型对象。可以说我们调用D3DXCreateMesh 就是为了创建并得到这个指针地址。后面关于我们创建好的网格模型的访问,都靠这个ppMesh 参数了。
需要注意的是,这个创建好网格模型对象的D3DXCreateMesh 函数我们通常很少直接去用它,而且直接用它往往也意义不大。它总是“ 真人不露相”,被封装在了其他Direct3D 函数之中,默默地为我们服务。
介绍完网格模型接口ID3DXMESH 相关的知识,下面就来看看从X 文件载入模型的具体步骤。

17.6 文件模型载入三步曲

17.6.1 三步曲之一:通过X文件加载网格模型

上面讲到了D3DXCreateMesh 函数很少去直接应用,因为我们常常都是通过载入X 文件来生成网格模型的,用到的函数是D3DXLoadMeshFromX 。我们可以在MSDN 中查到这个函数的声明如下:
 HRESULT  D3DXLoadMeshFromX(
  __in   LPCTSTR pFilename,
  __in   DWORD Options,
  __in   LPDIRECT3DDEVICE9 pD3DDevice,
  __out  LPD3DXBUFFER *ppAdjacency,
  __out  LPD3DXBUFFER *ppMaterials,
  __out  LPD3DXBUFFER *ppEffectInstances,
  __out  DWORD *pNumMaterials,
  __out  LPD3DXMESH *ppMesh
);
  • 第一个参数, LPCTSTR 类型的pFilename,显然就是一个指向我们需要加载的X 文件的磁盘路径和文件名的字符串了。
  • 第二个参数, DWORD 类型的Options ,表示创建网格时的附加选项,他的取值为D3DXMESH 枚举体中的一个或者多个值。这个参数我们刚才在讲D3DXCreateMesh 时已经讲过了,具体参看D3DXCreateMesh的第三个参数,在这里就不赘述了。
  • 第三个参数,LPDIRECT3DDEVICE9 类型的pD3DDevice,也就是我们的金钥匙, Direct3D设备的指针。
  • 第四个参数, LPD3DXBUFFER 类型的*ppAdjacency ,用于保存加载网格的邻接信息,也就是包含每个多边形周围的多边形信息的缓冲区的内存地址。
  • 第五个参数, LPD3DXBUFFER 类型的*ppMaterials ,用于保存网格的所有子集的材质,指向用于存储模型材质和纹理文件名的缓冲区的地址,而材质的数目存在之后第七个参数pNum扣laterials 中了。
  • 第六个参数,LPD3DXBUFFER 类型的*ppEffectlnstances ,用于存储网格模型的特殊效果,指向用于存储模型效果实例的缓冲区的内存地址,这个参数通常设为NULL 就可以了。
  • 第七个参数,DWORD 类型的*pNumMaterials,它配合着第五个参数,用于存储所有子集材质的数目。
  • 第八个参数,LPD3DXMESH 类型的*ppMesh ,指向我们从文件生成的Direct3D 网格模型指针的地址。可以说我们调用D3DXLoadM eshFrornX 就是为了从文件加载X 文件的模型信息并进行模型的创建,从而能得到这个指向创建好的模型的指针地址。后面关于我们创建好的网格模型的访问,都靠这个ppMesh 参数了。
可以发现,在上面我们的D3DXLoadMeshFromX 函数中引入了一个新的Direct3D 类型, 它就是LPD3DXBUFFER。LPD3DXBUFFER 因数据操作的方便性而诞生,我们称它为泛型数据结构。它的好处是可以存储顶点位置坐标、材质、纹理等多种类型的Direct3D 数据,而不必对每种数据都去声明一种函数接口类型。可使用接口函数ID3DXBuffer::GetBufferPointer()获取缓冲区中的数据, 使用ID3DXBuffer::GetBufferSize()获得缓冲区数据大小,这两个接口函数的声明如下:
LPVOID GetBufferPointer();
SIZE_T GetBufferSize();
没错,这就是原型声明,因为这两个函数都没有参数,所以它们的身子显得非常单薄。
比如,我们要从网格模型中提取材质属性和纹理文件名,那么代码就是像这样写:
// 读取材质和纹理数据
D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); 

17.6.2 三步曲之二:载入材质和纹理

如果之前的D3DXLoadMeshFromX 函数调用成功的话,那么参数ppMaterials 就会获得.x 文件中三维模型的材质和纹理等信息,而pNumMaterials 参数就会获得材质的数目。
X文件中的材质信息是以D3DXMATERIAL 结构类型的数组形式储存的。其中,该结构定义了D3DMATERIAL9 结构类型的成员和一个指向以NULL 结尾的字符串指针, 而该字符串用于指定与网格子集相关的纹理贴图文件名。
我们可以在MSDN 中查到D3DXMATERIAL 结构体的定义如下:
typedef struct D3DXMATERIAL {
  D3DMATERIAL9 MatD3D;
  LPSTR        pTextureFilename;
} D3DXMATERIAL, *LPD3DXMATERIAL;
当我们加载X 文件后,需要遍历整个D3DXMATERIAL 结构类型的数组,用于取出保存在ID3DXBuffer 接口对象中的材质信息。由于X 文件中并未存储具体的纹理数据, 它只包含纹理贴图的文件名,因此需要我们自己根据该文件名创建相应的纹理对象。
就像这样:
	// 从X文件中加载网格数据
	LPD3DXBUFFER pAdjBuffer  = NULL;
	LPD3DXBUFFER pMtrlBuffer = NULL;

	D3DXLoadMeshFromX(L"miki.X", D3DXMESH_MANAGED, g_pd3dDevice, 
		&pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh);

	// 读取材质和纹理数据
	D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息
	g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls];
	g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls];

	for (DWORD i=0; i 
  

17.6.3 三步曲之三:绘制网格模型

完成前两步做好准备工作之后,也就是生成X文件网格以及材质和纹理的读取之后,接下来就是把我们准备的内容绘制出来就行了。我们依然是用ID3DXMesh 接口的DrawSubset 方法绘制网格中的每个子集的。但是由于绘制的部分比较多,对每个部分的绘制,我们都需要专门为其进行材质和纹理的设置,然后才进行绘制,所以一般我们在绘制从X 文件读取的三维模型的时候, 一般用一个for 循环来进行绘制,就像这样:
	g_pd3dDevice->BeginScene();                     // 开始绘制
	
	// 用一个for循环,进行网格各个部分的绘制
	for (DWORD i = 0; i SetMaterial(&g_pMaterials[i]);
		g_pd3dDevice->SetTexture(0, g_pTextures[i]);
		g_pMesh->DrawSubset(i);
	}
	g_pd3dDevice->EndScene();                       // 结束绘制

一些关于使用模型的小技巧:
 用三维建模工具制作的三维模型通常比较复杂,多边形数量很多。而多边形数量越多,图形的渲染速度就越慢,所以在制作模型时,在不明显影响视觉效果的情况下,应尽量减少多边形的数量。
 三维模型自身的尺寸在模型制作时就确定好的, 不同的模型的尺寸可能是千差万别,因此在渲染网格模型时要针对模型进行适当的缩放。当然,最好是在制作或者导出模型时,就在建模软件中进行适当的缩放调整。
 渲染网格模型时,通常需要进行光照处理和纹理映射,所以需要对光照和纹理进行相关的设置。对于纹理,如果我们忘了进行相关的设直,Direct3D 也会自动使用默认设置,而Direct3D 中是没有设置默认光源的,所以如果忘记设置光源的话,通常会造成模型的显示不正确。所以大家记得养成好的习惯,在渲染前设直光源,需要长点心了。
 如果程序在读取X 文件时内存报错,看看是否把所有的纹理素材都放到X 文件所在的路径下了, 如果不行。把纹理全部取NULL,即g_pTextures[i]=NULL,然后注释掉接下来的
D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]);再试试。
 模型的旋转是绕自身坐标轴的旋转,而不是绕世界坐标系的三个坐标轴进行旋转,而且在旋转模型的时候,它的自身坐标轴也在不断改变,大家需要注意.
 在从网格模型数据中提取纹理文件名为网格模型创建纹理对象的时候,需要注意的是,提取的文件名可能是纹理文件的绝对路径。这个绝对路径常常是在利用三维建模软件时指定的路径,这个路径与当前纹理文件的路径一般是不一样的。因此在创建纹理的时候,常常就因为找不到纹理文件而报内存溢出类的错误.这个时候,我们就应该对提取到的纹理文件的绝对路径进行处理,即删除路径部分,只保留纹理文件名。这样在创建纹理对象时就可以从当前的路径搜索纹理文件。示例代码如下:
// Desc : 从绝对路径中提取纹理文件名
//------------------------------------------------------------------------
 voi d RemovePathFromFileName(LPSTR fullPath , LPWSTR fileName )
 {
   //先将full Path 的类型变换为LPWSTR
   WCHAR wszBuf [MAX_PATH] ;
   MultiByteToWideChar ( CP_ACP, 0, fullPath , -1, wszBuf, MAX_PATH ) ;
   wszBuf[MAX PATH - 1] = L'\0';

   WCHAR* wszFullPath = wszBuf ;

   //从绝对路径中提取文件名
   LPWSTR pch=wc srchr (wszFullPath, ’\\’ );
   if (pch)
       lstrcpy (fileName, ++pch) ;
   else
       lstrcpy (fileName, wszFullPath) ;
 }

17.6.4 总结与升华

总结一下,从X 文件读取模型并进行绘制其实很简单,就三步工作,简明扼要12个字的三步曲:加载网格,加载材质纹理, 绘制。
我们依然可以利用如下的核心代码米加强理解和记忆:

       //三部曲之一:从X文件中加载网格数据
	LPD3DXBUFFER pAdjBuffer  = NULL;
	LPD3DXBUFFER pMtrlBuffer = NULL;

	D3DXLoadMeshFromX(L"miki.X", D3DXMESH_MANAGED, g_pd3dDevice, 
		&pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh);

	//三部曲之二: 读取材质和纹理数据
	D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息
	g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls];
	g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls];

	for (DWORD i=0; i 
  

另外, 在载入模型三步曲之二中,这句g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse 是用于将材质对漫反射光的反应程度赋值给材质的环境光反应程度。这句加上和不加上渲染出来的模型会有不同的环境光效果,大家可以自己把这句注释起来重新编译运行一下。

void Direct3D_Render(HWND hwnd)
{
	g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(150, 150, 100), 1.0f, 0);
	// 三部曲之三:绘制
	g_pd3dDevice->BeginScene();                     // 开始绘制
	
	// 用一个for循环,进行网格各个部分的绘制
	for (DWORD i = 0; i SetMaterial(&g_pMaterials[i]);
		g_pd3dDevice->SetTexture(0, g_pTextures[i]);
		g_pMesh->DrawSubset(i);
	}
	g_pd3dDevice->EndScene();                       // 结束绘制
	g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示
	 
}

17.7 示例程序D3demo12

这个程序的核心代码,其实重点部分在前面的X 文件模型载入三步曲核心代码中已经贴出过,这里只需要了解它们是放在哪里的就可以了。
运行这个程序,我们便会得到如下的效果, 一个高质量的初音模型,非常精致:

17.8 章节小憩

学完精彩绝伦的这章,我们从对3D 建模一无所知到了解了3DS Max 和Maya 这两大三维建模软件的威力,并可以熟练地从3DS Max 中导出帅气的人物模型成X 文件,供我们写的程序所用,其可谓是一趟奇幻的旅程。









推荐阅读
  • 利用Git GUI将本地项目同步至GitHub的方法
    GitHub作为开发者不可或缺的工具,不仅提供了丰富的开源项目资源,还极大地便利了个人项目的管理和版本控制。本文将详细介绍如何使用Git GUI工具将本地开发的项目上传至GitHub。 ... [详细]
  • 解决远程桌面连接时的身份验证错误问题
    本文介绍了如何解决在尝试远程访问服务器时遇到的身份验证错误,特别是当系统提示‘要求的函数不受支持’时的具体解决步骤。通过调整Windows注册表设置,您可以轻松解决这一常见问题。 ... [详细]
  • 解决MATLAB中文件 'mischouse.tiff' 不存在的问题
    探讨如何解决在MATLAB中尝试访问文件 'mischouse.tiff' 时出现的文件不存在错误。 ... [详细]
  • Windows VC++ 运行时库的默认安装位置
    本文将详细介绍Windows系统中VC++运行时库的默认安装位置,以及如何通过简单步骤找到这些关键文件。适合需要了解或操作该库的用户阅读。 ... [详细]
  • iOS 小组件开发指南
    本文详细介绍了iOS小部件(Widget)的开发流程,从环境搭建、证书配置到业务逻辑实现,提供了一系列实用的技术指导与代码示例。 ... [详细]
  • 雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04
    雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04 ... [详细]
  • 本文详细介绍了在PHP中如何获取和处理HTTP头部信息,包括通过cURL获取请求头信息、使用header函数发送响应头以及获取客户端HTTP头部的方法。同时,还探讨了PHP中$_SERVER变量的使用,以获取客户端和服务器的相关信息。 ... [详细]
  • 本文概述了在GNU/Linux系统中,动态库在链接和运行阶段的搜索路径及其指定方法,包括通过编译时参数、环境变量及系统配置文件等方式来控制动态库的查找路径。 ... [详细]
  • 本文详细介绍了如何在PHP中使用Memcached进行数据缓存,包括服务器连接、数据操作、高级功能等。 ... [详细]
  • Redis: 高效的键值存储系统
    Redis是一款遵循BSD许可的开源高性能键值存储系统,它不仅支持多种数据类型的存储,还提供了数据持久化和复制等功能,显著区别于其他键值缓存解决方案。 ... [详细]
  • 本文详细介绍了如何调整打印机设置,以实现仅打印A4纸一半区域的需求,包括具体的步骤和注意事项。 ... [详细]
  • 探索OpenWrt中的LuCI框架
    本文深入探讨了OpenWrt系统中轻量级HTTP服务器uhttpd的工作原理及其配置,重点介绍了LuCI界面的实现机制。 ... [详细]
  • 本文总结了在多人协作开发环境中使用 Git 时常见的问题及其解决方案,包括错误合并分支的处理、使用 SourceTree 查找问题提交、Git 自动生成的提交信息解释、删除远程仓库文件夹而不删除本地文件的方法、合并冲突时的注意事项以及如何将多个提交合并为一个。 ... [详细]
  • 本文详细介绍了PHP中的几种超全局变量,包括$GLOBAL、$_SERVER、$_POST、$_GET等,并探讨了AJAX的工作原理及其优缺点。通过具体示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统上安装中文宋体字体的方法,包括操作系统的环境配置、字体管理工具的安装、字体文件的传输与缓存重建等步骤。 ... [详细]
author-avatar
高人arm
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有