共用方式為


在 DirectX 遊戲中載入資源

大多數遊戲在某些時候會本機儲存體或其他資料流載入資源和資產 (例如著色器、紋理、預先定義網格或其他圖形資料)。 我們在此將逐步引導您全面瞭解,載入這些檔案以在 DirectX C/C++ 通用 Windows 平台 (UWP) 遊戲中使用時必須考慮的事項。

例如,遊戲中多邊形物件的網格可能是使用其他工具建立的,並匯出為特定格式。 對於紋理也是如此,而且更甚者是:雖然大多數工具通常都可以編寫平面的、未壓縮的點陣圖,並且大多數圖形 API 都可以理解,但在遊戲中的使用效率可能非常低。 我們在此引導您完成載入三種不同類型的圖形資源以用於 Direct3D 的基本步驟:網格 (模型)、紋理 (點陣圖) 和已編譯的著色器物件。

您需要瞭解的資訊

技術

  • 並行模式程式庫 (ppltasks.h)

必要條件

  • 瞭解基本 Windows 執行階段
  • 了解非同步工作
  • 瞭解 3D 圖形程式設計的基本概念。

此範例也包含三個用於資源載入和管理的程式碼檔案。 您將會在本主題中遇到這些檔案中定義的程式碼物件。

  • BasicLoader.h/.cpp
  • BasicReaderWriter.h/.cpp
  • DDSTextureLoader.h/.cpp

您可以在下列連結中找到這些範例的完整程式碼。

主題 說明

BasicLoader 的完整程式碼

將圖形網格物件轉換並載入到記憶體中的類別和方法的完整程式碼。

BasicReaderWriter 的完整程式碼

完成類的程式碼以及讀取和寫入一般二進位制資料檔案的方法。 由 BasicLoader class 使用。

DDSTextureLoader 的完整程式碼

從記憶體載入 DDS 紋理的類別和方法的完整程式碼。

 

指示

非同步載入

使用並行模式程式庫 (PPL) 中的工作範本來處理非同步載入。 工作包含一個方法呼叫,後面跟著一個 lambda,用於在非同步呼叫完成後處理結果,通常遵循以下格式:

task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });.

可以使用 .then() 語法將任務鏈結在一起,如此當一個操作完成時,可以執行另一個依賴前一操作結果的非同步操作。 如此一來,您就可以在個別執行緒上載入、轉換和管理複雜資產,讓玩家幾乎看不見。

如需詳細資訊,請參閱 C++ 中的非同步程式設計。

現在,讓我們來看看宣告和建立非同步檔案載入方法 ReadDataAsync 的基本結構。

#include <ppltasks.h>

// ...
concurrency::task<Platform::Array<byte>^> ReadDataAsync(
        _In_ Platform::String^ filename);

// ...

using concurrency;

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

在此程式碼中,當您的程式碼呼叫上面定義的 ReadDataAsync 方法時,將建立一個工作來從檔案系統讀取緩衝區。 完成之後,鏈結工作會取得緩衝區,並使用靜態 DataReader 類型,從該緩衝區將位元組串流至陣列。

m_basicReaderWriter = ref new BasicReaderWriter();

// ...
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
      // Perform some operation with the data when the async load completes.          
    });

這是您對 ReadDataAsync 進行的呼叫。 完成時,您的程式碼會收到從提供的檔案讀取的位元組陣列。 由於 ReadDataAsync 本身被定義為工作,因此您可以在傳回位元組陣列時使用 lambda 執行特定操作,例如將該位元組資料傳遞給可以使用它的 DirectX 函式。

如果您的遊戲相當簡單,請在使用者啟動遊戲時,使用類似的方法載入您的資源。 您可以在從 IFrameworkView::Run 實作的呼叫序列中的某個點開始主遊戲迴圈之前執行此操作。 同樣地,您會以非同步方式呼叫您的資源載入方法,讓遊戲可以更快速啟動,因此玩家不必等到載入完成,再進行早期互動。

但是,在所有非同步載入完成之前,您不想正確啟動遊戲! 建立一些在載入完成時發出訊號的方法 (例如特定欄位),並在載入方法上使用 lambda 來在完成時設定該訊號。 在啟動使用這些已載入資源的任何元件之前,請先檢查變數。

以下是使用 BasicLoader.cpp 中定義的非同步方法,在遊戲啟動時載入著色器、網格和紋理的範例。 請注意,當所有載入方法完成時,它會在遊戲物件 m_loadingComplete 上設定一個特定欄位。

void ResourceLoading::CreateDeviceResources()
{
    // DirectXBase is a common sample class that implements a basic view provider. 
    
    DirectXBase::CreateDeviceResources(); 

    // ...

    // This flag will keep track of whether or not all application
    // resources have been loaded.  Until all resources are loaded,
    // only the sample overlay will be drawn on the screen.
    m_loadingComplete = false;

    // Create a BasicLoader, and use it to asynchronously load all
    // application resources.  When an output value becomes non-null,
    // this indicates that the asynchronous operation has completed.
    BasicLoader^ loader = ref new BasicLoader(m_d3dDevice.Get());

    auto loadVertexShaderTask = loader->LoadShaderAsync(
        "SimpleVertexShader.cso",
        nullptr,
        0,
        &m_vertexShader,
        &m_inputLayout
        );

    auto loadPixelShaderTask = loader->LoadShaderAsync(
        "SimplePixelShader.cso",
        &m_pixelShader
        );

    auto loadTextureTask = loader->LoadTextureAsync(
        "reftexture.dds",
        nullptr,
        &m_textureSRV
        );

    auto loadMeshTask = loader->LoadMeshAsync(
        "refmesh.vbo",
        &m_vertexBuffer,
        &m_indexBuffer,
        nullptr,
        &m_indexCount
        );

    // The && operator can be used to create a single task that represents
    // a group of multiple tasks. The new task's completed handler will only
    // be called once all associated tasks have completed. In this case, the
    // new task represents a task to load various assets from the package.
    (loadVertexShaderTask && loadPixelShaderTask && loadTextureTask && loadMeshTask).then([=]()
    {
        m_loadingComplete = true;
    });

    // Create constant buffers and other graphics device-specific resources here.
}

請注意,工作已使用 && 運算子進行彙總,以便僅當所有工作完成時才會觸發設定載入完成旗標的 lambda。 請注意,如果您有多個旗標,則可能會出現比賽條件。 例如,如果 lambda 依序將兩個旗標設為相同的值,則另一個執行緒如果在設定第二個旗標之前檢查它們,則可能只能看到第一個旗標設定。

您已瞭解如何以非同步方式載入資源檔。 同步檔案載入要簡單得多,您可以在 BasicReaderWriter 的完整程式碼BasicLoader 的完整程式碼中找到它們的範例。

當然,不同的資源和資產類型通常需要額外的處理或轉換,才能準備好在圖形管線中使用。 讓我們看看三種特定的資源類型:網格、紋理和著色器。

載入網格

網格是頂點資料,可以由遊戲中的程式碼按照程序產生,也可以從其他應用程式 (例如 3DStudio MAX 或 Alias WaveFront) 或工具匯出到檔案。 這些網格代表遊戲中的模型,從簡單的基本類型,如立方體和球體到汽車和房屋和人物。 它們通常也會包含色彩和動畫資料,視其格式而定。 我們將著重於只包含頂點資料的網格。

若要正確載入網格,您必須知道網格檔案中的資料格式。 我們上述簡單的 BasicReaderWriter 類型只是將資料當作位元組串流讀取;它不知道位元組資料代表一個網格,更不用說由另一個應用程式匯出的特定網格格式了! 您必須在將網格資料存入記憶體時執行轉換。

(您應該一律嘗試以盡可能接近內部表示法的格式封裝資產資料。這樣做將減少資源利用率並節省時間。)

讓我們從網格的檔案取得位元組資料。 範例中的格式假定該檔案是範例特定的格式,以 .vbo 結尾。 (同樣地,此格式與 OpenGL 的 VBO 格式不同。) 每個頂點本身都會對應到 BasicVertex 類型,該類型是 obj2vbo 轉換器工具程式碼中定義的結構。 .vbo 檔案中頂點資料的配置如下所示:

  • 資料串流的前 32 位元 (4 個位元組) 包含網格中的頂點數 (numVertices),表示為 uint32 值。
  • 資料流的下一個 32 位元 (4 個位元組) 包含網格中的索引數量 (numIndices),表示為 uint32 值。
  • 之後,後續的 (numVertices * sizeof(BasicVertex)) 位元包含頂點資料。
  • 最後一個 (numIndices * 16) 位元資料包含索引資料,表示為 uint16 值序列。

重點是:瞭解您已載入之網格資料的位元層級配置。 此外,請確定您與 endian-ness 一致。 所有 Windows 8 平台都是由小到大。

在範例中,您從 LoadMeshAsync 方法來執行此位元層級解釋。

task<void> BasicLoader::LoadMeshAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ meshData)
    {
        CreateMesh(
            meshData->Data,
            vertexBuffer,
            indexBuffer,
            vertexCount,
            indexCount,
            filename
            );
    });
}

CreateMesh 能解釋從檔案載入的位元組資料,並透過將頂點和索引清單分別傳遞到 ID3D11Device::CreateBuffer 並指定 D3D11_BIND_VERTEX_BUFFER 或 D3D11_BIND_INDEX_BUFFER 來為網格建立頂點緩衝區和索引緩衝區。 這是 BasicLoader 中使用的程式碼:

void BasicLoader::CreateMesh(
    _In_ byte* meshData,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount,
    _In_opt_ Platform::String^ debugName
    )
{
    // The first 4 bytes of the BasicMesh format define the number of vertices in the mesh.
    uint32 numVertices = *reinterpret_cast<uint32*>(meshData);

    // The following 4 bytes define the number of indices in the mesh.
    uint32 numIndices = *reinterpret_cast<uint32*>(meshData + sizeof(uint32));

    // The next segment of the BasicMesh format contains the vertices of the mesh.
    BasicVertex* vertices = reinterpret_cast<BasicVertex*>(meshData + sizeof(uint32) * 2);

    // The last segment of the BasicMesh format contains the indices of the mesh.
    uint16* indices = reinterpret_cast<uint16*>(meshData + sizeof(uint32) * 2 + sizeof(BasicVertex) * numVertices);

    // Create the vertex and index buffers with the mesh data.

    D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
    vertexBufferData.pSysMem = vertices;
    vertexBufferData.SysMemPitch = 0;
    vertexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC vertexBufferDesc(numVertices * sizeof(BasicVertex), D3D11_BIND_VERTEX_BUFFER);

    m_d3dDevice->CreateBuffer(
            &vertexBufferDesc,
            &vertexBufferData,
            vertexBuffer
            );
    
    D3D11_SUBRESOURCE_DATA indexBufferData = {0};
    indexBufferData.pSysMem = indices;
    indexBufferData.SysMemPitch = 0;
    indexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC indexBufferDesc(numIndices * sizeof(uint16), D3D11_BIND_INDEX_BUFFER);
    
    m_d3dDevice->CreateBuffer(
            &indexBufferDesc,
            &indexBufferData,
            indexBuffer
            );
  
    if (vertexCount != nullptr)
    {
        *vertexCount = numVertices;
    }
    if (indexCount != nullptr)
    {
        *indexCount = numIndices;
    }
}

您通常會為遊戲中使用的每一個網格建立頂點/索引緩衝區配對。 載入網格的位置和時機會由您決定。 如果您有很多網格,您可能只想在遊戲中的特定點從磁碟載入一些網格,例如在特定的預先定義載入狀態期間。 對於大型網格,例如地形資料,您可以從快取串流頂點,但這是更複雜的程序,而不屬於本主題的範圍。

同樣地,請知道您的頂點資料格式! 有許多方式可以代表用來建立模型的工具的頂點資料。 還有許多不同的方式來表示 Direct3D 的頂點資料的輸入配置,例如三角形清單和條帶。 有關頂點資料的更多資訊,請閱讀 Direct3D 11基本類型中的緩衝區簡介。

接下來,讓我們來看看載入紋理。

載入紋理

遊戲中最常見的資產,以及包含磁碟和記憶體中大部分檔案的資產,都是紋理。 就像網格一樣,紋理可以採用各種格式,而且您可以將它們轉換成 Direct3D 載入時可以使用的格式。 紋理也採用各種不同的類型,可用來建立不同的效果。 紋理的 MIP 層級可用於改善遠處物件的外觀和性能;污垢和光照貼圖用於在基礎紋理上分層效果和細節; 法線貼圖用於每像素光照計算。 在現代遊戲中,一般場景可能會有數千個個別紋理,您的程式碼必須有效地管理所有紋理!

與網格一樣,有許多特定格式可用於提高記憶體使用效率。 由於紋理很容易消耗大部分 GPU (和系統) 記憶體,因此它們通常會以某種方式進行壓縮。 您不需要對遊戲的紋理使用壓縮,並且可以使用任何您想要的壓縮/解壓縮演算法,只要您向 Direct3D 著色器提供它可以理解的格式的資料 (例如 Texture2D 點陣圖)。

Direct3D 提供 DXT 紋理壓縮演算法的支援,不過播放器圖形硬體中可能不支援每個 DXT 格式。 DDS 檔案也包含 DXT 紋理 (以及其他紋理壓縮格式),且以 .dds 結尾。

DDS 檔案是包含下列資訊的二進位檔:

  • 包含四個字元代碼值「DDS」(0x20534444) 的 DWORD (魔術數字)。

  • 檔案中資料的描述。

    使用 DDS_HEADER 對資料進行標題描述;像素格式使用 DDS_PIXELFORMAT 定義。 請注意,DDS_HEADERDDS_PIXELFORMAT 結構取代了已被取代的 DDSURFACEDESC2、DDSCAPS2 和 DDPIXELFORMAT DirectDraw 7 結構。 DDS_HEADER 是 DDSURFACEDESC2 和 DDSCAPS2 的二進位等效項。 DDS_PIXELFORMAT 是 DDPIXELFORMAT 的二進位等效項。

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    如果 DDS_PIXELFORMATdwFlags 的值設定為 DDPF_FOURCC 且 dwFourCC 設定為「DX10」,將存在額外的 DDS_HEADER_DXT10 結構,以容納無法表示為 RGB 像素格式的紋理陣列或 DXGI 格式,例如浮點格式、sRGB 格式等。當存在 DDS_HEADER_DXT10 結構時,整個資料描述將如下所示。

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • 指向包含主表面資料的位元組陣列的指標。

    BYTE bdata[]
    
  • 指向包含剩餘表面的位元組陣列的指標,例如;mipmap 層級、立方體貼圖中的面、體積紋理中的深度。 請點擊以下連結以取得更多有關紋理立方體貼圖體積紋理的 DDS 檔案配置的資訊。

    BYTE bdata2[]
    

許多工具會匯出至 DDS 格式。 如果您沒有工具可將紋理匯出至此格式,請考慮建立一個。 有關 DDS 格式以及如何在程式碼中使用它的更多詳細資訊,請閱讀 DDS 程式設計指南。 在我們的範例中,我們將使用 DDS。

與其他資源類型一樣,您可以從檔案中以位元組串流的形式讀取資料。 載入工作完成後,lambda 呼叫將執行程式碼 (CreateTexture 方法) 以將位元組串流處理為 Direct3D 可以使用的格式。

task<void> BasicLoader::LoadTextureAsync(
    _In_ Platform::String^ filename,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(
            GetExtension(filename) == "dds",
            textureData->Data,
            textureData->Length,
            texture,
            textureView,
            filename
            );
    });
}

在上一個代碼段中,Lambda 會檢查檔名是否具有「dds」的副檔名。 如果有,您假設它是 DDS 紋理。 如果沒有,那麼可以使用 Windows Imaging Component (WIC) API 來探索格式並將資料解碼為點陣圖。 無論哪種方式,結果都是一個 Texture2D 點陣圖 (或一個錯誤)。

void BasicLoader::CreateTexture(
    _In_ bool decodeAsDDS,
    _In_reads_bytes_(dataSize) byte* data,
    _In_ uint32 dataSize,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView,
    _In_opt_ Platform::String^ debugName
    )
{
    ComPtr<ID3D11ShaderResourceView> shaderResourceView;
    ComPtr<ID3D11Texture2D> texture2D;

    if (decodeAsDDS)
    {
        ComPtr<ID3D11Resource> resource;

        if (textureView == nullptr)
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                nullptr
                );
        }
        else
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                &shaderResourceView
                );
        }

        resource.As(&texture2D);
    }
    else
    {
        if (m_wicFactory.Get() == nullptr)
        {
            // A WIC factory object is required in order to load texture
            // assets stored in non-DDS formats.  If BasicLoader was not
            // initialized with one, create one as needed.
            CoCreateInstance(
                    CLSID_WICImagingFactory,
                    nullptr,
                    CLSCTX_INPROC_SERVER,
                    IID_PPV_ARGS(&m_wicFactory));
        }

        ComPtr<IWICStream> stream;
        m_wicFactory->CreateStream(&stream);

        stream->InitializeFromMemory(
                data,
                dataSize);

        ComPtr<IWICBitmapDecoder> bitmapDecoder;
        m_wicFactory->CreateDecoderFromStream(
                stream.Get(),
                nullptr,
                WICDecodeMetadataCacheOnDemand,
                &bitmapDecoder);

        ComPtr<IWICBitmapFrameDecode> bitmapFrame;
        bitmapDecoder->GetFrame(0, &bitmapFrame);

        ComPtr<IWICFormatConverter> formatConverter;
        m_wicFactory->CreateFormatConverter(&formatConverter);

        formatConverter->Initialize(
                bitmapFrame.Get(),
                GUID_WICPixelFormat32bppPBGRA,
                WICBitmapDitherTypeNone,
                nullptr,
                0.0,
                WICBitmapPaletteTypeCustom);

        uint32 width;
        uint32 height;
        bitmapFrame->GetSize(&width, &height);

        std::unique_ptr<byte[]> bitmapPixels(new byte[width * height * 4]);
        formatConverter->CopyPixels(
                nullptr,
                width * 4,
                width * height * 4,
                bitmapPixels.get());

        D3D11_SUBRESOURCE_DATA initialData;
        ZeroMemory(&initialData, sizeof(initialData));
        initialData.pSysMem = bitmapPixels.get();
        initialData.SysMemPitch = width * 4;
        initialData.SysMemSlicePitch = 0;

        CD3D11_TEXTURE2D_DESC textureDesc(
            DXGI_FORMAT_B8G8R8A8_UNORM,
            width,
            height,
            1,
            1
            );

        m_d3dDevice->CreateTexture2D(
                &textureDesc,
                &initialData,
                &texture2D);

        if (textureView != nullptr)
        {
            CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(
                texture2D.Get(),
                D3D11_SRV_DIMENSION_TEXTURE2D
                );

            m_d3dDevice->CreateShaderResourceView(
                    texture2D.Get(),
                    &shaderResourceViewDesc,
                    &shaderResourceView);
        }
    }


    if (texture != nullptr)
    {
        *texture = texture2D.Detach();
    }
    if (textureView != nullptr)
    {
        *textureView = shaderResourceView.Detach();
    }
}

當此程式碼完成時,記憶體中就會有一個從影像檔載入的 Texture2D。 與網格一樣,您的遊戲和任何指定場景中可能會有很多網格。 考慮為每個場景或每個關卡定期存取的紋理建立快取,而不是在遊戲或關卡開始時載入它們。

(上面範例中呼叫的 CreateDDSTextureFromMemory 方法可以在 Complete code for DDSTextureLoader 的完整程式碼中完整探索。)

此外,個別紋理或紋理「皮膚」可能會對應至特定的網格多邊形或表面。 此對應資料通常由藝術家或設計師用於建立模型和紋理的工具匯出。 確保在載入會出的資料時也擷取此資訊,因為在執行片段著色時,您將使用它將正確的紋理對應到相應的表面。

載入著色器

著色器是編譯後的高階著色器語言 (HLSL) 檔案,這些文件載入到記憶體中並在圖形管道的特定階段叫用。 最常見和最重要的著色器是頂點著色器和像素著色器,它們分別處理網格的各個頂點和場景視窗中的像素。 執行 HLSL 程式碼以變換幾何、套用光照效果和紋理,並對轉譯的場景執行後處理。

一款 Direct3D 遊戲可以有許多不同的著色器,每個著色器都編譯成單獨的 CSO (編譯著色器物件、.cso) 檔案。 通常,您不會有太多而需要動態載入它們,並且在大多數情況下,您可以簡單地在遊戲開始時載入它們,或者在每個關卡的基礎上載入它們 (例如用於下雨效果的著色器)。

BasicLoader 類別中的程式碼為不同的著色器提供了許多多載,包括頂點、幾何、像素和輪廓著色器。 下列程式碼涵蓋像素著色器做為範例。 (您可以在 BasicLoader 的完整程式碼中查看完整的程式碼。)

concurrency::task<void> LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
        
       m_d3dDevice->CreatePixelShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);
    });
}

在此範例中,您使用 BasicReaderWriter 執行個體 (m_basicReaderWriter) 將提供的已編譯著色器物件 (.cso) 檔案做為位元組串流讀入。 工作完成後,lambda 會使用從檔案載入的位元組資料來呼叫 ID3D11Device::CreatePixelShader。 您的回呼必須設定一些旗標來指出載入成功,並且您的程式碼必須在執行著色器之前檢查此旗標。

頂點著色器比較複雜。 針對頂點著色器,您也會載入定義頂點資料的個別輸入配置。 下列程式碼可用來以非同步方式載入頂點著色器,以及自訂頂點輸入配置。 確保您從網格載入的頂點資訊可以透過此輸入配置正確表示!

讓我們先建立輸入配置,再載入頂點著色器。

void BasicLoader::CreateInputLayout(
    _In_reads_bytes_(bytecodeSize) byte* bytecode,
    _In_ uint32 bytecodeSize,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC* layoutDesc,
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11InputLayout** layout
    )
{
    if (layoutDesc == nullptr)
    {
        // If no input layout is specified, use the BasicVertex layout.
        const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,    0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        };

        m_d3dDevice->CreateInputLayout(
                basicVertexLayoutDesc,
                ARRAYSIZE(basicVertexLayoutDesc),
                bytecode,
                bytecodeSize,
                layout);
    }
    else
    {
        m_d3dDevice->CreateInputLayout(
                layoutDesc,
                layoutDescNumElements,
                bytecode,
                bytecodeSize,
                layout);
    }
}

在此特定配置中,每個頂點都有頂點著色器處理的下列資料:

  • 模型座標空間中的 3D 座標位置 (x,y,z),表示為三個 32 位元浮點值。
  • 頂點的一般向量,也以三個 32 位元浮點值表示。
  • 已轉換的 2D 紋理座標值 (u,v),以一對 32 位元浮點值表示。

這些每個頂點輸入元素稱為 HLSL semantics 語意,它們是一組定義的暫存器,用於將資料傳入和傳出已編譯的著色器物件。 您的管線會針對您所載入網格中的每個頂點執行一次頂點著色器。 語意定義頂點著色器執行時的輸入 (和輸出),並為著色器的 HLSL 程式碼中的每頂點計算提供此資料。

現在,載入頂點著色器物件。

concurrency::task<void> LoadShaderAsync(
        _In_ Platform::String^ filename,
        _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
        _In_ uint32 layoutDescNumElements,
        _Out_ ID3D11VertexShader** shader,
        _Out_opt_ ID3D11InputLayout** layout
        );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11VertexShader** shader,
    _Out_opt_ ID3D11InputLayout** layout
    )
{
    // This method assumes that the lifetime of input arguments may be shorter
    // than the duration of this task.  In order to ensure accurate results, a
    // copy of all arguments passed by pointer must be made.  The method then
    // ensures that the lifetime of the copied data exceeds that of the task.

    // Create copies of the layoutDesc array as well as the SemanticName strings,
    // both of which are pointers to data whose lifetimes may be shorter than that
    // of this method's task.
    shared_ptr<vector<D3D11_INPUT_ELEMENT_DESC>> layoutDescCopy;
    shared_ptr<vector<string>> layoutDescSemanticNamesCopy;
    if (layoutDesc != nullptr)
    {
        layoutDescCopy.reset(
            new vector<D3D11_INPUT_ELEMENT_DESC>(
                layoutDesc,
                layoutDesc + layoutDescNumElements
                )
            );

        layoutDescSemanticNamesCopy.reset(
            new vector<string>(layoutDescNumElements)
            );

        for (uint32 i = 0; i < layoutDescNumElements; i++)
        {
            layoutDescSemanticNamesCopy->at(i).assign(layoutDesc[i].SemanticName);
        }
    }

    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
       m_d3dDevice->CreateVertexShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);

        if (layout != nullptr)
        {
            if (layoutDesc != nullptr)
            {
                // Reassign the SemanticName elements of the layoutDesc array copy to point
                // to the corresponding copied strings. Performing the assignment inside the
                // lambda body ensures that the lambda will take a reference to the shared_ptr
                // that holds the data.  This will guarantee that the data is still valid when
                // CreateInputLayout is called.
                for (uint32 i = 0; i < layoutDescNumElements; i++)
                {
                    layoutDescCopy->at(i).SemanticName = layoutDescSemanticNamesCopy->at(i).c_str();
                }
            }

            CreateInputLayout(
                bytecode->Data,
                bytecode->Length,
                layoutDesc == nullptr ? nullptr : layoutDescCopy->data(),
                layoutDescNumElements,
                layout);   
        }
    });
}

在此程式碼中,讀入頂點著色器的 CSO 檔案的位元組資料後,即可透過呼叫 ID3D11Device::CreateVertexShader 建立頂點著色器。 之後,您會在相同的 Lambda 中建立著色器的輸入配置。

其他著色器類型 (例如輪廓著色器和幾何著色器) 也可能需要特定的設定。 BasicLoader 的完整程式碼Direct3D 資源載入範例中提供了各種著色器載入方法的完整程式碼。

備註

此時,您應該瞭解並能夠建立或修改方法,以非同步方式載入常見的遊戲資源和資產,例如網格、紋理和已編譯的著色器。