本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
文章链接: http://blog.csdn.net/zhmxy555/article/details/8223965
作者:毛星云(浅墨) 邮箱: [email protected]
这篇文章里,我们将迈出精通Direct3D的坚实步伐,先透彻理解Direct3D中动画显示方面的交换链技术的原理,然后看看GDI与Direct3D编程习惯的一些思想上的关联,接着简单学习Direct3D中二维文本的绘制方法,然后按照“五步曲”的思路,系统地学习Direct3D绘制的套路,且文章最后进行了相关源代码的赏析以及源代码的下载。看完这篇文章,也许你会豁然开朗,哦,原来Direct3D交换链技术可以这样来理解,原来Direct3D和GDI的编程套路其实差不多,原来Direct3D的绘制渲染过程是这样的按部就班,容易掌握。 需要说明的是,渲染过程暂时不考虑光照,矩阵变换等等,因为这是后面专门来展开细讲的内容。
由于文章比较长,浅墨依然在这里给大家配一个目录。这篇文章大体分为以下六个部分:
一、深入理解Direct3D动画显示技术――交换链:六道轮回
二、Direct3D中的“绘制金钥匙”――Direct3D设备接口
三、Direct3D中二维文本的绘制
四、起承转合的艺术――Direct3D渲染五步曲
Ⅰ.Direct3D渲染五步曲概述
Ⅱ. Direct3D渲染五步曲之一:清屏操作
Ⅲ.Direct3D渲染五步曲之二:开始绘制
Ⅳ. Direct3D渲染五步曲之三:正式绘制
Ⅴ. Direct3D渲染五步曲之四:结束绘制
Ⅵ. Direct3D渲染五步曲之五:翻转显示
Ⅶ.Direct3D渲染五步曲代码整体赏析
五、获取每秒帧数(FPS)函数的写法
六、详细注释的源代码欣赏
一、深入理解Direct3D动画显示技术――交换链:六道轮回
Direct3D的工作模式与电影的播放原理类似。当播放电影图像时,影片图像以每秒24帧的速度连续地闪动,由于这些图像之间的差别很小,以及人眼的滞留作用,所以实际看到的就好像是连续的动作画面。
Direct3D中,使用了一种称作交换链(Pape Flipping)的技术,来让画面能够平滑的过渡。交换链由两个或者两个以上的表面组成,而每个表面都是存储着2D图形的一个线性数组,其中每个元素都表示着屏幕上的一个像素。
刚刚我们讲到的只是2D图形,而对于三维物体呢,我们还需要一个称作深度的信息,Direct3D则使用深度缓冲区为最终绘制的图像的每个像素都存储一个深度信息,深度缓冲区只单单包含了特定像素的深度信息而不含图像数据的表面信息(表面信息上面讲过,由表面信息来存储,就是一个存储着2D图形的线性数组)。
前台缓冲区和后台缓冲区是位于系统内存或显存里的内存块,对应于将要显示的二维显示区域。前台缓冲区是显示在显示屏上的,我们可以看到的内容。而后台缓冲区则主要用于图形绘制的准备工作,属于我们熟知的“幕后”(想要上演一出完美而杰出的表演,幕后准备工作肯定是要准备充分的)。这样我们的图像在经过在后台缓冲区中的打理后,变得光鲜和毫无瑕疵,在后台缓冲区打理完成后,也就是后台缓冲区中的内容准备好之后,就可以和前台缓冲区进行一个交换操作,这就是我们所说的交换链页面翻转。通过前台缓冲区和后台缓冲区的配合,运用交换链技术,就可以流畅而高效地绘制出漂亮无瑕的动画图像来。
下面我们通过一幅图来具体看看这神奇的交换链翻转操作到底是怎样完成的:
对这幅多后台缓冲区编码翻转演示图,我们可以这样理解:
在Direct3D中,通常是通过在一系列后台缓冲区中生成动画帧(也就是一幅图像),然后再将他们通过交换链技术,逐个提交到前台来显示,实现华丽的动画效果。其中,这一系列的后台缓冲区被组织成交换链。所以我们可以这样说,交换就链是按顺序逐个提交到前台来显示的多个后台缓冲区的集合。
在Direct3D中创建的每一个渲染设备至少要有一个交换链,在我们的Direct3D初始化四步曲中的第三步里面,我们填充了D3DPRESENT_PARAMETERS结构体,其中我们设置的BackBufferCount 成员会告诉Direct3D我们创建的Direct3D设备对象的交换链中,有多少个后台缓冲区,图中我们就有两个后台缓冲区,就是这段代码:
D3DPRESENT_PARAMETERS d3dpp; d3dpp.BackBufferCount = 2;
需要注意的是,上面这两句只是告诉了Direct3D相关的信息,没有真正的去创建,而交换链正式的创建,是在四步曲的第四步里面,调用IDirect3D9::CreateDevice()方法的时候。IDirect3D9::CreateDevice()方法完成了Direct3D设备对象和相应交换链的创建。
创建完成后,我们自然需要“驱动”交换链进行翻转操作,而交换链的翻转操作就是在我们今天主要讲解的Direct3D渲染五步曲里的最后一步“翻转显示”之中,也就是调用IDirect3DDevice9::Present()函数,进行页面的翻转和显示。这个函数在接下来会讲解具体用法,在这里我们知道它是进行页面翻转操作的就够了。正如图中所表示的,第一行中是原始的未进行翻转操作的交换链状态,前台缓冲区,第一后台缓冲区,第二后台缓冲区的顺序为ABC,我们调用一次IDirect3DDevice9::Present()函数之后,页面就发生了翻转,这样本来在前台缓冲区中的A就被B挤到了队伍最末尾,B代替了A原来的位置。C也顺利晋级,从第二缓冲区的位置晋升到第一缓冲区,也就是C到了B原来的位置,而我们苦逼的A,只能感叹美好的时光总是短暂的,因为它从最风光的前台缓冲区,一下子跌落到万丈深渊,到了地位最卑微的第二缓冲区。真可谓是辛辛苦苦几十年,一夜回到解放前啊。不过正所谓美好的时光总是短暂的,每一次IDirect3DDevice9::Present()函数的调用,都在进行着转盘式的轮回,正所谓风水轮流转,领导轮流做。经过三次的翻转,ABC这三位先生已经经过了一个轮回,历史总是惊人的相似,这三位先生又回到了第一次相遇的“那天”,也就是未经过翻转之前的状态。
经过上面的讲解,我们可以总结一下,交换链其实就是在进行一个风水轮流转的过程,按部就班,万年不变地进行着一次又一次的轮回。他们的轮回生命由IDirect3D9::CreateDevice()方法来赋予。演员个数由D3DPRESENT_PARAMETERS结构体中的BackBufferCount成员指定,而驱动这个轮回的,就是我们的IDirect3DDevice9::Present()函数。
其中Present函数位于我们接下来要讲解的渲染实现函数Direct3D_Render()中, 这个函数我们在消息循环进行了调用,这样就会驱动着我们的程序进行着每秒成千上万次的绘制操作,也就每秒钟进行着成千上万次的渲染五步曲挨着走,也就进行着每秒成千上万次的IDirect3DDevice9::Present()函数的调用(当然也同时进行着成千上万次的缓冲区内容的绘制工作),驱动着交换链进行着成千上万次的页面翻转操作,这样就完成了高质量的动画绘制。
最后我们需要知道的是,我们在调用IDirect3DDevice9::Present()函数函数,请求并进行页面翻转时,是指向前台缓冲区和后台缓冲区表面内存的指针在进行着调换操作。也就是说,页面翻转是经过交换指向表面内存的指针来实现的,而不是通过复制表面的内容实现的。我们都知道,指针这个东西,使用起来方便,环保,资源占用小。
交换链技术利用这样的指针交换操作,实现了高效而流畅的动画绘制。
二、Direct3D中的“绘制金钥匙”――Direct3D设备接口
其实,使用Direct3D绘制3D图形和我们之前使用的GDI绘制2D图形的方法是异曲同工的,毕竟都是微软那些强人们写出来的,风格或多或少会有一定的相似之处。
看过浅墨之前游戏开发笔记里GDI游戏编程讲解的朋友们应该都知道,在GDI编程中,有一把金钥匙叫hdc,也就是传说中的设备描述表的句柄。我们要在屏幕上采用GDI绘制图形,都是在与hdc这个家伙打交道。我们进行各种透明处理,搞什么双缓冲甚至多缓冲的最终目的,都是想要把流畅的动画绘制到hdc之中。
如果看过浅墨之前游戏开发笔记里GDI游戏编程讲解,大家肯定对这个句子非常的熟悉:
hdc = GetDC(hwnd);
就是这个语句让我们用CreateWindow创建出来的窗口句柄hwnd与hdc有了剪不断理还乱的联系。就是它,成就了hdc这把GDI中绘制的金钥匙。
我们再来看看Direct3D这边。
在上一讲,Direct3D初始化四步曲中我们讲到了初始化Direct3D的第三步是填内容,也就是填充D3DPRESENT_PARAMETERS结构体。
D3DPRESENT_PARAMETERS结构体的第八个参数,HWND类型的hDeviceWindow,很显然,就是我们熟知的窗口句柄,这里指定我们需要在哪个窗口上进行绘制,我们通常都填hwnd。也就是这样写:
D3DPRESENT_PARAMETERS d3dpp; d3dpp.hDeviceWindow = hwnd;然后在初始化Direct3D的第四步是创设备填内容中,我们这样写
pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice)))CreateDevice函数的倒数第二个参数中我们指定的就是D3DPRESENT_PARAMETERS的实例d3dpp。
然后最后一个参数,IDirect3DDevice9类型的**ppReturnedDeviceInterface就是我们指向Direct3D设备接口的句柄。
这样我们就间接地把hwnd和Direct3D设备联系起来了,这就是通过CreateDevice 方法创建出来的Direct3D设备能趾高气扬地作为Direct3D中“绘制金钥匙”的资本了。
举个具体例子,在浅墨为这篇文章写的配套demo之中,g_pd3dDevice就是我们的绘制金钥匙,后面的绘制操作就是通过它来完成的,比如拿起g_pd3dDevice指一下Present,也就是渲染五步曲的最终步骤了:
g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示这个函数具体后面有作为重点讲到,这里只是提一下。
所以我们要Direct3D中进行绘制,拿起Direct3D中的金钥匙――“Direct3D设备”一番使用就好了。
三、Direct3D中二维文本的绘制
因为我们的Direct3D知识才开讲不久,目前浅墨觉得将文字显示相关内容全部拿出来讲比较的不现实,所以这一小节里我们先暂时讲解一下本篇文章配套程序中需要用到的地方,便于大家对本节配套源程序的理解。
在Direct3D中,ID3DXFont接口负责着Direct3D应用程序中创建字体以及实现二维文本的绘制,该接口封装了Windows字体和Direct3D设备指针。其实,ID3DXFont内部实际上还是借用的GDI实现的文本的绘制。
首先我们来看一下将要使用到的Direct3D中用于创建字体的一个函数,他很憨厚老实,人如其名,就叫D3DXCreateFont。我们可以在DirectX SDK中查到这个憨厚老实的D3DXCreateFont函数的声明是这样的:
HRESULT D3DXCreateFont( __in LPDIRECT3DDEVICE9 pDevice, __in INT Height, __in UINT Width, __in UINT Weight, __in UINT MipLevels, __in BOOL Italic, __in DWORD CharSet, __in DWORD OutputPrecision, __in DWORD Quality, __in DWORD PitchAndFamily, __in LPCTSTR pFacename, __out LPD3DXFONT *ppFont );
然后是每个变量的依次介绍:
◆ 第一个参数,LPDIRECT3DDEVICE9类型的pDevice,也就是我们的Direct3D绘制金钥匙――Direct3D设备的指针
◆ 第二个参数,INT类型的Height,很显然,表示字体的高度。
◆ 第三个参数,UINT类型的Width,依然很显然,表示字体的宽度。
◆ 第四个参数,UINT类型的Weight,还是很显然,表示字体的权重值。
◆ 第五个参数,UINT类型的MipLevels,字体的过滤属性。
◆ 第六个参数,BOOL类型的Italic,表示是否为斜体,TRUE表示是斜体,FLASE表示不是斜体。
◆ 第七个参数,DWORD类型的CharSet,表示字体所使用的字符集,通常我们设为默认值DEFAULT_CHARSET,表示使用默认字符集。
◆ 第八个参数,DWORD类型的OutputPrecision,表示输出文本的精度,通常设为默认值OUT_DEFAULT_PRECIS。
◆ 第九个参数,DWORD类型的Quality,表示指定字符的输出质量,通常也设为DEFAULT_QUALITY。
◆ 第十个参数,DWORD类型的PitchAndFamily,用于指定字体的索引号,通常都设为0.
◆ 第十一个参数,LPCTSTR类型的pFacename,指定我们想要创建的字体名称,比如“微软雅黑”“浪漫雅圆”等等。
◆ 第十二个参数,LPD3DXFONT类型的*ppFont,用于存储我们新储存的字体指针。它也是一把钥匙,我们要进行字体绘制相关的操作,全都靠它了。
设置起来是非常简单的,也就是填空题,按部就班,一个一个填,而且有章可循,非常好写。
完成了字体的创建,下面就是要调用绘制文本的函数了。也就是ID3DXFont::DrawText的调用,下面看一下这个函数的原型:
INT DrawText( [in] LPD3DXSPRITE pSprite, [in] LPCTSTR pString, [in] INT Count, [in] LPRECT pRect, [in] DWORD Format, [in] D3DCOLOR Color );
这个函数的参数我们也介绍一下:
◆ 第一个参数,LPD3DXSPRITE类型的pSprite,指定字符串所属的ID3DXSprite对象接口,我们可以把它设为0,表示在当前窗口绘制字符串。
◆ 第二个参数,LPCTSTR类型的pString,指定我们将要绘制的字符串内容。
◆ 第三个参数,INT类型的Count,指定绘制字符的个数,如果取-1的话,就表示函数会自动绘制到字符串结束为止。
◆ 第四个参数,LPRECT类型的pRect,表示用于绘制字符串的矩形区域位置。
◆ 第五个参数,DWORD类型的Format,指定字符串在上面设置的这个参数,pRect矩形区域中的摆放属性。比较常用的属性浅墨打在了下面这张表上面了,他们之间用“|”符号联合起来使用,比如 DT_CENTER | DT_VCENTER。
Format参数的取值
精析
DT_BOTTOM
表示字符串位于rect底部,和DT_SINGLELINE共存
DT_CALCRECT
根据字符串长度自动调节矩形区域大小
DT_CENTER
表示字符串水平居中
DT_LEFT
表示字符串左对齐
DT_NOCLIP
表示不对字符串进行裁剪
DT_RIGHT
表示字符串右对齐
DT_SINGLELINE
表示字符串单行显示
DT_TOP
表示字符串位于矩形区域顶部
DT_VCENTER
表示字符串位于矩形区域垂直居中
◆ 第六个参数,D3DCOLOR类型的Color,显而易见,它是用于指定我们字符串显示的颜色值的,属于我们之前讲过的D3DCOLOR结构体,可以随意从中挑选并设置。
其实在Direct3D中绘制2D文本的方式并不只这一种,但是这种方式用起来最舒服。说起来就两步:
1. 调用D3DXCreateFont创建字体
2.拿起创建的字体,调用ID3DXFont::DrawText进行文本的绘制。
看到这里如果你觉得累了,就看几张精美的游戏画面截图吧,今天贴出的依然是来自EA的游戏大作,采用CryEngine3游戏引擎制作的《孤岛危机2》:
CryEngine3引擎作为目前全球顶尖的游戏引擎之一,有着相当震撼的画面效果:
好了,了解完周边知识,美图也欣赏了,下面我们来看看这篇文章中的主角。
四、起承转合的艺术――Direct3D渲染五步曲
开始正式讲解本篇文章的正餐了――Direct3D绘制五步曲。
Ⅰ.Direct3D渲染五步曲概述
上节的初始化五步曲与大家见面后,不少读者表示对Direct3D_Render()这个函数很感兴趣。因为上篇文章里篇幅有限,所以那时候Direct3D_Render()函数体里面是空的,这篇文章的主要目的,就是进行这个函数的书写。
其实Direct3D_Render()也就是浅墨自定义的一个用于完成我们Direct3D绘制过程的函数而已,名字可以随便取的,比如叫D3DRender(),叫Render(),叫Draw()都是可以的,这个要看心情,当然也要符合比较科学的命名规范,你取PlayDota()都没人会说你,不过这样取名的话显然除了你没人知道这个函数到底是要干嘛的- -,就不合适了。
我们准备在这个Direct3D_Render()函数中,简单干净的完成这渲染五步曲操作,不多做一点赘余的操作,因为我们在消息循环中是这样写的:
//消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } }
这就表示了,我们无时不刻地在调用Direct3D_Render()函数,每秒钟调用成千上万次都是可能的。所以为了写出执行效率高的程序,我们一定要保证Direct3D_Render()函数中的干净整洁。
下面我们就来看看,Direct3D_Render()函数需要些的渲染五步曲,是哪五步:
1. 渲染五步曲之一:清屏操作
2. 渲染五步曲之二:开始场景
3. 渲染五步曲之三:正式绘制
4. 渲染五步曲之四:结束场景
5. 渲染五步曲之五:翻转显示
所以,渲染五步曲连起来说,也就是简单的二十个字:
清屏操作,开始场景,正式绘制,结束场景,翻转显示
这五步非常的好理解,有点写文章里起承转合的味道。
值得提出的是,渲染五步曲都有一个统一的指挥棒,那就是Direct3D中的“绘制金钥匙”――Direct3D设备接口,我们可以看到,每一步里面都是拿着个IDirect3DDevice9接口的指针对象g_pd3dDevice->这样指一下。
下面我们进行各个击破,分别进行细讲。首先来看下第一步。
Ⅱ. Direct3D渲染五步曲之一:清屏操作
每当绘制画面之前呢,我们都需要通过IDirect3DDevice9接口的Clear方法将后台缓冲区中的内容进行清空,并设置我们喜欢的表面填充颜色等。
我们可以在DirectX SDK中查到。Direct3D渲染五步曲的第一步中的主角IDirect3DDevice9::Clear的原型声明是这样的:
HRESULT Clear( [in] DWORD Count, [in] const D3DRECT *pRects, [in] DWORD Flags, [in] D3DCOLOR Color, [in] float Z, [in] DWORD Stencil );
下面我们来分别进行各个成员的讲解。
◆ 第一个参数,DWORD类型的Count,指定了接下来的一个参数pRect指向的矩形数组中矩形的数量。我们可以这样说,Count和pRects是一对好基友-o-。如果pRects我们将其设为NULL的话,这参数必须设为0。而如果pRects为有效的矩形数组的指针的话,这个Count必须就为一个非零值了。
◆ 第二个参数,const D3DRECT类型的*pRects,指向一个D3DRECT结构体的数组指针,表明我们需要清空的目标矩形区域。
◆ 第三个参数,DWORD类型的Flags,指定我们需要清空的缓冲区。它为D3DCLEAR_STENCIL、D3DCLEAR_TARGET、D3DCLEAR_ZBUFFER的任意组合,分别表示模板缓冲区、颜色缓冲区、深度缓冲区,用“|”连接。
◆ 第四个参数,D3DCOLOR类型的Color,用于指定我们在清空颜色缓冲区之后每个像素对应的颜色值,这里的颜色用D3DCOLOR表示,后面我们会讲到,这里我们只需要知道一种D3DCOLOR_XRGB(R, G, B)就可以了,这里的R,G,B为我们设定的三原色的值,都在0到255之间取值,比如D3DCOLOR_XRGB(123, 76, 228)。
◆ 第五个参数,float类型的Z,用于指定清空深度缓冲区后每个像素对应的深度值。
◆ 第六个参数,DWORD类型的Stencil,用于指定清空模板缓冲区之后模板缓冲区中每个像素对应的模板值。
所以,渲染五步曲的第一步就是用一下这个Clear方法:
//其中g_pd3dDevice表示我们创建的有效的Direct3D绘制”金钥匙”――Direct3D设备对象 g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
Ⅲ.Direct3D渲染五步曲之二:开始绘制
这个取名还真是不好取,浅墨想了半天,没办法,只好憨厚老实地给它取名叫“开始绘制”。
其实过程非常简单,就是简单地写一句:
//其中g_pd3dDevice表示我们创建的有效的Direct3D绘制”金钥匙”――Direct3D设备对象 g_pd3dDevice->BeginScene();
其中IDirect3DDevice9::BeginScene()没有参数,如果调用成功,返回值就为HRESULT。这个函数和IDirect3DDevice9:: EndScene()是一对好基友,都是形影不离,要么都不出现,要么肯定是成对出现的。BeginScene(开始绘制)对EndScene(结束绘制),这不是天造地设的一对是什么呢?哈哈。
Ⅳ. Direct3D渲染五步曲之三:正式绘制
正式绘制的就像BeginScene()和EndScene()这对好基友的电灯泡一样,总是乐此不疲地出现在他们两者中间,而且通常是大段大段的代码,弄得BeginScene()和EndScene()经常是相隔千万里,君住长江头,我住长江尾。
正式绘制这一步并没有确切的固定代码,我们想绘制什么内容,就写什么样的句子。这也是我后面讲解的重点所在,能写出不同的句子来绘制不同的游戏画面。
如果是按我们本篇文章的配套程序来分析的话,这里我们就写的是如下的代码:
//在纵坐标100处,写第一段文字 g_FontPosition.top = 100;//指定文字的纵坐标 g_pFont->DrawText(0, _T("《浅墨DirectX提高班》之三"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(68,139,256)); //在纵坐标250处,写第二段文字 g_FontPosition.top = 250; g_pFont->DrawText(0, _T("游戏开发的世界,我们随浅墨来降服你了~!"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(255,255,255)); //在纵坐标400处,写第三段文字 g_FontPosition.top = 400; g_pFont->DrawText(0, _T("闪闪惹人爱"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256));//采用随机RGB值,做出“闪闪惹人爱”的特效 //在窗口右上角处,显示每秒帧数 int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(168,39,136));
这里的写法是千变万化的,一千个人眼中有一千个哈姆雷特。你的灵感你的才华,可以在这一步里面尽情发挥,来勾勒出属于你自己的3D世界。
Ⅴ. Direct3D渲染五步曲之四:结束绘制
这一步也非常非常简单,就是调用一下“Direct3D绘制双人组”之一的EndScene()方法。向Direct3D表示,我们的绘制完成了~
所以这一步也就是这样写:
//其中g_pd3dDevice表示我们创建的有效的Direct3D绘制”金钥匙”――Direct3D设备对象 g_pd3dDevice->EndScene(); // 结束绘制
Ⅵ. Direct3D渲染五步曲之五:翻转显示
只是绘制完成了还不够,如果不进行翻转显示操作,我们是看不到绘制的结果的。因为我们绘制的内容是在幕后完成的,我们需要把幕后的内容翻转到前台,就要用到我们在文章开头部分讲交换链时候提到的那个非常重要的函数Present(),我们来看一下这个函数在Direct3D SDK中的原型:
HRESULT Present( [in] const RECT *pSourceRect, [in] const RECT *pDestRect, [in] HWND hDestWindowOverride, [in] const RGNDATA *pDirtyRegion );
接着我们看一下这个主持着交换链“六道轮回”的Present方法的具体参数构成:
◆第一个参数,const RECT类型的*pSourceRect,表示指向复制源矩形区域的指针。显然一般我们都将其设为NULL。
◆第二个参数,const RECT类型的*pDestRect,表示指向复制目标矩形区域的指针。显然一般我们也将其设为NULL。
◆第三个参数,HWND类型的hDestWindowOverride,表示指向当前绘制的窗口句柄。如果我们设为0或NULL就表示取我们之前初始化四步曲里面第三步填充的D3DPRESENT_PARAMETERS结构体中的hDeviceWindows的值。显然一般我们依然将其设为NULL。
◆第四个参数,const RGNDATA类型的*pDirtyRegion,表示指向最小更新区域的指针。显然一般我们依然将其设为NULL。
所以来说,这个代表着Direct3D渲染五步曲最后一步的,主持着交换链“六道轮回”的Present方法调用起来也是非常简单的,简单明了四个NULL:
g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示
Ⅶ.Direct3D渲染五步曲代码整体赏析
DirectX渲染五步曲,其实非常的简单,只要记住20个字就OK:
清屏操作,开始绘制,正式绘制,结束绘制,翻转显示。
具体代码非常简单(先忽略掉正式绘制实现代码),有点起承转合的味道,但基本上千篇一律,也就是:
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);//五步曲之一,清屏操作 g_pd3dDevice->BeginScene(); // 五步曲之二,开始绘制 /*五步曲之三,正式绘制。一千个人眼中有一千个哈姆雷特,在这里按喜好填入相关代码,进行正式绘制操作*/ g_pd3dDevice->EndScene(); // 五步曲之四,结束绘制 g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 五步曲之五,翻转与显示很多时候,一、二,四、五这四步的代码基本上不用变,因为它们其实就是Direct3D中绘制过程约定俗成的套路,我们很多时候只是简单地遵守这个套路来。要得出不同的绘制图形,我们就把第三步正式绘制的代码改改就好了
五、获取每秒帧数(FPS)函数的写法
看到FPS,很多朋友就激动了,这里的FPS可不是游戏里的第一人称射击类游戏(如CF,CS,Call of the Duty等等)的简称,FPS(First-Person Shooter Game),而是Frame Per Second,每秒帧速率。
为了之后我们的Direct3D程序能更清晰地在运行时实时看到帧率,来评估我们的程序的绘制效率,这里我们来手动写一个测帧率的函数Get_FPS()函数。为了达到显示帧速率的效果,在上节笔记的基础上,主要有三处地方需要添加实现代码。
第一步,添加两个全局变量。
首先我们定义两个全局变量:
float g_FPS = 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50]; //包含帧速率的字符数组float 类型的g_FPS用于表示帧速率的值,wchar_t类型的g_strFPS[30]表示包含帧速率的字符数组。
第二步,Get_FPS()函数的具体实现
这步就直接贴代码了:
//***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; }
代码中已经是行行注释了,非常简单明了,这里的具体思路就看代码就可以了。
第三步,在Direct3D_Render()函数中调用Get_FPS()函数,并进行文本的显示
//在窗口右上角处,显示每秒帧数 int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(168,39,136));其中swprintf_s函数的第二个参数中的0.3表示保留三位有效数字。
这样在Direct3D_Render()函数每秒钟成千上万次地被调用地过程中,也就让Get_FPS()被成千上万次调用,也让这段显示FPS的文字也成千上万次被调用了。
六、详细注释的源代码欣赏
理论部分讲解完成,落到实处还是需要多写代码多练习,下面我们就贴出详细注释的Direct3D渲染五步曲的完整代码:
//***************************************************************************************** // //【Visual C++】游戏开发笔记系列配套源码 三十四 浅墨DirectX提高班之三 起承转合的艺术:Direct3D渲染五步曲 // VS2010版 // 2012年 11月25日 Create by 浅墨 //图标素材: 古剑奇谭 红玉 //此刻心情:多年以后,当你回忆往昔,唯一让你觉得真实,和骄傲的,是你现在昂首挺胸、用力走过的人生。 // //***************************************************************************************** //***************************************************************************************** // Desc: 头文件定义部分 //***************************************************************************************** #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> //***************************************************************************************** // Desc: 库文件定义部分 //***************************************************************************************** #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "winmm.lib ") //***************************************************************************************** // Desc: 宏定义部分 //***************************************************************************************** #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #define WINDOW_TITLE _T("【Visual C++游戏开发笔记】博文配套demo之三十四 浅墨DirectX提高班之三 起承转合的艺术:Direct3D渲染五步曲") //为窗口标题定义的宏 #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } } //自定义一个SAFE_RELEASE()宏,便于资源的释放 //***************************************************************************************** // Desc: 全局变量声明部分 //***************************************************************************************** LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D设备对象 ID3DXFont* g_pFont=NULL; //字体COM接口 float g_FPS = 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50]; //包含帧速率的字符数组 RECT g_FontPosition = {0, 0, 0, 0};//定义一个矩形,用于字体位置的设定 //***************************************************************************************** // Desc: 全局函数声明部分 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd); void Direct3D_CleanUp( ); float Get_FPS(); //***************************************************************************************** // Name: WinMain( ) // Desc: Windows应用程序入口函数 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //开始设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINDCLASSEX,用于之后窗口的各项初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口的样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //从全局的::LoadImage函数从本地加载自定义ico图标 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一个以空终止的字符串,指定窗口类的名字。 if( !RegisterClassEx( &wndClass ) ) //设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜闻乐见的创建窗口函数CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd))) { MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口 } MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true); //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 ShowWindow( hwnd, nShowCmd ); //调用Win32函数ShowWindow来显示窗口 UpdateWindow(hwnd); //对窗口进行更新,就像我们买了新房子要装修一样 //消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //***************************************************************************************** // Name: WndProc() // Desc: 对窗口消息进行处理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数WndProc { switch( message ) //switch语句开始 { case WM_PAINT: // 客户区重绘消息 Direct3D_Render(hwnd); //调用Direct3D_Render函数,进行画面的绘制 ValidateRect(hwnd, NULL); // 更新客户区的显示 break; //跳出该switch语句 case WM_KEYDOWN: // 键盘按下消息 if (wParam == VK_ESCAPE) // ESC键 DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息 break; case WM_DESTROY: //窗口销毁消息 Direct3D_CleanUp(); //调用Direct3D_CleanUp函数,清理COM接口对象 PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM_DESTROY消息 break; //跳出该switch语句 default: //若上述case条件都不符合,则执行该default语句 return DefWindowProc( hwnd, message, wParam, lParam ); //调用缺省的窗口过程来为应用程序没有处理的窗口消息提供缺省的处理。 } return 0; //正常退出 } //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 // 1.初始化四步曲之一,创建Direct3D接口对象 // 2.初始化四步曲之二,获取硬件设备信息 // 3.初始化四步曲之三,填充结构体 // 4.初始化四步曲之四,创建Direct3D设备接口 //***************************************************************************************** HRESULT Direct3D_Init(HWND hwnd) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,创接口】:创建Direct3D接口对象, 以便用该Direct3D对象创建Direct3D设备对象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口对象的创建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口对象,并进行DirectX版本协商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:获取硬件设备信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件顶点运算,我们就采用硬件顶点运算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件顶点运算,无奈只好采用软件顶点运算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填内容】:填充D3DPRESENT_PARAMETERS结构体 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 2; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,创设备】:创建Direct3D设备接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口对象的使命完成,我们将其释放掉 return S_OK; } HRESULT Objects_Init() { //创建字体 if(FAILED(D3DXCreateFont(g_pd3dDevice, 30, 0, 0, 1, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("宋体"), &g_pFont))) return E_FAIL; //初始化字体位置 g_FontPosition.top = 0; g_FontPosition.left = 0; g_FontPosition.right = SCREEN_WIDTH; g_FontPosition.bottom = SCREEN_HEIGHT; return S_OK; } //***************************************************************************************** // Name: Direct3D_Render() // Desc: 进行图形的渲染操作 // Point:【Direct3D渲染五步曲】 // 1.渲染五步曲之一,清屏操作 // 2.渲染五步曲之二,开始绘制 // 3.渲染五步曲之三,正式绘制 // 4.渲染五步曲之四,结束绘制 // 5.渲染五步曲之五,翻转显示 //***************************************************************************************** //***************************************************************************************** // Name: Direct3D_Render() // Desc: 使用Direct3D进行渲染 //***************************************************************************************** void Direct3D_Render(HWND hwnd) { //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); //定义一个矩形,用于获取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之二】:开始绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene(); // 开始绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之三】:正式绘制,在这里我们写了四段文字 //-------------------------------------------------------------------------------------- //在纵坐标100处,写第一段文字 g_FontPosition.top = 100;//指定文字的纵坐标 g_pFont->DrawText(0, _T("《浅墨DirectX提高班》之三"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(68,139,256)); //在纵坐标250处,写第二段文字 g_FontPosition.top = 250; g_pFont->DrawText(0, _T("游戏开发的世界,我们随浅墨来降服你了~!"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(255,255,255)); //在纵坐标400处,写第三段文字 g_FontPosition.top = 400; g_pFont->DrawText(0, _T("闪闪惹人爱"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256));//采用随机RGB值,做出“闪闪惹人爱”的特效 //在窗口右上角处,显示每秒帧数 int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(168,39,136)); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:结束绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene(); // 结束绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:显示翻转 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示 } //***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; } //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 对Direct3D的资源进行清理,释放COM接口对象 //***************************************************************************************** void Direct3D_CleanUp() { //释放COM接口对象 SAFE_RELEASE(g_pFont) SAFE_RELEASE(g_pd3dDevice) }
这里我们新添加了一个全局函数HRESULT Objects_Init() ,用于完成了字体的初始化相关工作,后面我们会讲到的顶点缓冲区和索引缓冲区等物体绘制相关的准备工作,都会在这个函数中进行。其中我们实现了“闪闪惹人爱”的闪动文字效果,采用在三处RGB值的填写区域用了随机函数rand(),也就是这样写:rand() % 256。
//在纵坐标400处,写第三段文字 g_FontPosition.top = 400; g_pFont->DrawText(0, _T("闪闪惹人爱"), -1, &g_FontPosition, DT_CENTER, D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256));//采用随机RGB值,做出“闪闪惹人爱”的特效
这样每一次调用Direct3D_Render()函数,就会随机出不同的RGB值的颜色组合达到闪闪惹人爱的闪字效果。
我们编译并运行代码,就可以得到如下窗口:
可以看到,在不同的瞬间,右上角的FPS值和“闪闪惹人爱”字样的颜色是不同的:
消化完这篇文章里介绍的知识,我们就可以熟练地掌握Direct3D的绘制过程了,就是五步曲,非常的简单,只要记住20个字就OK:
清屏操作,开始绘制,正式绘制,结束绘制,翻转显示。
具体代码非常简单(先忽略掉正式绘制实现代码),有点起承转合的味道,但基本上千篇一律,也就是:
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); //五步曲之一,清屏操作 g_pd3dDevice->BeginScene(); // 五步曲之二,开始绘制 /*五步曲之三,正式绘制。一千个人眼中有一千个哈姆雷特,在这里按喜好填入相关代码,进行正式绘制操作*/ g_pd3dDevice->EndScene(); // 五步曲之四,结束绘制 g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 五步曲之五,翻转与显示
文章最后,依旧是放出本篇文章配套源代码的下载:
本节笔记配套源代码请点击这里下载:
【浅墨DirectX提高班】配套源代码之三
其中图标素材使用的是新生代国产武侠单机游戏《古剑奇谭》中的红玉
以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。
浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。
另外,浅墨刚刚参加了2012年CSDN年度博客之星的评选活动。
在这里,恳请支持浅墨,喜欢游戏开发系列文章的朋友们去投浅墨一票,有了大家的支持,浅墨会更用心地写出更优秀的博客文章来与大家分享,把技术分享这种信仰传递下去。大家的支持就是浅墨继续写下去的动力~~~
如果文章中有什么疏漏的地方,也请大家指正。
文章最后,依然送给大家一句话,以此共勉,这是乔布斯的一句经典台词:
你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。
下周一,让我们离游戏开发的梦想更进一步。
下周一,游戏开发笔记,我们,不见不散。