13.1 使用DirectX9繪圖引擎

2023-10-10 09:00:14

DirectX 9 是由微軟開發的一組多媒體應用程式介面API,用於建立和執行基於Windows平臺的多媒體應用程式,尤其是遊戲。它是DirectX系列中的一個版本,於2002年釋出,是DirectX系列中的一個重要版本,DirectX 9在其釋出時引入了許多新的功能和效能優化,成為當時PC遊戲開發的主要標準,許多經典的PC遊戲使用了DX9作為其圖形和音訊渲染引擎。雖然後續出現了更多強大的引擎,但本質上都是可以相容Dx9的。

在使用Dx9引擎之前讀者需要自行下載該繪製庫,當然在課件中筆者已經為大家準備了綠色版,讀者可自行解壓到指定目錄下,在目錄下有一個Developer Runtime其內部是引擎執行時所需要的執行環境,讀者可根據不同的需求安裝對應位數的執行庫,安裝成功後則可設定開發目錄,一般而言我們只需要關注Include引入目錄,以及Lib庫目錄即可。

讀者可自行開啟屬性頁面,並選中VC++目錄自行設定,如下圖所示;

13.1.1 初始化變數

在開始使用繪製庫之前我們需要一個可被自由繪製的畫布程式,該程式必須使用D3Dx9引擎生成以便於後續文章的測試工作,一般而言,使用DirectX 9繪製圖形的流程包括初始化、建立資源、設定渲染狀態和頂點格式、更新資料、繪製圖形、渲染和清理資源構成,在使用之前讀者需要引入Dx9的標頭檔案以及所需定義部分,如下所示;

#include <windows.h> 
#include <tchar.h> 
#include <d3d9.h>
#pragma comment( lib, "d3d9.lib") 

#define null NULL
#define RETURN return
#define FVF ( D3DFVF_XYZRHW | D3DFVF_DIFFUSE ) 

LPDIRECT3D9             g_pD3D = NULL;
LPDIRECT3DDEVICE9       g_pd3dDevice = NULL;
LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL;

struct CUSTOMVERTEX
{
    float x, y, z, rhw;
    DWORD color;
};

13.1.2 LPDIRECT3D9

其中定義的全域性指標LPDIRECT3D9DX9中的一個指標型別,表示一個Direct3D 9的頂層物件。頂層物件是Direct3D物件模型的頂級結構,它為應用程式提供了一組方法來進行3D圖形渲染。LPDIRECT3D9介面可以用來建立和操作Direct3D 9裝置物件IDirect3DDevice9以及其他與圖形渲染相關的物件。在使用DX9進行圖形渲染之前,必須通過呼叫Direct3DCreate9函數來建立一個IDirect3D9介面的範例,並通過LPDIRECT3D9型別的指標進行存取和操作。例如,使用下面的程式碼可以建立一個LPDIRECT3D9物件:

LPDIRECT3D9 d3d9 = Direct3DCreate9(D3D_SDK_VERSION);

這將建立一個指向Direct3D 9的頂層物件的指標,並將其分配給變數d3d9。通過這個LPDIRECT3D9物件,應用程式就可以執行各種與圖形渲染相關的操作,如建立頂點快取、紋理物件等。在程式結束時,應用程式必須通過呼叫LPDIRECT3D9物件的Release方法來釋放所有建立的Direct3D物件,以防止記憶體漏失。

d3d9->Release();

13.1.3 LPDIRECT3DDEVICE9

第二個全域性變數LPDIRECT3DDEVICE9DirectX 9中表示3D裝置的指標型別,它是使用Direct3D進行3D渲染的關鍵物件。LPDIRECT3DDEVICE9物件表示著本次渲染中的3D物件在硬體上的運算環境,通過它可以對3D物件進行變換、光照和紋理等操作。通過LPDIRECT3D9物件建立的步驟通常包括以下幾個步驟:

1.建立一個LPDIRECT3D9物件,通過Direct3DCreate9函數建立,如下所示:

LPDIRECT3D9 d3d9 = Direct3DCreate9(D3D_SDK_VERSION);

2.使用LPDIRECT3D9物件建立一個IDirect3DDevice9物件,可以通過呼叫LPDIRECT3D9物件的CreateDevice方法來建立,如下所示:

LPDIRECT3DDEVICE9 d3dDevice;

D3DPRESENT_PARAMETERS presentParams = {0};
presentParams.Windowed = TRUE;
presentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;

d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &presentParams, &d3dDevice);

這個程式碼片段建立了一個視窗化的3D裝置並將其儲存到變數d3dDevice中。其中D3DADAPTER_DEFAULTD3DDEVTYPE_HAL參數列示選擇預設顯示介面卡和硬體抽象層,hWnd引數為視窗控制程式碼,D3DCREATE_HARDWARE_VERTEXPROCESSING表示使用硬體進行頂點計算,&presentParams為一個D3DPRESENT_PARAMETERS結構體指標,用於設定呈現引數。

3.初始化3D裝置物件,可以設定一些統一的裝置狀態,如渲染狀態、混合模式等,它將禁用光照計算。如下所示:

d3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

4.在進行有效的渲染之前,必須在每一幀開始時呼叫BeginScene方法,以告知Direct3D範例開始渲染,如下所示:

d3dDevice->BeginScene();

5.渲染3D物件,通過LPDIRECT3DDEVICE9物件進行繪製,並進行相應的3D資料應用操作,如下所示:

d3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

渲染結束後,可以呼叫EndScene方法通知Direct3D範例結束渲染並顯示影象,如下所示:

d3dDevice->EndScene();

最後,使用SwapChain顯示影象,如下所示:

d3dDevice->Present(NULL, NULL, NULL, NULL);

在程式退出時,藉助於LPDIRECT3DDEVICE9物件的Release方法釋放所有建立的Direct3D物件,以避免記憶體漏失。

d3dDevice->Release();

13.1.4 LPDIRECT3DVERTEXBUFFER9

LPDIRECT3DVERTEXBUFFER9是DirectX 9中表示頂點緩衝區的指標型別,它被用來儲存3D網格的頂點資料,是Direct3D遊戲開發中的一個重要概念之一。頂點緩衝區是一個可以包含頂點資料的記憶體塊,它可以儲存可繪製的幾何體(三角形、四邊形等)的頂點資料。

使用LPDIRECT3DVERTEXBUFFER9物件儲存頂點資料,可以充分利用硬體加速能力,提高渲染效率和圖形效能,優化遊戲效能。

頂點緩衝區由頂點格式和頂點資料兩部分組成:

  • 頂點格式(Vertex Format): 表示頂點資料的結構和排列方式。使用D3DVertexElement9D3DVertexDeclaration9等API進行建立、宣告以及管理。
  • 頂點資料(Vertex Data): 包含了網格的所有頂點資料,如座標、法線、顏色、紋理座標等。可以使用LPDIRECT3DVERTEXBUFFER9物件儲存,同時還可以使用其他緩衝區型別如索引緩衝區(LPDIRECT3DINDEXBUFFER9)來儲存索引資料,方便後續渲染處理。

建立LPDIRECT3DVERTEXBUFFER9物件的步驟通常如下:

首先,宣告並建立一個頂點緩衝區物件。在建立LPDIRECT3DVERTEXBUFFER9物件時,需要指定緩衝區大小、緩衝區用法等引數。

LPDIRECT3DVERTEXBUFFER9 pVertexBuffer = NULL;

device->CreateVertexBuffer(vertexBufferSize, D3DUSAGE_WRITEONLY, FVF, D3DPOOL_MANAGED, &pVertexBuffer, NULL);

寫入頂點資料到頂點緩衝區,使用Lock方法可以將頂點緩衝區鎖定,返回已鎖定的頂點緩衝區指標,並且允許應用程式與鎖定的資料進行讀寫操作,然後使用Unlock方法來解鎖。

float* pBuffer = NULL;

pVertexBuffer->Lock(0, 0, (void**)&pBuffer, 0);

memcpy(pBuffer, vertices, vertexBufferSize);

pVertexBuffer->Unlock();

繪製幾何體時,可以使用SetStreamSource方法指定頂點緩衝區、頂點格式以及偏移量。最後呼叫DrawPrimitive方法進行繪製。

device->SetFVF(FVF);
device->SetStreamSource(0, pVertexBuffer, 0, sizeof(Vertex));
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, numTriangles);

13.1.5 初始化繪圖引擎

接著我們來看一下我們是如何初始化一個D3D引擎的,InitD3D函數會在遊戲程式啟動時被呼叫,以初始化3D裝置和相關環境,為後續的3D圖形渲染操作做好準備。初始化部分答題可總結為三步,首先呼叫Direct3DCreate9用於建立一個Dx9引擎畫布,接著填充D3DPRESENT_PARAMETERS結構,最後通過使用CreateDevice實現對裝置的建立,當建立成功則會將指標儲存在LPDIRECT3D9這個全域性結構指標內。

HRESULT InitD3D(HWND hWnd)
{
  g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
  D3DPRESENT_PARAMETERS d3dpp;
  ZeroMemory(&d3dpp, sizeof(d3dpp));
  d3dpp.Windowed = TRUE;
  d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
  d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice);
  return S_OK;
}

上述流程具體分析,步驟如下:

使用Direct3DCreate9函數建立一個LPDIRECT3D9物件,該物件表示頂層Direct3D物件,負責管理和控制DX操作。

g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);

建立並設定D3DPRESENT_PARAMETERS結構體,該結構體用於描述渲染裝置的一些基本屬性,如視窗模式、後臺緩衝區格式、交換模式等。

D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

在上述程式碼中,使用ZeroMemory()函數將d3dpp物件中除第1個成員外所有成員的值都重置為0。還設定了視窗模式(Windowed = TRUE,表示視窗化模式),後臺緩衝區格式(BackBufferFormat = D3DFMT_UNKNOWN,表示使用預設格式),以及交換模式(SwapEffect = D3DSWAPEFFECT_DISCARD,表示丟棄當前幀並替換為下一幀)。

使用CreateDevice函數建立一個IDirect3DDevice9物件,並儲存在變數g_pd3dDevice中。

g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice);

在這裡,第1個引數(D3DADAPTER_DEFAULT)表示使用預設顯示介面卡;第2個引數(D3DDEVTYPE_HAL)指定使用硬體抽象層,表示硬體加速;第3個引數(hWnd)是視窗控制程式碼;第4個引數(D3DCREATE_HARDWARE_VERTEXPROCESSING)表示使用硬體進行頂點處理。最後,使用&d3dpp、&g_pd3dDevice引數傳遞裝置建立資訊。最後,返回S_OK表示函數執行成功。

初始化部分的第二步則是呼叫InitVB這個函數,該函數用於建立頂點緩衝區,可以用於儲存3D網格的頂點資料,方便後續的渲染處理;

HRESULT InitVB()
{
  CUSTOMVERTEX v[] =
  {
    100, 000, 0, 1, 0xffff0000,
    300, 50, 0, 1, 0xff00ff00,
    500, 400, 0, 1, 0xff0000ff
  };

  g_pd3dDevice->CreateVertexBuffer(3 * sizeof(v), 0, FVF, D3DPOOL_DEFAULT, &g_pVB, 0);

  void* vb;
  g_pVB->Lock(0, 0, (void**)&vb, 0);
  memcpy(vb, v, sizeof(v));
  g_pVB->Unlock();
  return S_OK;
}

上述程式碼中,首先宣告一個CUSTOMVERTEX型別的陣列v,並將其作為輸入引數,其中每一個元素表示一個自定義的頂點,包括位置座標和顏色。

CUSTOMVERTEX v[] =
{
    100, 000, 0, 1, 0xffff0000,
    300, 50, 0, 1, 0xff00ff00,
    500, 400, 0, 1, 0xff0000ff
};

在程式碼中,每個元素都包含了頂點的x、y、z座標、齊次座標w,以及頂點的顏色。

呼叫CreateVertexBuffer函數,建立一個頂點緩衝區物件,並將其儲存在變數g_pVB中。該函數的第1個參數列示緩衝區大小,即儲存頂點資料的位元組數,這裡是3個頂點乘以每個頂點40個位元組(即一個CUSTOMVERTEX型別的大小);第2個引數是填充位元組的數值,設為0表示不填充;第3個引數是頂點格式,表示每個頂點包含的資訊,和CUSTOMVERTEX資料結構一致;第4個引數是緩衝區型別,表示緩衝區的使用方式,D3DPOOL_DEFAULT表示快取區將用於GPU讀寫操作。最後,&g_pVB是返回的頂點緩衝區物件。

g_pd3dDevice->CreateVertexBuffer(3 * sizeof(v), 0, FVF, D3DPOOL_DEFAULT, &g_pVB, 0);

對頂點緩衝區進行鎖定,使用Lock函數使緩衝區可讀寫,並將頂點資料寫入緩衝區中。這裡使用void*型別的指標vb指向頂點緩衝區中的第一個元素,並使用memcpy()函數將頂點陣列的資料拷貝到頂點緩衝區中。並使用Unlock()函數解除頂點緩衝區的鎖定。最後返回S_OK,作為函數執行成功的標誌。

void* vb;
g_pVB->Lock(0, 0, (void**)&vb, 0);
memcpy(vb, v, sizeof(v));
g_pVB->Unlock();

接著對視窗中的圖形進行著色及初始化,

void Render()
{
  g_pd3dDevice->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(176, 196, 222), 1, 0);
  // 設定背景色 黑色
  // g_pd3dDevice->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1, 0);

  g_pd3dDevice->BeginScene();
  g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));
  g_pd3dDevice->SetFVF(FVF);
  //g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 10);
  g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4);
  g_pd3dDevice->EndScene();

  g_pd3dDevice->Present(0, 0, 0, 0);
}

使用Clear函數清除背景,並設定新的背景色。這裡使用D3DCOLOR_XRGB(176, 196, 222),表示顏色值為R:176, G:196, B:222的淺藍色。

g_pd3dDevice->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(176, 196, 222), 1, 0);

使用BeginScene函數開始渲染場景。

g_pd3dDevice->BeginScene();

設定頂點著色器的輸入資料來源。使用SetStreamSource函數設定使用的頂點緩衝區,其中第1個引數是流編號,第2個引數是頂點緩衝區物件,第3個引數是緩衝區內頂點資料的起始點,第4個引數是頂點結構體的大小。

g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

設定頂點格式。使用SetFVF函數描述頂點的結構,這裡的FVF常數是一個結構體標記符號,用於描述頂點的型別和結構。

g_pd3dDevice->SetFVF(FVF);

使用DrawPrimitive函數或DrawIndexedPrimitive函數繪製圖形,這裡使用的是後者。該函數繪製在緩衝區中的三角形列表,根據輸入的位置在緩衝區中查詢三角形點,再連線相鄰的三角形點,形成3D圖形。第1個引數(D3DPT_TRIANGLELIST)表示三角形列表,第2個引數是起始頂點索引,第3個引數是最小頂點索引,第4個引數是被繪製的總頂點數,第5個引數(0)表示要跳過的資料數量,第6個引數(4)表示每個圖元的頂點數。

g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4);

使用EndScene函數結束本次渲染。

g_pd3dDevice->EndScene();

使用Present函數展示渲染結果到視窗中。

g_pd3dDevice->Present(0, 0, 0, 0);

當有了上述初始化函數的封裝後,接著我們就可以在主函數內通過CreateWindow函數建立一個表單,並在初始化流程內通過呼叫InitD3D(hWnd)以及InitVB()對D3D引擎初始化,初始化後進入到該程式的訊息迴圈內,在訊息迴圈內除了通過TranslateMessage捕獲訊息外,還需要不間斷的呼叫Render()用於動態重新整理D3D表單顯示,這樣則可實現動態繪製一個完整表單並載入繪圖引擎的目的;

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    message == WM_CLOSE ? PostQuitMessage(0) : (void)0;
    return DefWindowProc(hWnd, message, wParam, lParam);
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR, int)
{
    wchar_t cn[] = L"LySharkGame";
    WNDCLASS wc;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = cn;
    RegisterClass(&wc);

    DWORD cxScreen = GetSystemMetrics(SM_CXSCREEN);
    DWORD cyScreen = GetSystemMetrics(SM_CYSCREEN);

    HWND hWnd = CreateWindow(cn, TEXT("LySharkGame"), WS_OVERLAPPEDWINDOW, (cxScreen - 1024) / 2, (cyScreen - 768) / 2, 1024, 768, NULL, NULL, hInstance, NULL);
    ShowWindow(hWnd, SW_SHOW);

    InitD3D(hWnd);
    InitVB();

    MSG msg;
    ZeroMemory(&msg, sizeof(msg));
    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            Render();
        }
    }
    return 0;
}

至此我們就得到了一個具有D3D功能的表單,當讀者開啟該表單時即可看到一個標題為LySharkGame的表單,該表單大小為標準的1024x768這個表單輸出效果如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/c0fa8f9c.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!