http://blog.csdn.net/SeanSeanSeanSeanSean/archive/2009/03/19/4006128.aspx
D3d9的一些更新 收藏
由于Aug 8造成的D3D9恐惧症已经完全消除了,这一章将会给大家介绍将3D引擎转向D3D9的各个方面,包括终于出现的全屏幕模式。从这章以后,我将使用D3D9作为讲解的语言继续D2D教程。
【OP结束,开始正片】
『Why?』
估计大家首先要问的就是“Why?”为什么要前进到D3D9?理由如下:
1、D3D9修复了D3D8已知的所有Bug,因此运行起来更稳定,速度也要快。
2、D3D9提供了许多便利的新功能,虽然绝大多数是面向3D的,但是也有不少2D适用的,比如IDirect3DDevice9::StretchRect,以及对IDirect3DSurface9的改进等等。D3DX库就更多了,比如D3DXSaveSurfaceToFileInMemory,一开始没发现这个函数有啥用处,现在基本离不开了。
3、HLSL。就像上一话我说的那样,D2D教程以后会有PixelShader的内容。我可不想拿汇编来写Shader,会死人的(祝贺我吧,终于抛弃汇编Shader了……)。虽然说这不是决定性的理由,因为还有Cg什么的,不过我想编写显卡无关的代码,因此我不去研究Cg(反正和HLSL差不多)以及R2VB之类。
4、ID3DXFont,往下看你就知道了。
《D3D的变化》
『界面名称变化』
一句话:8改成9就行。
『“创建”型方法的一个统一变化』
许多Create*()方法,比如创建设备、创建纹理、创建顶点缓冲等等,多了一个HANDLE* pSharedHandle参数,无用,NULL之(看来微软原打算弄个共享句柄之类,不过被D3D10巨大的变化浮云了)
『创建D3D设备的变化』
D3DPRESENT_PARAMS的FullScreen_PresentationInterval变成了PresentationInterval,也就是说即使在窗口模式下也可以做到垂直同步来防止撕裂现象(2D的福音啊)。相应的,D3DSWAPEFFECT_COPY_VSYNC消失了,反正这个效果也不咋的,消失了也好。
要做到垂直同步需要给PresentationInterval赋值D3DPRESENT_INTERVAL_DEFAULT或D3DPRESENT_INTERVAL_ONE。其中D3DPRESENT_INTERVAL_ONE的效果比D3DPRESENT_INTERVAL_DEFAULT好一点,不过相应的也会占用多一点点系统资源……真的只有一点点而已,实在是无所谓的……
如果不要垂直同步,想要看看实际祯速的话,D3DPRESENT_INTERVAL_IMMEDIATE。
注意在窗口模式下,你只能使用这三种Present模式,全屏幕模式下就可以使用别的(但是要首先检测D3DCAPS9以查看显卡是否支持)。不过我感觉对99%的游戏来说,有这三个就足够了。
另外在窗口模式下,BackBufferFormat也可以设置成D3DFMT_UNKNOWN,D3D会自动获取当前桌面的格式设定成后备缓冲的格式,省去GetDisplayMode。实际上,窗口模式下的后备缓冲已经不需要和桌面格式相同,你可以通过IDirect3D9::CheckDeviceFormatConversion来检查,如果这个设备支持这两种颜色格式之间的转换,就可以给程序的后备缓冲设定上不同的格式。我试过在桌面格式为32Bit(D3DFMT_X8R8G8B8)时将程序的后备缓冲格式设置为D3DFMT_R5G6B5(16Bit),发现了速度提升,也就是说这个设定是有意义的。
可创建的设备类型多了一种D3DDEVTYPE_NULLREF,在安装了D3D SDK的机子上等同于D3DDEYTYPE_REF,在其他的机子上,这种设备实际上没有创建真正意义的D3D设备,只是允许你创建的纹理、表面等资源,但是Render、Present等操作都会无效(实际上这些资源都创建在了D3DPOOL_SCRATCH池里,不管你设定使用的是什么POOL)。也就是说,仅仅在模拟基本的运行而已。你可以用这个设备来编写一个利用D3DX函数库进行图像格式转换的程序,比如把一大堆不同的格式转换成易于D3D9使用的DDS格式。因为实际上没有创建设备,你甚至可以编写成控制台的,通过GetConsoleWindow的方法获得HWND。Mercury 3用的MIF格式的转换器就是这么做出来的。注意D3DDEVTYPE_NULLREF只能用在IDirect3D::CreateDevice时,其他的方法都不行。
『创建表面的变化』
创建表面(Surface)的方法变成了IDirect3DDevice9::CreateOffscreenPlainSurface,参数很简单不用多说,需要注意的是可以选择POOL了。
『设定FVF的变化』
设定FVF时,原来通过IDirect3DDevice8::SetVertexShader,现在有了一个专门用来设定FVF的方法:IDirect3DDevice9::SetFVF。这是个很好的变化,省得把FVF和Shader弄混(题外话:也就是因为这个变化,让Shader在设备Reset后得以保存,不错不错)
『获取后备缓冲』
D3D9现在允许有多个后备缓冲交换链,不过对于2D来说,基本不需要这种东西,IDirect3DDevice9::GetBackBuffer多出来的第一个参数赋值0即可。如果你有兴趣,可以去研究一下这个玩意,有时候可以用来做分场。
『SetStreamSource』
这个方法的功能被扩展了,对比参数就可以知道,多出来的OffsetInBytes允许你选择一个顶点缓冲的Offset,D3D9将从这个Offset之后开始读取数据。因此你可以把几组用来渲染纹理的正方形顶点存储到一个顶点缓冲里面。
『SetSamplerState』
这个是D3D9的新方法,把原先SetTextureStageState的一些功能独立了出来,和2D关系最密切的就是纹理过滤了。原先的D3DTSS_MINFILTER变成了D3DSAMP_MINFILTER,相应的D3DTSS_MAGFILTER也变成D3DSAMP_MAGFILTER,D3DTSS_MAXANISOTROPY变成D3DSAMP_MAXANISOTROPY。另外还有更多的,比如纹理寻址等。你去看一下D3DSAMPLERSTATETYPE枚举类型的内容就知道它“迁移”了些什么。
这个变化对于Shader来说很方便。改成Sampler的东西在PixelShader过程也会有效,而没有更改的东西在PixelShader就不会有效了。D3D8时候把这些全都放在了一起,容易造成混乱。
『SetRenderTarget』
D3D9现在允许多重RenderTarget存在,不过我们基本上只用一个,RenderTargetIndex设为0,第二个参数仍然是需要设定的表面。与D3D8相同的是,在设定之前仍然需要先通过GetSurfaceLevel获得表面才行。
『顶点缓冲的锁定』
注意IDirect3DVertexBuffer9::Lock的第三个参数,从原来的BYTE**变成了void**。也就是这样了……
『其他的一些变化』
1、CopyRects变成了UpdateSurface。和UpdateTexture一样,只能从D3DPOOL_SYSTEMMEM拷贝到D3DPOOL_DEFAULT
2、增加了一个比较有用的IDirect3DDevice9::ColorFill方法,作用是向D3DPOOL_DEFAULT的某个区域填充颜色,和Clear的功能类似,但是在使用目的上要比Clear明确的多,并且由于不牵扯深度缓冲之类,速度要快一些。
3、增加了一个IDirect3DDevice9::StretchRect方法,通过这个方法就可以在D3DPOOL_DEFAULT的表面或纹理之间进行带过滤器的缩放操作,免去利用Render的过程,非常有用。不过这个方法由于使用了硬件处理,限制较多,请大家仔细看SDK文档的Remarks部分。
《D3DX的变化》
D3DX的变化实际上相当的多,但正如我一开始所说,基本都是面向3D的。需要我们注意的有以下几种:
1、D3DX***FromFile之类的函数支持的图像格式增加了,不过所增加的都是很少见的格式。平时基本上还是用BMP、TGA和PNG就足够。
2、增加了D3DXSave***ToFileInMemory,将会把文件写入内存。这个函数的作用似乎不是很容易想到,但是如果你要写一个集成了转换、打包功能的工具,这个就很有用了,省去了通过临时文件操作造成的各种问题。另外如果你熟悉某种图形文件的格式的话,还可以通过直接访问这个文件获得RAW信息。注意,这类函数写入的是一个ID3DXBuffer,这个东西很简单,只有两个特定的方法,一看便懂,不再多言。
3、增加了一个ID3DXLine,可以方便你在2D上画线,创建ID3DXLine的方法是D3DXCreateLine。这个东西也不复杂,使用方法有点像ID3DXSprite,稍微研究一下就能弄懂,注意每次Draw的是D3DPT_LINESTRIP。用它比直接用顶点缓冲的好处是可以方便的打开反锯齿,效果嘛……基本满意。
4、增加了一个ID3DXRenderToSurface,“理论上来说”方便了利用RenderTarget的过程……不过我感觉反而弄得复杂了。创建的方法是D3DXCreateRenderToSurface,有心情的朋友自己研究看看吧,我就不讲了。
ID3DXSprite和ID3DXFont在Summer 2004的DX9 SDK(也就是第一版DX9.0c)开始发生了很大变化,下面详述:
『ID3DXSprite』
你会发现ID3DXSprite::DrawTransform不见了,取而代之的是其功能被整合到ID3DXSprite::SetTransform里面,也就是说为了缩放和旋转,我们不得不和矩阵打交道了。其实也不会太复杂,因为我们只是做一些矩阵运算,学过线性代数的朋友肯定会很熟悉,就算你不怎么熟悉线性代数,也没关系,D3DX函数库提供了现成的矩阵运算函数,你只要用就行了。
D3DXMatrixScaling
D3DXMatrixRotationZ
D3DXMatrixTranslation
按照顺序调用这三个函数……或许学过3D的马上就想到这点了,的确是没错啦。注意顺序哦:Scaling -> Rotation -> Translation,简称SRT(看过全金属狂潮吗?看过的话这个单词很好记吧^_^),弄错了可是得不到正确结果的。
你是不是想到把同一个D3DXMATRIX当作参数使用三次?错啦!你要用矩阵乘法。创建三个D3DXMATRIX,比如mat1、mat2、mat3,分别用这三个函数将其创建为缩放矩阵、旋转矩阵和平移矩阵,然后在ID3DXSprite::SetTransform时,这样写:
SetTransform(mat1 * mat2 * mat3);
有够麻烦的是不?ID3DXSprite方便了做3D的,可害苦了做2D的,所以我已经不直接用这个了(什么叫不直接用?往下看)。
『ID3DXFont』
大家来欢呼吧!Summer 2004改进的ID3DXFont彻底枪毙掉了上一话那个字体引擎……
这东西的改进,怎么说呢,应该说是改头换面吧,速度、效果都和以前不是一个数量级。可怜的PixelFont,才存在了一话就要被抛弃了。
ID3DXFont多出来的几个方法,Preload*()这类的,就是把一些常用的字的字模提前读取到内存里面加快速度,同时还可以使用ID3DXSprite渲染,进一步加快速度。虽然内部仍然有GDI的部分,不过很明显工作方式发生了极大的变化。根据我的估计,这次的ID3DXFont很聪明的利用GDI获得文字的轮廓,然后通过纹理来渲染。这样的速度就快得多了,而且文字质量也得到了很好的控制,基本和直接用GDI的质量相同了。
由于PreloadCharacters()和PreloadGlyphs()不是那么好理解,一般用PreloadText()就行。建议将所有ASCII字符、标点符号和部分汉字预读进去。这个预读过程略微有点慢,而且根据预读的文字数量和你创建文字的字号,占用的内存也不同。这里给大家一堆文字,你Copy过去就行:
const char strPreloadText[] = " 1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM~!@#$%^&*()-=[]//;',./_+{}|:/"<>? 、。·ˉˇ¨〃—~‖…‘’“”〔〕〈〉《》「」『』〖〗【】!"#¥%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}我人有的和主产不为这工要在第一上是中国经已发了民同";
注意第一个字符是空格哦!把空格预读进去可是很重要的^_^
看上去并不多,因为要考虑到内存占用及速度,我只预读了一些符号和五笔的一键字。这些字符在24号字时候已经占用了快1MB了,比起PixelFont字库占用的要大得多。天知道ID3DXFont到底预读了些什么……
PreloadText()的第二个参数不要用strlen,sizeof(strPreloadText)即可。
然后就是利用ID3DXSprite来渲染。注意ID3DXFont::DrawText的第一个参数就是LPD3DXSPRITE,因此如果要利用ID3DXSprite,要将ID3DXFont::DrawText放到ID3DXSprite::Begin和ID3DXSprite::End之间。这就是我刚才说的不直接用ID3DXSprite的意思,ID3DXFont会完成ID3DXSprite的全部调用,你不用担心。
另外你应该注意到ID3DXSprite::Begin增加了参数,实际上DX文档里面没说,但是示例里面有,如果想让ID3DXSprite发挥作用并且最大幅度的提升效率,参数上设定D3DXSPRITE_ALPHABLEND | D3DXSPRITE_SORT_TEXTURE即可。意思很明白:打开Alpha过滤和纹理筛选。这里DX文档上有个错误一直没改:文档里给出的是D3DXSprite__SORT_TEXTURE,但是你可以试试,绝对报错。
剩下的就没啥了,ID3DXFont的使用方法上一话已经讲过。要注意的是D3DXCreateFont和D3DXCreateFontIndirect都发生了变化。D3DXCreateFont已经不再牵扯GDI了,D3DXCreateFontIndirect所使用的结构也变成了D3DXFONT_DESC,相对于LOGFONT结构,除去了一些用不着的参数,增加了一个MipLevels,就是MipMap等级啦,不用多说,2D下只用1。其他的上一话都有。实际上由于D3DXCreateFont已经不再关联GDI,D3DXCreateFontIndirect的存在仅仅是由于历史原因(为了兼容像我这种人的使用习惯),大家还是用D3DXCreateFont吧,省事。
截图就不贴了,没啥意义。你可能觉得直接向后备缓冲上DrawText还不够好看,那么就先画到一张纹理上,然后将纹理错位渲染到后备缓冲并且打开线型过滤,就可以达到和PixelFont相同的效果了。
速度嘛……我画了整整一屏幕字,在不缓冲文字的情况下(这个“缓冲文字”和ID3DXFont的文字缓冲可不是一回事啊!看过上一话的都应该知道我这里指的是什么),速度仍然在120FPS以上。或许你会觉得速度还是有点慢,但是,如果用D3D8的ID3DXFont画上这么一屏幕,基本就只剩20FPS了。
使用ID3DXFont替换掉PixelFont的优势就是可以方便的自定义字体字号了,并且也不再受GB2312字库的限制。所以大家都换了吧……都换了吧……把PixelFont忘了吧……
『稳定的DX9 SDK版本』
我现在用的是April 2006,而且应该会用很长时间。August 2006我是肯定不会去用啦!即使我不再恐惧D3D9,也会对这个SDK避让三分的。其实对于2D,我感觉用到April 2006就足够了,之后的DX9 SDK主要在D3DX的3D函数库部分进行更改……其实也是秋后的蚂蚱蹦达不了几天,D3D10马上就要出来了。要说D3D10啊……你还是看我另外一篇日志好了,总之打死我都不拿它做2D。
实际上仅仅是2D的话,从D3D8转向D3D9并没有多少变化,主要是稳定嘛!只要你不调用一些D3D9专用的功能,即使拿D3D9来做2D,在绝大多数显卡上还是能够运行的。嗯……GF2等级以上吧,GF2之前的,也太老了,无视好了。
《再上点菜好了:全屏幕模式》
其实并不是多么复杂的问题,让我拖了这么久……不拖了,这里就教给大家如何做全屏幕模式以及如何处理设备丢失的问题。
『创建全屏幕模式』
D3DPRESENT_PARAMS里面,Windowed设定为false,并且一定要设定BackBufferWidth和BackBufferHeight,完毕。
哈哈,就这么简单,或许早就有人尝试过了,但是你试试按下Alt+Tab,再切换回去,保证你什么都看不到。
之前曾经说过,DX8之前的版本,在全屏幕下工作比在窗口下容易,到DX8之后就则完全颠倒过来。因为在窗口模式下不用担心设备丢失(除非你更改桌面分辨率),全屏幕模式下就会有这个问题了。下面详述:
『设备、资源丢失』
设备丢失会发生在全屏幕模式下切换回桌面时(不论是通过Alt+Tab还是QQ上有人给你发了张图片-_-bbb),而且如果在调用IDirect3DDevice9::Reset(从现在开始就是D3D9了啊!忘记D3D8吧……)的时候发生错误,设备也会丢失。
设备丢失会造成资源丢失:所有创建在D3DPOOL_DEFAULT池的资源都会丢失,需要重新创建,其内容当然也会消失,需要重写。
然而创建在D3DPOOL_SYSTEMMEM和D3DPOOL_SCRATCH池的资源不会受到影响。创建在D3DPOOL_MANAGED池的资源也不会丢失,而且在设备重新可用的时候,D3DPOOL_MANAGED池的资源也可以立即投入使用,内容也不会改变。看这个池名字:托管池就能知道,D3D帮你处理了所有问题。
因此避免设备丢失后资源丢失的简易方法就是将所有资源创建在D3DPOOL_MANAGED池内。不过这并不是个好方法,这意味着不能用渲染对象——记得吗?RenderTarget只能创建在D3DPOOL_DEFAULT。实际上最好的方法是跟踪所有D3DPOOL_DEFAULT资源,比如利用std::list,将所有D3DPOOL_DEFAULT资源勾住,在设备发生丢失的时候释放掉资源,设备可以继续使用的时候重新创建资源,记得把数据写回去。对于其他的池就不用这么折腾了。
『当设备丢失之后』
不论通过任何方式发生了设备丢失,所有的操作几乎都会失效,只有Release()可以用——其实D3D会保证有部分操作可以成功,但是也仅仅是“可以”成功而不是“一定”成功,所以你还不如认定丢失的时候全都会失败比较好——以及IDirect3DDevice9::TestCooperativeLevel。因此在设备丢失之后,你应该停止整个游戏循环,而通过反复调用IDirect3DDevice9::TestCooperativeLevel判断设备是否可用。
『IDirect3DDevice9::TestCooperativeLevel』
这个方法检测当前的设备状态。返回值有四种:D3D_OK一切正常,D3DERR_DEVICELOST设备丢失,D3DERR_DEVICENOTRESET设备可以Reset。另外还有D3D9新增的D3DERR_DRIVERINTERNALERROR,遇到这个你就完蛋了,基本不可能恢复了,终止程序吧。
按照顺序来讲,如果游戏在正常运行,D3D_OK会返回;如果发生了设备丢失并且在这个时候不能恢复,比如全屏幕模式的时候用户切换到了Windows桌面,就会返回D3DERR_DEVICELOST;如果用户又切换回了游戏,设备可以恢复了(还没恢复呢!只是“可以”恢复而已),就会返回D3DERR_DEVICENOTRESET。
另外,IDirect3DDevice9::Present也会返回类似的值,不过你最好别指望这个,老老实实的用TestCooperativeLevel。因为Present在设备可以恢复的时候还是返回D3DERR_DEVICELOST(外一句:D3D10的时候TestCooperativeLevel就会完全整合到Present里面了,可喜可贺可喜可贺)
『处理设备丢失』
看下面的伪代码:
switch (IDirect3DDevice9::TestCooperativeLevel()){
case D3D_OK:
GameLoop();
break;
case D3DERR_DEVICELOST:
break;
case D3DERR_DEVICENOTRESET
OnLostDevice();
IDirect3DDevice9::Reset();
OnResetDevice();
break;
default:
QuitGame();
break;
}
GameLoop()就是你的游戏运行的过程了。把这个switch写在我们游戏框架的GameMain()部分,具体的位置可以看任何一话附带的源代码。
好像我一直没有讲IDirect3DDevice9::Reset的参数啊?因为只有一个参数,就是指向D3DPRESENT_PARAMS的指针。把你第一次创建设备时使用的D3DPRESENT_PARAMS结构保存起来,供Reset来用。
OnLostDevice()就是Release掉所有D3DPOOL_DEFAULT的资源,OnResetDevice()就是Create*()恢复啦!你可能注意到ID3DXFont、ID3DXSprite等等都有同名的方法,就是在这个时候调用的。如果你没有这么做,也就是说还保留着任何D3DPOOL_DEFAULT的资源的话,IDirect3DDevice9::Reset就一定会失败。
另外在OnResetDevice里面你还要重新进行SetRenderState、SetSamplerState等等,Reset之后这些东西也丢失了。实际上Reset和重新创建一次设备类似,所不同的是重新创建设备的话你需要连D3DPOOL_MANAGED的资源也Release掉。这个话题就不讨论了。
从代码可以看出来,D3DERR_DEVICELOST时程序什么都没做,只是在傻等。我认为这是一个好习惯,因为实在不能保证在D3DERR_DEVICELOST时除了Release还能干什么,与其这样还不如等设备能用了再说。
实在懒得管资源的话,全部D3DPOOL_MANAGED好了。至于渲染对象?自己想办法。
『人工制造“设备丢失”』
“干嘛还要制造设备丢失啊?”如果更改游戏分辨率、色深、切换全屏幕及窗口状态,进行这样的操作也要通过Reset,同样的,Reset之前也要释放掉所有D3DPOOL_DEFAULT资源(其实严格来说,还有更多的资源也要释放,不过在2D下基本不会创建这类资源,你就不用管了)并且调用ID3DXSprite::OnLostDevice之类的方法。这就是人工制造“设备丢失”了。实际上在这个过程设备并没有真正的丢失,只是会有一段时间处于不可用的状态,此时Reset尚未返回,整个D3D设备就好像死了一样。举个例子,你切换桌面分辨率,会有那么一段时间显示器上什么都不显示,然后很快就正常了。和这个现象是同一个原因。Reset成功后记得恢复资源。
你可能注意到这里的Reset和上面的Reset不是一回事。的确是这样,这里是为了重设状态而不是恢复设备。因此更改分辨率、色深的Reset需要写到switch外面,也就是别和它搅和的意思-_-bb。而且你只需要OnLostDevice -> Reset -> OnResetDevice。记住:正确的调用Reset不会造成设备丢失,这个概念别弄混了。
『切换全屏幕模式时的注意事项』
注意WindowStyle的变化。切换成全屏幕模式后,只能使用WS_POPUP,不然显示会变得怪怪的,你可以通过SetWindowLongPtr函数更改窗口外观,第二个参数指定GWL_STYLE即可。别忘了WS_VISIBLE啊!不然你什么都看不见。
『更详细的文档』
我这里只是简单讨论了造成设备丢失的原因及处理方法,更详细的内容你可以参考DX SDK文档的Lost Device文章,人家是权威的。
【以上,正片结束,后面是ED】
我们前进到了D3D9,赶上了时代。
我们创建了全屏幕游戏,赶上了时代。
我却变得一脑子浆糊,被观众抛弃了。
哈哈,开玩笑啦,不过这一话很乱倒是真的,因为不论是更新到D3D9还是设备丢失,牵扯的东西都太散太杂,结果弄得这一话也是一盘散沙(居然又没有附带代码)。唉,大家就忍了吧,忍不了的话就来PIA我吧。
关于更新至D3D9更多的内容,你可以参考SDK文档的《Converting to Direct3D 9》。
【以上,ED结束,后面是……】
第一季完结了……
回过头来看看,从第一话创建一个Windows窗口,到这一话的设备丢失,话题的层次一直在深入,现在已经深入到了不再是“学习”而是“研究”的范围。我也不再想仅仅是搞“教学”而是想和大家“讨论”。不过第一季主要还是教学吧。能坚持着看D2D教程到现在的,应该基本能够写出完整的2D Demo来了吧。如果有什么问题的话,欢迎提出,我在看到后会立刻回答的……只要你这个问题不太RP的话……
那么,第二季会是什么样子?
第二季就不再是教学了,而开始我和大家的讨论过程。第二季的第一话,也就是第09话,我将提供一些高级技巧给大家,并希望有兴趣的朋友和我一起进行这些技巧的研究。另外在第二季里面,我们还要创建一个2D图形引擎。原来打算给大家讲解Medux 2,不过现在感觉这东西实在小儿科,绝对会让大家B4的。那么既然如此,干脆介绍Mercury 3好了,有意见无?
透漏一点下一话的内容吧:模糊精度和多次纹理渲染,嘿嘿,听上去挺高深的是不?实际上超级简单,就看你能不能想到而已。
希望你在看完这一话之后,返回去再把前面的内容看看,相信你会得到新的收获。搞不好你还能抓出几个Bug呢!因为我是想到什么写什么,没个章法,Bug是难免