共用方式為


使用 DirectX 裝置資源

瞭解在 Windows 市集 DirectX 遊戲中 Microsoft DirectX 圖形基礎結構 (DXGI) 的角色。 DXGI 是一組 API,可用來設定和管理低階圖形和圖形適配卡資源。 如果沒有它,你就無法將遊戲的圖形繪製到視窗。

以這種方式思考 DXGI:若要直接存取 GPU 並管理其資源,您必須有方法向您的應用程式描述它。 GPU 所需的最重要的資訊是繪製圖元的位置,以便將這些像素傳送到螢幕。 通常這被稱為「後緩衝區」,這是 GPU 記憶體中的一個位置,您可以在此繪製圖元,然後透過重新整理訊號將其「翻轉」或「交換」並傳送至畫面。 DXGI 可讓您取得該位置與使用該緩衝區的方法(稱為 交換鏈結,因為它是可交換緩衝區的鏈結,允許多個緩衝策略)。

若要這樣做,您需要存取權來寫入交換鏈,以及顯示交換鏈當前後台緩衝區之視窗的控制代碼。 您也需要連線兩者,以確保作系統會在您要求其執行此動作時,使用後台緩衝區的內容重新整理視窗。

繪製到畫面的整體過程如下所示:

  • 取得應用程式的 CoreWindow
  • 取得 Direct3D 裝置和其上下文的介面。
  • 建立交換鏈,以在 CoreWindow中顯示渲染的影像。
  • 建立一個用於繪圖的渲染目標,並用像素填滿它。
  • 呈現交換鏈結!

為您的應用程式建立視窗

我們需要做的第一件事是建立視窗。 首先,藉由填入 WNDCLASS的實例來建立窗口類別,然後使用 RegisterClass註冊它。 窗口類別包含視窗的基本屬性,包括所使用的圖示、靜態訊息處理函式(稍後會更多內容),以及窗口類別的唯一名稱。

if(m_hInstance == NULL) 
    m_hInstance = (HINSTANCE)GetModuleHandle(NULL);

HICON hIcon = NULL;
WCHAR szExePath[MAX_PATH];
    GetModuleFileName(NULL, szExePath, MAX_PATH);

// If the icon is NULL, then use the first one found in the exe
if(hIcon == NULL)
    hIcon = ExtractIcon(m_hInstance, szExePath, 0); 

// Register the windows class
WNDCLASS wndClass;
wndClass.style = CS_DBLCLKS;
wndClass.lpfnWndProc = MainClass::StaticWindowProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = m_hInstance;
wndClass.hIcon = hIcon;
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = m_windowClassName.c_str();

if(!RegisterClass(&wndClass))
{
    DWORD dwError = GetLastError();
    if(dwError != ERROR_CLASS_ALREADY_EXISTS)
        return HRESULT_FROM_WIN32(dwError);
}

接下來,您會建立視窗。 我們也需要提供視窗的大小資訊,以及我們剛才建立的窗口類別名稱。 當您呼叫 CreateWindow時,您會取得一個稱為 HWND 的視窗不透明指標。 您必須保留 HWND 指標,並隨時使用它來參考視窗,包括終結或重新建立視窗。 特別是,在建立用來在視窗中繪製的 DXGI 交換鏈結時,這一點尤為重要。

m_rc;
int x = CW_USEDEFAULT;
int y = CW_USEDEFAULT;

// No menu in this example.
m_hMenu = NULL;

// This example uses a non-resizable 640 by 480 viewport for simplicity.
int nDefaultWidth = 640;
int nDefaultHeight = 480;
SetRect(&m_rc, 0, 0, nDefaultWidth, nDefaultHeight);        
AdjustWindowRect(
    &m_rc,
    WS_OVERLAPPEDWINDOW,
    (m_hMenu != NULL) ? true : false
    );

// Create the window for our viewport.
m_hWnd = CreateWindow(
    m_windowClassName.c_str(),
    L"Cube11",
    WS_OVERLAPPEDWINDOW,
    x, y,
    (m_rc.right-m_rc.left), (m_rc.bottom-m_rc.top),
    0,
    m_hMenu,
    m_hInstance,
    0
    );

if(m_hWnd == NULL)
{
    DWORD dwError = GetLastError();
    return HRESULT_FROM_WIN32(dwError);
}

Windows 桌面應用程式模型包含鉤入 Windows 訊息迴圈的機制。 基於此掛鉤撰寫 "StaticWindowProc" 函式,以處理視窗訊息,並將其作為主要程式迴圈的基礎。 這必須是靜態函式,因為 Windows 會在任何類別實例的內容之外呼叫它。 以下是靜態訊息處理函式的簡單範例。

LRESULT CALLBACK MainClass::StaticWindowProc(
    HWND hWnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
    )
{
    switch(uMsg)
    {
        case WM_CLOSE:
        {
            HMENU hMenu;
            hMenu = GetMenu(hWnd);
            if (hMenu != NULL)
            {
                DestroyMenu(hMenu);
            }
            DestroyWindow(hWnd);
            UnregisterClass(
                m_windowClassName.c_str(),
                m_hInstance
                );
            return 0;
        }

        case WM_DESTROY:
            PostQuitMessage(0);
            break;
    }
    
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

這個簡單的範例只會檢查程序結束條件:WM_CLOSE,在要求關閉視窗時傳送,WM_DESTROY,這會在窗口實際從畫面中移除之後傳送。 完整、生產應用程式也需要處理其他視窗化事件—如需視窗化事件的完整清單,請參閱 視窗通知

主要程式迴圈本身必須透過讓 Windows 有機會執行靜態訊息處理程序來確認 Windows 訊息。 藉由分流行為協助程式有效率地執行:每次迭代應選擇處理新的 Windows 訊息(如果有),若佇列中沒有訊息便應渲染新的畫面。 以下是一個非常簡單的範例:

bool bGotMsg;
MSG  msg;
msg.message = WM_NULL;
PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);

while (WM_QUIT != msg.message)
{
    // Process window events.
    // Use PeekMessage() so we can use idle time to render the scene. 
    bGotMsg = (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE) != 0);

    if (bGotMsg)
    {
        // Translate and dispatch the message
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        // Update the scene.
        renderer->Update();

        // Render frames during idle time (when no messages are waiting).
        renderer->Render();

        // Present the frame to the screen.
        deviceResources->Present();
    }
}

取得 Direct3D 裝置和環境的介面

使用 Direct3D 的第一個步驟是取得 Direct3D 硬體 (GPU) 的介面,其表示為 ID3D11DeviceID3D11DeviceContext實例。 前者是 GPU 資源的虛擬表示法,後者是轉譯管線和進程的裝置無關的抽象概念。 以下是一種簡單的方法:ID3D11Device 包含您經常呼叫的圖形方法,通常是在任何轉譯發生之前,取得並設定開始繪製圖元所需的資源集。 另一方面,ID3D11DeviceContext包含呼叫每個畫面的方法:載入緩衝區和檢視和其他資源、變更輸出合併和轉譯器狀態、管理著色器,以及繪製透過狀態和著色器傳遞這些資源的結果。

此過程有一個非常重要的部分:設定功能層級。 功能層級會告知 DirectX 應用程式支援的最低硬體層級,D3D_FEATURE_LEVEL_9_1為最低功能集,D3D_FEATURE_LEVEL_11_1為目前最高。 如果您想要達到最廣泛的受眾,您應該至少支援 9_1。 請花一些時間閱讀 Direct3D 功能層級, 並自行評估您希望遊戲支援的最低和最大功能層級,並瞭解您選擇的含意。

取得 Direct3D 裝置和裝置內容的參考(指標),並將它們作為類別層級變數儲存在 DeviceResources 實例中(作為 ComPtr 智慧指標)。 每當您需要存取 Direct3D 裝置或裝置內容時,請使用這些參考。

D3D_FEATURE_LEVEL levels[] = {
    D3D_FEATURE_LEVEL_11_1
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1,
};

// This flag adds support for surfaces with a color-channel ordering different
// from the API default. It is required for compatibility with Direct2D.
UINT deviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

#if defined(DEBUG) || defined(_DEBUG)
deviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

// Create the Direct3D 11 API device object and a corresponding context.
Microsoft::WRL::ComPtr<ID3D11Device>        device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;

hr = D3D11CreateDevice(
    nullptr,                    // Specify nullptr to use the default adapter.
    D3D_DRIVER_TYPE_HARDWARE,   // Create a device using the hardware graphics driver.
    0,                          // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
    deviceFlags,                // Set debug and Direct2D compatibility flags.
    levels,                     // List of feature levels this app can support.
    ARRAYSIZE(levels),          // Size of the list above.
    D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for Windows Store apps.
    &device,                    // Returns the Direct3D device created.
    &m_featureLevel,            // Returns feature level of device created.
    &context                    // Returns the device immediate context.
    );

if (FAILED(hr))
{
    // Handle device interface creation failure if it occurs.
    // For example, reduce the feature level requirement, or fail over 
    // to WARP rendering.
}

// Store pointers to the Direct3D 11.1 API device and immediate context.
device.As(&m_pd3dDevice);
context.As(&m_pd3dDeviceContext);

建立交換鏈結

沒問題:您有一個要繪製的視窗,而且有介面可傳送數據,並將命令提供給 GPU。 現在讓我們看看如何將它們結合在一起。

首先,您會告訴 DXGI 要用於交換鏈屬性的值。 使用 DXGI_SWAP_CHAIN_DESC 結構來執行此動作。 六個字段對於傳統型應用程式特別重要:

  • Windowed:指出交換鏈是全螢幕模式還是限制在視窗。 將此設定為 TRUE,以將交換鏈結放在您稍早建立的視窗中。
  • BufferUsage:將此設定為 DXGI_USAGE_RENDER_TARGET_OUTPUT。 這表示交換鏈將成為繪圖表面,使您可以將其用作 Direct3D 繪製目標。
  • SwapEffect:將此設定為 DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL。
  • 格式:DXGI_FORMAT_B8G8R8A8_UNORM 格式指定 32 位顏色:每個 RGB 顏色通道各 8 位,Alpha 通道 8 位。
  • BufferCount:將此設定為 2,以實現傳統的雙緩衝機制,從而避免畫面撕裂。 如果您的圖形內容在單一畫面轉譯時需要花費超過一個監視器的刷新週期,請將緩衝區計數設定為 3。例如,如果以 60 Hz 的速率刷新,閾值超過 16 毫秒。
  • SampleDesc:此字段會控制多重取樣。 將 計數 設為 1,並將 質量 設為 0,用於翻轉模型交換鏈結。 (若要搭配翻轉模型交換鏈使用樣本多重化,請先在獨立的樣本多重化渲染目標上繪製,然後在呈現之前,將該目標解析至交換鏈。範例代碼提供於 Windows 市集應用程式中的樣本多重化。)

指定交換鏈的組態之後,您必須使用建立 Direct3D 裝置(和裝置內容)的相同 DXGI 工廠來建立交換鏈。

簡短格式:

取得您先前創建的 ID3D11Device 的一個參考。 將它向上轉型為 IDXGIDevice3(如果您尚未這麼做),然後呼叫 IDXGIDevice::GetAdapter 以取得 DXGI 適配卡。 呼叫 IDXGIAdapter::GetParent 來取得該配接器的父處理站(IDXGIAdapter 繼承自 IDXGIObject),現在您可以使用該處理站呼叫 createSwapChainForHwnd來建立交換鏈結,如下列程式代碼範例所示。

DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(DXGI_SWAP_CHAIN_DESC));
desc.Windowed = TRUE; // Sets the initial state of full-screen mode.
desc.BufferCount = 2;
desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SampleDesc.Count = 1;      //multisampling setting
desc.SampleDesc.Quality = 0;    //vendor-specific flag
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
desc.OutputWindow = hWnd;

// Create the DXGI device object to use in other factories, such as Direct2D.
Microsoft::WRL::ComPtr<IDXGIDevice3> dxgiDevice;
m_pd3dDevice.As(&dxgiDevice);

// Create swap chain.
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
Microsoft::WRL::ComPtr<IDXGIFactory> factory;

hr = dxgiDevice->GetAdapter(&adapter);

if (SUCCEEDED(hr))
{
    adapter->GetParent(IID_PPV_ARGS(&factory));

    hr = factory->CreateSwapChain(
        m_pd3dDevice.Get(),
        &desc,
        &m_pDXGISwapChain
        );
}

如果您剛開始,最好使用此處顯示的組態。 現在,如果您已經熟悉舊版 DirectX,您可能會問:「為什麼我們不同時建立裝置和交換鏈,而是回頭走遍所有這些類別?」答案是效率:交換鏈是 Direct3D 裝置資源,裝置資源與創建它們的特定 Direct3D 裝置相關聯。 如果您使用新的交換鏈結建立新的裝置,則必須使用新的 Direct3D 裝置重新建立所有裝置資源。 因此,通過使用相同的工廠建立交換鏈(如上所示),您可以重新創建交換鏈,並繼續使用您已載入的 Direct3D 裝置資源!

現在您已從作業系統取得視窗、存取 GPU 及其資源的方法,以及用來顯示轉譯結果的交換鏈。 剩下的就是把整個系統連接在一起!

建立用於繪圖的渲染目標

著色器管線需要資源來繪製圖元。 建立此資源最簡單的方式是將 ID3D11Texture2D 資源定義為用於圖元著色器繪製的後台緩衝區,然後將該紋理讀入交換鏈結。

若要達成此目的,您需要建立一個渲染目標 檢視。 在 Direct3D 中,檢視是存取特定資源的方法。 在這種情況下,視圖允許像素著色器在完成其每個像素操作時寫入到紋理中。

讓我們看看此程序代碼。 當您在交換鏈上設定DXGI_USAGE_RENDER_TARGET_OUTPUT時,這讓基礎 Direct3D 資源可以用作繪圖表面。 因此,若要取得轉譯目標檢視,我們只需要從交換鏈取得後緩衝區,並建立連結至後緩衝區資源的轉譯目標檢視。

hr = m_pDXGISwapChain->GetBuffer(
    0,
    __uuidof(ID3D11Texture2D),
    (void**) &m_pBackBuffer);

hr = m_pd3dDevice->CreateRenderTargetView(
    m_pBackBuffer.Get(),
    nullptr,
    m_pRenderTarget.GetAddressOf()
    );

m_pBackBuffer->GetDesc(&m_bbDesc);

此外,還需要建立 深度樣板緩衝區。 深度模板緩衝區只是一種特定形式的 ID3D11Texture2D 資源,通常用來根據場景中物體與攝影機的距離,判斷光柵化期間哪些圖元具有繪製優先順序。 深度樣板緩衝區也可用於樣板效果,其中特定圖元會在點陣化期間捨棄或忽略。 這個緩衝區的大小必須與轉譯目標相同。 請注意,您無法讀取或渲染到幀緩衝區的深度樣板紋理,因為著色器管線在最終光柵化之前和過程中會獨佔使用。

同時為深度模板緩衝區建立視圖作為 ID3D11DepthStencilView。 此檢視會告知著色器管線如何解譯基礎 ID3D11Texture2D 資源,因此如果您未提供此檢視,則不會執行每個像素深度測試,並且場景中的物件可能至少會有些顛倒錯亂!

CD3D11_TEXTURE2D_DESC depthStencilDesc(
    DXGI_FORMAT_D24_UNORM_S8_UINT,
    static_cast<UINT> (m_bbDesc.Width),
    static_cast<UINT> (m_bbDesc.Height),
    1, // This depth stencil view has only one texture.
    1, // Use a single mipmap level.
    D3D11_BIND_DEPTH_STENCIL
    );

m_pd3dDevice->CreateTexture2D(
    &depthStencilDesc,
    nullptr,
    &m_pDepthStencil
    );

CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);

m_pd3dDevice->CreateDepthStencilView(
    m_pDepthStencil.Get(),
    &depthStencilViewDesc,
    &m_pDepthStencilView
    );

最後一個步驟是建立檢視區。 這會定義畫面上顯示後台緩衝區的可見矩形;您可以變更檢視區的參數,來變更畫面上顯示的緩衝區部分。 在全螢幕交換鏈結的情況下,此程式碼會以整個視窗大小或螢幕解析度為目標。 為了有趣,請變更提供的座標值,並觀察結果。

ZeroMemory(&m_viewport, sizeof(D3D11_VIEWPORT));
m_viewport.Height = (float) m_bbDesc.Height;
m_viewport.Width = (float) m_bbDesc.Width;
m_viewport.MinDepth = 0;
m_viewport.MaxDepth = 1;

m_pd3dDeviceContext->RSSetViewports(
    1,
    &m_viewport
    );

從無到有在視窗中繪製圖元的方法就是這樣! 在開始時,最好熟悉一下 DirectX 如何透過 DXGI 管理你所需的核心資源,以開始繪製圖元。

接下來,您將查看圖形管線的結構;請參閱 瞭解 DirectX 應用程式樣本的轉譯管線

下一個

使用著色器和著色器資源