轉譯架構 I:轉譯簡介

注意

本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。

到目前為止,我們已討論如何建構通用 Windows 平台 (UWP) 遊戲,以及如何定義處理遊戲流程的狀態機器。 現在可以進一步瞭解如何開發轉譯架構了。 我們看看範例遊戲如何使用 Direct3D 11 轉譯遊戲場景。

Direct3D 11 包含一組 API,可存取高效能圖形硬體的進階功能,適用於為圖形密集型應用程式 (例如:遊戲) 建立 3D 圖形。

在畫面上轉譯遊戲圖形,基本上就意味著轉譯一連串的螢幕畫面格。 每格畫面都必須根據檢視轉譯場景中的可見物件。

若要轉譯畫面格,您必須將必要的場景資訊傳遞至硬體,才能呈現在畫面上。 若想在螢幕上顯示任何內容,您需要在遊戲一開始執行,就立即開始轉譯。

目標

您可以設定基本的轉譯架構,以顯示UWP DirectX 遊戲的圖形輸出。 整個過程可簡略拆解成以下三個步驟。

  1. 建立與圖形介面的連接。
  2. 建立繪製圖形所需的資源。
  3. 轉譯畫面格以顯示圖形。

本主題說明如何轉譯圖形 (涵蓋步驟 1 和 3)。

轉譯架構 II:遊戲轉譯>一文說明步驟 2:如何設定轉譯架構,以及如何在轉譯之前準備好資料。

開始使用

建議您先熟悉基本圖形和轉譯概念。 如果您不熟悉 Direct3D 和轉譯,請參閱本主題的「詞彙和概念」一節,內有圖形和轉譯專用詞彙的簡介。

在此遊戲中,GameRenderer 類別代表此範例遊戲的轉譯器。 其負責建立和維護用來產生遊戲視覺效果的所有 Direct3D 11 和 Direct2D 物件。 此外也會維護 Simple3DGame 物件的參照,該物件用來擷取要轉譯的物件清單,以及平視顯示 (HUD) 的遊戲狀態。

在本教學課程的這個部分,我們會著重於在遊戲中轉譯 3D 物件。

建立與圖形介面的連接

如需有關存取硬體以進行轉譯的資訊,請參閱<定義遊戲的 UWP 應用程式架構>主題。

The App::Initialize 方法

std::make_shared 函式 (如下所示) 用於建立 shared_ptrDX::DeviceResources,其也提供裝置的存取權。

在 Direct3D 11 中,裝置用來分配和終結物件、轉譯基本類型,以及透過圖形驅動程式與圖形卡通訊。

void Initialize(CoreApplicationView const& applicationView)
{
    ...

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

轉譯畫面格以顯示圖形

遊戲場景必須在遊戲啟動時轉譯。 轉譯指示始於 GameMain::Run 方法,如下所示。

流程簡化如下:

  1. 更新
  2. 轉譯
  3. 展示

GameMain::Run 方法

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible) // if the window is visible
        {
            switch (m_updateState)
            {
            ...
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

更新

請參閱<管理遊戲流程>主題,取得有關如何在 GameMain::Update 方法中更新遊戲狀態的詳細資訊。

轉譯

GameMain::Run 呼叫 GameRenderer::Render 方法,即可實作轉譯。

如已啟用立體轉譯,則有兩個轉譯階段,分別是左眼和右眼轉譯。 在每個轉譯階段,我們都會將轉譯目標與深度樣板檢視繫結至裝置。 之後則會清除深度樣板檢視。

注意

使用其他方法也可做到立體轉譯,例如:使用頂點執行個體或幾何著色器的單階段立體轉譯。 雙階段轉譯方法較慢,但更方便達到立體轉譯。

一旦遊戲開始執行並載入資源,我們就會更新投影矩陣,每轉譯階段更新一次。 每個檢視的物件都稍有不同。 接下來,我們要設定圖形轉譯管線

注意

請參閱<建立及載入 DirectX 圖形資源>,瞭解有關如何載入資源的詳細資訊。

在此範例遊戲中,轉譯器的用途是對所有物件使用標準頂點配置。 這可簡化著色器設計,且無論物件形狀為何,著色器之間都能輕鬆變更。

GameRenderer::Render 方法

我們將 Direct3D 內容設定為使用輸入頂點配置。 「輸入配置」物件會描述頂點緩衝區資料如何串流至轉譯管線

接下來,我們將 Direct3D 內容設為採用稍早定義的常數緩衝區,頂點著色器管線階段和像素著色器管線階段會用到這些緩衝區。

注意

請參閱<轉譯架構 II:遊戲轉譯>,瞭解有關常數緩衝區定義的詳細資訊。

管線中所有的著色器都會使用相同的輸入配置和常數緩衝區集,因此每格畫面都要設定一次。

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    int renderingPasses = 1;
    if (stereoEnabled)
    {
        renderingPasses = 2;
    }

    for (int i = 0; i < renderingPasses; i++)
    {
        // Iterate through the number of rendering passes to be completed.
        // 2 rendering passes if stereo is enabled.
        if (i > 0)
        {
            // Doing the Right Eye View.
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetViewRight() };

            // Resets render targets to the screen.
            // OMSetRenderTargets binds 2 things to the device.
            // 1. Binds one render target atomically to the device.
            // 2. Binds the depth-stencil view, as returned by the GetDepthStencilView method, to the device.
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-omsetrendertargets

            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());

            // Clears the depth stencil view.
            // A depth stencil view contains the format and buffer to hold depth and stencil info.
            // For more info about depth stencil view, go to: 
            // https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-stencil-view--dsv-
            // A depth buffer is used to store depth information to control which areas of 
            // polygons are rendered rather than hidden from view. To learn more about a depth buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-buffers
            // A stencil buffer is used to mask pixels in an image, to produce special effects. 
            // The mask determines whether a pixel is drawn or not,
            // by setting the bit to a 1 or 0. To learn more about a stencil buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/stencil-buffers

            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // Direct2D -- discussed later
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmapRight());
        }
        else
        {
            // Doing the Mono or Left Eye View.
            // As compared to the right eye:
            // m_deviceResources->GetBackBufferRenderTargetView instead of GetBackBufferRenderTargetViewRight
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetView() };

            // Same as the Right Eye View.
            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // d2d -- Discussed later under Adding UI
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmap());
        }

        const float clearColor[4] = { 0.5f, 0.5f, 0.8f, 1.0f };

        // Only need to clear the background when not rendering the full 3D scene since
        // the 3D world is a fully enclosed box and the dynamics prevents the camera from
        // moving outside this space.
        if (i > 0)
        {
            // Doing the Right Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetViewRight(), clearColor);
        }
        else
        {
            // Doing the Mono or Left Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetView(), clearColor);
        }

        // Render the scene objects
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            if (stereoEnabled)
            {
                // When doing stereo, it is necessary to update the projection matrix once per rendering pass.

                auto orientation = m_deviceResources->GetOrientationTransform3D();

                ConstantBufferChangeOnResize changesOnResize;
                // Apply either a left or right eye projection, which is an offset from the middle
                XMStoreFloat4x4(
                    &changesOnResize.projection,
                    XMMatrixMultiply(
                        XMMatrixTranspose(
                            i == 0 ?
                            m_game->GameCamera().LeftEyeProjection() :
                            m_game->GameCamera().RightEyeProjection()
                            ),
                        XMMatrixTranspose(XMLoadFloat4x4(&orientation))
                        )
                    );

                d3dContext->UpdateSubresource(
                    m_constantBufferChangeOnResize.get(),
                    0,
                    nullptr,
                    &changesOnResize,
                    0,
                    0
                    );
            }

            // Update variables that change once per frame.
            ConstantBufferChangesEveryFrame constantBufferChangesEveryFrameValue;
            XMStoreFloat4x4(
                &constantBufferChangesEveryFrameValue.view,
                XMMatrixTranspose(m_game->GameCamera().View())
                );
            d3dContext->UpdateSubresource(
                m_constantBufferChangesEveryFrame.get(),
                0,
                nullptr,
                &constantBufferChangesEveryFrameValue,
                0,
                0
                );

            // Set up the graphics pipeline. This sample uses the same InputLayout and set of
            // constant buffers for all shaders, so they only need to be set once per frame.
            // For more info about the graphics or rendering pipeline, see
            // https://learn.microsoft.com/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

            // IASetInputLayout binds an input-layout object to the input-assembler (IA) stage. 
            // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
            // Set up the Direct3D context to use this vertex layout. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
            d3dContext->IASetInputLayout(m_vertexLayout.get());

            // VSSetConstantBuffers sets the constant buffers used by the vertex shader pipeline stage.
            // Set up the Direct3D context to use these constant buffers. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-vssetconstantbuffers

            ID3D11Buffer* constantBufferNeverChanges{ m_constantBufferNeverChanges.get() };
            d3dContext->VSSetConstantBuffers(0, 1, &constantBufferNeverChanges);
            ID3D11Buffer* constantBufferChangeOnResize{ m_constantBufferChangeOnResize.get() };
            d3dContext->VSSetConstantBuffers(1, 1, &constantBufferChangeOnResize);
            ID3D11Buffer* constantBufferChangesEveryFrame{ m_constantBufferChangesEveryFrame.get() };
            d3dContext->VSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            ID3D11Buffer* constantBufferChangesEveryPrim{ m_constantBufferChangesEveryPrim.get() };
            d3dContext->VSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);

            // Sets the constant buffers used by the pixel shader pipeline stage. 
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-pssetconstantbuffers

            d3dContext->PSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            d3dContext->PSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);
            ID3D11SamplerState* samplerLinear{ m_samplerLinear.get() };
            d3dContext->PSSetSamplers(0, 1, &samplerLinear);

            for (auto&& object : m_game->RenderObjects())
            {
                // The 3D object render method handles the rendering.
                // For more info, see Primitive rendering below.
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        // Start of 2D rendering
        ...
    }
}

基本轉譯

轉譯場景時,應循環查看所有需要轉譯的物件。 以下步驟將針對各物件重複執行 (基本類型)。

  • 使用模型的世界轉換矩陣和材質資訊,更新常數緩衝區 (m_constantBufferChangesEveryPrim)
  • m_constantBufferChangesEveryPrim 涵蓋每個物件的參數。 其中包括物件至世界的轉換矩陣,以及用於計算光源的色彩和反射指數等材質屬性。
  • 將 Direct3D 內容設為使用網格物件資料的輸入頂點配置,以將資料串流至轉譯管線的輸入組合器 (IA) 階段。
  • 將 Direct3D 內容設為在 IA 階段中使用索引緩衝區。 提供基本資訊:類型、資料順序。
  • 提交繪製呼叫,以繪製已建立索引的基本非執行個體。 GameObject::Render 方法會以指定基本類型專用的資料更新基本常數緩衝區。 這會導致對內容呼叫 DrawIndexed,以繪製該基本類型的幾何形狀。 具體而言,此繪製呼叫會將命令和資料排入圖形處理器 (GPU) 的佇列,並由常數緩衝區資料參數化。 每個繪製呼叫會對各頂點執行一次頂點著色器,然後對基本類型中各個三角形的每個像素,執行一次像素著色器。 紋理是像素著色器進行轉譯時所用狀態的一部分。

以下是使用多個常數緩衝區的原因。

  • 遊戲會使用多個常數緩衝區,但各基本類型只需要更新這些緩衝區一次。 如先前所述,常數緩衝區就像是針對各基本類型執行的著色器輸入。 有些資料是靜態的 (m_constantBufferNeverChanges);有些資料在整個畫面格中保持固定 (m_constantBufferChangesEveryFrame),例如相機的位置;有些資料是基本類型特定,例如色彩和紋理 (m_constantBufferChangesEveryPrim)。
  • 遊戲轉譯器會將這些輸入劃分為不同的常數緩衝區,以最佳化 CPU 和 GPU 使用的記憶體頻寬。 此方式也有助於將 GPU 需要追蹤的資料量降到最低。 GPU 有大量的命令佇列,且每次遊戲呼叫 Draw 時,該命令都會與相關聯的資料一起排入佇列。 遊戲更新基本常數緩衝區並發出下一個 Draw 命令時,圖形驅動程式會將該命令和相關聯的資料排入佇列。 如果遊戲繪製了 100 個基本類型,則佇列中可能會有 100 個常數緩衝區資料副本。 為了將遊戲傳送至 GPU 的資料量降到最低,遊戲會使用個別的基本常數緩衝區,只包含每個基本類型的更新項目。

GameObject::Render 方法

void GameObject::Render(
    _In_ ID3D11DeviceContext* context,
    _In_ ID3D11Buffer* primitiveConstantBuffer
    )
{
    if (!m_active || (m_mesh == nullptr) || (m_normalMaterial == nullptr))
    {
        return;
    }

    ConstantBufferChangesEveryPrim constantBuffer;

    // Put the model matrix info into a constant buffer, in world matrix.
    XMStoreFloat4x4(
        &constantBuffer.worldMatrix,
        XMMatrixTranspose(ModelMatrix())
        );

    // Check to see which material to use on the object.
    // If a collision (a hit) is detected, GameObject::Render checks the current context, which 
    // indicates whether the target has been hit by an ammo sphere. If the target has been hit, 
    // this method applies a hit material, which reverses the colors of the rings of the target to 
    // indicate a successful hit to the player. Otherwise, it applies the default material 
    // with the same method. In both cases, it sets the material by calling Material::RenderSetup, 
    // which sets the appropriate constants into the constant buffer. Then, it calls 
    // ID3D11DeviceContext::PSSetShaderResources to set the corresponding texture resource for the 
    // pixel shader, and ID3D11DeviceContext::VSSetShader and ID3D11DeviceContext::PSSetShader 
    // to set the vertex shader and pixel shader objects themselves, respectively.

    if (m_hit && m_hitMaterial != nullptr)
    {
        m_hitMaterial->RenderSetup(context, &constantBuffer);
    }
    else
    {
        m_normalMaterial->RenderSetup(context, &constantBuffer);
    }

    // Update the primitive constant buffer with the object model's info.
    context->UpdateSubresource(primitiveConstantBuffer, 0, nullptr, &constantBuffer, 0, 0);

    // Render the mesh.
    // See MeshObject::Render method below.
    m_mesh->Render(context);
}

MeshObject::Render 方法

void MeshObject::Render(_In_ ID3D11DeviceContext* context)
{
    // PNTVertex is a struct. stride provides us the size required for all the mesh data
    // struct PNTVertex
    //{
    //  DirectX::XMFLOAT3 position;
    //  DirectX::XMFLOAT3 normal;
    //  DirectX::XMFLOAT2 textureCoordinate;
    //};
    uint32_t stride{ sizeof(PNTVertex) };
    uint32_t offset{ 0 };

    // Similar to the main render loop.
    // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
    ID3D11Buffer* vertexBuffer{ m_vertexBuffer.get() };
    context->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);

    // IASetIndexBuffer binds an index buffer to the input-assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetindexbuffer.
    context->IASetIndexBuffer(m_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0);

    // Binds information about the primitive type, and data order that describes input data for the input assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetprimitivetopology.
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // Draw indexed, non-instanced primitives. A draw API submits work to the rendering pipeline.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-drawindexed.
    context->DrawIndexed(m_indexCount, 0, 0);
}

DeviceResources::Present 方法

我們會呼叫 DeviceResources::P resent 方法以顯示放置於緩衝區的內容。

我們以「交換鏈結」一詞表示向用戶顯示畫面格的緩衝區集合。 每次應用程式在螢幕上呈現新的畫面格,交換鏈結中的第一個緩衝區就會取代已顯示緩衝區的位置。 此程序稱為交換或翻轉。 如需詳細資訊,請參閱<交換鏈結>。

  • IDXGISwapChain1 介面的 Present 方法會指示 DXGI 封鎖,直到系統開始垂直同步處理 (VSync),此時應用程式將進入睡眠狀態直到下次 VSync。 這可確保系統不會浪費任何週期,轉譯永遠不會在螢幕上顯示的畫面格。
  • ID3D11DeviceContext3 介面的 DiscardView 方法會捨棄轉譯目標的內容。 只有系統已完全覆寫現有內容,此作業才有效。 若使用已修改 (dirty) 或捲動 (scroll) 矩形,則應移除此呼叫。
  • 使用相同的 DiscardView 方法,捨棄深度樣板的內容。
  • HandleDeviceLost 方法用來管理要移除的 device 情境。 如果裝置因連線中斷或驅動程序升級而遭到移除,則必須重新建立所有裝置資源。 如需詳細資訊,請參閱<處理 Direct3D 11 中的裝置已移除案例>。

提示

若要達到流暢的畫面播放速率,您必須確保 VSyncs 之間的時間能處理轉譯畫面格的工作量。

// Present the contents of the swap chain to the screen.
void DX::DeviceResources::Present()
{
    // The first argument instructs DXGI to block until VSync, putting the application
    // to sleep until the next VSync. This ensures we don't waste any cycles rendering
    // frames that will never be displayed to the screen.
    HRESULT hr = m_swapChain->Present(1, 0);

    // Discard the contents of the render target.
    // This is a valid operation only when the existing contents will be entirely
    // overwritten. If dirty or scroll rects are used, this call should be removed.
    m_d3dContext->DiscardView(m_d3dRenderTargetView.get());

    // Discard the contents of the depth stencil.
    m_d3dContext->DiscardView(m_d3dDepthStencilView.get());

    // If the device was removed either by a disconnection or a driver upgrade, we 
    // must recreate all device resources.
    if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
    {
        HandleDeviceLost();
    }
    else
    {
        winrt::check_hresult(hr);
    }
}

下一步

本主題說明如何在顯示器上轉譯圖形,且下文將簡介一些使用的轉譯詞彙。 請參閱<轉譯架構 II:遊戲轉譯>主題,瞭解有關轉譯的詳細資訊,以及轉譯前如何準備必要資料。

詞彙和概念

簡單的遊戲場景

具有數個光源的物件即可構成一個簡單的遊戲場景。

物件形狀是由空間中的一組 X、Y、Z 座標所定義。 將轉換矩陣套用至 X、Y、Z 位置座標,即可確定遊戲世界中的實際轉譯位置。 也可能會有一組紋理座標 (U 和 V),用於指定材質如何套用至物件。 此可定義物件的表面屬性,並讓您查看物件表面是粗糙 (如網球) 或光滑亮澤 (如保齡球)。

轉譯架構會使用場景和物件資訊逐格重建場景,使其躍然於顯示器螢幕。

轉譯管線

轉譯管線是指將 3D 場景資訊轉譯為螢幕顯示影像的處理程序。 在 Direct3D 11 中,此管線可程式化。 您可以根據轉譯需求調整階段。 對於提供通用著色器核心的階段,可使用 HLSL 程式設計語言進行程式化。 這又稱為「圖形轉譯管線」,或簡稱「管線」

若要建立此管線,請務必熟悉這些詳細資料。

如需詳細資訊,請參閱<瞭解 Direct3D 11 轉譯管線>和<圖形管線>。

HLSL

HLSL 是 DirectX 的高階著色器語言。 透過 HLSL,您可以為 Direct3D 管線建立類 C 程式語言的可程式化著色器。 如需詳細資訊,請參閱 HLSL

著色器

您可將著色器視為一組指示,用來決定物件轉譯時的表面呈現方式。 使用 HLSL 進行程式設計的即稱為 HLSL 著色器。 [HLSL])(#hlsl) 著色器的原始程式碼檔案具有 .hlsl 副檔名。 這些著色器可在組建期間或執行階段進行編譯,並在執行階段設為適當的管線階段。 已編譯的著色器物件具有 .cso 副檔名。

Direct3D 9 著色器可用著色器模型 1、著色器模型 2 和著色器模型 3 進行設計;Direct3D 10 著色器只能使用著色器模型 4 設計。 Direct3D 11 著色器可在著色器模型 5 上設計。 Direct3D 11.3 和 Direct3D 12 可在著色器模型 5.1 上設計;Direct3D 12 也可在著色器模型 6 上設計。

頂點著色器和像素著色器

資料會以基本類型資料流的形式進入圖形管線,並由各種著色器 (例如:頂點和像素著色器) 進行處理。

頂點著色器會處理頂點,通常會執行轉換、面板設定和光源設定之類的作業。 像素著色器提供豐富的著色技術,例如:每像素光線和後續處理。 該著色器會結合常數變數、紋理資料、每頂點插補值及其他資料,以按像素產生輸出。

著色器階段

在轉譯管線中,定義一系列不同的著色器,以處理此基本類型資料流,就稱為著色器階段。 實際階段取決於 Direct3D 版本,但通常包含頂點、幾何和像素階段。 另外還有其他階段,例如:用於鑲嵌的輪廓和網域著色器,以及計算著色器。 這所有階段全都可以使用 HLSL 程式化。 如需詳細資訊,請參閱<圖形管線>。

各種著色器檔案格式

以下是著色器程式碼檔案副檔名。

  • 副檔名為 .hlsl 的檔案會保留 [HLSL])(#hlsl) 原始程式碼。
  • 副檔名為 .cso 的檔案會保留已編譯的著色器物件。
  • 副檔名為 .h 的檔案是標頭檔,但在著色器程式碼內容中,該標頭檔會定義保留著色器資料的位元組陣列。
  • 副檔名為 .hlsli 的檔案包含常數緩衝區的格式。 在範例遊戲中,檔案為 [著色器]>[ConstantBuffers.hlsli]。

注意

在執行階段載入 .cso 檔案,或在可執行檔程式碼中新增 .h 檔案,即可內嵌著色器。 不過,這兩種做法不能同時用於同一個著色器。

DirectX 深入解析

Direct3D 11 包含一組 API,可用來為圖形密集型應用程式建立圖形,例如遊戲應用程式,在此情況下,我們通常會想用好一點的圖形卡處理大量運算作業。 本節將簡要說明 Direct3D 11 圖形程式設計概念:資源、子資源、裝置和裝置內容。

資源

您可以將資源 (也稱為裝置資源) 想成是物件轉譯方式的相關資訊,例如:紋理、位置或色彩。 資源會提供資料給管線,並定義在場景中轉譯的內容。 您可以從遊戲媒體載入資源,或在執行階段動態建立。

事實上,資源即 Direct3D 管線在記憶體中可存取的區域。 為了讓管線有效地存取記憶體,提供給管線的資料 (例如,輸入幾何、著色器資源及紋理) 必須儲存在資源中。 所有 Direct3D 資源衍生兩種類型的資源︰緩衝區或紋理。 每個管線階段可使用多達 128 種資源。 如需詳細資訊,請參閱資源

子資源

子資源一詞是指資源的子集。 Direct3D 可參照整個資源,也可參照資源的子集。 如需詳細資訊,請參閱<子資源>。

深度樣板

深度樣板資源包含用以保存深度和樣板資訊的格式和緩衝區。 其使用紋理資源建立而成。 如需如何建立深度樣板資源的詳細資訊,請參閱<設定深度樣板功能>。 我們會透過以 ID3D11DepthStencilView 介面實作的深度樣板檢視,存取深度樣板資源。

深度資訊會指出多邊形的哪些區域位在其他物件後面,有助我們判斷隱藏的區域。 樣板資訊指出哪些像素有遮罩。 該資訊可用來產生特殊效果,因為其可判斷是否要繪製像素;將位元設為 1 或 0。

如需詳細資訊,請參閱<深度樣板檢視>、<深度緩衝區>和<樣板緩衝區>。

轉譯目標

轉譯目標是可在轉譯階段結尾寫入的資源。 通常使用 ID3D11Device::CreateRenderTargetView 方法建立,並以交換鏈結背景緩衝區 (也就是資源) 作為輸入參數。

每個轉譯目標也應有對應的深度樣板檢視,因為在使用轉譯目標之前以 OMSetRenderTargets 進行設定時,亦需要深度樣板檢視。 我們透過以 ID3D11RenderTargetView 介面實作的轉譯目標檢視,存取轉譯目標資源。

裝置

您可以想像裝置是用來分配和終結物件、轉譯基本類型,以及透過圖形驅動程式與圖形卡通訊。

更精確來說,Direct3D 裝置其實是 Direct3D 的轉譯元件。 裝置封裝並儲存呈現狀態、執行轉換照明作業,並將影像點陣化到表面。 如需詳細資訊,請參閱<裝置>。

裝置是由 ID3D11Device 介面轉譯。 換句話說,ID3D11Device 介面代表虛擬顯示卡,用來建立裝置擁有的資源。

ID3D11Device 有不同版本。 ID3D11Device5 是最新版本,會將新方法新增至 ID3D11Device4 中的方法。 如需 Direct3D 如何與基礎硬體通訊的詳細資訊,請參閱<Windows 裝置驅動程式模型 (WDDM) 架構>。

每個應用程式都須有至少一個裝置;大部分應用程式只建立一個。 呼叫 D3D11CreateDeviceD3D11CreateDeviceAndSwapChain,並使用 D3D_DRIVER_TYPE 旗標指定驅動程式類型。 根據所需的功能,各裝置都可使用一或多項裝置內容。 如需詳細資訊,請參閱<D3D11CreateDevice 函式>。

裝置內容

裝置內容可用來設定管線狀態,並使用裝置擁有的資源產生轉譯命令。

Direct3D 11 會實作兩類裝置內容:一種用於立即轉譯,另一種用於延遲轉譯。這兩種內容都以 ID3D11DeviceContext 介面轉譯。

ID3D11DeviceContext 介面有不同版本;ID3D11DeviceContext4 會將新方法新增至 ID3D11DeviceContext3 中的方法。

Windows 10 Creators Update 已引進 ID3D11DeviceContext4,其為最新版的 ID3D11DeviceContext 介面。 若應用程式的目標設定為 Windows 10 Creators Update 和更新版本,則應使用此介面,而非舊版介面。 如需詳細資訊,請參閱<ID3D11DeviceContext4>。

DX::DeviceResources

DX::DeviceResources 類別位於 DeviceResources.cpp/.h 檔案,並控制所有 DirectX 裝置資源。

緩衝區

緩衝區資源是完整類型化資料的集合,可依元素區分。 您可使用緩衝區儲存各種資料,包括位置向量、法線向量、頂點緩衝區中的紋理座標、索引緩衝區中的索引或裝置狀態。 緩衝區項目可包含封裝的資料值 (例如 R8G8B8A8 表面值)、單一 8 位元整數,或四個 32 位元浮點值。

有三種類型的緩衝區可用:頂點緩衝區、索引緩衝區和常數緩衝區。

頂點緩衝區

包含用來定義幾何的頂點資料。 頂點資料包含位置座標、色彩資料、紋理座標資料,以及法線資料等。

索引緩衝區

包含頂點緩衝區的整數位移,可用來更有效率地轉譯基本類型。 索引緩衝區包含一組循序的 16 位元或 32 位元索引;每個索引都會用來識別頂點緩衝區中的頂點。

常數緩衝區 (又稱「著色器常數緩衝區」)

可讓您有效率地將著色器資料提供給管線。 對於執行每個基本類型的著色器,您可將常數緩衝區作為著色器的輸入,並儲存轉譯管線的資料流輸出階段結果。 在概念上,常數緩衝區看起來就像是單一元素頂點緩衝區。

緩衝區的設計和實作

您可以根據數據類型設計緩衝區,例如:在範例遊戲中,可設計一個緩衝區用於靜態資料、一個用於在整個畫面保持不變的資料,還有一個用於基本類型專用的資料。

所有緩衝區類型都是由 ID3D11Buffer 介面封裝,您可呼叫 ID3D11Device::CreateBuffer 以建立緩衝區資源。 但是必須先將緩衝區繫結至管線,才能存取該緩衝區。 緩衝區可同時繫結至多個管線階段,以進行讀取。 緩衝區也可繫結至單一管線階段以進行寫入,但無法繫結同一個緩衝區同時進行讀取和寫入。

您可以透過下列方式繫結緩衝區。

  • 呼叫 ID3D11DeviceContext 方法 (例如:ID3D11DeviceContext::IASetVertexBuffersID3D11DeviceContext::IASetIndexBuffer) 以繫結至輸入組合器階段。
  • 呼叫 ID3D11DeviceContext::SOSetTargets 以繫結至串流輸出階段。
  • 呼叫著色器方法 (例如:ID3D11DeviceContext::VSSetConstantBuffers) 以繫結至著色器階段。

如需詳細資訊,請參閱<Direct3D 11 中的緩衝區簡介>。

DXGI

Microsoft DirectX Graphics Infrastructure (DXGI) 是一個子系統,可封裝 Direct3D 所需的一些低階工作。 在多執行緒應用程式中使用 DXGI 時,請務必特別小心,確保不會發生鎖死。 如需詳細資訊,請參閱<多執行緒和 DXGI

功能層級

Direct3D 11 引進了「功能層級」概念,用於處理新機器和現有機器中的多樣視訊卡。 功能層級是一組定義明確的圖形處理器 (GPU) 功能。

每張視訊卡會根據安裝的 GPU 實作特定層級的 DirectX 功能。 在舊版 Microsoft Direct3D 中,您可以找出實作視訊卡的 Direct3D 版本,然後據以進行應用程式的程式設計。

若使用功能層級,您建立裝置時,可嘗試為要求的功能層級建立裝置。 若裝置可正常建立,代表已有該功能層級;若不能建立,代表硬體不支援該功能層級。 您可以嘗試在較低的功能層級重新建立裝置,或選擇結束應用程式。 例如,12_0 功能層級須使用 Direct3D 11.3 或 Direct3D 12,以及著色器模型 5.1。 如需詳細資訊,請參閱<Direct3D 功能層級:各功能層級概觀>。

若使用功能層級,您可以開發適用於 Direct3D 9、Microsoft Direct3D 10 或 Direct3D 11 的應用程式,然後在 9、10 或 11 硬體上執行該應用程式 (但有些例外狀況)。 如需詳細資訊,請參閱<Direct3D 功能層級>。

立體轉譯

立體轉譯可用來增強深度錯覺。 其使用兩個影像,分別從左、右眼在顯示螢幕上呈現場景。

在數學上,我們會採用立體投影矩陣 (一般單眼投影矩陣的輕微左右水平位移),以達到此目的。

我們進行了兩個轉譯階段,在此範例遊戲中完成立體轉譯。

  • 繫結至正確的轉譯目標、套用右投影,然後繪製基本物件。
  • 繫結至左轉譯目標、套用左投影,然後繪製基本物件。

相機和座標空間

遊戲已有程式碼,可在自己的座標系統 (有時稱為世界空間或場景空間) 中更新世界。 所有物件 (包括相機) 都是在這個空間定位和定向。 如需詳細資訊,請參閱<座標系統>。

頂點著色器會使用下列演算法進行繁重的工作,將模型座標轉換成裝置座標 (其中,V 為向量、M 為矩陣)。

V(device) = V(model) x M(model-to-world) x M(world-to-view) x M(view-to-device)

  • M(model-to-world) 是將模型座標轉換成世界座標的矩陣,也稱為「世界轉換矩陣」。 此由基本類型提供。
  • M(world-to-view) 是將世界座標轉換成檢視座標的矩陣,也稱為「檢視轉換矩陣」。
    • 此由相機的檢視矩陣提供。 由相機的位置及視角向量定義 (look at 向量直接從相機指向場景;look up 向量則是垂直向上)。
    • 在範例遊戲中,m_viewMatrix 是檢視轉換矩陣,使用 Camera::SetViewParams 計算而得。
  • M(view-to-device) 是將檢視座標轉換成裝置座標的矩陣,也稱為「投影轉換矩陣」。
    • 此由相機的投影提供。 可提供最終場景中實際可見多少空間的相關資訊。 視野 (FoV)、外觀比例和裁剪平面可定義投影轉換矩陣。
    • 在範例遊戲中,m_projectionMatrix 可定義投影座標的轉換,使用 Camera::SetProjParams 計算而得 (若為立體投影,可使用兩個投影矩陣:兩個視角各一)。

VertexShader.hlsl 中的著色器程式碼會從常數緩衝區載入這些向量和矩陣,並針對每個頂點執行此轉換。

座標轉換

Direct3D 使用三個轉換,將 3D 模型座標變更為像素座標 (螢幕空間)。 這些轉換是世界轉換、檢視轉換和投影轉換。 如需詳細資訊,請參閱<轉換概觀>。

世界轉換矩陣

世界轉換會將座標從模型空間 (頂點是相對於模型本地原點來定義) 變更為世界空間 (頂點是相對於場景中所有物件通用的原點來定義)。 基本上,世界轉換是將模型置於世界 (顧名思義)。 如需詳細資訊,請參閱<世界轉換>。

檢視轉換矩陣

檢視轉換在世界空間中定位檢視器,並將頂點轉換為相機空間。 在相機空間中,相機 (檢視器) 位於原點,並朝正向 z 軸。 如需詳細資訊,請移至<檢視轉換>頁面。

投影轉換矩陣

投影轉換會將檢視範圍轉換為長方體形狀。 檢視範圍是場景中的 3D 體積,定位相對於檢視區相機。 檢視區是 2D 矩形,3D 場景會投影至該區。 如需詳細資訊,請參閱<檢視區和裁剪

由於檢視範圍的近端小於遠端,因此會使相機附近的物件產生擴大效果;這也是將透視圖套用至場景的方式。 因此,接近玩家的物件看起來較大;較遠的物件看起來較小。

在數學上,投影轉換通常是一個縮放比例和透視投影的矩陣。 運作方式如同相機鏡頭。 如需詳細資訊,請參閱<投影轉換>。

取樣器狀態

取樣器狀態會以紋理定址模式、篩選和詳細程度,決定紋理資料的取樣方式。 每次從紋理讀取紋理像素 (texel) 時都會進行取樣。

紋理包含紋理像素陣列。 每個紋理像素的位置都以 (u,v) 表示,其中 u 是寬度、v 是高度,且會根據紋理寬度和高度對應至 0 到 1 之間的值。 取樣紋理時,產生的紋理座標可用來定址紋理像素。

紋理座標低於 0 或高於 1 時,紋理定址模式會定義紋理座標如何處理紋理像素的位置。 例如:使用 TextureAddressMode.Clamp時,所有在 0-1 範圍以外的座標在取樣前的最大值都限為 1、最小值則限為 0。

如果多邊形的紋理太大或太小,則篩選符合空間的紋理。 放大篩選會放大紋理,縮小篩選則減少紋理,以符合較小的區域。 紋理放大會針對一或多個位址重複執行取樣紋理像素,這會產生較模糊的影像。 紋理縮小的作業比較複雜,因為需要將多個紋理像素值合併成單一值。 這有可能造成失真或鋸齒狀邊緣,視紋理資料而定。 最常見的縮小方式是使用 mipmap。 Mipmap 是多層級紋理。 每一層大小都會比前一層小 2 次方,直到 1 x 1 紋理。 使用縮小紋理時,遊戲會依轉譯當下所需的大小選擇最接近的 mipmap 層級。

BasicLoader 類別

BasicLoader 是簡單的載入器類別,支援從磁碟上的檔案載入著色器、紋理和網格。 其提供同步及非同步方法。 在此範例遊戲,BasicLoader.h/.cpp 檔案位於 [公用程式] 資料夾中。

如需詳細資訊,請參閱<基本載入器>。