Laden von Ressourcen im DirectX-Spiel

Die meisten Spiele laden Ressourcen (z. B. Shader, Strukturen, vordefinierte Gitter oder andere Grafikdaten) aus dem lokalen Speicher oder einem anderen Datenstrom. Hier erhalten Sie eine allgemeine Übersicht darüber, was Sie beim Laden dieser Dateien berücksichtigen müssen, um sie in Ihrem DirectX C/C++-Speil für Universelle Windows-Plattform (UWP) zu verwenden.

Beispielsweise wurden die Meshes 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 Strukturen 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), Strukturen (Bitmaps) und kompilierte Shaderobjekte.

Wichtige Informationen

Technologie

  • Parallel Patterns Library (ppltasks.h)

Voraussetzungen

  • Grundlagen der Windows-Runtime
  • Grundlagen asynchroner Aufgaben
  • Grundlegende Konzepte der 3D-Grafikprogrammierung.

Dieses Beispiel enthält auch drei Codedateien für das Laden und Verwalten von Ressourcen. In diesem Thema lernen Sie die in diesen Dateien definierten Codeobjekte kennen.

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

Den vollständigen 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 Grafik-Gitterobjekten 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-Struktur aus dem Arbeitsspeicher lädt.

 

Anweisungen

Asynchrones Laden

Das asynchrone Laden erfolgt mithilfe der Aufgabenvorlage aus der Parallel Patterns Library (PPL). 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 Ressourcen 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++".

Sehen wir uns nun die grundlegende Struktur zum Deklarieren und Erstellen einer asynchronen Methode zum Laden von Dateien an, 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 Fall 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 mithilfe des statischen Typs DataReader in eine Matrix.

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 sehen Sie den Aufruf von ReadDataAsync. Nach Abschluss des Vorgangs empfängt Ihr Code eine Matrix von Bytes, die aus der bereitgestellten Datei gelesen werden. Da ReadDataAsync selbst als Aufgabe definiert ist, können Sie eine Lambda-Funktion verwenden, um einen bestimmten Vorgang auszuführen, wenn die Byte-Matrix zurückgegeben wird, z. B. das Übergeben dieser Bytedaten an eine DirectX-Funktion, die es verwenden kann.

Wenn Ihr Spiel einfach genug ist, laden Sie Ihre Ressourcen mit einer Methode wie dieser, wenn der Benutzer das Spiel startet. Sie können dies tun, bevor Sie die Standard-Spielschleife von irgendeinem Punkt in der Aufrufsequenz Ihrer Implementierung IFrameworkView::Run starten. 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 ersten Interaktionen kommt.

Sie sollten das Spiel jedoch erst 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 für ihre Lademethoden, um dieses Signal festzulegen, wenn der Vorgang abgeschlossen ist. Überprüfen Sie die Variable, bevor Sie Komponenten starten, die diese geladenen Ressourcen verwenden.

Hier ist ein Beispiel, in dem die in BasicLoader.cpp definierten asynchronen Methoden zum Laden von Shadern, einem Gitter und einer Struktur verwendet werden, wenn das Spiel gestartet wird. Ein bestimmtes Feld wird für das Spielobjekt m_loadingComplete festgelegt, 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. Wenn Sie über mehrere Flags verfügen, haben Sie die Möglichkeit der Wettlaufsituation. Wenn die Lambda-Funktion beispielsweise zwei Flags sequenziell auf denselben Wert festlegt, wird in einem anderen Thread möglicherweise nur das erste Flag festgelegt, wenn es überprüft wird, bevor das zweite Flag festgelegt wird.

Sie haben gelernt, 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 vollständigen Code für BasicLoader.

Natürlich erfordern unterschiedliche 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, Strukturen und Shader.

Laden von Gittern

Gitter sind Vertex-Daten, die entweder durch Code in Ihrem Spiel generiert oder aus einer anderen App (z. B. 3DStudio 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, Häusern und Figuren. Je nach Format enthalten sie häufig auch Farb- und Animationsdaten. Wir konzentrieren uns auf Gitter, die ausschließlich Vertex-Daten enthalten.

Um ein Gitter richtig zu laden, müssen Sie das Format der Daten in der Datei für das Gitter kennen. Unser oben erwähnter einfacher Typ BasicReaderWriter liest einfach die Daten als Byte-Datenstrom. Es ist nicht bekannt, dass die Bytedaten ein Gitter darstellen oder gar ein bestimmtes Gitterformat, wie es von einer anderen Anwendung exportiert wird! Sie müssen die Konvertierung ausführen, während Sie die Gitterdaten in den Arbeitsspeicher übertragen.

(Sie sollten immer versuchen, Ressourcendaten 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 Byte-Daten aus der Gitterdatei abrufen. Das Format im Beispiel geht davon aus, dass es sich bei der Datei um ein beispielspezifisches Format handelt, das mit .vbo versehen ist. (Auch hier ist das Format nicht mit dem VBO-Format von OpenGL identisch.) Jeder Vertex selbst ist dem Typ BasicVertex zugeordnet, der im Code für das Obj2vbo-Konvertertool definiert ist. Das Layout der Vertex-Daten in der .vbo-Datei sieht wie folgt aus:

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

Wichtig: Kennen Sie das Layout der Bitebene der Gitterdaten, die Sie geladen haben. Achten Sie außerdem darauf, dass Sie mit der Bygte-Reihenfolge (Endian) konsistent sind. Windows 8-Plattformen sind wenig endisch.

Im Beispiel rufen Sie eine Methode ,CreateMesh, aus der Methode LoadMeshAsync auf, um diese Interpretation auf Bit-Ebene 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 Byte-Daten und erstellt einen Vertexpuffer und einen Indexpuffer für das Gitter, indem die Vertex- bzw. Indexlisten an ID3D11Device::CreateBuffer übergeben und entweder D3D11_BIND_VERTEX_BUFFER oder D3D11_BIND_INDEX_BUFFER angegeben werden. Hier sehen Sie den Code, der in BasicLoader verwendet 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 davon vom Datenträger an bestimmten Punkten im Spiel laden, z. B. während bestimmter vordefinierter Ladezustände. Bei großen Gittern wie Geländedaten können Sie die Eckpunkte aus einem Cache streamen, das ist jedoch ein komplexerer Vorgang und nicht Teil dieses Themas.

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

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

Laden von Strukturen

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 Strukturen. Wie Gitter können Strukturen in einer Vielzahl von Formaten enthalten sein, und Sie konvertieren sie in ein Format, das Direct3D verwenden kann, wenn Sie sie laden. Es gibt viele verschiedene Typen von Strukturen. Sie werden verwendet, um verschiedene Effekte zu erzeugen. MIP-Ebenen für Strukturen 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 Basisstruktur zu schichten; und normale Karten werden bei Beleuchtungsberechnungen pro Pixel verwendet. In einem modernen Spiel kann eine normale Szene möglicherweise Tausende einzelner Strukturen 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 Strukturen ganz leicht einen großen Teil des GPU-Speichers (und des Systems) belegen können, werden sie häufig komprimiert. Sie müssen keine Komprimierung für die Strukturen 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-Strukturen (und andere Strukturkomprimierungsformate) und beginnen mit .dds.

Bei der DDS-Datei handelt es sich um eine Binärdatei, die die folgenden Informationen enthält:

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

  • Eine Beschreibung der Daten in der Datei.

    Die Daten werden mit einer Kopfzeilenbeschreibung mithilfe von DDS_HEADER beschrieben. Das Pixelformat wird mithilfe von DDS_PIXELFORMAT definiert. Die Strukturen DDS_HEADER und DDS_PIXELFORMAT ersetzen die veralteten Strukturen DDSURFACEDESC2, DDSCAPS2 und DDPIXELFORMAT DirectDraw 7. DDS_HEADER ist das binäre Äquivalent von 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 und dwFourCC auf "DX10" festgelegt ist, wird eine zusätzliche DDS_HEADER_DXT10-Struktur vorhanden sein, um die Textur-Matrix oder DXGI-Formate aufzunehmen, die nicht als RGB-Pixelformat wie Gleitkommaformate, sRGB-Formate usw. ausgedrückt werden können. Wenn die Struktur DDS_HEADER_DXT10 vorhanden ist, sieht die gesamte Datenbeschreibung wie folgt aus.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • Ein Zeiger auf eine Byte-Matrix, das die Standard-Oberflächendaten enthält.

    BYTE bdata[]
    
  • Ein Zeiger auf eine Byte-Matrix, die die übrigen Oberflächen enthält, z. B. Mipmap-Ebenen, Gesichter in einer Würfelzuordnung, Tiefen in einer Volumenstruktur. Folgen Sie diesen Links für weitere Informationen zum DDS-Dateilayout für eine: Struktur, Würfelzuordnung oder Volumenstruktur.

    BYTE bdata2[]
    

Viele Tools exportieren in das DDS-Format. Wenn Sie nicht über ein Tool zum Exportieren Ihrer Struktur in dieses Format verfügen, sollten Sie ein solches erstellen. Weitere Informationen zum DDS-Format und zur Verwendung in Ihrem Code finden Sie im Programmierhandbuch für DDS. In diesem Beispiel wird DDS verwendet.

Wie bei anderen Ressourcentypen lesen Sie die Daten aus einer Datei als Bytestrom. Nach Abschluss der Ladeaufgabe führt der Lambda-Aufruf Code (die Methode CreateTexture) 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 Ausschnitt überprüft die Lambda-Funktion, ob der Dateiname die Erweiterung "dds" aufweist. Wenn dies der Fall ist, handelt es sich wahrscheinlich um eine DDS-Struktur. Wenn nicht, verwenden Sie die WINDOWS Imaging Component (WIC)-APIs, um das Format zu ermitteln und die Daten als Bitmap zu decodieren. Das Ergebnis ist in beiden Fällen 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();
    }
}

Wenn dieser Code abgeschlossen ist, haben Sie eine Texture2D im Arbeitsspeicher, die aus einer Bilddatei geladen wird. Wie bei Gittern haben Sie wahrscheinlich viele davon in Ihrem Spiel und in jeder Szene. Erwägen Sie das Erstellen von Caches für regelmäßig aufgerufene Strukturen pro Szene oder Ebene, anstatt sie alle zu laden, wenn das Spiel oder das Level gestartet wird.

(Die Methode CreateDDSTextureFromMemory, die im obigen Beispiel aufgerufen wird, kann komplett im vollständigen Code für DDSTextureLoader untersucht werden.)

Außerdem können einzelne Strukturen oder Struktur-"Skins" bestimmten Gitter-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 Strukturen verwendet hat. Stellen Sie sicher, dass Sie diese Informationen auch beim Laden der exportierten Daten erfassen, da sie damit beim Ausführen einer Fragmentschattierung die richtigen Strukturen den entsprechenden Oberflächen zuordnen.

Laden von Shadern

Shader sind kompilierte High Level Shader Language Dateien (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 Eckpunkte ihres Gitters bzw. die Pixel im Viewport der Szene verarbeiten. Der HLSL-Code wird ausgeführt, um die Geometrie zu transformieren, Beleuchtungseffekte und Strukturen 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 Klasse BasicLoader 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 kompletten Code in Vollständiger Code für BasicLoader nachschlagen.)

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 Instanz BasicReaderWriter (m_basicReaderWriter), um in der bereitgestellten kompilierten Shader-Objektdatei (.cso) als Bytestrom zu lesen. Nach Abschluss dieser Aufgabe ruft Lambda ID3D11Device::CreatePixelShader mit den aus der Datei geladenen Byte-Daten auf. Ihr Rückruf muss ein Flag festlegen, das angibt, dass der Ladevorgang erfolgreich war, und Ihr Code muss dieses Flag überprüfen, bevor der Shader ausgeführt wird.

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

Erstellen wir nun das Eingabelayout, 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 Eckpunkt, der auch als drei 32-Bit-Gleitkommawerte dargestellt wird.
  • Ein transformierter 2D-Texturkoordinatenwert (u, v), dargestellt als 32-Bit-Gleitkommawerte.

Diese Eingabeelemente pro Vertex werden als HLSL-Semantik bezeichnet, und sie sind eine Reihe definierter Register, die zum Übergeben von Daten an und von Ihrem kompilierten Shaderobjekt verwendet werden. Die Pipeline führt den Vertex-Shader einmal für jeden Vertex im Gitter aus, den Sie geladen haben. Die Semantik definiert die Eingabe (und Ausgabe) des Vertex-Shaders während der Ausführung und stellt diese Daten für die Pro-Vertex-Berechnungen im HLSL-Code ihres 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 Byte-Daten für die CSO-Datei des Vertex-Shaders gelesen haben, erstellen Sie den Vertex-Shader durch Aufrufen von ID3D11Device::CreateVertexShader. Danach erstellen Sie ihr Eingabelayout für den Shader in derselben Lambda-Funktion.

Andere Shader-Typen, z. B. Hull- und Geometrie-Shader, können auch eine bestimmte Konfiguration erfordern. Vollständiger Code für eine Vielzahl von Shader-Lademethoden wird im vollständigen Code für BasicLoader und im Beispiel zum Laden von Direct3D-Ressourcen bereitgestellt.

Hinweise

An diesem Punkt sollten Sie Methoden zum asynchronen Laden allgemeiner Spielressourcen und Ressourcen wie Gitter, Strukturen und kompilierte Shader verstanden haben und sie bei Bedarf anpassen können.