Поделиться через


Загрузка ресурсов в игре DirectX

Большинство игр, в некоторых точках, загружают ресурсы и ресурсы (например, шейдеры, текстуры, предопределенные сетки или другие графические данные) из локального хранилища или другого потока данных. Здесь мы рассмотрим высокоуровневое представление о том, что необходимо учитывать при загрузке этих файлов для использования в игре DirectX C/C++ универсальная платформа Windows (UWP).

Например, сетки для многоугольников в игре могли быть созданы с помощью другого инструмента и экспортированы в определенный формат. То же самое верно для текстур, и тем более: в то время как плоская, несжатая растровая карта может быть обычно написана большинством инструментов и понятна большинством графических API, она может быть чрезвычайно неэффективной для использования в вашей игре. Здесь мы рассмотрим основные шаги по загрузке трех различных типов графических ресурсов для использования с Direct3D: сетками (моделями), текстурами (растровыми изображениями) и скомпилированных объектов шейдера.

Это важно знать

Технологии

  • Библиотека параллельных шаблонов (ppltasks.h)

Необходимые компоненты

  • Основные среда выполнения Windows
  • Общие сведения об асинхронных задачах
  • Ознакомьтесь с основными понятиями трехмерного программирования графики.

Этот пример также содержит три файла кода для загрузки ресурсов и управления ими. В этом разделе вы увидите объекты кода, определенные в этих файлах.

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

Полный код для этих примеров можно найти по следующим ссылкам.

Раздел Описание

Полный код для BasicLoader

Полный код для класса и методов, которые преобразуют и загружают объекты графической сетки в память.

Полный код для BasicReaderWriter

Полный код для класса и методов для чтения и записи двоичных файлов данных в целом. Используется классом BasicLoader .

Полный код для DDSTextureLoader

Полный код для класса и метода, который загружает текстуру DDS из памяти.

 

Instructions

Асинхронная загрузка

Асинхронная загрузка обрабатывается с помощью шаблона задачи из библиотеки параллельных шаблонов (PPL). Задача содержит вызов метода, за которым следует лямбда-код, который обрабатывает результаты асинхронного вызова после завершения и обычно соответствует формату:

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 определена как задача, можно использовать лямбда-операцию для выполнения определенной операции при возврате массива байтов, например передачу данных байтов в функцию DirectX, которая может использовать ее.

Если ваша игра достаточно проста, загрузите ресурсы с помощью такого метода, как это, когда пользователь запускает игру. Это можно сделать перед началом основного цикла игры с некоторой точки в последовательности вызовов реализации IFrameworkView::Run . Опять же, вы вызываете методы загрузки ресурсов асинхронно, чтобы игра может начаться быстрее, и поэтому игроку не придется ждать завершения загрузки, прежде чем участвовать в ранних взаимодействиях.

Однако вы не хотите запускать игру правильно, пока не завершится все асинхронная загрузка! Создайте некоторый метод для сигнала при завершении загрузки, например определенное поле, и используйте лямбда-файлы в методах загрузки, чтобы задать этот сигнал после завершения. Перед запуском всех компонентов, использующих загруженные ресурсы, проверьте переменную.

Ниже приведен пример использования асинхронных методов, определенных в 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.
}

Обратите внимание, что задачи были агрегированы с помощью оператора &>, чтобы лямбда-код, задающий флаг завершения загрузки, активируется только после завершения всех задач. Обратите внимание, что если у вас несколько флагов, у вас есть возможность гонки условий. Например, если лямбда-файл устанавливает два флага последовательно в одно и то же значение, другой поток может видеть только первый набор флагов, если он проверяет их до установки второго флага.

Вы узнали, как асинхронно загружать файлы ресурсов. Синхронные загрузки файлов гораздо проще, и вы можете найти примеры из них в полном коде для BasicReaderWriter и Полного кода для BasicLoader.

Конечно, различные типы ресурсов и ресурсов часто требуют дополнительной обработки или преобразования, прежде чем они будут готовы к использованию в графическом конвейере. Рассмотрим три конкретных типа ресурсов: сетки, текстуры и шейдеры.

Загрузка сетки

Сетки — это данные вершин, которые создаются процедурно по коду в игре или экспортируются в файл из другого приложения (например, 3DStudio MAX или Alias WaveFront) или средства. Эти сетки представляют модели в игре, от простых примитивов, таких как куби и сферы до автомобилей и домов и символов. Они часто содержат данные цвета и анимации, а также в зависимости от их формата. Мы сосредоточимся на сетках, содержащих только данные вершин.

Чтобы правильно загрузить сетку, необходимо знать формат данных в файле сетки. Наш простой тип BasicReaderWriter выше просто считывает данные в виде потока байтов. Он не знает, что данные байтов представляют сетку, гораздо меньше определенного формата сетки, как экспортируется другим приложением! При переносе данных сетки в память необходимо выполнить преобразование.

(Вы всегда должны пытаться упаковать данные активов в формате, который максимально близок к внутреннему представлению. Это приведет к снижению использования ресурсов и экономии времени.)

Давайте получим данные байтов из файла сетки. В этом примере предполагается, что файл является суффиксом конкретного формата с помощью VBO. (Опять же, этот формат не совпадает с форматом VBO OpenGL.) Каждая вершина сопоставляется с типом BasicVertex , который является структурой, определенной в коде для средства преобразователя obj2vbo. Макет данных вершин в VBO-файле выглядит следующим образом:

  • Первые 32 бита (4 байта) потока данных содержат количество вершин (numVertices) в сетке, представленное как значение uint32.
  • Следующие 32 бита (4 байта) потока данных содержат количество индексов в сетке (numIndices), представленное как значение uint32.
  • После этого последующие биты (numVertices * sizeof(BasicVertex)) содержат данные вершин.
  • Последние (numIndices * 16) биты данных содержат данные индекса, представленные в виде последовательности значений uint16.

Это так: знают макет битового уровня загруженных данных сетки. Кроме того, убедитесь, что вы согласованы с эндиан-ness. Все платформы Windows 8 являются маленькими.

В примере вы вызываете метод CreateMesh из метода 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-файл — это двоичный файл, содержащий следующие сведения:

  • 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;
    

    Если для значений dwFlags в DDS_PIXELFORMAT задано значение DDPF_FOURCC и dwFourCC задано значение DX10, DDS_HEADER_DXT10 для размещения массивов текстур или форматов DXGI, которые не могут быть выражены как форматы пикселей RGB, такие как форматы с плавающей запятой, форматы sRGB и т. д. Когда DDS_HEADER_DXT10 структура присутствует, все описание данных будет выглядеть следующим образом.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • Указатель на массив байтов, содержащий основные данные поверхности.

    BYTE bdata[]
    
  • Указатель на массив байтов, содержащий оставшиеся поверхности, такие как; Уровни mipmap, лица на карте куба, глубины в текстуре тома. Дополнительные сведения о макете файла DDS см. по следующим ссылкам: текстура, карта куба или текстура тома.

    BYTE bdata2[]
    

Многие средства экспортируются в формат DDS. Если у вас нет инструмента для экспорта текстуры в этот формат, попробуйте создать его. Дополнительные сведения о формате DDS и его работе в коде см . в руководстве по программированию для DDS. В нашем примере мы будем использовать DDS.

Как и в случае с другими типами ресурсов, вы считываете данные из файла в виде потока байтов. После завершения задачи загрузки лямбда-вызов запускает код ( метод 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
            );
    });
}

В предыдущем фрагменте лямбда-код проверяет, имеет ли имя файла расширение dds. Если это так, предполагается, что это текстура DDS. Если нет, используйте API компонента образов Windows (WIC), чтобы обнаружить формат и декодировать данные в виде растрового изображения. В любом случае результатом является растровое изображение 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();
    }
}

После завершения этого кода у вас есть текстура2D в памяти, загруженная из файла изображения. Как и с сетками, у вас, вероятно, много из них в вашей игре и в любой конкретной сцене. Рекомендуется создавать кэши для регулярного доступа к текстурам на сцену или на уровне, а не загружать их все при запуске игры или уровня.

(The Метод 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) в виде потока байтов. После завершения этой задачи лямбда-код вызывает 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. После этого создайте макет ввода для шейдера в той же лямбда-лямбда-схеме.

Другие шейдеры, такие как шейдеры корпуса и геометрии, также могут требовать определенную конфигурацию. Полный код для различных методов загрузки шейдера представлен в полном коде для BasicLoader и в примере загрузки ресурсов Direct3D.

Замечания

На этом этапе вы должны понимать и иметь возможность создавать или изменять методы асинхронной загрузки общих игровых ресурсов и ресурсов, таких как сетки, текстуры и скомпилированные шейдеры.