【 Visual C++】游戏开发笔记之三——绘制图元




使用Direct3D绘制图元

我们接下来的部分将介绍使用Direct3D在屏幕上绘图的图元。计算机图形学中的图元是可以渲染到屏幕上的基本形状。将图元组合在一起可以构成不同的形状和物体,诸如人物模型、场景物体、墙、建筑物等。最常用的图元是三角形。在游戏中常使用三角形,这是因为这样可以优化现代图形硬件,有效处理这类图元。其他图元包括四方形、直线和多边形。多边形被认为是图形,它是由直线构成的封闭区域。

Direct3D中在屏幕上绘制图元的方法主要有两种。

第一种是使用指针将几何图形数据数组发送给某个Direct3D函数。通常很少使用这种方法,因为这种方法运行缓慢,而且效率很低。这种方法之所以运行缓慢,是因为在Direct3D内部要对数据做一些不必要的工作,这样才能将数据下传给流水线,流水线负责绘制每个对象的每一帧。第二种方法是在绘制几何图形时使用一种名为顶点缓存的结构。顶点缓存是一个Direct3D对象,它可以按照Direct3D格式存储几何图形数据。借助用户指针(第一种方法),每次在程序中调用渲染函数时,Direct3D都不得不创建一个临时顶点缓存。这很浪费时间,尤其是如果信息没有发生变化,而且从技术角度而言,只需要创建顶点缓存一次时。为了使用用户指针在屏幕上绘制图元或图元组,就要调用Direct3D设备对象中的DrawPrimitive()函数。

该函数原型如下所示:

HRESULT DrawPrimitiveUP(
D3DPRIMITIVETYPE PrimitiveType,
UINT PrimitiveCount,
CONST void* pVertexStreamZeroData,
UINT VertexStreamZeroStride
);


DrawPrimitiveup()函数的参数包括要绘制的图元类型、图元数量、保存要绘制的数据数组、顶点流大小。第一个参数是图元类型,如果要绘制的图元是一些点,那么该参数值可以设为D3DPT_POINTLIST;如果要绘制的图元是一些线段,那么该参数值可以设为D3DPT_LINELIST(例如:点1和点2连接,点3和点4连接);如果要绘制的图元是连接的线段,那么该参数值可以设为D3DPT_LINESTRIP(例如:点1和点2相连,点2和点3相连);如果要绘制的图元是一些离散的三角形,那么该参数值可以设为D3DPT_TRIANGLELIST(例如:点1、点2和点3构成一个三角形,而点4、点5和点6构成另一个三角形);如果要绘制的图元是一些相连的三角形,那么该参数值可以设为D3DPT_TRIANGLESTRIP(例如:点1、点2和点3构成一个三角形,点2、点3和点4构成另一个三角形);也可以设为D3DPT_TRIANGLEFAN(例如:点1、点2和点3构成一个三角形,而点1、点3和点4构成另一个三角形)。当处理三角形时,所选的三角形类型取决于将数据发送给渲染API的方式。如果发送给Direct3DAPI一列三角形,而三角形中的每个点互不相同,那么D3DPT_TRIANGLELIST就是最佳选择。


其余参数非常简单明了。DrawPrimitiveUP()函数的第二个参数是发送给函数的图元总数。如果将要绘制10个三角形,那么应该设该参数为10,除非只想绘制某一部分图元。第三个参数是发送给该函数的图元数据数组。第四个参数和最后一个参数是每个顶点的大小(单位:字节)。如果每个顶点都只指定了x、y、z浮点坐标位置数据,那么该参数值应该是12个字节。这因为它包含三个值,每个值分别是4个字节。


Direct3D顶点缓存

第二种方法(也即我们使用的方法)是使用Direct3D顶点缓存来存储和渲染几何图形数据。顶点缓存是LPDIRECT3DVERTEXBUFFER9类型的结构。该结构是LDirect3DVertexBuffer9的指针。在创建顶点指针时,要将其和正确渲染对象所需的全部信息一起加载。顶点缓存并不包含顶点数据。它同样可以保存顶点颜色、方向以及显示对象所需的其他属性。

调用Direct3D对象设备的CreateVertexBuffer()函数可以创建顶点缓存。只有成功创建Direct3D设备对象后,才可以调用该函数。

下面我们贴出CreateVertexBuffer()的函数原型。

HRESULT CreateVertexBuffer(UINT Length,
DWORD Usage,DWORD FVF, D3DPOOL Pool,
IDirect3DVertexBuffer9** ppVertexBuffer,
HANDLE* pSharedHandle);

CreateVertexBuffer()函数的第一个参数是顶点缓存所需的字节数。该参数值是正确存储一组几何图形(例如:人物模型、级别等)所需的最大数量(单位:字节)。第二个参数是一个描述顶点缓存使用方法的标识符,该值可设为0。第三个参数是一个描述使用顶点缓存的顶点结构的标识符或标识符集。例如,Direct3D需要知道数据的顺序和数据类型(先是顶点位置、接下来是顶点颜色等),这样才可以正确地绘制几何图形。第四个参数是放置资源的正确内存类。第五个参数是顶点缓存(LPDIRECT3DVERTEXBUFFER9类型),函数是否成功决定了是否可以创建该缓存。CreateVertexBuffer()函数中的第一个参数为保留参数,值总是为0。

创建顶点缓存后,接下来要做的工作是锁定顶点缓存。调用顶点缓存的Lock()函数就可以获取顶点缓存指针,从而将数据复制给该顶点缓存。Lock()函数将锁定一列数据,这样就可以处理该数据了。该函数的参数包括:为上锁的顶点缓存增加的偏移量(0代表锁定全部缓存)、要锁定的数据量、指向锁定内存区的指针以及一个指定锁存数据方法的标识符。将数据复制到缓存的最简单方法是使用标准函数memcpy()。一旦复制完数据,就可以调用Unlock()。每调用一次Lock()函数就必须相应地调用一次Unlock()函数。本章稍后将介绍如何创建和显示几何图形。


坐标系

默认情况下,Direct3D使用的是左手坐标系。这意味着虚拟3D网格上的每个正坐标远离观察者的方向。例如,Z轴的正坐标是冲向屏幕里面的,Y轴的正坐标是向上的,而X轴的正坐标是向右的。图1.3给出了该坐标系的示意图。

对OpenGL用户而言,一定要记住,OpenGL使用的是右手坐标系,而Direct3D使用的是左手坐标系。这意味着除了Y轴之外,所有轴的正方向都是相反的。

虽然也可将Direct3D设为右手坐标系,但默认情况下,它使用的是左手坐标系。下面将介绍许多和坐标系打交道的函数。每个函数都有另一个可供选择的使用右手坐标系的形式。这样就可以很轻松地从Direct3D移植到OpenGL。



我们来写一个Lines(线段)演示程序。该演示程序将在屏幕中间绘制两条白色线段。

该程序的main源文件的全局部分代码如下所示:


#include<d3d9.h>
#define WINDOW_CLASS "UGPDX"
#define WINDOW_NAME "Drawing Lines"
// Function Prototypes...
bool InitializeD3D(HWND hWnd, bool fullscreen);
bool InitializeObjects();
void RenderScene();
void Shutdown();
// Direct3D object and device.
LPDIRECT3D9 g_D3D = NULL;
LPDIRECT3DDEVICE9 g_D3DDevice = NULL;
// Vertex buffer to hold the geometry.
LPDIRECT3DVERTEXBUFFER9 g_VertexBuffer = NULL;
// A structure for our custom vertex type
struct stD3DVertex
{
float x, y, z;
unsigned long color;
};
// Our custom FVF, which describes our custom vertex structure.
#define D3DFVF_VERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)


上段代码的开始部分包含了Direct3D头文件d3d9.h。该文件是必需的,只有包含该文件才能使用Direct3D函数和结构。代码中接下来的两行是全局定义语句,这两个语句定义了窗口类名称和窗口标题。WinMain()函数将用到这两个字符串,并且为方便起见,可以在全局区对它们进行处理。

定义语句后面的几行代码是代码中将要用到的不同函数的原型定义。InitializeD3D()函数用于在程序中设置和创建Direct3D。InitializeObjects()函数用于创建演示程序中要绘制在屏幕上的物体。RenderScene()函数用于在屏幕上渲染图形,而Shutdown()函数用于在程序退出时清除程序。

函数原型后面声明了一个顶点缓存对象。该顶点缓存用于保存要在屏幕上显示的几何图形数据。由于该对象使用动态内存存储数据,所以在程序退出前必须在程序的某个地方释放它。这些内存中未释放的四处浮动的对象会产生很严重的内存泄漏问题。如果有许多存储大量数据的顶点缓存,那么这个问题尤为突出。

顶点缓存后面是一个定义场景中单个3D点的结构。该点结构由位置和颜色组成。在该演示程序中,由于渲染的是白色线段,因此所有点的图案背影是白色的。以后的演示程序将介绍Direct3D所定义的3D点的其他属性。由于该演示程序只渲染线段,因此渲染场景不需要太多代码。

全局部分的最后一行代码是顶点格式标识符。该顶点格式也可称为灵活顶点格式(FVF:FlexibleVertexFormat)。Direct3D通过该标识符可以知道顶点(或点)结构格式。该结构指定了顶点的位置和颜色。因此,还必须指定要处理包含坐标(D3DFVF_XYZ)和颜色(D3DFVF_DIFFUSE)的点的顶点格式。由于Direct3D不知道数据发送给它的方法,因此需要用到像FVF这样的内容,这样Direct3D才知道如何处理这些数据。

源文件中接下来的内容是消息过程MsgProc()函数、WinMain()函数和InitializeD3D()函数的实现代码。由于在笔记一和二中已经介绍过这些函数,因此此处不再介绍。这三个函数后面是InitializeObjects()函数的实现代码。如前所述,该函数将创建和设置全部要渲染的演示对象。InitializeObjects()函数的全部代码如下所示。


InitializeObjects()函数的全部代码:

bool InitializeObjects()
{
unsigned long col = D3DCOLOR_XRGB(255, 255, 255);
// Fill in our structure to draw an object.
// x, y, z, color.
stD3DVertex objData[] =
{
{ 420.0f, 150.0f, 0.5f, col, },
{ 420.0f, 350.0f, 0.5f, col, },
{ 220.0f, 150.0f, 0.5f, col, },
{ 220.0f, 350.0f, 0.5f, col, },
};
// Create the vertex buffer.
if(FAILED(g_D3DDevice->CreateVertexBuffer(sizeof(objData), 0,
D3DFVF_VERTEX, D3DPOOL_DEFAULT, &g_VertexBuffer,
NULL))) return false;
// Fill the vertex buffer.
void *ptr;
if(FAILED(g_VertexBuffer->Lock(0, sizeof(objData),
(void**)&ptr, 0))) return false;
memcpy(ptr, objData, sizeof(objData));
g_VertexBuffer->Unlock();
return true;
}


InitializeObjects()函数开始先计算用于遮蔽线段的白色颜色值。宏D3DCOLOR_XRGB将以RGB(红、绿、蓝)格式指定的颜色值转换成Direct3D希望使用的格式。计算完颜色值后就创建了对象。为了创建两条线段,需要指定四个点。前两个点构成一条线段,后两个点构成另一条线段,并指定每个点的x、y、z坐标位置和颜色值。这和顶点结构以及顶点FVF完全符合。

计算完数据后,创建顶点缓存保存数据。调用CreateVertexBuffer()函数可以创建顶点缓存。该函数的参数包括要存储的对象大小、指定使用方式的标识符(其使用方式可以是D3DUSAGE枚举变量中的任意值)、描述顶点数据布局(D3DFVF_VERTEX)的顶点格式、可以是D3DPOOL枚举变量中任意值的池标识符(该标识符指定了内存中放置资源的正确位置),以及通过调用该函数创建的顶点缓存。最后一个参数为保留参数,通常设为NULL(空)。

一旦完成顶点缓存的分配,就将数据复制到顶点缓存中。首先,调用Lock()函数锁定顶点缓存。Lock()函数可以获取一个指向顶点缓存中已分配内存的指针,这样就可以操作该指针。Lock()函数的参数包括从0位置(该位置指定了内存的起始位置)开始的偏移值(单位:字节)、要用的字节数、指向包含顶点数据的内存的指针,以及锁定缓存时用的标识符(该参数值可以设为D3DLOCK_DISCARD、D3DLOCK_NO_DIRTY_UPDATE、D3DLOCK_NOSYSLOCK、D3DLOCK_READONLY或D3DLOCK_NOOVERWRITE)。如果要用的字节数设为0,表明指定的是全部缓存。

一旦得到一个指向内存的指针,就可以将数据复制到缓存中。在处理数组时,复制数据的最简单方法是使用memcpy()这个标准的C函数。这样就可以将全部数据复制到缓存中,并做好使用准备。调用Unlock()函数可以将顶点缓存设置为准备就绪。不可以使用锁定的顶点缓存,每调用一个Lock()函数就要调用一个对应的Unlock()函数。

一旦创建完顶点缓存,就可以将其渲染到屏幕上。调用演示程序中的RenderScene()函数可以完成渲染工作。在消息循环过程中,WinMain()将调用RenderScene()函数。如果在消息循环过程中没有处理任何消息,WinMain()就会调用RenderScene()函数用要显示的新内容来更新屏幕。因为这里没有动画,所以和要显示的动画帧没有差别。源文件中的最后两个函数是RenderScene()和Shutdown()。这两个函数的代码如下所示。

Lines演示程序的RenderScene()和Shutdown()函数的源代码:

void RenderScene()
{
// Clear the backbuffer.
g_D3DDevice->Clear(0, NULL, D3DCLEAR_TARGET,
D3DCOLOR_XRGB(0,0,0), 1.0f, 0);
// Begin the scene. Start rendering.
g_D3DDevice->BeginScene();
// Render object.
g_D3DDevice->SetStreamSource(0, g_VertexBuffer, 0,
sizeof(stD3DVertex));
g_D3DDevice->SetFVF(D3DFVF_VERTEX);
g_D3DDevice->DrawPrimitive(D3DPT_LINELIST, 0, 2);
// End the scene. Stop rendering.
g_D3DDevice->EndScene();
// Display the scene.
g_D3DDevice->Present(NULL, NULL, NULL, NULL);
}
void Shutdown()
{
if(g_D3DDevice != NULL) g_D3DDevice->Release();
if(g_D3D != NULL) g_D3D->Release();
if(g_VertexBuffer != NULL) g_VertexBuffer->Release();
g_D3DDevice = NULL;
g_D3D = NULL;
g_VertexBuffer = NULL;
}


Shutdown()函数对顶点缓存增加了一个释放调用,前面对界面和设备对象已经做过这样的处理。RenderScene()函数除了在BeginScene()和EndScene()之间代码稍有不同之外,和空白窗口演示程序中的代码完全相同。一旦启动场景,就先调用SetStreamObjects()函数将顶点缓存设为渲染流。SetStreamObjects()函数的参数包括要设置的流索引(从0到最大流数–1)、开始渲染缓存的偏移字节量和顶点跨度。顶点跨度指的是正在渲染的缓存中,在几何图形中定义单个点时用到的顶点结构大小。

完成流设置后,就可以设置顶点格式FVF。这样,Direct3D就可以知道正在发送的数据格式。SetFVF()函数中最合适的标识符是用来指定顶点格式的标识符。源文件全局部分的最后一行代码是一个#define语句(参见程序清单1.11),该语句定义了绘制线段必须用到的名为D3DFVF_VERTEX标识符。对程序中用到的每个顶点结构而言,都应该有一个匹配的顶点格式。

绘制出顶点缓存内容的最后一个步骤是调用DrawPrimitive()函数。DrawPrimitive()函数将绘制出当前设置为流的顶点缓存内容。该函数的参数包括:要渲染的图元类型、开始渲染的起始顶点索引(开始为0)以及要渲染的图元数目。由于这里指定在屏幕上绘制两条线段,因此最后一个参数的值为2。图元类型(第一个参数)可以是D3DPT_POINTLIST、D3DPT_LINELIST、D3DPT_LINESTRIP、D3DPT_TRIANGLELIST、D3DPT_TRANGLESTRIP或D3DPT_TRIANGLEFAN。

到此,就完成了全部的演示程序。


下面贴出Lines演示程序的全部代码:

#include<d3d9.h>

#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")

#define WINDOW_CLASS "UGPDX"
#define WINDOW_NAME  "Drawing Lines"

// Function Prototypes...
bool InitializeD3D(HWND hWnd, bool fullscreen);
bool InitializeObjects();
void RenderScene();
void Shutdown();


// Direct3D object and device.
LPDIRECT3D9 g_D3D = NULL;
LPDIRECT3DDEVICE9 g_D3DDevice = NULL;

// Vertex buffer to hold the geometry.
LPDIRECT3DVERTEXBUFFER9 g_VertexBuffer = NULL;


// A structure for our custom vertex type
struct stD3DVertex
{
    float x, y, z, rhw;
    unsigned long color;
};

// Our custom FVF, which describes our custom vertex structure.
#define D3DFVF_VERTEX (D3DFVF_XYZRHW | D3DFVF_DIFFUSE) 


LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
   switch(msg)
      {
         case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
            break;

         case WM_KEYUP:
            if(wp == VK_ESCAPE) PostQuitMessage(0);
            break;
      }

   return DefWindowProc(hWnd, msg, wp, lp);
}


int WINAPI WinMain(HINSTANCE hInst, HINSTANCE prevhInst,
                   LPSTR cmdLine, int show)
{
   // Register the window class
   WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc,
                     0, 0, GetModuleHandle(NULL), NULL, NULL,
                     NULL, NULL, WINDOW_CLASS, NULL };
   RegisterClassEx(&wc);

   // Create the application's window
   HWND hWnd = CreateWindow(WINDOW_CLASS, WINDOW_NAME,
                            WS_OVERLAPPEDWINDOW, 100, 100,
                            640, 480, GetDesktopWindow(), NULL,
                            wc.hInstance, NULL);

   // Initialize Direct3D
   if(InitializeD3D(hWnd, false))
      {
         // Show the window
         ShowWindow(hWnd, SW_SHOWDEFAULT);
         UpdateWindow(hWnd);

         // Enter the message loop
         MSG msg;
         ZeroMemory(&msg, sizeof(msg));

         while(msg.message != WM_QUIT)
            {
               if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
                  {
                     TranslateMessage(&msg);
                     DispatchMessage(&msg);
                  }
               else
                  RenderScene();
            }
      }

   // Release any and all resources.
   Shutdown();

   // Unregister our window.
   UnregisterClass(WINDOW_CLASS, wc.hInstance);
   return 0;
}


bool InitializeD3D(HWND hWnd, bool fullscreen)
{
   D3DDISPLAYMODE displayMode;

   // Create the D3D object.
   g_D3D = Direct3DCreate9(D3D_SDK_VERSION);
   if(g_D3D == NULL) return false;


   // Get the desktop display mode.
   if(FAILED(g_D3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,
      &displayMode))) return false;


   // Set up the structure used to create the D3DDevice
   D3DPRESENT_PARAMETERS d3dpp;
   ZeroMemory(&d3dpp, sizeof(d3dpp));


   if(fullscreen)
      {
         d3dpp.Windowed = FALSE;
         d3dpp.BackBufferWidth = 640;
         d3dpp.BackBufferHeight = 480;
      }
   else
      d3dpp.Windowed = TRUE;
   d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
   d3dpp.BackBufferFormat = displayMode.Format;


   // Create the D3DDevice
   if(FAILED(g_D3D->CreateDevice(D3DADAPTER_DEFAULT,
      D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING,
      &d3dpp, &g_D3DDevice))) return false;


   // Initialize any objects we will be displaying.
   if(!InitializeObjects()) return false;

   return true;
}


bool InitializeObjects()
{
   unsigned long col = D3DCOLOR_XRGB(255, 255, 255);

   // Fill in our structure to draw an object.
   // x, y, z, rhw, color.
   stD3DVertex objData[] =
   {
	   { 420.0f, 150.0f, 0.5f, 1.0f, col, },
	   { 420.0f, 350.0f, 0.5f, 1.0f, col, },
	   { 220.0f, 150.0f, 0.5f, 1.0f, col, },
	   { 220.0f, 350.0f, 0.5f, 1.0f, col, },
   };
   
   // Create the vertex buffer.
   if(FAILED(g_D3DDevice->CreateVertexBuffer(sizeof(objData), 0,
             D3DFVF_VERTEX, D3DPOOL_DEFAULT, &g_VertexBuffer,
             NULL))) return false;

   // Fill the vertex buffer.
   void *ptr;

   if(FAILED(g_VertexBuffer->Lock(0, sizeof(objData),
      (void**)&ptr, 0))) return false;

   memcpy(ptr, objData, sizeof(objData));
  

   g_VertexBuffer->Unlock();

   return true;
}


void RenderScene()
{
   // Clear the backbuffer.
   g_D3DDevice->Clear(0, NULL, D3DCLEAR_TARGET,
                      D3DCOLOR_XRGB(0,0,0), 1.0f, 0);

   // Begin the scene.  Start rendering.
   g_D3DDevice->BeginScene();

      // Render object.
      g_D3DDevice->SetStreamSource(0, g_VertexBuffer, 0,
                                   sizeof(stD3DVertex));
      g_D3DDevice->SetFVF(D3DFVF_VERTEX);
      g_D3DDevice->DrawPrimitive(D3DPT_LINELIST, 0, 2);

   // End the scene.  Stop rendering.
   g_D3DDevice->EndScene();

   // Display the scene.
   g_D3DDevice->Present(NULL, NULL, NULL, NULL);
}


void Shutdown()
{
   if(g_D3DDevice != NULL) g_D3DDevice->Release();
   if(g_D3D != NULL) g_D3D->Release();
   if(g_VertexBuffer != NULL) g_VertexBuffer->Release();

   g_D3DDevice = NULL;
   g_D3D = NULL;
   g_VertexBuffer = NULL;
}



编译并运行该程序,就可以在屏幕中间看到两条白色垂直并列的线段。



笔记三到这里就结束了。


本篇文章之前有部分笔误,谢谢各位热心的指出,现已更正。


本节源代码请点击这里下载:【Visual C++】Code_Note_3


请大家继续关注【 Visual C++】游戏开发笔记系列。



非常希望能与大家一起交流,共同学习和进步。


最后,谢谢大家一直的支持~~~



end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值