在 DirectX 游戏中加载资源

大多数游戏(在某些时候)都会从本地存储或其他数据流中加载资源和资产(例如着色器、纹理、预定义网格或其他图形数据)。 在这里,我们将指导你简单了解加载这些文件以便在 DirectX C/C++ 通用 Windows 平台 (UWP) 游戏中使用时必须考虑的事项。

例如,可能已使用其他工具创建游戏中的多边形对象的网格,并已将它们导出为特定的格式。 对于纹理更是如此:尽管未压缩的平面位图通常是由大多数工具编写的,并且能够被大多数图形 API 理解,但在游戏中的使用效率可能极低。 在这里,我们将指导你完成一些基本步骤,以加载要与 Direct3D 配合使用的三种不同类型的图形资源:网格(模型)、纹理(位图)和已编译的着色器对象。

需要了解的事项

技术

  • 并行模式库 (ppltasks.h)

先决条件

  • 了解基本 Windows 运行时
  • 了解异步任务
  • 了解 3D 图形编程基本概念。

此示例还包含三个用来加载和管理资源的代码文件。 你将在本主题中遇到这些文件中定义的代码对象。

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

可以在以下链接中找到这些示例的完整代码。

主题 说明

BasicLoader 的完整代码

用来转换图形网格对象并将其加载到内存中的类和方法的完整代码。

BasicReaderWriter 的完整代码

一般用来读取和写入二进制数据文件的类和方法的完整代码。 由 BasicLoader 类使用。

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 值序列。

要点在于:了解已加载的网格数据的位级别布局。 此外,还应确保与字节序保持一致。 所有 Windows 8 平台都是小端序平台。

在此示例中,从 LoadMeshAsync 方法调用 CreateMesh 方法,以执行此位级解释。

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 文件是一个二进制文件,其中包含以下信息:

  • 一个 DWORD(幻数),包含四个字符代码值“DDS”(0x20534444)。

  • 文件中的数据的描述。

    使用 DDS_HEADER 描述具有标头描述的数据;使用 DDS_PIXELFORMAT 定义像素格式。 请注意,DDS_HEADER 和 DDS_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 图像处理组件 (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。 和网格一样,你的游戏和任何给定的场景中可能具有很多 Texture2D。 考虑为每个场景或每个级别中经常被访问的纹理创建缓存,而不是在游戏或级别启动时加载所有这些纹理。

(在 DDSTextureLoader 的完整代码中,可以全面了解上述示例中调用的 CreateDDSTextureFromMemory 方法。)

此外,还可以将单独的纹理或者纹理“皮肤”映射到特定的网格多边形或表面。 这些映射数据通常由美术师或设计师用来创建模型和纹理的工具导出。 请确保在加载导出的数据时也捕获这些信息,因为在执行片段着色时,需要使用这些信息将正确的纹理映射到相应的表面。

加载着色器

着色器是一些经过编译的高级着色器语言 (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 语义,它们是所定义的一组寄存器,用来将数据传入和传出经过编译的着色器对象。 对于已加载的网格中的每个顶点,管道都会运行一次顶点着色器。 语义在顶点着色器运行时定义顶点着色器的输入(和输出),并在着色器的 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 资源加载示例中提供了多种着色器加载方法的完整代码。

注解

此时,你应当了解并能够创建或修改用来异步加载常见游戏资源和资产的方法,例如网格、纹理和经过编译的着色器。