Condividi tramite


Framework di rendering II: Rendering del gioco

Nota

Questo argomento fa parte della serie di esercitazioni Creare un semplice gioco UWP (Universal Windows Platform) con DirectX. L'argomento in tale collegamento imposta il contesto per la serie.

In Framework di rendering I abbiamo illustrato come prendere le informazioni sulla scena e presentarle alla schermata di visualizzazione. A questo punto, faremo un passo indietro per saperne di più su come preparare i dati per il rendering.

Nota

Se non è ancora stato scaricato il codice di gioco più recente per questo esempio, andare in Esempio di gioco Direct3D. Questo esempio fa parte di una vasta raccolta di esempi di funzionalità UWP. Per istruzioni su come scaricare l'esempio, vedere Applicazioni di esempio per lo sviluppo di Windows.

Obiettivo

Un rapido riepilogo sull'obiettivo. È per comprendere come configurare un framework di rendering di base per visualizzare l'output grafico per un gioco DirectX UWP. Possiamo raggrupparli generalmente nei seguenti tre passaggi.

  1. Stabilire una connessione alla nostra interfaccia grafica
  2. Preparazione: creare le risorse necessarie per disegnare la grafica
  3. Visualizzare la grafica: eseguire il rendering del frame

Framework di rendering I: Introduzione al rendering è stato spiegato come viene eseguito il rendering della grafica, coprendo i passaggi 1 e 3.

Questo articolo illustra come configurare altri componenti di questo framework e preparare i dati necessari prima che possa verificarsi il rendering, ovvero il passaggio 2 del processo.

Progettare il renderer

Il renderer è responsabile della creazione e della gestione di tutti gli oggetti D3D11 e D2D utilizzati per generare le visuali del gioco. La classe GameRenderer è il renderer per questo gioco di esempio ed è progettato per soddisfare le esigenze di rendering del gioco.

Questi sono alcuni concetti che è possibile utilizzare per agevolare la progettazione del renderer per il gioco:

  • Poiché le API Direct3D 11 sono definite come API COM, è necessario fornire riferimenti ComPtr agli oggetti definiti da queste API. Questi oggetti vengono liberati automaticamente quando il loro ultimo riferimento esaurisce il proprio ambito quando l'app termina. Per maggiori informazioni, vedere ComPtr. Esempio di questi oggetti: buffer costanti, oggetti shader - shader di vertici, shader di pixel e oggetti risorsa shader.
  • I buffer costanti sono definiti in questa classe per contenere vari dati necessari per il rendering.
    • Utilizzare più buffer costanti con frequenze diverse per ridurre la quantità di dati che devono essere inviati alla GPU per frame. Questo esempio separa le costanti in buffer diversi in base alla loro frequenza di aggiornamento. Si tratta di una procedura consigliata per la programmazione Direct3D.
    • In questo gioco di esempio vengono definiti 4 buffer costanti.
      1. m_constantBufferNeverChanges contiene i parametri di illuminazione. Viene impostato una sola volta nel metodo FinalizeCreateGameDeviceResources e non cambia mai più.
      2. m_constantBufferChangeOnResize contiene la matrice di proiezione. La matrice di proiezione dipende dalle dimensioni e dalle proporzioni della finestra. È impostata in CreateWindowSizeDependentResources e quindi aggiornata dopo il caricamento delle risorse nel metodo FinalizeCreateGameDeviceResources. Se il rendering è in 3D viene modificato anche due volte per frame.
      3. m_constantBufferChangesEveryFrame contiene la matrice di visualizzazione. Questa matrice dipende dalla posizione della telecamera e dalla direzione di visuale (normale alla proiezione) e cambia una sola volta per frame nel metodo Render. Questo argomento è stato descritto in precedenza in Framework di rendering I: Introduzione al rendering, nel metodo GameRenderer::Render.
      4. m_constantBufferChangesEveryPrim contiene la matrice del modello e le proprietà materiali di ogni primitiva. La matrice del modello trasforma i vertici dalle coordinate locali in coordinate globali. Queste costanti sono specifiche di ogni primitiva e vengono aggiornate per ogni chiamata di disegno. Questo argomento è stato descritto in precedenza in Framework di rendering I: Introduzione al Rendering in Rendering delle primitive.
  • Anche gli oggetti risorsa shader che contengono trame per le primitive vengono definiti in questa classe.
    • Alcune trame sono predefinite (DDS è un formato di file che può essere utilizzato per archiviare trame compresse e non compresse. Le trame DDS vengono utilizzate per le pareti e il pavimento del mondo, nonché per le sfere di munizioni).
    • In questo gioco di esempio, gli oggetti risorsa shader sono: m_sphereTexture, m_cylinderTexture, m_ceilingTexture, m_floorTexture, m_wallsTexture.
  • Gli oggetti shader vengono definiti in questa classe per calcolare le nostre primitive e trame.
    • In questo gioco di esempio, gli oggetti shader sono m_vertexShader, m_vertexShaderFlat, and m_pixelShader, m_pixelShaderFlat.
    • Lo shader di vertici elabora le primitive e l'illuminazione di base e lo shader di pixel (talvolta denominato shader di frammenti) elabora le trame e gli effetti per pixel.
    • Esistono due versioni di questi shader (normali e flat) per il rendering di primitive diverse. Il motivo per cui abbiamo versioni diverse è che le versioni flat sono molto più semplici e non realizzano evidenziazioni speculari o effetti di illuminazione per pixel. Queste vengono utilizzate per le pareti e rendono il rendering più veloce su dispositivi meno potenti.

GameRenderer.h

Esaminiamo ora il codice nell'oggetto classe del renderer del gioco di esempio.

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

Costruttore

Esaminiamo quindi il costruttore GameRenderer del gioco di esempio e confrontiamolo con il costruttore Sample3DSceneRenderer fornito nel modello App DirectX 11.

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

Creare e caricare risorse grafiche DirectX

Nel gioco di esempio (e nel modello App DirectX 11 di Visual Studio (Windows universale)), la creazione e il caricamento delle risorse del gioco vengono implementati utilizzando i seguenti due metodi che vengono chiamati dal costruttore GameRenderer:

Il metodo CreateDeviceDependentResources

Nel modello App DirectX 11, questo metodo viene utilizzato per caricare shader di vertici e di pixel in modo asincrono, creare lo shader e il buffer costante, creare una mesh con vertici contenenti informazioni su posizione e colore.

Nel gioco di esempio, tali operazioni degli oggetti di scena sono invece suddivisi tra i metodi CreateGameDeviceResourcesAsync e FinalizeCreateGameDeviceResources.

Per questo gioco di esempio, cosa entra in questo metodo?

  • Variabili istanziate (m_gameResourcesLoaded = false e m_levelResourcesLoaded = false) che indicano se le risorse sono state caricate o meno prima di procedere al rendering, poiché le carichiamo in modo asincrono.
  • Poiché il rendering di HUD e sovraimpressione si trovano in oggetti di classe separati, chiamare qui i metodi GameHud::CreateDeviceDependentResources e GameInfoOverlay::CreateDeviceDependentResources.

Ecco il codice per 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();
}

Di seguito è riportato un elenco dei metodi utilizzati per creare e caricare le risorse.

  • CreateDeviceDependentResources
    • CreateGameDeviceResourcesAsync (Aggiunto)
    • FinalizeCreateGameDeviceResources (Aggiunto)
  • CreateWindowSizeDependentResources

Prima di approfondire gli altri metodi utilizzati per creare e caricare risorse, creiamo per prima cosa il renderer e vediamo come si adatta nel ciclo del gioco.

Creare il renderer

Il GameRenderer viene creato nel costruttore di GameMain. Esso chiama anche gli altri due metodi, CreateGameDeviceResourcesAsync e FinalizeCreateGameDeviceResources che vengono aggiunti per contribuire a creare e caricare risorse.

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

    ...
}

Il metodo CreateGameDeviceResourcesAsync

CreateGameDeviceResourcesAsync viene chiamato dal metodo del costruttore GameMain nel ciclo create_task, perché stiamo caricando risorse del gioco in modo asincrono.

CreateDeviceResourcesAsync è un metodo eseguito come serie separata di attività asincrone per caricare le risorse del gioco. Poiché è previsto che venga eseguito su un thread separato, ha accesso solo ai metodi del dispositivo Direct3D 11 (quelli definiti in ID3D11Device) e non ai metodi di contesto del dispositivo (i metodi definiti in ID3D11DeviceContext), quindi non esegue alcun rendering.

Il metodo FinalizeCreateGameDeviceResources viene eseguito nel thread principale e ha accesso ai metodi del contesto del dispositivo Direct3D 11.

In linea di principio:

  • Utilizzare esclusivamente metodi ID3D11Device in CreateGameDeviceResourcesAsync perché sono a thread libero, il che significa che possono essere eseguiti in qualsiasi thread. È anche previsto che non vengano eseguiti nello stesso thread in cui è stato creato GameRenderer.
  • Non utilizzare metodi in ID3D11DeviceContext qui perché devono essere eseguiti in un singolo thread e nello stesso thread di GameRenderer.
  • Utilizzare questo metodo per creare buffer costanti.
  • Utilizzare questo metodo per caricare trame (come i file .dds) e informazioni sui shader (come i file .cso) negli shader.

Questo metodo viene utilizzato per:

  • Creare i 4 buffer costanti: m_constantBufferNeverChanges, m_constantBufferChangeOnResize, m_constantBufferChangesEveryFrame, m_constantBufferChangesEveryPrim
  • Creare un oggetto sampler-state che incapsula le informazioni di campionamento per una trama
  • Creare un gruppo di attività contenente tutte le attività asincrone create dal metodo. Esso attende il completamento di tutte queste attività asincrone e quindi chiama FinalizeCreateGameDeviceResources.
  • Creare un caricatore utilizzando Basic Loader. Aggiungere le operazioni di caricamento asincrono del caricatore come attività nel gruppo di attività creato in precedenza.
  • Vengono utilizzati metodi quali BasicLoader::LoadShaderAsync e BasicLoader::LoadTextureAsync per caricare:
    • oggetti shader compilati (VertextShader.cso, VertexShaderFlat.cso, PixelShader.cso e PixelShaderFlat.cso). Per maggiori informazioni, andare a Vari formati di file per shader.
    • trame specifiche del gioco (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;
    }
}

Il metodo FinalizeCreateGameDeviceResources

Il metodo FinalizeCreateGameDeviceResources viene chiamato dopo il completamento di tutte le attività di caricamento delle risorse presenti nel metodo CreateGameDeviceResourcesAsync.

  • Inizializzare constantBufferNeverChanges con le posizioni e il colore della luce. Carica i dati iniziali nei buffer costanti con una chiamata al metodo del contesto del dispositivo ID3D11DeviceContext::UpdateSubresource.
  • Poiché le risorse caricate in modo asincrono hanno completato il caricamento, è tempo di associarle agli oggetti di gioco appropriati.
  • Per ogni oggetto gioco, creare la mesh e il materiale utilizzando le trame che sono state caricate. Associare quindi la mesh e il materiale all'oggetto del gioco.
  • Per l'oggetto del gioco dei bersagli, la trama composta da anelli colorati concentrici, con un valore numerico nella parte superiore, non viene caricata da un file di trama. Viene invece generata in modo procedurale utilizzando il codice in TargetTexture.cpp. La classe TargetTexture crea le risorse necessarie per disegnare la trama in una risorsa fuori schermo in fase di inizializzazione. La trama risultante viene quindi associata agli oggetti di gioco dei bersagli appropriati.

FinalizeCreateGameDeviceResources e CreateWindowSizeDependentResources condividono parti di codice simili per questi:

  • Utilizzare SetProjParams per accertarsi che la telecamera abbia la matrice di proiezione corretta. Per maggiori informazioni, andare a Telecamera e spazio delle coordinate.
  • Gestire la rotazione dello schermo post-moltiplicando la matrice di rotazione 3D nella matrice di proiezione della telecamera. Aggiornare quindi il buffer costante ConstantBufferChangeOnResize con la matrice di proiezione risultante.
  • Impostare la m_gameResourcesLoaded variabile globale booleana per indicare che le risorse sono ora caricate nei buffer, pronte per il passaggio successivo. Ricordare che questa variabile è stata inizializzata per la prima volta come FALSE nel metodo del costruttore di GameRenderer tramite il metodo GameRenderer::CreateDeviceDependentResources.
  • Quando questa m_gameResourcesLoaded è TRUE, il rendering degli oggetti scena può avere luogo. Questo argomento è stato trattato nell'articolo Framework di rendering I: Introduzione al rendering, nel metodo GameRenderer::Render.
// 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;
}

Il metodo CreateWindowSizeDependentResource

I metodi CreateWindowSizeDependentResources vengono chiamati ogni volta che cambiano le dimensioni della finestra, l'orientamento, il rendering abilitato per stereo o la risoluzione. Nel gioco di esempio aggiorna la matrice di proiezione in ConstantBufferChangeOnResize.

Le risorse delle dimensioni della finestra vengono aggiornate in questo modo:

  • Il framework dell'App ottiene uno dei diversi eventi possibili che indicano una modifica dello stato della finestra.
  • Il ciclo principale del gioco viene quindi informato sull'evento e chiama CreateWindowSizeDependentResources nell'istanza della classe principale (GameMain), che chiama quindi l'implementazione CreateWindowSizeDependentResources nella classe del renderer del gioco (GameRenderer).
  • Il lavoro principale di questo metodo consiste nell'accertarsi che gli oggetti visivi non diventino confusi o non validi a causa di una modifica delle proprietà della finestra.

Per questo gioco di esempio, una serie di chiamate al metodo corrisponde al metodo FinalizeCreateGameDeviceResources. Per la procedura dettagliata del codice, andare alla sezione precedente.

Le regolazioni per il rendering delle dimensioni di HUD e finestra di sovraimpressione del gioco sono descritte in Aggiungere un'interfaccia utente.

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

Passaggi successivi

Questo è il processo di base per implementare il framework di rendering grafico di un gioco. Più grande è il gioco, più astrazioni si dovrebbero mettere in atto per gestire gerarchie di tipi di oggetto e comportamenti di animazione. È necessario implementare metodi più complessi per il caricamento e la gestione di asset, quali mesh e trame. Impariamo ora come aggiungere un'interfaccia utente.