Freigeben über


Ressourcen in Ihrem DirectX-Spiel laden

Die meisten Spiele laden irgendwann Ressourcen und Assets (z. B., Shader, Texturen, vordefinierte Meshes oder andere Grafikdaten) aus dem lokalen Speicher oder einem anderen Datenstrom. Hier werden Sie durch eine allgemeine Übersicht darüber geführt, was Sie beim Laden dieser Dateien berücksichtigen müssen, um sie in Ihrem DirectX C/C++-Spiel für die universelle Windows-Plattform (UWP) zu verwenden.

Beispielsweise wurden die Gitter für polygonale Objekte in Ihrem Spiel möglicherweise mit einem anderen Tool erstellt und in ein bestimmtes Format exportiert. Das gleiche gilt für Texturen und vieles mehr: Während eine flache, nicht komprimierte Bitmap häufig von den meisten Tools geschrieben und von den meisten Grafik-APIs verstanden werden kann, kann sie für die Verwendung in Ihrem Spiel äußerst ineffizient sein. Hier führen wir Sie durch die grundlegenden Schritte zum Laden von drei verschiedenen Arten von Grafikressourcen für die Verwendung mit Direct3D: Gitter (Modelle), Texturen (Bitmaps) und kompilierte Shaderobjekte.

Was Sie wissen müssen

Technologieen

  • Parallel Patterns Library (ppltasks.h)

Voraussetzungen

  • Verstehen Sie die Grundlagen der Windows Runtime
  • Grundlegendes zu asynchronen Aufgaben
  • Grundlegende Konzepte der 3D-Grafikprogrammierung verstehen.

Dieses Beispiel enthält auch drei Codedateien für das Laden und Verwalten von Ressourcen. Im Verlauf dieses Themas stoßen Sie auf die in diesen Dateien definierten Codeobjekte.

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

Der vollständige Code für diese Beispiele finden Sie unter den folgenden Links.

Thema BESCHREIBUNG

Vollständiger Code für BasicLoader

Vollständiger Code für eine Klasse und Methoden zum Konvertieren und Laden von Grafikgitterobjekten in den Arbeitsspeicher.

Vollständiger Code für BasicReaderWriter

Vollständiger Code für eine Klasse und Methoden zum Lesen und Schreiben von Binärdatendateien im Allgemeinen. Wird von der BasicLoader-Klasse verwendet.

Vollständiger Code für DDSTextureLoader

Vollständiger Code für eine Klasse und Methode, die eine DDS-Textur aus dem Arbeitsspeicher lädt.

 

Anweisungen

Asynchrones Laden

Das asynchrone Laden wird mithilfe der -Task--Vorlage aus der Parallel Patterns Library (PPL) behandelt. Eine Aufgabe enthält einen Methodenaufruf gefolgt von einer Lambda-Funktion, die die Ergebnisse des asynchronen Aufrufs nach Abschluss verarbeitet, und folgt in der Regel dem Format von:

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

Aufgaben können mithilfe der Syntax .then() verkettet werden, sodass bei Abschluss eines Vorgangs ein anderer asynchroner Vorgang, der von den Ergebnissen des vorherigen Vorgangs abhängt, ausgeführt werden kann. Auf diese Weise können Sie komplexe Objekte in separaten Threads laden, konvertieren und verwalten, so dass sie für den Spieler fast unsichtbar erscheinen.

Weitere Informationen finden Sie unter "Asynchrone Programmierung in C++".

Werfen wir nun einen Blick auf die grundlegende Struktur für die Deklaration und Erstellung einer Methode zum asynchronen Laden von Dateien, 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;
    });
}

Wenn Ihr Code in diesem Code die oben definierte ReadDataAsync-Methode aufruft, wird eine Aufgabe erstellt, um einen Puffer aus dem Dateisystem zu lesen. Nach Abschluss des Vorgangs übernimmt eine verkettete Aufgabe den Puffer und streamt die Bytes aus diesem Puffer unter Zuhilfenahme des statischen DataReader- Typs in ein Array.

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

Hier ist der Aufruf, den Sie für ReadDataAsyncmachen. Nach Abschluss des Vorgangs erhält Ihr Code ein Array von Bytes, die aus der bereitgestellten Datei gelesen wurden. Da ReadDataAsync selbst als Aufgabe definiert ist, können Sie eine Lambda-Funktion verwenden, um einen bestimmten Vorgang auszuführen, wenn das Bytearray zurückgegeben wird, z. B. das Übergeben dieser Bytedaten an eine DirectX-Funktion, die es verwenden kann.

Wenn Ihr Spiel ausreichend einfach ist, laden Sie Ihre Ressourcen mit einer Methode wie dieser, wenn der Benutzer das Spiel startet. Bevor Sie die Hauptspielschleife starten, können Sie dies von irgendeinem Punkt in der Aufrufsequenz Ihrer IFrameworkView::Run Implementierung tun. Auch hier rufen Sie die Methoden zum Laden von Ressourcen asynchron auf, damit das Spiel schneller starten kann und der Spieler nicht warten muss, bis das Laden abgeschlossen ist, bevor es zu frühen Interaktionen kommt.

Sie wollen das Spiel aber erst dann starten, wenn der gesamte asynchrone Ladevorgang abgeschlossen ist. Erstellen Sie eine Methode zum Signalisieren, wenn das Laden abgeschlossen ist, z. B. ein bestimmtes Feld, und verwenden Sie die Lambdas in Ihrer Lademethode(n), um dieses Signal festzulegen, wenn sie fertig ist. Überprüfen Sie die Variable, bevor Sie Komponenten starten, die diese geladenen Ressourcen verwenden.

Im Folgenden finden Sie ein Beispiel, in dem die in BasicLoader.cpp definierten asynchronen Methoden zum Laden von Shadern, einem Gitter und einer Textur verwendet werden, wenn das Spiel gestartet wird. Beachten Sie, dass ein bestimmtes Feld für das Spielobjekt festgelegt wird, m_loadingComplete, wenn alle Lademethoden abgeschlossen sind.

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

Beachten Sie, dass die Aufgaben mithilfe des &&-Operators aggregiert wurden, sodass die Lambda-Funktion, die das Flag "Ladevorgang abgeschlossen" festlegt, nur ausgelöst wird, wenn alle Aufgaben abgeschlossen sind. Beachten Sie, dass Sie, wenn Sie über mehrere Flaggen verfügen, die Möglichkeit der Rennbedingungen haben. Wenn die Lambda-Funktion beispielsweise zwei Flags nacheinander auf denselben Wert setzt, kann in einem anderen Thread möglicherweise nur das erste Flag gesehen werden, wenn er überprüft wird, bevor das zweite Flag gesetzt wird.

Sie haben gesehen, wie Ressourcendateien asynchron geladen werden. Synchrone Dateiladevorgänge sind viel einfacher, und Sie finden Beispiele dafür im vollständigen Code für BasicReaderWriter und Complete-Code für BasicLoader.

Natürlich erfordern unterschiedliche Ressourcen- und Ressourcentypen häufig eine zusätzliche Verarbeitung oder Konvertierung, bevor sie in Ihrer Grafikpipeline verwendet werden können. Sehen wir uns drei spezifische Ressourcentypen an: Gitter, Texturen und Shader.

Laden von Gittern

Meshes sind Vertexdaten, die entweder durch Code in Ihrem Spiel generiert oder aus einer anderen App (z. B. 3D Studio Max oder Alias/Wavefront) oder einem Tool in eine Datei exportiert werden. Diese Gitter stellen die Modelle in Ihrem Spiel dar, von einfachen Grundtypen wie Würfeln und Kugeln bis hin zu Autos und Häusern und Figuren. Sie enthalten häufig auch Farb- und Animationsdaten, je nach Format. Wir konzentrieren uns auf Gitter, die nur Vertexdaten enthalten.

Um ein Gitter richtig zu laden, müssen Sie das Format der Daten in der Datei für das Gitter kennen. Unser einfacher BasicReaderWriter liest die Daten einfach als Bytestream ein; es ist nicht bekannt, dass die Bytedaten ein Gitter darstellen, geschweige denn ein bestimmtes Gitterformat, wie es von einer anderen Anwendung exportiert wurde! Sie müssen die Konvertierung vornehmen, während Sie die Mesh-Daten in den Arbeitsspeicher übertragen.

(Sie sollten immer versuchen, Objektdaten in einem Format zu verpacken, das der internen Darstellung so nah wie möglich ist. Dadurch wird die Ressourcenauslastung reduziert und Zeit gespart.)

Lassen Sie uns die Bytedaten aus der Gitterdatei abrufen. Das Format im Beispiel nimmt an, dass es sich bei der Datei um ein beispielspezifisches Format handelt, das die Endung .vbo hat. (Auch hier ist dieses Format nicht mit dem VBO-Format von OpenGL identisch.) Jeder Vertex selbst ist dem BasicVertex- Typ zugeordnet, der im Code für das Obj2vbo-Konvertertool definiert ist. Das Layout der Vertexdaten in der VBO-Datei sieht wie folgt aus:

  • Die ersten 32 Bits (4 Byte) des Datenstroms enthalten die Anzahl der Scheitelpunkte (numVertices) im Gitter, dargestellt als uint32-Wert.
  • Die nächsten 32 Bits (4 Byte) des Datenstroms enthalten die Anzahl der Indizes im Gitter (NumIndices), dargestellt als uint32-Wert.
  • Danach enthalten die nachfolgenden Bits (numVertices * sizeof(BasicVertex)) die Vertexdaten.
  • Die letzten (NumIndices * 16) Bits der Daten enthalten die Indexdaten, dargestellt als eine Sequenz von uint16-Werten.

Der Punkt ist dies: Kennen Sie das Layout der Bitebene der Gitterdaten, die Sie geladen haben. Achten Sie außerdem darauf, dass Sie mit Endianität konsistent sind. Alle Windows 8 Plattformen sind Little-Endian.

Im Beispiel rufen Sie eine Methode ,CreateMesh, aus der LoadMeshAsync-Methode auf, um diese Bitebeneninterpretation durchzuführen.

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 interpretiert die aus der Datei geladenen Bytedaten und erstellt einen Vertexpuffer sowie einen Indexpuffer für das Gitter, indem die Vertex- bzw. Indexlisten an ID3D11Device::CreateBuffer übergeben werden und entweder D3D11_BIND_VERTEX_BUFFER oder D3D11_BIND_INDEX_BUFFER angegeben wird. Hier ist der Code, der in BasicLoaderverwendet wird:

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

Normalerweise erstellen Sie ein Vertex-/Indexpufferpaar für jedes Gitter, das Sie in Ihrem Spiel verwenden. Wo und wann Sie die Gitter laden, liegt bei Ihnen. Wenn Sie viele Gitter haben, möchten Sie möglicherweise nur einige von dem Datenträger an bestimmten Punkten im Spiel laden, z. B. während bestimmter vordefinierter Ladezustände. Bei großen Netzen, wie Geländedaten, können Sie die Scheitelpunkte aus einem Cache streamen, das ist jedoch ein komplexerer Vorgang und wird nicht im Rahmen dieses Themas behandelt.

Auch hier, kennen Sie Ihr Vertex-Datenformat! Es gibt viele Möglichkeiten, Vertexdaten in allen Tools darzustellen, die zum Erstellen von Modellen verwendet werden. Es gibt auch viele verschiedene Möglichkeiten, das Eingabelayout der Vertexdaten in Direct3D darzustellen, z. B. Dreieckslisten und Streifen. Weitere Informationen zu Vertexdaten finden Sie unter Einführung in Puffer in Direct3D 11 und Grundtypen.

Als Nächstes sehen wir uns das Laden von Texturen an.

Laden von Texturen

Die am häufigsten verwendeten Ressourcen in einem Spiel – und das Element, das die meisten Dateien auf dem Datenträger und im Arbeitsspeicher umfasst – sind Texturen. Wie Meshs können Texturen in einer Vielzahl von Formaten vorliegen, und Sie konvertieren sie beim Laden in ein Format, das Direct3D verwenden kann. Texturen kommen auch in einer Vielzahl von Typen und werden verwendet, um verschiedene Effekte zu erzeugen. MIP-Ebenen für Texturen können verwendet werden, um das Aussehen und die Leistung von Abstandsobjekten zu verbessern; Schmutz- und Lichtkarten werden verwendet, um Effekte und Details über eine Basistextur zu verschichten; und normale Karten werden bei Beleuchtungsberechnungen pro Pixel verwendet. In einem modernen Spiel kann eine typische Szene möglicherweise Tausende einzelner Texturen aufweisen, und Ihr Code muss sie effektiv verwalten!

Ebenso wie Gitter gibt es eine Reihe bestimmter Formate, die verwendet werden, um die Speichernutzung effizient zu gestalten. Da Texturen auf einfache Weise einen großen Teil des GPU-Speichers (und des Systems) verbrauchen können, werden sie häufig komprimiert. Sie müssen keine Komprimierung für die Texturen Ihres Spiels verwenden, und Sie können alle gewünschten Komprimierungs-/Dekomprimierungsalgorithmen verwenden, solange Sie die Direct3D-Shader mit Daten in einem Format bereitstellen, das sie verstehen kann (z. B. eine Texture2D-Bitmap ).

Direct3D bietet Unterstützung für die DXT-Texturkomprimierungsalgorithmen, obwohl jedes DXT-Format möglicherweise nicht in der Grafikhardware des Spielers unterstützt wird. DDS-Dateien enthalten DXT-Texturen (und andere Texturkomprimierungsformate) und sind mit .dds suffixiert.

Eine DDS-Datei ist eine Binärdatei, die die folgenden Informationen enthält:

  • Ein DWORD (magische Zahl), der den vierstellige Codewert "DDS" (0x20534444) enthält.

  • Eine Beschreibung der Daten in der Datei.

    Die Daten werden mit einer Kopfzeilenbeschreibung mithilfe von DDS_HEADERbeschrieben; Das Pixelformat wird mithilfe von DDS_PIXELFORMATdefiniert. Beachten Sie, dass die Strukturen DDS_HEADER und DDS_PIXELFORMAT die veralteten Strukturen DDSURFACEDESC2, DDSCAPS2 und DDPIXELFORMAT von DirectDraw 7 ersetzen. DDS_HEADER ist das binäre Äquivalent zu DDSURFACEDESC2 und DDSCAPS2. DDS_PIXELFORMAT ist das binäre Äquivalent von DDPIXELFORMAT.

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    Wenn der Wert von dwFlags in DDS_PIXELFORMAT auf DDPF_FOURCC festgelegt ist und dwFourCC auf "DX10" gesetzt ist, wird eine zusätzliche DDS_HEADER_DXT10 Struktur hinzugefügt, um Texturarrays oder DXGI-Formate, die nicht als RGB-Pixelformat ausgedrückt werden können, wie beispielsweise Gleitkommaformate oder sRGB-Formate, aufzunehmen. Wenn die DDS_HEADER_DXT10 Struktur vorhanden ist, stellt sich die gesamte Datenbeschreibung folgendermaßen dar.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • Ein Zeiger auf ein Byte-Array, das die Hauptflächendaten enthält.

    BYTE bdata[]
    
  • Ein Zeiger auf ein Array von Bytes, das die verbleibenden Oberflächen enthält, z. B. Mipmap-Level, Gesichter in einer Würfelkarte, Tiefen in einer Volumetextur. Folgen Sie diesen Links, um weitere Informationen zum DDS-Dateilayout für eine: Textur, eine Würfelkarteoder eine Volumentextur.

    BYTE bdata2[]
    

Viele Tools exportieren in das DDS-Format. Wenn Sie nicht über ein Tool zum Exportieren Ihrer Textur in dieses Format verfügen, sollten Sie eins erstellen. Weitere Informationen zum DDS-Format und darüber, wie Sie es in Ihrem Code verwenden, finden Sie im Programmierhandbuch für DDS. In unserem Beispiel verwenden wir DDS.

Wie bei anderen Ressourcentypen lesen Sie die Daten aus einer Datei als Bytestream. Nach Abschluss der Ladeaufgabe führt der Lambda-Aufruf Code (die CreateTexture-Methode ) aus, um den Bytestrom in einem Format zu verarbeiten, das Direct3D verwenden kann.

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

Im vorherigen Codeausschnitt überprüft die Lambda-Funktion, ob der Dateiname die Erweiterung "dds" aufweist. Wenn dies der Fall ist, gehen Sie davon aus, dass es sich um eine DDS-Textur handelt. Wenn nicht, verwenden Sie die WINDOWS Imaging Component (WIC)-APIs, um das Format zu ermitteln und die Daten als Bitmap zu decodieren. Auf beide Weise ist das Ergebnis eine Texture2D- Bitmap (oder ein Fehler).

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

Nach Abschluss dieses Codes verfügen Sie über eine Texture2D- im Arbeitsspeicher, die aus einer Bilddatei geladen wurde. Wie bei Gittern haben Sie wahrscheinlich viele davon in Ihrem Spiel und in jeder bestimmten Szene. Erwägen Sie das Erstellen von Caches für häufig verwendete Texturen, anstatt sie alle zu laden, wenn das Spiel oder das Level gestartet wird, pro Szene oder Ebene.

(Die im obigen Beispiel aufgerufene Methode CreateDDSTextureFromMemory kann im vollständigen Code für DDSTextureLoaderumfassend erkundet werden.)

Außerdem können einzelne Texturen oder Textur-"Skins" bestimmten Mesh-Polygonen oder Oberflächen zugeordnet werden. Diese Zuordnungsdaten werden in der Regel vom Tool exportiert, das ein Künstler oder Designer zum Erstellen des Modells und der Texturen verwendet hat. Stellen Sie sicher, dass Sie diese Informationen auch erfassen, wenn Sie die exportierten Daten laden, da Sie sie verwenden werden, um beim Ausführen der Fragmentschattierung den entsprechenden Oberflächen die richtigen Texturen zuzuordnen.

Laden von Shadern

Shader sind kompilierte Dateien der High Level Shader Language (HLSL), die in den Arbeitsspeicher geladen und in bestimmten Phasen der Grafikpipeline aufgerufen werden. Die gängigsten und wichtigsten Shader sind die Vertex- und Pixel-Shader, die die einzelnen Vertizes Ihres Netzes bzw. die Pixel im Viewport der Szene verarbeiten. Der HLSL-Code wird ausgeführt, um die Geometrie zu transformieren, Beleuchtungseffekte und Texturen anzuwenden und die Nachbearbeitung für die gerenderte Szene durchzuführen.

Ein Direct3D-Spiel kann über eine Reihe verschiedener Shader verfügen, die jeweils in einer separaten CSO-Datei (Compiled Shader Object, .cso) kompiliert wurden. Normalerweise haben Sie nicht so viele, dass Sie sie dynamisch laden müssen, und in den meisten Fällen können Sie sie einfach laden, wenn das Spiel gestartet wird, oder pro Level (z. B. ein Shader für Regeneffekte).

Der Code in der BasicLoader--Klasse stellt eine Reihe von Überladungen für verschiedene Shader bereit, einschließlich Vertex, Geometrie, Pixel und Hull-Shader. Der folgende Code behandelt Pixel-Shader als Beispiel. (Sie können den Komplettcode in Komplettcode für BasicLoadereinsehen.)

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

In diesem Beispiel verwenden Sie die BasicReaderWriter Instanz (m_basicReaderWriter), um die bereitgestellte kompilierte Shader-Objektdatei (.cso) als Byte-Strom zu lesen. Nach Abschluss dieser Aufgabe ruft die Lambda ID3D11Device::CreatePixelShader mit den aus der Datei geladenen Bytedaten auf. Ihre Callback-Funktion muss ein Flag festlegen, das angibt, dass der Ladevorgang erfolgreich war, und Ihr Code muss dieses Flag überprüfen, bevor Sie den Shader ausführen.

Vertex-Shader sind etwas komplexer. Für einen Vertex-Shader laden Sie auch ein separates Eingabelayout, das die Vertexdaten definiert. Der folgende Code kann verwendet werden, um einen Vertex-Shader asynchron zusammen mit einem benutzerdefinierten Vertexeingabelayout zu laden. Stellen Sie sicher, dass die Vertexinformationen, die Sie aus Ihren Meshes laden, durch dieses Eingabelayout richtig dargestellt werden können!

Lasst uns das Eingabelayout erstellen, bevor Sie den Vertex-Shader laden.

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

In diesem speziellen Layout weist jeder Vertex die folgenden Daten auf, die vom Vertex-Shader verarbeitet werden:

  • Eine 3D-Koordinatenposition (x, y, z) im Koordinatenbereich des Modells, dargestellt als ein Trio von 32-Bit-Gleitkommawerten.
  • Ein normaler Vektor für den Scheitelpunkt, der auch als drei 32-Bit-Gleitkommawerte dargestellt wird.
  • Ein transformierter 2D-Texturkoordinatenwert (u, v), dargestellt als Paar von 32-Bit-Gleitkommawerten.

Diese Eingabeelemente pro Vertex werden HLSL-Semantikgenannt und sind eine Reihe von definierten Registern, die verwendet werden, um Daten an und von Ihrem kompilierten Shaderobjekt zu übergeben. Die Pipeline führt den Vertex-Shader einmal für jeden Vertex im Gitter aus, den Sie geladen haben. Die Semantik definiert die Eingabe in und die Ausgabe des Vertex-Shaders während seiner Ausführung und stellt diese Daten für Ihre vertex-spezifischen Berechnungen im HLSL-Code des Shaders bereit.

Laden Sie nun das Vertex-Shaderobjekt.

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

Sobald Sie in diesem Code die Bytedaten für die CSO-Datei des Vertex-Shaders gelesen haben, erstellen Sie den Vertex-Shader, indem Sie ID3D11Device::CreateVertexShaderaufrufen. Danach erstellen Sie Ihr Eingabelayout für den Shader in derselben Lambda-Funktion.

Andere Shadertypen, z. B. Hull- und Geometrie-Shader, können auch eine bestimmte Konfiguration erfordern. Vollständiger Code für eine Vielzahl von Shaderlademethoden wird in Vollständiger Code für BasicLoader- und im Direct3D-Ressourcenladebeispielbereitgestellt.

Bemerkungen

An diesem Punkt sollten Sie in der Lage sein, Methoden zum asynchronen Laden allgemeiner Spielressourcen und Assets wie Meshes, Texturen und kompilierte Shader zu verstehen und zu erstellen oder zu ändern.