3D游戏编程入门(十三)使用D3D绘制图形<下>
恩,我们继续D3D的基本框架介绍。
刚才我们已经创建了WINDOWS窗口,也初始化D3D程序获得D3D设备指针了,接下俩,我们来处理消息循环。
这里值得注意的是,我们的图象循环是始终进行的,它一定是在一个死循环中,直到我们获得WM_QUIT消息终止了整个程序,我们才能从循环中出来,而消息处理也是始终进行的,我们可以把它们放在一个循环中。那么我们是否可以这么写?
while(msg.message != WM_QUIT)
{
fMessage = PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE);
if(fMessage)
{
//处理消息
TranslateMessage(&msg);
DispatchMessage(&msg);
//绘制
Render();
}
else
{
}
}
结果显然是不行的,这样将导致我们仅仅在获得有效消息时才进行绘制,而大部分时间我们并未得到有效的消息,所以我们只能将Render();绘制函数放到else中,这样,当我们没有获得有效消息时,将持续的进行绘制工作,当我们获得有效消息时则停止绘制。这样会不会导致一个情况,就是大量的有效信息被频繁发送,而导致我们无法进行图象绘制呢?呵呵,别担心,我们可以在消息处理中加入WM_PAINT的情况,在这里加入渲染,就不会有停止绘制的危险了。
在Render()中,是我们绘制图形的所有方法。我将其分为几大步骤
- 清空后台缓存中的图形绘制区域。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);
- 首先,我们清空图形绘制区,D3D中方法为:
HRESULT Clear(
DWORD Count, //清除的矩形区域数量,一般设置为0代表就清理某一块连续面
const D3DRECT *pRects,//清除的矩形区域数组指针,这个比较麻烦,我们直接清除的话用NULL就好
DWORD Flags, //清除标志,指定清除哪一个缓冲区,可选D3DCLEAR_STENCIL、D3DCLEAR_TARGET、D3DCLEAR_ZBUFFER的组合,分别代表摸版缓冲区,颜色缓冲区,深度缓冲区,我们可以用位或符进行组合
D3DCOLOR Color, //清除后重置的颜色,假若想填充黑色,则D3DCOLOR_XRGB(0, 0, 0);
float Z, //清除后重置的深度值,从0-1.0,设置为1就好,设置为0的话,就会遮挡住我们之后的图象,因为从z轴来说,我们的填充背景色比图象还将靠近我们的屏幕
DWORD Stencil //呵呵,这个不懂,填0就好。
);
之后BeginScene来告诉D3D设备开始渲染工作,
开始正式绘制工作了,我们首先需要设置一个FVF灵活顶点格式,来存放顶点信息,我这里仅设置了顶点的坐标和颜色了。
(1):
//自定义顶点格式
struct CUSTOMVERTEX
{
FLOAT x, y, z, rhw; // 三维坐标
DWORD colour; // 顶点颜色
};
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE) //定义说明FVF格式的具体模式
(2):之后我们需要对每个顶点进行相应的设置
//设置顶点位置,颜色信息
CUSTOMVERTEX cvVertices[] =
{
{250.0f, 100.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(255, 0, 0),}, //顶点1红色
{400.0f, 350.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 255, 0),}, //顶点2绿色
{100.0f, 350.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 0, 255),}, //顶点3蓝色
};
这样我们获得了三个顶点,他们的三维坐标和颜色我们都已经设置好。呃,值得注意的是,我们设置顶点时应该按照逆时针的顺序安置三个顶点,否则在系统处理时会默认的设置顺时针图元为不可见。
(3):接下来我们设置一个顶点缓存区来存放我们的顶点信息
HRESULT CreateVertexBuffer(
UINT Length, //顶点缓冲区的大小,按字节数算
DWORD Usage, //顶点缓冲区属性
DWORD FVF, //灵活顶点格式
D3DPOOL Pool, //顶点缓冲区内存位置
IDirect3DVertexBuffer9** ppVertexBuffer, //顶点缓冲区指针地址,这个是为了输出而准备的,之后我们可以从这里获得顶点缓冲区的地址,方便访问
HANDLE* pHandle //保留参数,置为0,DX为了支持以后的开发,很多函数中都保留这类暂时无用参数,设为0就可以
)
Length顶点缓冲区大小我们可以是先sizeof()获得FVF灵活顶点格式的结构体大小再乘以顶点个数,也可直接使用上面我们定义的cvVertices这个结构数组大小来确定。
Usage顶点缓冲区属性,取值可以为0或下列中任意类型或组合
- D3DUSAGE_DONOTCLIP 禁用裁剪,表示顶点缓冲区中的顶点不进行裁剪,当设置该属性时,渲染状态D3DRS_CLIPPING必须设为FALSE
- D3DUSAGE_DYNAMIC 使用动态内存,这就是说将我们的顶点缓存放在一个十分活跃的缓存区中,在系统调用时将更加优先对此区域的存取,但过大的结构不适合如此设置,因为优先权并不适合给过大块的内存使用。用比较土的话来说,如果都优先了,那还有什么 优先可言?
- D3DUSAGE_WRITEONLY 只写属性,不能进行读操作,设置该属性可以提高系统性能
- D3DUSAGE_SOFTWAREPROCESSING 使用软件进行顶点运算,否则使用硬件计算
其他还有些绘制曲线和点的设置,我就不再介绍了,3D游戏中一般很少使用。
DWORD FVF灵活顶点格式,就是我们刚才自己定义的结构体格式D3DFVF_CUSTOMVERTEX
D3DPOOL Pool缓冲区内存位置,我们通常使用D3DPOOL_DEFAULT或D3DPOOL_MANAGED这两种类型,其区别是前者将顶点缓冲放置显存后将不再进行管理,后者将顶点缓冲放置显存之后,还将在系统内存中备份一份,当我们的程序由于某种原因损坏到顶点缓冲区时,系统将会根据系统内存的备份对其进行恢复,当然,虽然安全性有提高,却牺牲了一些系统内存和运行效率,可以根据情况进行不同的设置。
我们看下这段的完整代码:
//创建顶点缓冲
if(FAILED(g_pD3DDevice->CreateVertexBuffer
(3 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX,
D3DPOOL_DEFAULT, &g_pVertexBuffer,NULL
)))
{
return E_FAIL;
}
(4):将顶点信息拷贝到顶点缓冲区 我们已经在显存中开辟了一块空间来存放顶点信息,也有了顶点信息的结构,我们接下来就需要将顶点信息载入到显存中。 为避免在此过程中,顶点缓冲又被其他程序占用,我们先锁定它,使其不受任何影响和修改
//锁定顶点缓冲
if(FAILED(g_pVertexBuffer->Lock(0, sizeof(cvVertices),(void**)&pVertices, 0)))
{
return E_FAIL;
}
//拷贝顶点信息
memcpy(pVertices, cvVertices, sizeof(cvVertices));
//解锁
g_pVertexBuffer->Unlock()
锁定我们调用Lock函数,他的参数依次为“加锁内存的起始位置”,“加锁内存的长度大小”,“返回的内存指针地址(方便我们访问该块内存)”,“加锁的属性(设置为0则可)”。
锁定好了之后,我们使用mencpy将第二个参数的内存内容拷贝到第一个参数的内存中,第三个参数是拷贝内容的大小。
在拷贝完毕后,我们解锁Unlock(呵呵,不用参数),则我们已经将顶点信息完美的拷贝到了我们的显卡缓存中了。
(6):渲染数据流链接
我们绘制图形之前,需要先将顶点缓冲区和渲染数据流连接。函数声明如下
HRESULT SetStreamSource(
UINT StreamNumber, //渲染数据流序号
IDirect3DVertexBuffer9 *pStreamData, //进行绑定连接的顶点缓冲区指针
UINT OffsetInBytes, //进行绑定连接的渲染数据流的起始位置
UINT Stride //渲染数据流中一个顶点所占的内存的大小
);
假使我们有比较多的顶点进行渲染,我们在StreamNumber这里可以传入渲染数据流的序号进行控制。
- pStreamData进行记录顶点缓冲区的地址。
- OffsetInBytes表示在渲染数据流中有效顶点的起始位置,这里是以字节数进行表示。
- Stride表示渲染数据流每两个相邻的顶点之间的间隔,通常来说它应该等于我们每个顶点在顶点缓存中所占的字节数。
(7):设置渲染数据流中的灵活顶点格式
这个很容易理解,不说了。函数为
HRESULT SetFVF(
DWORD FVF //灵活顶点格式
);
(8):绘制图形
一切都准备好了,我们就可以将渲染数据流中的图元绘制到后台缓冲了。调用开始我们说过的绘制三角形的函数
HRESULT DrawPrimitive(
D3DPRIMITIVETYPE PrimitiveType, //绘制的图元类型
UINT StartVertex, //绘制的起始顶点的索引值
UINT PrimitiveCount //绘制的图元数量
);
- 结束绘制
有BeginScence当然要有EndScence咯,用它来通知我们的D3D设备,我们的图形渲染工作结束了
g_pD3DDevice->EndScene();
- 屏幕翻转
我们现在所有的图象都已经提供给了后台缓冲区了,我们要告诉系统,将我们的图象放置到前台,让我们看到。则需要调用这个函数。
HRESULT Present(
CONST RECT *pSourceRect, //复制源的矩形区域指针
CONST RECT *pDestRect, //复制目的地的矩形区域指针
HWND hDestWindowOverride, //Direct3D设备窗口句柄
CONST RGNDATA *pDirtyRegion //最小更新区域指针
);
如果你之前都是按照我的代码来设计的话,这里参数全传NULL就可以了,我们使用其默认值不会出现任何问题的。
- 结束D3D程序
我们也画完了,也贴出来了,但是我们不能就这么一走了之哦,程序还占用着系统资源呢,我们必须去手动释放这些被占用的资源。
在我们关闭窗口时,我们通常会需要释放所有的资源,这时窗口会收到一个WM_DESTROY的消息,我们在这个消息的判断中加上释放资源的方法。就现阶段,我们仅仅需要释放的就是顶点缓存,D3D设备指针,D3D指针。之后我们讲到索引缓存等的时候,就需要释放更多了,所以我们干脆写个方法,把所有需要释放的东西放在一起,方便我们的调试整理
#define SafeRelease(pObject) if(pObject != NULL){pObject->Release(); pObject=NULL;}
//释放建立的对象
void CleanUp()
{
SafeRelease(g_pVertexBuffer);
SafeRelease(g_pD3DDevice);
SafeRelease(g_pD3D);
}
CleanUp();
//注销wndclass类
UnregisterClass("base", wc.hInstance);
大家可以看到,我在前面先define自己定义了一个SafeRelease的方法,而在其中加上了一个意外判断之后再调用了系统给予我们的 Release方法,这是一个比较好的习惯,它将帮助我们更安全的释放工作。在动态内存释放时,我们尽量也使用自己写的SafeFree方法来替代系统的free。
UnregisterClass是一个Windows API函数,它注销WNDCLASS类并释放内存,其中第一个参数是类名,第2个是应用程序实例句柄,在创建Windows时我有很详细的介绍,这里不说了。
基本上,使用D3D建立一个三角形就这么多了,一会我再将整理好的完整代码发上,大家可以参考。具体的函数名和参数可能一时半会记不住,这是很正常的,但是大体的流程框架一定要熟记于心哦~:)