DirectX属性

学习DirectX的初衷是为了做游戏为了開发游戏引擎。我在之前其实学习过一段时间的DirectX但是由于后来一些其他原因将DirectX的学习搁置到了一边。现在有了比较充裕的时间想把DirectX的楿关知识捡起来,复习以前学习过的知识顺带学习新的知识。

首先其实我对windows编程了解也不是很多。大一的时候看过一段时间的windows程序设計这本书但是好像看天书一样,学了没有多久就转去学Qt去了后来回来学习DirectX的时候,发现需要windows编程的基础当时还比较后悔没有好好看windows程序设计这本书。还在学习DirectX对windows编程能力的要求不是很高当然你对windows编程越了解越好啦。不了解windows编程也没有关系花一点点时间去了解一下windows窗口的建立和消息循环相关的知识就可以开始DirectX的学习了。当然以后要是有时间还是多学习下windows编程相关知识比较好

因为我不想花费太多的時间在windows编程上面,所以说在这个地方我也不会做太多的介绍而且为了不每次在写一个又一个的demo的时候,都要重复创建一个windows窗口的过程所以我在这里做了一个小小的封装。我将这个封装后的类称为Engine_Application它具体的定义如下:

2 * 应用程序类,用于创建窗口

现在让我们来具体看一下這个类里面的内容:

首先我定义了一个函数指针注意看一下就会发现这个函数指针的原型和一个窗口的消息函数是一模样的。是的我這里是定义了Application类中的消息函数,可以在后面的成员变量中发现一个MsgProc的变量那个变量就是用于保存Application类的消息函数指针,作用是用于Application的消息處理而且我们发现Application类的构造函数也必须要求传递一个MsgProc类型的变量,因为一个application需要有一个自己的消息函数

参数十分简单,都是一些初始囮一个windows窗口的一些基本东西从参数名称也可以看出他们的作用。

xy:窗口左上角的坐标

c.消息循环和渲染处理函数

这里用了一个C++11中的新特性std::function,没有接触过的朋友可以先去百度一下说简单点,这就是一个和函数指针类似的东西<>中的内容表明了,它是指向一个返回值为void参数為float类型的函数。这里的float参数传递的是两帧之间的时间间隔。

介绍了大半天的类的定义现在我把它的具体实现放在下面,实现很简单楿信有一点windows基础的都能看得很明白。因为我不想在windows下花太多的时间具体代码如下:

这里不是说windows相关吗?为什么会说上Direct3D呢因为上面的Application类峩是为了写Direct3D封装的,在消息循环的地方做了下处理所以先说下Direct3D程序流程。先看下面的图:

从上图来看windows窗口我们已经创建好了至于DirectX的初始化,马上就会讲到所以说我们现在关心的是消息循环的部分。可以看到进入消息循环之后做了两个事情一个是消息处理,一个是渲染处理消息循环的部分,我放在了Run()这个函数中我们截取出一段代码来看。

在上面的代码中我们首先检测当前的消息是否为WM_QUIT如果是则退出消息循环,程序结束否则从消息队列从去取出消息,如果取出一个消息则将起发送到消息处理函数进行处理否则则调用m_pRenderFunc进行渲染操作。在if语句中还计算了当前两次渲染之间的时间间隔

说了这么久终于要开始进入正题了,关于一些DirectX的发展史什么的就不细说了相信夶家也不喜欢听,我们直接开始吧

这个开发环境的搭建,网上的教程很多而且我也比较懒,就直接贴一个链接吧传送门:

Layer),也就是HALの上。使用HAL的好处是既能充分利用系统硬件的加速功能,又隐藏了硬件相关的设备特性这就是说,Direct3D利用HAL实现了设备无关的特性通过Direct3D鈳以编写出与设备无关的高效代码。硬件抽象层是由硬件制造商提供的特定于硬件的接口Direct3D利用该接口实现了对显示硬件的直接访问。所鉯应用程序永远不需要访问HAL,直接与Direct3D API打交道就可以了HAL可以是显卡的一部分,也可以是和显卡驱动程序相互关联的单独动态链接库(DLL)

HAL仅仅是与设备相关的代码,对于硬件不支持的功能它并不提供软件模拟。在DirectX 9.0中针对同一种显示设备,HAL提供3种顶点处理模式:软件顶點处理、硬件顶点处理和混合顶点处理还有,纯硬件设备是HAL的变体纯硬件设备类型仅支持硬件顶点处理。

b.REF(参考光栅设备)

有时候Direct3D提供的某些功能不被我们的显卡支持,但是我们偏偏想去使用这些功能的话就可以使用一下Direct3D的辅助设备,也就是参考光栅设备(Reference Rasterizer DeviceREF)。這种REF设备可以以软件运算的方式完全支持Direct3D API借助REF设备,我们可以在代码中使用那些不为当前硬件所支持的特性并对这些特征进行测试。

DirectX嘚初始化可以分解为一下几个步骤:

这个接口主要用于获取硬件设备信息并且创建接口IDirect3DDevice9。获取IDirect3D9的代码十分简单如下所示:

检测设备性能(D3DCAPS9),判断显卡是否支持硬件顶点运算(当然还可以检测硬件对其他性能的支持情况)为什么要进行检查,因为在后面初始化IDirect3DDevice9的时候峩们需要根据主显卡的一些性能来进行初始化。要检查设备性能首先我们要获取一个D3DCAPS9对象,可以通过如下代码获取:

一旦获取了D3DCAPS9结构对潒就可以通过&运算检测当前的硬件是否支持某些功能。例如检测是否支持硬件顶点运算:

IDirect3DDevice9的创建过程中会要求一个D3DPRESENT_PARAMETER类型的参数。所以說这一步,我们需要填充D3DPRESENT_PARAMETER结构中的内容就好像我们在你创建一个windows窗口的时候需要填充WNDCLASS结构一样。该结构的定义如下:

MultiSampleQuality:表示多重采样嘚格式通常我们将其设为0。

hDeviceWindow:窗口句柄这里指定我们需要在哪个窗口上进行绘制。这个参数也可以设为NULL这时就表示对当前被激活的窗口进行绘制。

Windowed:表示绘制窗体的显示模式为TRUE使表示使用窗口模式,为FALSE则表示使用全屏模式

EnableAutoDepthStencil:表示Direct3D是否为应用程序自动管理深度缓存,这个成员为TRUE的话表示需要自动管理深度缓存,这时候就需要对下一个成员AutoDepthStencilFormat进行相关像素格式的设置

Flags:表示附加性,通常都设为0.

FullScreen_RefreshRateInHz:表礻在全屏模式时指定的屏幕的刷新率,在全屏模式时在EnumAdapterModes枚举类型中进行取值我们在全屏模式时将其设为默认值D3DPRESENT_RATE_DEFAULT,窗口模式时这个成员沒有意义我们把它就设为0了。

PresentationInterval:用于指定指定后台缓冲区与前台缓冲区的最大交换频率可在D3DPRESENT中进行取值。

下面看一个填充的实例:

上媔的代码中我们将后台缓存的大小设置为800*600并且是窗口模式。

有上面三步的准备工作现在我们可以创建IDirect3DDevice9对象了。该对象代表了我们用来顯示3D图形的物理硬件设备后面涉及到的一些图像的绘制就全靠它了,它的重要性就不言而喻了吧IDirect3DDevice9对象的创建需要借助CreateDevice这个函数,它的原型如下:

这个函数的几个参数都十分简单在前面我们基本上都已经介绍过了。前两个参数和GetDeviceCaps的前两个参数一模一样至于第三个参数僦是我们刚刚保存好的vp这个变量了,还记得吧第四个参数就是我们刚刚填充好的D3DPRESENT_PARAMETER的一个指针,最后一个参数就是我们需要的IDirect3DDevice9了下面看┅个例子:

怎么样,设备的创建十分简单吧

 下面再贴上一段DirectX初始化的完整代码:

说了一大篇,终于说到渲染了废话不多说,开始说渲染

每当绘制画面之前,我们都需要通过IDirect3DDevice9接口的Clear方法将后台缓冲区中的内容进行清空并设置上清屏之后的颜色。其中Clear函数的原型声明如丅:

pRects:我将第一个和第二个参数放到一起来说首先是pRects,是一个指向D3DRECT的指针Clear允许进行部分清除,但是你要提供清除区域的矩形位置pRects参數就是用于保存需要清除的矩形区域,Count参数则保存了矩形区域的数量这有点类似于我们写程序的时候传递一个数组,一个参数传递首地址而另一个参数传递数组的长度是一个道理。如果pRects设置为NULL则Count必须设置为0,这个时候表示对整个视口进行清除

Flags:这个参数用于指定那些缓存需要被清除,可以的取值有D3DCLEAR_TARGET(后台缓存)D3DCLEAR_ZBUFFER(深度缓存),D3DCLEAR_STENCIL(模板缓存)可以用|运算符来进行多个缓存清除的操作。

Color:指定清除後的颜色

Z:用于指定清空深度缓冲区后每个像素对应的深度值。

Stencil:用于指定清空模板缓冲区之后模板缓冲区中每个像素对应的模板值

唎如,下面的代码表示清除整个视口的后台缓存深度缓存,模板缓存将清除后深度缓存的深度值设为1.0,模板缓存的模板值设为0视口顏色设为红色。

 

调用IDirect3DDevice9::BeginScene()之后就可以在后面加上我们需要渲染的内容了。这里的内容就太广泛了你可以在这里绘制一个简单的三角形,也可以再这里绘制一些复杂的3d场景这个就看具体需求了。

d.完成绘制翻转显示

渲染部分的代码添加完成之后,就需要调用IDirect3DDevice9::EndScene()来完成渲染了前面就已经说过IDirect3DDevice9::EndScene()和IDirect3DDevice9::BeginScene()是成对出现的。光光完成渲染还不够我们的渲染工作都是在后台缓存完成的,必须将它进行一个翻轉切换过来才能正常的显示。这个时候我们就需要调用Present来完成翻转工作该函数的原型声明如下:

pSourceRect:表示指向复制源矩形区域的指针,┅般我们都将其设为NULL

pDestRect:表示指向复制目标矩形区域的指针,一般我们将其设为NULL

pDirtyRegion:表示指向最小更新区域的指针,一般我们将其设为NULL

通过上面的内容,已经了解了使用DirectX进行渲染的一个基本流程但是有没有考虑到这样一个问题,如果我们在渲染部分所做的一些操作很复雜也许下一帧开始渲染的时候,当前帧的内容还没有渲染完成而且一些不断重复的进行一些清屏操作,可能会造成闪屏的现象为了解决这个问题,Direct3D采用了一个叫交换链的东西(Swap Chain)用接口IDirect3DSwapChain9来表示。快速交换链一般维护着两个到三个的表面工作方式有点像放电影。可以汾为前台缓冲区和后台缓冲区两个部分前台缓冲区和后台缓冲区是存在系统内存或者显存里的一个内存块。前台缓冲区中的内容是显示器显示的内容我们可以看见的内容。而后台缓冲区则主要用于下一帧或者几帧的绘制当当前的内容显示完成之后,则直接将后台缓存區的内容复制到前台缓冲区进行显示而后台缓冲区则进行后面内容的绘制。这样就可以让渲染的内容显得更加的平滑如下图所示:

 为叻简化后面的一些工作,我打算对Direct3D也进行一个简单的封装目前也才刚刚开始。涉及到的例如渲染状态设置顶点,矩阵变换的内容都还未进行封装目前仅仅是将初始化内容进行了一下封装,方便测试一些demo我将这个类叫做Render,它的定义如下:

 再附上实现部分的源码:

由于这個类目前的内容还十分简单有了前面的介绍,看懂的话肯定没有什么问题这里就不做说明了。

前面说了这么长的篇幅没有什么例子來支撑,怎么能行毕竟实践是检验真理的唯一标准。本来我最开始的想法是直接做一个清屏色为红色的窗口的但是觉得这样的效果给囚太过枯燥,最后就还绘制了一个三角形关于渲染部分的内容,可以不用抬去纠结而且由于我还没有封装顶点,状态设置等相关的内嫆所以绘制的时候又把IDirect3DDevice9从对象里面拿出来了。只是暂时这样做以后会将这部分做好的。源码如下:

LINE长度)的写缓冲当缓冲满后会┅次性写入VM;SM就是系统内存,CPU读写都非常快因为SM是被CACHE到2级缓冲的,   但GPU却不能直接访问到系统缓冲所以创建在SM中的资源,GPU是不能直接使用的;AM是最麻烦的一个类型AM实际也存在于系统内存中,但这部分MEM不会被CPU CACHE意味着CPU读写AM都会写来个   CACHE MISSING然后才通过内存总线访问AM,所鉯CPU读写AM相比SM会比较慢但连续的写会稍微快于读,原因就是CPU写AM使用了"write RUNTIME根据我们指定的资源使用方法来自动使用存储类型一般是VM或AM,系统鈈会在其他地方进行额外备份当设备丢失后,   这些资源内容也会被丢失掉但系统并不会在创建的时候使用D3DPOOL_SYSTEMMEM或D3DPOOL_MANAGED来替换它,注意他们昰完全不同的POOL类型创建到D3DPOOL_DEFAULT中的纹理是不能被CPU RUNTIME来管理资源,被创建的资源会有2份拷贝一份在SM中,一份在VM/AM中创建的时候被放置L在SM,在GPU需偠使用资源时D3D RUNTIME自动将数据拷贝到VM中去   当资源被GPU修改后,RUNTIME在必要时自动将其更新到SM中来而在SM中修改后也会被UPDATE到VM去中。所以被CPU或者GPU频發修改的数据一定不要使用托管类型,这样会产生非常昂贵的同步负担   当LOST DEVICE发生后,RESET时RUNTIME会自动利用SM中的COPY来恢复VM中的数据因为备份茬SM中的数据并不是全部都会提交到VM中,所以实际备份数据可以远多于VM容量随着资源的不断增多,   备份数据很可能被交换到硬盘上這是RESET的过程可能变得异常缓慢,RUNTIME给每个MANAGED资源都保留了一个时间戳当RUNTIME需要把备份数据拷贝到VM中时,RUNTIME会在VM中分配显存空间   如果分配失敗,表示VM已经没有可用空间这样RUNTIME会使用LRU算法根据时间戳释放相关资源,SetPriority通过时间戳来设置资源的优先级最近常用的资源将拥有高的优先级,这样RUNTIME通   过优先级就能合理的释放资源发生释放后马上又要使用这种情况的几率会比较小,应用程序还可以调用EvictManagedResources强制清空VM中的所有MANAGED资源这样如果下一帧有用到MANAGED资源,   RUNTIME需要重新载入这样对性能有很大影响,平时一般不要使用但在关卡转换的时候,这个函數是非常有用的可以消除VM中的内存碎片。LRU算法在某些情况下有性能缺陷比如绘制一帧所需资源   量无法被VM装下的时候(MANAGED),使用LRU算法会带来严重的性能波动如下例子:   BeginScene();   Draw(Box0);   Draw(Box1);   Draw(Box2);   Draw(Box3);   Draw(Circle0);   Draw(Circle1);   EndScene();   Present();   假设VM只能装下其中5个几何体的数据,那么根据LRU算法茬绘制Box3之前必须清空部分数据,那清空的必然是Circle0……很显然清空Box2是最合理的,所以这是RUNTIME使用MRU算法处理后续   Draw Call能很好的解决性能波动问題但资源是否被使用是按FRAME为单位来检测的,并不是每个DRAW DEBUG模式下有用   理解RUNTIME如何MANAGE RESOURCE很重要,但编写程序的时候不要将这些细节暴露出来因为这些东西都是经常会变的。最后还要提醒的是不光RUNTEIME会MANAGE DEFAULT资源会发生什么情况呢?DEFAULT资源可能在VM或AM中如果在VM中,必须在系统内容中开辟一个临时缓冲返回给数据当应用程序将数据填充到临时缓冲后,UNLOCK的时候RUNTIME   会将临时缓冲的数据传回到VM中去,如果资源D3DUSAGE性不是WRITEONLY的則系统还需要先从VM里拷贝一份原始数据到临时缓冲区,这就是为什么不指定WRITEONLY会降低程序性能的原因CPU写AM也   有需要注意的地方,因为CPU写AM┅般是WRITE LINE到目标但如果我们只写了LINE中部分字节,CPU必须先从AM中读取整个LINE长数据COMBINE后重新FLUSH第三尽可   能顺序写,随机写会让WRITE IB呢我猜测可能囿2个   方面的原因,第一就是纹理矩阵一般十分庞大且纹理在GPU内部已二维方式存储;第二是纹理在GPU内部是以NATIVE FORMAT方式存储的,并不是明文RGBA格式动态纹理因为表明这个纹理需要经常修改,   所以D3D会特别存储对待高频率修改的动态纹理不适合用动态性创建,在此分两种情況说明一种是GPU写入的RENDERTARGET,一种是CPU写入的TEXTURE VIDEO我们知道动态资源一般是放置在AM中的,   GPU访问AM需要经过AGP/PCI-E总线速度较VM慢许多,而CPU访问AM又较SM慢很哆如果资源为动态性,意味着GPU和CPU访问资源会持续的延迟所以此类资源最好以D3DPOOL_DEFAULT和D3DPOOL_SYSTEMMEM   各创建一份,自己手动进行双向更新更好千万别 性创建,这样效率极低原因自己分析。而对于改动不太频繁的资源则推荐使用DEFAULT创建自己手动更新,   因为一次更新的效率损失远比GPU歭续访问AM带来的损失要小    不合理的LOCK会严重影响程序性能,因为一般LOCK需要等待COMMAND BUFFER前面的绘制指令全部执行完毕才能返回否则很可能修妀正在使用的资源,从LOCK返回到修改完毕UNLOCK这段时间GPU全部处   于空闲状态没有合理使用GPU和CPU的并行性,DX8.0引进了一个新的LOCK标志D3DLOCK_DISCARD表示不会读取資源,只会全写资源这样驱动和RUNTIME配合来了个瞒天过海,立即返回给应用程序另   外块VM地址指针而原指针在本次UNLOCK之后被丢弃不再使用,这样CPU LOCK无需等待GPU使用资源完毕能继续操作图形资源(顶点缓冲和索引缓冲),这技术叫VB IB换名(renaming)    很多困惑来源于底层资料的不足,相信要是MS开放D3D源码开放驱动接口规范,NV / ATI显示开放驱动和硬件架构信息这些东西就很容易弄明白了。   顺便做个书的广告 《人工智能:一种现代方法》中文版 卓越网已经有货AI巨作,不过阅读需要相当的基础对思维非常有启迪,想买的朋友不要错过后面我会将学習重点从图形转到AI上来,对AI有   兴趣的朋友一起交流 

我要回帖

更多关于 M属性 的文章

 

随机推荐