Renderingframework II: Rendering von Spielen

Hinweis

Dieses Thema ist Teil der Tutorialreihe Erstellen eines einfachen Universelle Windows-Plattform -Spiels (UWP) mit DirectX. Das Thema unter diesem Link legt den Kontext für die Reihe fest.

Im Renderingframework I haben wir beschrieben, wie wir die Szeneninformationen auf dem Bildschirm anzeigen. Nun gehen wir einen Schritt zurück und erfahren, wie Sie die Daten für das Rendering vorbereiten.

Hinweis

Wenn Sie den neuesten Spielcode für dieses Beispiel nicht heruntergeladen haben, wechseln Sie zu Direct3D-Beispielspiel. Dieses Beispiel ist Teil einer großen Sammlung von UWP-Featurebeispielen. Anweisungen zum Herunterladen des Beispiels finden Sie unter Beispielanwendungen für die Windows-Entwicklung.

Ziel

Kurze Zusammenfassung des Ziels. Es ist zu verstehen, wie Sie ein grundlegendes Renderingframework einrichten, um die Grafikausgabe für ein UWP DirectX-Spiel anzuzeigen. Wir können sie lose in diese drei Schritte gruppieren.

  1. Herstellen einer Verbindung mit unserer Grafikschnittstelle
  2. Vorbereitung: Erstellen Sie die Ressourcen, die wir zum Zeichnen der Grafiken benötigen.
  3. Anzeigen der Grafiken: Rendern des Frames

Renderingframework I: Intro to rendering erläutert, wie Grafiken gerendert werden, wobei die Schritte 1 und 3 behandelt werden.

In diesem Artikel wird erläutert, wie Sie andere Teile dieses Frameworks einrichten und die erforderlichen Daten vorbereiten, bevor das Rendern erfolgen kann. Dies ist Schritt 2 des Prozesses.

Entwerfen des Renderers

Der Renderer ist für das Erstellen und Verwalten aller D3D11- und D2D-Objekte verantwortlich, die zum Generieren der Visuals des Spiels verwendet werden. Die GameRenderer-Klasse ist der Renderer für dieses Beispielspiel und wurde entwickelt, um die Renderinganforderungen des Spiels zu erfüllen.

Dies sind einige Konzepte, die Sie verwenden können, um den Renderer für Ihr Spiel zu entwerfen:

  • Da Direct3D 11-APIs als COM-APIs definiert sind, müssen Sie ComPtr-Verweise auf die von diesen APIs definierten Objekte bereitstellen. Diese Objekte werden automatisch freigegeben, wenn ihr letzter Verweis den gültigen Bereich verlässt und die App beendet wird. Weitere Informationen finden Sie unter ComPtr. Beispiel für diese Objekte: Konstantenpuffer, Shaderobjekte – Vertexshader, Pixelshader und Shaderressourcenobjekte.
  • Konstantenpuffer werden in dieser Klasse definiert, um verschiedene Daten zu enthalten, die für das Rendering benötigt werden.
    • Verwenden Sie mehrere Konstantenpuffer mit unterschiedlichen Frequenzen, um die Datenmenge zu reduzieren, die pro Frame an die GPU gesendet werden muss. In diesem Beispiel werden Konstanten basierend auf der Häufigkeit, mit der sie aktualisiert werden müssen, in verschiedene Puffer unterteilt. Dies ist die empfohlene Methode für die Direct3D-Programmierung.
    • In diesem Beispielspiel werden vier Konstantenpuffer definiert.
      1. m_constantBufferNeverChanges enthält die Beleuchtungsparameter. Sie wird einmal in der FinalizeCreateGameDeviceResources-Methode festgelegt und ändert sich nie wieder.
      2. m_constantBufferChangeOnResize enthält die Projektionsmatrix. Die Projektionsmatrix hängt von der Größe und dem Seitenverhältnis des Fensters ab. Sie wird in CreateWindowSizeDependentResources festgelegt und dann aktualisiert, nachdem Ressourcen in der FinalizeCreateGameDeviceResources-Methode geladen wurden. Beim Rendern in 3D wird es auch zweimal pro Frame geändert.
      3. m_constantBufferChangesEveryFrame enthält die Ansichtsmatrix. Diese Matrix ist abhängig von der Kameraposition und Blickrichtung (normal zur Projektion) und ändert sich einmal pro Frame in der Render-Methode . Dies wurde zuvor im Renderingframework I: Einführung in das Rendering unter der GameRenderer::Render-Methode erläutert.
      4. m_constantBufferChangesEveryPrim enthält die Modellmatrix und die Materialeigenschaften der einzelnen Grundtypen. Die Modellmatrix transformiert Scheitelpunkte aus lokalen Koordinaten in globale Koordinaten. Diese Konstanten gelten speziell für die einzelnen Grundtypen und werden für jeden Draw-Aufruf aktualisiert. Dies wurde weiter oben im Renderingframework I: Einführung in das Rendering unter dem primitiven Rendering erläutert.
  • Shaderressourcenobjekte, die Texturen für die Grundtypen enthalten, werden ebenfalls in dieser Klasse definiert.
    • Einige Texturen sind vordefiniert (DDS ist ein Dateiformat, das zum Speichern komprimierter und nicht komprimierter Texturen verwendet werden kann. DDS-Texturen werden sowohl für die Wände und den Boden der Welt als auch für die Munitionskugeln verwendet.)
    • In diesem Beispielspiel sind Shaderressourcenobjekte: m_sphereTexture, m_cylinderTexture, m_ceilingTexture, m_floorTexture, m_wallsTexture.
  • Shaderobjekte werden in dieser Klasse definiert, um unsere Grundtypen und Texturen zu berechnen.
    • In diesem Beispielspiel werden die Shaderobjekte m_vertexShader, m_vertexShaderFlat und m_pixelShaderm_pixelShaderFlat.
    • Der Vertexshader verarbeitet die Grundtypen und die grundlegende Beleuchtung. Der Pixelshader (auch als Fragmentshader bezeichnet) verarbeitet die Texturen und alle pixelgenauen Effekte.
    • Es existieren zwei Versionen dieser Shader (regulär und flach) zum Rendern verschiedener Grundtypen. Der Grund, warum wir verschiedene Versionen haben, ist, dass die flachen Versionen viel einfacher sind und keine Glanzlichter oder Beleuchtungseffekte pro Pixel verwenden. Sie werden für die Wände verwendet und beschleunigen das Rendern auf Geräten mit geringerer Leistung.

GameRenderer.h

Sehen wir uns nun den Code im Rendererklassenobjekt des Beispielspiels an.

// Class handling the rendering of the game
class GameRenderer : public std::enable_shared_from_this<GameRenderer>
{
public:
    GameRenderer(std::shared_ptr<DX::DeviceResources> const& deviceResources);

    void CreateDeviceDependentResources();
    void CreateWindowSizeDependentResources();
    void ReleaseDeviceDependentResources();
    void Render();
    // --- end of async related methods section

    winrt::Windows::Foundation::IAsyncAction CreateGameDeviceResourcesAsync(_In_ std::shared_ptr<Simple3DGame> game);
    void FinalizeCreateGameDeviceResources();
    winrt::Windows::Foundation::IAsyncAction LoadLevelResourcesAsync();
    void FinalizeLoadLevelResources();

    Simple3DGameDX::IGameUIControl* GameUIControl() { return &m_gameInfoOverlay; };

    DirectX::XMFLOAT2 GameInfoOverlayUpperLeft()
    {
        return DirectX::XMFLOAT2(m_gameInfoOverlayRect.left, m_gameInfoOverlayRect.top);
    };
    DirectX::XMFLOAT2 GameInfoOverlayLowerRight()
    {
        return DirectX::XMFLOAT2(m_gameInfoOverlayRect.right, m_gameInfoOverlayRect.bottom);
    };
    bool GameInfoOverlayVisible() { return m_gameInfoOverlay.Visible(); }
    // --- end of rendering overlay section
...
private:
    // Cached pointer to device resources.
    std::shared_ptr<DX::DeviceResources>        m_deviceResources;

    ...

    // Shader resource objects
    winrt::com_ptr<ID3D11ShaderResourceView>    m_sphereTexture;
    winrt::com_ptr<ID3D11ShaderResourceView>    m_cylinderTexture;
    winrt::com_ptr<ID3D11ShaderResourceView>    m_ceilingTexture;
    winrt::com_ptr<ID3D11ShaderResourceView>    m_floorTexture;
    winrt::com_ptr<ID3D11ShaderResourceView>    m_wallsTexture;

    // Constant buffers
    winrt::com_ptr<ID3D11Buffer>                m_constantBufferNeverChanges;
    winrt::com_ptr<ID3D11Buffer>                m_constantBufferChangeOnResize;
    winrt::com_ptr<ID3D11Buffer>                m_constantBufferChangesEveryFrame;
    winrt::com_ptr<ID3D11Buffer>                m_constantBufferChangesEveryPrim;

    // Texture sampler
    winrt::com_ptr<ID3D11SamplerState>          m_samplerLinear;

    // Shader objects: Vertex shaders and pixel shaders
    winrt::com_ptr<ID3D11VertexShader>          m_vertexShader;
    winrt::com_ptr<ID3D11VertexShader>          m_vertexShaderFlat;
    winrt::com_ptr<ID3D11PixelShader>           m_pixelShader;
    winrt::com_ptr<ID3D11PixelShader>           m_pixelShaderFlat;
    winrt::com_ptr<ID3D11InputLayout>           m_vertexLayout;
};

Konstruktor

Als Nächstes untersuchen wir den GameRenderer-Konstruktor des Beispielspiels und vergleichen ihn mit dem Sample3DSceneRenderer-Konstruktor , der in der DirectX 11-App-Vorlage bereitgestellt wird.

// Constructor method of the main rendering class object
GameRenderer::GameRenderer(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
    m_gameInfoOverlay(deviceResources),
    m_gameHud(deviceResources, L"Windows platform samples", L"DirectX first-person game sample")
{
    // m_gameInfoOverlay is a GameHud object to render text in the top left corner of the screen.
    // m_gameHud is Game info rendered as an overlay on the top-right corner of the screen,
    // for example hits, shots, and time.

    CreateDeviceDependentResources();
    CreateWindowSizeDependentResources();
}

Erstellen und Laden von DirectX-Grafikressourcen

Im Beispielspiel (und in der Vorlage DirectX 11-App (Universelles Windows) von Visual Studio wird das Erstellen und Laden von Spielressourcen mithilfe der beiden Methoden implementiert, die vom GameRenderer-Konstruktor aufgerufen werden:

CreateDeviceDependentResources-Methode

In der DirectX 11-App-Vorlage wird diese Methode verwendet, um Vertex- und Pixelshader asynchron zu laden, den Shader und konstanten Puffer zu erstellen und ein Gitter mit Scheitelpunkten zu erstellen, die Positions- und Farbinformationen enthalten.

Im Beispielspiel werden diese Vorgänge der Szenenobjekte stattdessen auf die Methoden CreateGameDeviceResourcesAsync und FinalizeCreateGameDeviceResources aufgeteilt.

Was wird für dieses Beispielspiel in diese Methode eingefügt?

  • Instanziierte Variablen (m_gameResourcesLoaded = false und m_levelResourcesLoaded = false), die angeben, ob Ressourcen geladen wurden, bevor sie in den Rendervorgang übergehen, da sie asynchron geladen werden.
  • Da sich HUD- und Overlayrendering in separaten Klassenobjekten befinden, rufen Sie hier die Methoden GameHud::CreateDeviceDependentResources und GameInfoOverlay::CreateDeviceDependentResources auf.

Hier ist der Code für GameRenderer::CreateDeviceDependentResources.

// This method is called in GameRenderer constructor when it's created in GameMain constructor.
void GameRenderer::CreateDeviceDependentResources()
{
    // instantiate variables that indicate whether resources were loaded.
    m_gameResourcesLoaded = false;
    m_levelResourcesLoaded = false;

    // game HUD and overlay are design as separate class objects.
    m_gameHud.CreateDeviceDependentResources();
    m_gameInfoOverlay.CreateDeviceDependentResources();
}

Im Folgenden finden Sie eine Liste der Methoden, die zum Erstellen und Laden von Ressourcen verwendet werden.

  • CreateDeviceDependentResources
    • CreateGameDeviceResourcesAsync (hinzugefügt)
    • FinalizeCreateGameDeviceResources (hinzugefügt)
  • CreateWindowSizeDependentResources

Bevor wir uns mit den anderen Methoden vertraut machen, die zum Erstellen und Laden von Ressourcen verwendet werden, erstellen wir zunächst den Renderer und sehen uns an, wie er in die Spielschleife passt.

Erstellen des Renderers

Der GameRenderer wird im GameMain-Konstruktor erstellt. Außerdem werden die beiden anderen Methoden CreateGameDeviceResourcesAsync und FinalizeCreateGameDeviceResources aufgerufen, die hinzugefügt werden, um Ressourcen zu erstellen und zu laden.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);

    // Creation of GameRenderer
    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);

    ...

    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    ...
}

CreateGameDeviceResourcesAsync-Methode

CreateGameDeviceResourcesAsync wird von der GameMain-Konstruktormethode in der create_task-Schleife aufgerufen, da wir Spielressourcen asynchron laden.

CreateDeviceResourcesAsync ist eine Methode, die als gesonderte Reihe asynchroner Aufgaben ausgeführt wird, um die Spielressourcen zu laden. Da die Ausführung in einem separaten Thread erwartet wird, hat er nur Zugriff auf die Direct3D 11-Gerätemethoden (die in ID3D11Device definierten) und nicht auf die Gerätekontextmethoden (die in ID3D11DeviceContext definierten Methoden), sodass kein Rendering ausgeführt wird.

Die FinalizeCreateGameDeviceResources-Methode wird im Standard Thread ausgeführt und hat Zugriff auf die Direct3D 11-Gerätekontextmethoden.

Grundsätzlich gilt:

  • Verwenden Sie nur ID3D11Device-Methoden in CreateGameDeviceResourcesAsync , da sie freethreaded sind, was bedeutet, dass sie in jedem Thread ausgeführt werden können. Es wird auch erwartet, dass sie nicht im selben Thread ausgeführt werden, in dem der GameRenderer erstellt wurde.
  • Verwenden Sie hier keine Methoden in ID3D11DeviceContext , da sie in einem einzelnen Thread und im selben Thread wie GameRenderer ausgeführt werden müssen.
  • Verwenden Sie diese Methode, um Konstantenpuffer zu erstellen.
  • Verwenden Sie diese Methode, um Texturen (wie die DDS-Dateien) und Shaderinformationen (z. B. die CSO-Dateien) in die Shader zu laden.

Diese Methode wird für Folgendes verwendet:

  • Erstellen Sie die vier Konstantenpuffer: m_constantBufferNeverChanges, m_constantBufferChangeOnResize, m_constantBufferChangesEveryFrame, m_constantBufferChangesEveryPrim
  • Erstellen eines Samplerzustandsobjekts , das Samplinginformationen für eine Textur kapselt
  • Erstellen Sie eine Aufgabengruppe, die alle von der -Methode erstellten asynchronen Aufgaben enthält. Es wartet auf den Abschluss all dieser asynchronen Aufgaben und ruft dann FinalizeCreateGameDeviceResources auf.
  • Erstellen Sie ein Ladeprogramm mit dem Basic-Ladeprogramm. Fügen Sie die asynchronen Ladevorgänge des Ladeprogramms der zuvor erstellten Aufgabengruppe als Aufgaben hinzu.
  • Methoden wie BasicLoader::LoadShaderAsync und BasicLoader::LoadTextureAsync werden zum Laden verwendet:
    • kompilierte Shaderobjekte (VertextShader.cso, VertexShaderFlat.cso, PixelShader.cso und PixelShaderFlat.cso). Weitere Informationen finden Sie unter Verschiedene Shaderdateiformate.
    • spielspezifische Texturen (Assets\seafloor.dds, metal_texture.dds, cellceiling.dds, cellfloor.dds, cellwall.dds).
IAsyncAction GameRenderer::CreateGameDeviceResourcesAsync(_In_ std::shared_ptr<Simple3DGame> game)
{
    auto lifetime = shared_from_this();

    // Create the device dependent game resources.
    // Only the d3dDevice is used in this method. It is expected
    // to not run on the same thread as the GameRenderer was created.
    // Create methods on the d3dDevice are free-threaded and are safe while any methods
    // in the d3dContext should only be used on a single thread and handled
    // in the FinalizeCreateGameDeviceResources method.
    m_game = game;

    auto d3dDevice = m_deviceResources->GetD3DDevice();

    // Define D3D11_BUFFER_DESC. See
    // https://learn.microsoft.com/windows/win32/api/d3d11/ns-d3d11-d3d11_buffer_desc
    D3D11_BUFFER_DESC bd;
    ZeroMemory(&bd, sizeof(bd));

    // Create the constant buffers.
    bd.Usage = D3D11_USAGE_DEFAULT;
    ...

    // Create the constant buffers: m_constantBufferNeverChanges, m_constantBufferChangeOnResize,
    // m_constantBufferChangesEveryFrame, m_constantBufferChangesEveryPrim
    // CreateBuffer is used to create one of these buffers: vertex buffer, index buffer, or 
    // shader-constant buffer. For CreateBuffer API ref info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11device-createbuffer.
    winrt::check_hresult(
        d3dDevice->CreateBuffer(&bd, nullptr, m_constantBufferNeverChanges.put())
        );

    ...

    // Define D3D11_SAMPLER_DESC. For API ref, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/ns-d3d11-d3d11_sampler_desc.
    D3D11_SAMPLER_DESC sampDesc;

    // ZeroMemory fills a block of memory with zeros. For API ref, see
    // https://learn.microsoft.com/previous-versions/windows/desktop/legacy/aa366920(v=vs.85).
    ZeroMemory(&sampDesc, sizeof(sampDesc));

    sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    ...

    // Create a sampler-state object that encapsulates sampling information for a texture.
    // The sampler-state interface holds a description for sampler state that you can bind to any 
    // shader stage of the pipeline for reference by texture sample operations.
    winrt::check_hresult(
        d3dDevice->CreateSamplerState(&sampDesc, m_samplerLinear.put())
        );

    // Start the async tasks to load the shaders and textures.

    // Load compiled shader objects (VertextShader.cso, VertexShaderFlat.cso, PixelShader.cso, and PixelShaderFlat.cso).
    // The BasicLoader class is used to convert and load common graphics resources, such as meshes, textures, 
    // and various shader objects into the constant buffers. For more info, see
    // https://learn.microsoft.com/windows/uwp/gaming/complete-code-for-basicloader.
    BasicLoader loader{ d3dDevice };

    std::vector<IAsyncAction> tasks;

    uint32_t numElements = ARRAYSIZE(PNTVertexLayout);

    // Load shaders asynchronously with the shader and pixel data using the
    // BasicLoader::LoadShaderAsync method. Push these method calls into a list of tasks.
    tasks.push_back(loader.LoadShaderAsync(L"VertexShader.cso", PNTVertexLayout, numElements, m_vertexShader.put(), m_vertexLayout.put()));
    tasks.push_back(loader.LoadShaderAsync(L"VertexShaderFlat.cso", nullptr, numElements, m_vertexShaderFlat.put(), nullptr));
    tasks.push_back(loader.LoadShaderAsync(L"PixelShader.cso", m_pixelShader.put()));
    tasks.push_back(loader.LoadShaderAsync(L"PixelShaderFlat.cso", m_pixelShaderFlat.put()));

    // Make sure the previous versions if any of the textures are released.
    m_sphereTexture = nullptr;
    ...

    // Load Game specific textures (Assets\\seafloor.dds, metal_texture.dds, cellceiling.dds,
    // cellfloor.dds, cellwall.dds).
    // Push these method calls also into a list of tasks.
    tasks.push_back(loader.LoadTextureAsync(L"Assets\\seafloor.dds", nullptr, m_sphereTexture.put()));
    ...

    // Simulate loading additional resources by introducing a delay.
    tasks.push_back([]() -> IAsyncAction { co_await winrt::resume_after(GameConstants::InitialLoadingDelay); }());

    // Returns when all the async tasks for loading the shader and texture assets have completed.
    for (auto&& task : tasks)
    {
        co_await task;
    }
}

FinalizeCreateGameDeviceResources-Methode

Die FinalizeCreateGameDeviceResources-Methode wird aufgerufen, nachdem alle Aufgaben zum Laden von Ressourcen in der CreateGameDeviceResourcesAsync-Methode abgeschlossen sind.

  • Initialisieren Sie constantBufferNeverChanges mit den Lichtpositionen und der Farbe. Lädt die anfänglichen Daten mit einem Gerätekontextmethodenaufruf von ID3D11DeviceContext::UpdateSubresource in die Konstantenpuffer.
  • Da asynchron geladene Ressourcen vollständig geladen wurden, ist es an der Zeit, sie den entsprechenden Spielobjekten zuzuordnen.
  • Erstellen Sie für jedes Spielobjekt das Gitter und das Material mithilfe der texturen, die geladen wurden. Ordnen Sie dann das Gitter und das Material dem Spielobjekt zu.
  • Für das Zielspielobjekt wird die Textur, die aus konzentrischen farbigen Ringen mit einem numerischen Wert oben besteht, nicht aus einer Texturdatei geladen. Stattdessen wird sie prozedural mit dem Code in TargetTexture.cpp generiert. Die TargetTexture-Klasse erstellt die erforderlichen Ressourcen, um die Textur zur Initialisierungszeit in eine Offscreen-Ressource zu zeichnen. Die resultierende Textur wird dann den entsprechenden Zielspielobjekten zugeordnet.

FinalizeCreateGameDeviceResources und CreateWindowSizeDependentResources verwenden ähnliche Codeteile für diese:

  • Verwenden Sie SetProjParams , um sicherzustellen, dass die Kamera über die richtige Projektionsmatrix verfügt. Weitere Informationen finden Sie unter Kamera- und Koordinatenraum.
  • Behandeln Sie die Bildschirmrotation, indem Sie die 3D-Drehungsmatrix nach der Projektionsmatrix der Kamera multiplizieren. Aktualisieren Sie dann den Konstantenpuffer ConstantBufferChangeOnResize mit der resultierenden Projektionsmatrix.
  • Legen Sie die m_gameResourcesLoadedbooleschen globalen Variablen fest, um anzugeben, dass die Ressourcen jetzt in die Puffer geladen werden, sodass sie für den nächsten Schritt bereit sind. Denken Sie daran, dass wir diese Variable zuerst als FALSE in der Konstruktormethode von GameRenderer über die GameRenderer::CreateDeviceDependentResources-Methode initialisiert haben.
  • Wenn dieser m_gameResourcesLoadedTRUE ist, kann das Rendern der Szenenobjekte erfolgen. Dies wurde im Artikel I: Einführung in das Renderingframework unter GameRenderer::Render-Methode behandelt.
// This method is called from the GameMain constructor.
// Make sure that 2D rendering is occurring on the same thread as the main rendering.
void GameRenderer::FinalizeCreateGameDeviceResources()
{
    // All asynchronously loaded resources have completed loading.
    // Now associate all the resources with the appropriate game objects.
    // This method is expected to run in the same thread as the GameRenderer
    // was created. All work will happen behind the "Loading ..." screen after the
    // main loop has been entered.

    // Initialize the Constant buffer with the light positions
    // These are handled here to ensure that the d3dContext is only
    // used in one thread.

    auto d3dDevice = m_deviceResources->GetD3DDevice();

    ConstantBufferNeverChanges constantBufferNeverChanges;
    constantBufferNeverChanges.lightPosition[0] = XMFLOAT4(3.5f, 2.5f, 5.5f, 1.0f);
    ...
    constantBufferNeverChanges.lightColor = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);

    // CPU copies data from memory (constantBufferNeverChanges) to a subresource 
    // created in non-mappable memory (m_constantBufferNeverChanges) which was created in the earlier 
    // CreateGameDeviceResourcesAsync method. For UpdateSubresource API ref info, 
    // go to: https://msdn.microsoft.com/library/windows/desktop/ff476486.aspx
    // To learn more about what a subresource is, go to:
    // https://msdn.microsoft.com/library/windows/desktop/ff476901.aspx

    m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
        m_constantBufferNeverChanges.get(),
        0,
        nullptr,
        &constantBufferNeverChanges,
        0,
        0
        );

    // For the objects that function as targets, they have two unique generated textures.
    // One version is used to show that they have never been hit and the other is 
    // used to show that they have been hit.
    // TargetTexture is a helper class to procedurally generate textures for game
    // targets. The class creates the necessary resources to draw the texture into 
    // an off screen resource at initialization time.

    TargetTexture textureGenerator(
        d3dDevice,
        m_deviceResources->GetD2DFactory(),
        m_deviceResources->GetDWriteFactory(),
        m_deviceResources->GetD2DDeviceContext()
        );

    // CylinderMesh is a class derived from MeshObject and creates a ID3D11Buffer of
    // vertices and indices to represent a canonical cylinder (capped at
    // both ends) that is positioned at the origin with a radius of 1.0,
    // a height of 1.0 and with its axis in the +Z direction.
    // In the game sample, there are various types of mesh types:
    // CylinderMesh (vertical rods), SphereMesh (balls that the player shoots), 
    // FaceMesh (target objects), and WorldMesh (Floors and ceilings that define the enclosed area)

    auto cylinderMesh = std::make_shared<CylinderMesh>(d3dDevice, (uint16_t)26);
    ...

    // The Material class maintains the properties that represent how an object will
    // look when it is rendered.  This includes the color of the object, the
    // texture used to render the object, and the vertex and pixel shader that
    // should be used for rendering.

    auto cylinderMaterial = std::make_shared<Material>(
        XMFLOAT4(0.8f, 0.8f, 0.8f, .5f),
        XMFLOAT4(0.8f, 0.8f, 0.8f, .5f),
        XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f),
        15.0f,
        m_cylinderTexture.get(),
        m_vertexShader.get(),
        m_pixelShader.get()
        );

    ...

    // Attach the textures to the appropriate game objects.
    // We'll loop through all the objects that need to be rendered.
    for (auto&& object : m_game->RenderObjects())
    {
        if (object->TargetId() == GameConstants::WorldFloorId)
        {
            // Assign a normal material for the floor object.
            // This normal material uses the floor texture (cellfloor.dds) that was loaded asynchronously from
            // the Assets folder using BasicLoader::LoadTextureAsync method in the earlier 
            // CreateGameDeviceResourcesAsync loop

            object->NormalMaterial(
                std::make_shared<Material>(
                    XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f),
                    XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f),
                    XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f),
                    150.0f,
                    m_floorTexture.get(),
                    m_vertexShaderFlat.get(),
                    m_pixelShaderFlat.get()
                    )
                );
            // Creates a mesh object called WorldFloorMesh and assign it to the floor object.
            object->Mesh(std::make_shared<WorldFloorMesh>(d3dDevice));
        }
        ...
        else if (auto cylinder = dynamic_cast<Cylinder*>(object.get()))
        {
            cylinder->Mesh(cylinderMesh);
            cylinder->NormalMaterial(cylinderMaterial);
        }
        else if (auto target = dynamic_cast<Face*>(object.get()))
        {
            const int bufferLength = 16;
            wchar_t str[bufferLength];
            int len = swprintf_s(str, bufferLength, L"%d", target->TargetId());
            auto string{ winrt::hstring(str, len) };

            winrt::com_ptr<ID3D11ShaderResourceView> texture;
            textureGenerator.CreateTextureResourceView(string, texture.put());
            target->NormalMaterial(
                std::make_shared<Material>(
                    XMFLOAT4(0.8f, 0.8f, 0.8f, 0.5f),
                    XMFLOAT4(0.8f, 0.8f, 0.8f, 0.5f),
                    XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f),
                    5.0f,
                    texture.get(),
                    m_vertexShader.get(),
                    m_pixelShader.get()
                    )
                );

            texture = nullptr;
            textureGenerator.CreateHitTextureResourceView(string, texture.put());
            target->HitMaterial(
                std::make_shared<Material>(
                    XMFLOAT4(0.8f, 0.8f, 0.8f, 0.5f),
                    XMFLOAT4(0.8f, 0.8f, 0.8f, 0.5f),
                    XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f),
                    5.0f,
                    texture.get(),
                    m_vertexShader.get(),
                    m_pixelShader.get()
                    )
                );

            target->Mesh(targetMesh);
        }
        ...
    }

    // The SetProjParams method calculates the projection matrix based on input params and
    // ensures that the camera has been initialized with the right projection
    // matrix.  
    // The camera is not created at the time the first window resize event occurs.

    auto renderTargetSize = m_deviceResources->GetRenderTargetSize();
    m_game->GameCamera().SetProjParams(
        XM_PI / 2,
        renderTargetSize.Width / renderTargetSize.Height,
        0.01f,
        100.0f
        );

    // Make sure that the correct projection matrix is set in the ConstantBufferChangeOnResize buffer.

    // Get the 3D rotation transform matrix. We are handling screen rotations directly to eliminate an unaligned 
    // fullscreen copy. So it is necessary to post multiply the 3D rotation matrix to the camera's projection matrix
    // to get the projection matrix that we need.

    auto orientation = m_deviceResources->GetOrientationTransform3D();

    ConstantBufferChangeOnResize changesOnResize;

    // The matrices are transposed due to the shader code expecting the matrices in the opposite
    // row/column order from the DirectX math library.

    // XMStoreFloat4x4 takes a matrix and writes the components out to sixteen single-precision floating-point values at the given address. 
    // The most significant component of the first row vector is written to the first four bytes of the address, 
    // followed by the second most significant component of the first row, and so on. The second row is then written out in a 
    // like manner to memory beginning at byte 16, followed by the third row to memory beginning at byte 32, and finally 
    // the fourth row to memory beginning at byte 48. For more API ref info, go to: 
    // https://msdn.microsoft.com/library/windows/desktop/microsoft.directx_sdk.storing.xmstorefloat4x4.aspx

    XMStoreFloat4x4(
        &changesOnResize.projection,
        XMMatrixMultiply(
            XMMatrixTranspose(m_game->GameCamera().Projection()),
            XMMatrixTranspose(XMLoadFloat4x4(&orientation))
            )
        );

    // UpdateSubresource method instructs CPU to copy data from memory (changesOnResize) to a subresource 
    // created in non-mappable memory (m_constantBufferChangeOnResize ) which was created in the earlier 
    // CreateGameDeviceResourcesAsync method.

    m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
        m_constantBufferChangeOnResize.get(),
        0,
        nullptr,
        &changesOnResize,
        0,
        0
        );

    // Finally we set the m_gameResourcesLoaded as TRUE, so we can start rendering.
    m_gameResourcesLoaded = true;
}

CreateWindowSizeDependentResource-Methode

CreateWindowSizeDependentResources-Methoden werden jedes Mal aufgerufen, wenn sich die Fenstergröße, -ausrichtung, das Stereo-fähige Rendering oder die Auflösung ändert. Im Beispielspiel wird die Projektionsmatrix in ConstantBufferChangeOnResize aktualisiert.

Fenstergrößenressourcen werden auf folgende Weise aktualisiert:

  • Das App-Framework ruft eines von mehreren möglichen Ereignissen ab, die auf eine Änderung des Fensterzustands hinweisen.
  • Ihre Standard Spielschleife wird dann über das Ereignis informiert und ruft CreateWindowSizeDependentResources für die Standard-Klasse (GameMain) instance auf, die dann die CreateWindowSizeDependentResources-Implementierung in der GameRenderer-Klasse (GameRenderer) aufruft.
  • Die primäre Aufgabe dieser Methode besteht darin, sicherzustellen, dass die Visuals aufgrund einer Änderung der Fenstereigenschaften nicht verwechselt oder ungültig werden.

Für dieses Beispielspiel entspricht eine Reihe von Methodenaufrufen der FinalizeCreateGameDeviceResources-Methode . Exemplarische Vorgehensweise für Code finden Sie im vorherigen Abschnitt.

Die Anpassungen der Spiel-HUD und der Überlagerungsfenstergröße werden unter Benutzeroberfläche hinzufügen behandelt.

// Initializes view parameters when the window size changes.
void GameRenderer::CreateWindowSizeDependentResources()
{
    // Game HUD and overlay window size rendering adjustments are done here
    // but they'll be covered in the UI section instead.

    m_gameHud.CreateWindowSizeDependentResources();

    ...

    auto d3dContext = m_deviceResources->GetD3DDeviceContext();
    // In Sample3DSceneRenderer::CreateWindowSizeDependentResources, we had:
    // Size outputSize = m_deviceResources->GetOutputSize();

    auto renderTargetSize = m_deviceResources->GetRenderTargetSize();

    ...

    m_gameInfoOverlay.CreateWindowSizeDependentResources(m_gameInfoOverlaySize);

    if (m_game != nullptr)
    {
        // Similar operations as the last section of FinalizeCreateGameDeviceResources method
        m_game->GameCamera().SetProjParams(
            XM_PI / 2, renderTargetSize.Width / renderTargetSize.Height,
            0.01f,
            100.0f
            );

        XMFLOAT4X4 orientation = m_deviceResources->GetOrientationTransform3D();

        ConstantBufferChangeOnResize changesOnResize;
        XMStoreFloat4x4(
            &changesOnResize.projection,
            XMMatrixMultiply(
                XMMatrixTranspose(m_game->GameCamera().Projection()),
                XMMatrixTranspose(XMLoadFloat4x4(&orientation))
                )
            );

        d3dContext->UpdateSubresource(
            m_constantBufferChangeOnResize.get(),
            0,
            nullptr,
            &changesOnResize,
            0,
            0
            );
    }
}

Nächste Schritte

Dies ist der grundlegende Prozess zum Implementieren des Grafikrenderingframeworks eines Spiels. Je größer Ihr Spiel, desto mehr Abstraktionen müssten Sie einsetzen, um Hierarchien von Objekttypen und Animationsverhalten zu verarbeiten. Sie müssen komplexere Methoden zum Laden und Verwalten von Ressourcen wie Gittern und Texturen implementieren. Als Nächstes erfahren Sie, wie Sie eine Benutzeroberfläche hinzufügen.