恩,我们继续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()中,是我们绘制图形的所有方法。我将其分为几大步骤

  1. 清空后台缓存中的图形绘制区域。g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
  2. 设置开始绘制g_pD3DDevice->BeginScene();
  3. 开始实际绘制
  4. 结束绘制 g_pD3DDevice->EndScene();
  5. 屏幕翻转 g_pD3DDevice->Present(NULL, NULL, NULL, NULL);

  1. 首先,我们清空图形绘制区,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就好。
 );
  1. 之后BeginScene来告诉D3D设备开始渲染工作,

  2. 开始正式绘制工作了,我们首先需要设置一个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      //绘制的图元数量
 );
  1. 结束绘制

有BeginScence当然要有EndScence咯,用它来通知我们的D3D设备,我们的图形渲染工作结束了

g_pD3DDevice->EndScene();

  1. 屏幕翻转

我们现在所有的图象都已经提供给了后台缓冲区了,我们要告诉系统,将我们的图象放置到前台,让我们看到。则需要调用这个函数。

 HRESULT Present(         
 CONST RECT *pSourceRect,    //复制源的矩形区域指针
     CONST RECT *pDestRect,    //复制目的地的矩形区域指针
    HWND hDestWindowOverride,    //Direct3D设备窗口句柄
    CONST RGNDATA *pDirtyRegion  //最小更新区域指针
 );

如果你之前都是按照我的代码来设计的话,这里参数全传NULL就可以了,我们使用其默认值不会出现任何问题的。

  1. 结束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建立一个三角形就这么多了,一会我再将整理好的完整代码发上,大家可以参考。具体的函数名和参数可能一时半会记不住,这是很正常的,但是大体的流程框架一定要熟记于心哦~:)