Compartir a través de


Cargar recursos en tu juego DirectX

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

Por ejemplo, las mallas para objetos poligonales del juego podrían haberse creado con otra herramienta y exportado a un formato específico. Lo mismo sucede con las texturas, y más aún: mientras que un mapa de bits plano y sin comprimir lo pueden escribir normalmente la mayoría de las herramientas y entender la mayoría de las API de gráficos, su uso en el juego puede ser extremadamente ineficaz. Aquí le guiaremos a través de 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

  • Parallel Patterns Library (ppltasks.h)

Requisitos previos

  • Descripción de Windows Runtime básico
  • Descripción de las tareas asíncronas
  • Descripción de 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ás 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 la 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 para DDSTextureLoader

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

 

Instrucciones

Carga asíncrona

La carga asíncrona se controla mediante la plantilla task de la Parallel Patterns Library (PPL). Una task contiene una llamada de método seguida de una expresión lambda que procesa los resultados de la llamada asíncrona una vez completada, 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 juntas mediante la sintaxis .then(), de modo que cuando se complete una operación, se pueda ejecutar otra operación asíncrona que dependa de los resultados de la operación anterior. De este modo, puedes cargar, convertir y administrar recursos complejos en subprocesos independientes de una manera casi invisible para el jugador.

Para obtener más información, lee Programación asíncrona 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 DataReader estático.

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

Esta es la llamada que realizas a ReadDataAsync. Cuando se completa, el código recibe una matriz de bytes leídos del archivo proporcionado. Dado que ReadDataAsync se define como una tarea, puedes 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 usarla.

Si el juego es lo suficientemente sencillo, carga 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 llamada de la implementación IFrameworkView::Run. De nuevo, llamas a los métodos de carga de recursos de forma asíncrona para que el juego pueda empezar más rápido y, por tanto, el jugador no tenga que esperar hasta que se complete la carga antes de participar en las primeras interacciones.

Sin embargo, no quieres iniciar el juego propiamente dicho hasta que se haya completado toda la carga asíncrona. Crea algún método para señalizar cuando se complete la carga, como un campo específico, y usa las expresiones lambda en los métodos de carga para definir esa señal cuando termine. Comprueba la variable antes de iniciar los componentes que usen esos recursos cargados.

Este es un ejemplo de uso de los métodos asíncronos definidos en BasicLoader.cpp para cargar sombreadores, una malla y una textura cuando se inicia el juego. Observa que define 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.
}

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

Has visto cómo cargar archivos de recursos de forma asíncrona. Las cargas de archivos sincrónicas son mucho más sencillas y puedes encontrar ejemplos de ellas en Código completo para BasicReaderWriter y Código completo para BasicLoader.

Por supuesto, los distintos tipos de recursos a menudo requieren procesamiento o conversión adicionales antes de estar listos para usarse en la 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, 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, debes conocer el formato de los datos del archivo de la malla. Nuestro tipo sencillo BasicReaderWriter anterior simplemente lee los datos como una secuencia de bytes; no sabe que los datos de bytes representan una malla, mucho menos un formato de malla específico tal como lo exporta otra aplicación. Debes realizar la conversión a medida que incorpores los datos de la malla a la memoria.

(Siempre debes 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 de bytes del archivo de la malla. En el formato del ejemplo se supone que el archivo está en un formato específico de muestra 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 código de la herramienta de conversión de 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) de la malla, representado como un valor uint32.
  • Los siguientes 32 bits (4 bytes) del flujo de datos contienen el número de índices (numIndices) de la malla, representado como un valor uint32.
  • Después de eso, los bits subsiguientes (numVertices * sizeof(BasicVertex)) contienen los datos de vértices.
  • Los últimos bits (numIndices * 16) de datos contienen los datos de índices, representados como una secuencia de valores uint16.

El bojetivo es este: conocer el diseño a nivel de bits de los datos de malla que has cargado. Además, asegúrate de que es coherente con el orden de los bytes. Todas las plataformas de Windows 8 son little-endian.

En el ejemplo, se llama a un método CreateMesh desde el método LoadMeshAsync para realizar esta interpretación a 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 índices 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 el juego. Dónde y cuándo se cargan las mallas lo decides tú. Si tienes muchas mallas, es posible que solo quieras cargar algunas 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, puedes transmitir los vértices desde una memoria caché, pero es un procedimiento más complejo que no entra en el ámbito de este tema.

De nuevo, debes conocer 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 franjas. Para obtener más información sobre los datos de vértices, lee Introducción a los búferes en Direct3D 11 y Primitivos.

A continuación, echemos un vistazo a la carga de 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 las texturas. Al igual que las mallas, las texturas pueden tener una variedad de formatos y debes convertirlas a un formato que Direct3D pueda usar al cargarlos. Las texturas también vienen en una gran 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 los objetos de distancia; los mapas de tierra y de luz se usan para establecer capas de efectos 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 administrarlas 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 compresión en las texturas del juego, y puedes usar cualquier algoritmo de compresión o descompresión que quieras siempre que proporciones datos a los sombreadores de Direct3D en un formato que pueda entender (como un mapa de bits Texture2D).

Direct3D ofrece compatibilidad con los algoritmos de compresión de texturas DXT, aunque es posible que no se admitan todos los formatos DXT en el hardware gráfico del jugador. Los archivos DDS contienen texturas DXT (y otros formatos de compresión de texturas) y llevan el sufijo .dds.

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

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

  • Una descripción de los datos en el archivo.

    Los datos se describen con una descripción de encabezado mediante DDS_HEADER; el formato de píxel se define mediante DDS_PIXELFORMAT. Ten en cuenta que las estructuras DDS_HEADER y DDS_PIXELFORMAT reemplazan a las estructuras DDSURFACEDESC2, DDSCAPS2 y DDPIXELFORMAT de DirectDraw 7 en desuso. 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 como DDPF_FOURCC y dwFourCC se establece como "DX10", una estructura de DDS_HEADER_DXT10 adicional estará presente para acomodar matrices de texturas o formatos DXGI que no se pueden expresar como un formato 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;
    
  • Puntero a una matriz de bytes que contiene los datos principales de superficie.

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

    BYTE bdata2[]
    

Muchas herramientas se exportan al formato DDS. Si no tienes una herramienta para exportar la textura a este formato, considera 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, lee Guía de programación para DDS. En nuestro ejemplo, usaremos DDS.

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 la secuencia de bytes en un formato que Direct3D pueda 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í, se supone que es una textura DDS. Si no es así, usa las API del Windows Imaging Component (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();
    }
}

Cuando se completa este código, tendrás una Texture2D en la memoria cargada desde un archivo de imagen. Al igual que mallas, probablemente tengas muchas de ellas en el juego y en cualquier escena determinada. Considera la posibilidad de crear cachés para texturas a las que se acceda regularmente por escena o por nivel, en lugar de cargarlas todas cuando se inicia el juego o nivel.

(El método CreateDDSTextureFromMemory llamado en la muestra anterior se puede explorar en Código completo para DDSTextureLoader).

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

Carga de sombreadores

Los sombreadores son archivos de lenguaje de sombreadores de alto nivel (HLSL) compilados que se cargan en la memoria y se 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 la malla y los píxeles de las ventanillas 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 tantos que necesites cargarlos dinámicamente y, en la mayoría de los casos, puedes cargarlos simplemente cuando el juego se inicia o en cada nivel (como un sombreador para efectos de lluvia).

El código de la clase BasicLoader proporciona una serie de sobrecargas para distintos sombreadores, incluidos los sombreadores de vértices, geometrías, píxeles y envolventes. El código siguiente cubre sombreadores de píxeles como ejemplo. (Puedes 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 BasicReaderWriter (m_basicReaderWriter) para leer el archivo del objeto de sombreador compilado (.cso) proporcionado como una secuencia de bytes. Una vez completada la tarea, la expresión lambda llama a ID3D11Device::CreatePixelShader con los datos de bytes cargados desde el archivo. La devolución de llamada debe establecer alguna marca que indique que la carga se realizó correctamente y el código debe comprobar esta marca 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 de vértices. 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úrate de que la información de vértices que cargues 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 transformada (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 hacia y desde el objeto de sombreador compilado. El proceso ejecuta el sombreador de vértices una vez para 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 del sombreador.

Ahora, carga 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 hayas leído los datos de bytes del archivo CSO del sombreador de vértices, crea 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 envolventes y geometrías, también pueden requerir una configuración específica. El código completo para una variedad de métodos de carga de sombreadores se proporciona en Código completo para BasicLoader y en la muestra de carga de recursos de Direct3D.

Comentarios

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