Compartir a través de


Carga de recursos en el juego DirectX

La mayoría de los juegos, en algún momento, cargan recursos y activos (como sombras, texturas, mallas predefinidas u otros datos gráficos) desde el almacenamiento local u otro flujo de datos. Aquí te guiaremos a través de una vista de alto nivel de lo que debes tener en cuenta al cargar estos archivos para usarlos en tu juego DirectX C/C++ Para la Plataforma universal de Windows (UWP).

Por ejemplo, las mallas para objetos poligonales del juego podrían haberse creado con otra herramienta y exportarse a un formato específico. Lo mismo sucede con las texturas, y mucho más: mientras que un mapa de bits plano y sin comprimir se puede escribir normalmente por la mayoría de las herramientas y entender por la mayoría de las API de gráficos, puede ser extremadamente ineficaz para su uso en el juego. Aquí le guiaremos por los pasos básicos para cargar tres tipos diferentes de recursos gráficos para su uso con Direct3D: mallas (modelos), texturas (mapas de bits) y objetos de sombreador compilados.

Lo que necesita saber

Tecnologías

  • Biblioteca de patrones paralelos (ppltasks.h)

Prerrequisitos

  • Entender los conceptos básicos de Windows Runtime
  • Comprender las tareas asincrónicas
  • Comprenda los conceptos básicos de la programación de gráficos 3D.

En este ejemplo también se incluyen tres archivos de código para la carga y administración de recursos. Encontrará los objetos de código definidos en estos archivos en este tema.

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

El código completo de estos ejemplos se puede encontrar en los vínculos siguientes.

Tema Descripción

Código completo para basicLoader

Código completo para una clase y métodos que convierten y cargan objetos de malla de gráficos en memoria.

Código completo para BasicReaderWriter

Código completo para una clase y métodos para leer y escribir archivos de datos binarios en general. Usado por la clase BasicLoader.

Código completo de DDSTextureLoader

Código completo para una clase y un método que carga una textura DDS desde la memoria.

 

Instrucciones

Carga asincrónica

La carga asincrónica se controla mediante la plantilla de tarea de la Biblioteca de Patrones Paralelos (PPL). Una tarea contiene una llamada de método seguida de una expresión lambda que procesa los resultados de la llamada asincrónica después de que se complete y normalmente sigue el formato de:

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

Las tareas se pueden encadenar mediante la sintaxis de .then() para que, cuando se complete una operación, se pueda ejecutar otra operación asincrónica que dependa de los resultados de la operación anterior. De este modo, puede cargar, convertir y administrar recursos complejos en subprocesos independientes de una manera que parezca casi invisible para el jugador.

Para obtener más información, lea programación asincrónica en C++.

Ahora, echemos un vistazo a la estructura básica para declarar y crear un método de carga de archivos asincrónico, 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;
    });
}

En este código, cuando el código llama al método ReadDataAsync definido anteriormente, se crea una tarea para leer un búfer desde el sistema de archivos. Una vez completada, una tarea encadenada toma el búfer y transmite los bytes de ese búfer a una matriz mediante el tipo estático 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.          
    });

Aquí tienes la llamada que haces a ReadDataAsync. Cuando la operación se completa, tu código recibe un arreglo de bytes leídos del archivo proporcionado. Dado que ReadDataAsync se define como una tarea, puede usar una expresión lambda para realizar una operación específica cuando se devuelve la matriz de bytes, como pasar esos datos de bytes a una función DirectX que pueda utilizarla.

Si el juego es lo suficientemente sencillo, cargue los recursos con un método similar al siguiente cuando el usuario inicie el juego. Puedes hacerlo antes de iniciar el bucle principal del juego desde algún punto en la secuencia de llamadas de tu IFrameworkView::Run implementación. De nuevo, llamas a los métodos de carga de recursos de forma asincrónica para que el juego pueda empezar más rápido y, por tanto, el jugador no tiene que esperar hasta que se complete la carga antes de participar en interacciones tempranas.

Sin embargo, no quieres iniciar el juego correctamente hasta que se haya completado toda la carga asincrónica. Cree algún método para indicar cuando se complete la carga, como un campo específico, y use las lambdas en sus métodos de carga para establecer esa señal cuando termine. Compruebe la variable antes de iniciar los componentes que usen esos recursos cargados.

Este es un ejemplo mediante los métodos asincrónicos definidos en BasicLoader.cpp para cargar sombreadores, una malla y una textura cuando se inicia el juego. Observe que establece un campo específico en el objeto de juego, m_loadingComplete, cuando finalizan todos los métodos de carga.

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.
}

Tenga en cuenta que las tareas se han agregado utilizando el operador && para que la expresión lambda que establece el indicador de carga completa se active solo cuando se completen todas las tareas. Ten en cuenta que si tienes varias banderas, tienes la posibilidad de condiciones de carrera. Por ejemplo, si la expresión lambda establece dos marcas secuencialmente al mismo valor, otro subproceso puede ver solo la primera marca si las examina antes de que se establezca la segunda marca.

Ha visto cómo cargar archivos de recursos de forma asincrónica. Las cargas de archivos sincrónicas son mucho más sencillas y puede encontrar ejemplos de ellos en Código completo para BasicReaderWriter y Código completo para BasicLoader.

Por supuesto, los distintos tipos de recursos y activos a menudo requieren procesamiento o conversión adicional antes de que estén listos para usarse en tu canalización de gráficos. Echemos un vistazo a tres tipos específicos de recursos: mallas, texturas y sombreadores.

Carga de mallas

Las mallas son datos de vértices, generados por código dentro del juego o exportados a un archivo desde otra aplicación (como 3DStudio MAX o Alias WaveFront) o herramienta. Estas mallas representan los modelos del juego, desde primitivos simples como cubos y esferas hasta coches y casas y personajes. A menudo contienen datos de color y animación, además, dependiendo de su formato. Nos centraremos en mallas que solo contienen datos de vértices.

Para cargar correctamente una malla, debe conocer el formato de los datos del archivo para la malla. Nuestro sencillo tipo BasicReaderWriter mencionado anteriormente simplemente lee los datos como una secuencia de bytes; no sabe que los datos en bytes representan una malla, y mucho menos un formato de malla específico como el exportado por otra aplicación. Debe realizar la conversión a medida que cargue los datos de malla en la memoria.

(Siempre debe intentar empaquetar los datos de recursos en un formato lo más cercano posible a la representación interna. Al hacerlo, se reducirá el uso de recursos y se ahorrará tiempo).

Vamos a obtener los datos binarios del archivo de malla. En el formato del ejemplo se supone que el archivo es un formato específico de ejemplo con el sufijo .vbo. De nuevo, este formato no es el mismo que el formato VBO de OpenGL. Cada vértice se asigna al tipo BasicVertex, que es un struct definido en el mismo código de la herramienta de conversión obj2vbo. El diseño de los datos de vértices en el archivo .vbo tiene este aspecto:

  • Los primeros 32 bits (4 bytes) del flujo de datos contienen el número de vértices (numVertices) en la malla, representado como un valor uint32.
  • Los siguientes 32 bits (4 bytes) del flujo de datos contienen el número de índices de la malla (numIndices), representado como un valor uint32.
  • Después de eso, los siguientes bits (numVertices * sizeof(BasicVertex)) contienen los datos de los vértices.
  • Los últimos bits (numIndices * 16) de datos contienen los datos de índice, representados como una secuencia de valores uint16.

El punto es el siguiente: debes conocer el diseño a nivel de bits de los datos de malla que has cargado. Además, asegúrese de que es coherente con la endian-ness. Todas las plataformas de Windows 8 son de orden de bytes pequeño.

En el ejemplo, se llama a un método CreateMesh desde el método LoadMeshAsync para realizar esta interpretación de nivel de bits.

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 interpreta los datos de bytes cargados desde el archivo y crea un búfer de vértices y un búfer de índice para la malla pasando las listas de vértices e índices, respectivamente, a ID3D11Device::CreateBuffer y especificando D3D11_BIND_VERTEX_BUFFER o D3D11_BIND_INDEX_BUFFER. Este es el código usado en 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;
    }
}

Normalmente creas un par de búferes de vértices/índices para cada malla que uses en tu juego. Dónde y cuándo se cargan las mallas depende de usted. Si tienes muchas mallas, es posible que solo quieras cargar algunos desde el disco en puntos específicos del juego, como durante estados de carga predefinidos específicos. Para mallas grandes, como los datos de terreno, puede transmitir los vértices desde una memoria caché, pero es un procedimiento más complejo y no en el ámbito de este tema.

De nuevo, conozca el formato de datos de vértices. Hay muchas maneras de representar datos de vértices en las herramientas que se usan para crear modelos. También hay muchas maneras diferentes de representar el diseño de entrada de los datos de vértices en Direct3D, como listas de triángulos y tiras. Para obtener más información sobre los datos de vértices, lea Introduction to Buffers in Direct3D 11 and Primitives.

A continuación, veamos cómo se cargan las texturas.

Carga de texturas

El recurso más común en un juego ,y el que comprende la mayoría de los archivos en disco y en memoria, son texturas. Al igual que las mallas, las texturas pueden tener una variedad de formatos y convertirlas en un formato que Direct3D puede usar al cargarlos. Las texturas también vienen en una amplia variedad de tipos y se usan para crear diferentes efectos. Los niveles MIP de texturas se pueden usar para mejorar la apariencia y el rendimiento de objetos lejanos; los mapas de suciedad y luz se utilizan para añadir efectos de capas y detalles sobre una textura base; y los mapas normales se usan en cálculos de iluminación por píxel. En un juego moderno, una escena típica puede tener potencialmente miles de texturas individuales, y el código debe administrarlos de forma eficaz.

Además, como las mallas, hay una serie de formatos específicos que se usan para hacer que el uso de memoria sea eficaz. Dado que las texturas pueden consumir fácilmente una gran parte de la memoria de GPU (y del sistema), a menudo se comprimen de alguna manera. No es necesario usar la compresión en las texturas de tu juego y puedes usar cualquier algoritmo de compresión o descompresión que quieras, siempre y cuando proporciones los sombreadores direct3D con datos en un formato que pueda entender (como un Texture2D mapa de bits).

Direct3D proporciona compatibilidad con los algoritmos de compresión de textura DXT, aunque es posible que no se admitan todos los formatos DXT en el hardware gráfico del reproductor. Los archivos DDS contienen texturas DXT (y otros formatos de compresión de texturas también) y se les añade el sufijo .dds.

Un archivo DDS es un archivo binario que contiene la siguiente información:

  • DWORD (número mágico) que contiene el valor de código de cuatro caracteres "DDS" (0x20534444).

  • Descripción de los datos del archivo.

    Los datos se describen con una descripción de encabezado mediante DDS_HEADER; el formato de píxel se define mediante DDS_PIXELFORMAT. Tenga en cuenta que las estructuras DDS_HEADER y DDS_PIXELFORMAT reemplazan las estructuras de DDSURFACEDESC2 en desuso, DDSCAPS2 y DDPIXELFORMAT DirectDraw 7. DDS_HEADER es el equivalente binario de DDSURFACEDESC2 y DDSCAPS2. DDS_PIXELFORMAT es el equivalente binario de DDPIXELFORMAT.

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    Si el valor de dwFlags en DDS_PIXELFORMAT se establece en DDPF_FOURCC y dwFourCC se establece en "DX10" una estructura de DDS_HEADER_DXT10 adicional estará presente para dar cabida a matrices de texturas o formatos DXGI que no se pueden expresar como formatos de píxel RGB, como formatos de punto flotante, Formatos sRGB, etc. Cuando la estructura DDS_HEADER_DXT10 está presente, toda la descripción de los datos tendrá este aspecto.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • Un puntero a una matriz de bytes que contiene los datos principales de la superficie.

    BYTE bdata[]
    
  • Puntero a una matriz de bytes que contiene las superficies restantes, como: niveles mipmap, caras en un mapa de cubo, profundidades en una textura de volumen. Siga estos vínculos para obtener más información sobre el diseño de archivo DDS para una textura de , un mapa de cubo de o una textura de volumen de .

    BYTE bdata2[]
    

Muchas herramientas se exportan al formato DDS. Si no tiene una herramienta para exportar la textura a este formato, considere la posibilidad de crear una. Para obtener más información sobre el formato DDS y cómo trabajar con él en el código, lea Guía de programación para DDS. En nuestro ejemplo, usaremos DDS.

Al igual que con otros tipos de recursos, se leen los datos de un archivo como un flujo de bytes. Una vez completada la tarea de carga, la llamada lambda ejecuta código (el método CreateTexture) para procesar el flujo de bytes en un formato que Direct3D puede usar.

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
            );
    });
}

En el fragmento de código anterior, la expresión lambda comprueba si el nombre de archivo tiene una extensión de "dds". Si es así, asumes que es una textura DDS. Si no es así, usa las API del componente de imágenes de Windows (WIC) para detectar el formato y descodificar los datos como un mapa de bits. En cualquier caso, el resultado es un mapa de bits Texture2D (o un error).

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();
    }
}

Una vez que este código se completa, tienes una Texture2D en memoria, cargada a partir de un archivo de imagen. Al igual que con las mallas, probablemente tengas muchas de ellas en tu juego y en cualquier escena. Considere la posibilidad de crear cachés para texturas a las que se accede con regularidad por escena o por nivel, en lugar de cargarlas todas cuando se inicia el juego o nivel.

(El método CreateDDSTextureFromMemory llamado en el ejemplo anterior se puede explorar en su totalidad en Código completo de DDSTextureLoader).

Además, las texturas individuales o las "pieles" pueden asignarse a polígonos o superficies de malla específicos. Normalmente, estos datos de mapeo se exportan mediante la herramienta que un artista o diseñador usó para crear el modelo y las texturas. Asegúrese de capturar esta información también al cargar los datos exportados, ya que lo usará para asignar las texturas correctas a las superficies correspondientes al realizar sombreado de fragmentos.

Carga de sombreadores

Los sombreadores son archivos de lenguaje de sombreador de alto nivel (HLSL) compilados que se cargan en la memoria e invocan en fases específicas de la canalización de gráficos. Los sombreadores más comunes y esenciales son los sombreadores de vértices y píxeles, que procesan los vértices individuales de tu malla y los píxeles en la(s) ventana(s) de la escena, respectivamente. El código HLSL se ejecuta para transformar la geometría, aplicar efectos de iluminación y texturas y realizar el posprocesamiento en la escena representada.

Un juego Direct3D puede tener varios sombreadores diferentes, cada uno compilado en un archivo CSO (Objeto de sombreador compilado, .cso). Normalmente, no tienes tantas que necesites cargarlas dinámicamente y, en la mayoría de los casos, puedes cargarlas simplemente cuando el juego se inicia o por nivel (como un sombreador para efectos de lluvia).

El código de la clase BasicLoader proporciona una serie de sobrecargas para diferentes sombreadores, como vértices, geometría, píxeles y sombreadores de casco. El código siguiente cubre sombreadores de píxeles como ejemplo. (Puede revisar el código completo en Código completo para 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);
    });
}

En este ejemplo, se usa la instancia de BasicReaderWriter (m_basicReaderWriter) para leer el archivo del objeto de sombreador compilado proporcionado (.cso) como una secuencia de bytes. Una vez completada esa tarea, la expresión lambda llama a ID3D11Device::CreatePixelShader con los datos de bytes cargados desde el archivo. Su devolución de llamada debe establecer algún marcador que indique que la carga se realizó correctamente, y su código debe comprobar este marcador antes de ejecutar el sombreador.

Los sombreadores de vértices son un poco más complejos. Para un sombreador de vértices, también se carga un diseño de entrada independiente que define los datos del vértice. El código siguiente se puede usar para cargar asincrónicamente un sombreador de vértices junto con un diseño de entrada de vértices personalizado. Asegúrese de que la información de vértices que cargue desde las mallas se pueda representar correctamente mediante este diseño de entrada.

Vamos a crear el diseño de entrada antes de cargar el sombreador de vértices.

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);
    }
}

En este diseño concreto, cada vértice tiene los siguientes datos procesados por el sombreador de vértices:

  • Posición de coordenada 3D (x, y, z) en el espacio de coordenadas del modelo, representada como un trío de valores de punto flotante de 32 bits.
  • Vector normal para el vértice, que también se representa como tres valores de punto flotante de 32 bits.
  • Valor de coordenada de textura 2D transformado (u, v), representado como un par de valores flotantes de 32 bits.

Estos elementos de entrada por vértice se denominan semántica de HLSL, y son un conjunto de registros definidos que se usan para pasar datos a y desde el objeto sombreador compilado. Tu canalización ejecuta el sombreador de vértices una vez por cada vértice de la malla que has cargado. La semántica define la entrada y la salida del sombreador de vértices a medida que se ejecuta, y proporciona estos datos para los cálculos por vértice en el código HLSL de tu sombreador.

Ahora, cargue el objeto sombreador de vértices.

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);   
        }
    });
}

En este código, una vez que has leído los datos de bytes del archivo CSO del sombreador de vértices, creas el sombreador de vértices llamando a ID3D11Device::CreateVertexShader. Después, crea el diseño de entrada para el sombreador en la misma lambda.

Otros tipos de sombreador, como sombreadores de casco y geometría, también pueden requerir una configuración específica. El código completo para una variedad de métodos de carga de sombreador se proporciona en código completo para BasicLoader y en el ejemplo de carga de recursos de Direct3D.

Observaciones

En este punto, debes comprender y poder crear o modificar métodos para cargar de forma asincrónica recursos y recursos comunes del juego, como mallas, texturas y sombreadores compilados.