在 DirectX 游戏中加载资源

大多数游戏(在某些时候)从本地存储或其他数据流加载资源和资产(例如着色器、纹理、预定义网格或其他图形数据)。 我们在这里为您介绍在加载这些文件以在 DirectX C/C++ 通用 Windows 平台(UWP)游戏中使用时必须考虑的关键事项。

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

需要了解的内容

技术

  • 并行模式库 (ppltasks.h)

先决条件

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

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

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

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

主题 DESCRIPTION

BasicLoader 的完整代码

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

BasicReaderWriter 的完整代码

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

DDSTextureLoader 的完整代码

从内存中加载 DDS 纹理的类和方法的完整代码。

 

说明书

异步加载

使用并行模式库(PPL)中的 任务 模板处理异步加载。 任务 包含一个方法调用,之后是一个 lambda,该 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 方法调用方法 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 文件是包含以下信息的二进制文件:

  • 一个包含四个字符代码值“DDS”(0x20534444)的 DWORD magic number。

  • 文件中数据的说明。

    数据通过使用 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 级别、立方体贴图中的面、体积纹理中的深度。 请按照以下链接了解有关 纹理立方体贴图卷纹理的详细信息。

    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。 与网格一样,你可能在你的游戏中和任何给定场景中有很多网格。 请考虑为定期访问的每个场景或每个级别的纹理创建缓存,而不是在游戏或级别启动时加载所有这些纹理。

在上述示例中调用的 CreateDDSTextureFromMemory 方法可在 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 语义,它们是一组定义的寄存器,用于向/从编译的着色器对象传递数据。 管道针对已加载的网格中的每个顶点运行一次顶点着色器。 语义定义了在运行顶点着色器时的输入和输出,并在着色器的 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 资源加载示例中,提供了多种着色器加载方法的完整代码。

注解

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