Compartilhar via


Estrutura de renderização II: renderização de jogos

Observação

Este tópico faz parte da série de tutoriais Criar um jogo simples da Plataforma Universal do Windows (UWP) com DirectX. O tópico nesse link define o contexto da série.

No Rendering framework I, abordamos como pegamos as informações da cena e as apresentamos à tela de exibição. Agora, vamos dar um passo atrás e aprender como preparar os dados para renderização.

Observação

Se não tiver feito download do código de jogo mais recente para essa amostra, acesse jogo de exemplo do Direct3D. Esta amostra faz parte de uma grande coleção de amostras de recursos UWP. Para obter instruções sobre como fazer o download da amostra, confira Aplicativos de amostra para desenvolvimento do Windows.

Objetivo

Recapitulação rápida do objetivo. É entender como configurar uma estrutura de renderização básica para exibir a saída gráfica de um jogo UWP DirectX. Podemos agrupá-los vagamente nessas três etapas.

  1. Estabeleça uma conexão com nossa interface gráfica
  2. Preparação: Crie os recursos necessários para desenhar os gráficos
  3. Exibir os gráficos: renderizar o quadro

Estrutura de renderização I: Introdução à renderização explicou como os gráficos são renderizados, abrangendo as etapas 1 e 3.

Este artigo explica como configurar outras partes dessa estrutura e preparar os dados necessários antes que a renderização possa acontecer, que é a Etapa 2 do processo.

Projetar o renderizador

O renderizador é responsável por criar e manter todos os objetos D3D11 e D2D usados para gerar os visuais do jogo. A classe GameRenderer é o renderizador deste jogo de exemplo e foi projetada para atender às necessidades de renderização do jogo.

Estes são alguns conceitos que você pode usar para ajudar a projetar o renderizador do seu jogo:

  • Como as APIs do Direct3D 11 são definidas como APIs COM , você deve fornecer referências ComPtr aos objetos definidos por essas APIs. Esses objetos são liberados automaticamente quando sua última referência sai do escopo quando o aplicativo é encerrado. Para obter mais informações, consulte ComPtr. Exemplo desses objetos: buffers constantes, objetos de sombreador – sombreador de vértice, sombreador de pixel e objetos de recurso de sombreador.
  • Os buffers constantes são definidos nesta classe para conter vários dados necessários para renderização.
    • Use vários buffers constantes com frequências diferentes para reduzir a quantidade de dados que devem ser enviados para a GPU por quadro. Este exemplo separa as constantes em buffers diferentes com base na frequência com que elas devem ser atualizadas. Essa é uma prática recomendada para programação Direct3D.
    • Neste jogo de exemplo, 4 buffers constantes são definidos.
      1. m_constantBufferNeverChanges contém os parâmetros de iluminação. Ele é definido uma vez no método FinalizeCreateGameDeviceResources e nunca mais é alterado.
      2. m_constantBufferChangeOnResize contém a matriz de projeção. A matriz de projeção depende do tamanho e da taxa de proporção da janela. Ele é definido em CreateWindowSizeDependentResources e, em seguida, atualizado depois que os recursos são carregados no método FinalizeCreateGameDeviceResources. Se estiver renderizando em 3D, ele também será alterado duas vezes por quadro.
      3. m_constantBufferChangesEveryFrame contém a matriz de exibição. Essa matriz depende da posição da câmera e da direção da aparência (a normal à projeção) e muda uma vez por quadro no método Render . Isso foi discutido anteriormente em Estrutura de renderização I: introdução à renderização, no método GameRenderer::Render.
      4. m_constantBufferChangesEveryPrim contém a matriz do modelo e as propriedades do material de cada primitivo. A matriz do modelo transforma vértices de coordenadas locais em coordenadas mundiais. Essas constantes são específicas para cada primitivo e são atualizadas para cada chamada de desenho. Isso foi discutido anteriormente em Estrutura de renderização I: Introdução à renderização, na renderização primitiva.
  • Os objetos de recurso de sombreador que contêm texturas para os primitivos também são definidos nessa classe.
    • Algumas texturas são predefinidas (DDS é um formato de arquivo que pode ser usado para armazenar texturas compactadas e não compactadas. As texturas DDS são usadas para as paredes e o chão do mundo, bem como para as esferas de munição.)
    • Neste jogo de exemplo, os objetos de recurso de sombreador são: m_sphereTexture, m_cylinderTexture, m_ceilingTexture, m_floorTexture m_wallsTexture.
  • Os objetos de sombreador são definidos nessa classe para calcular nossos primitivos e texturas.
    • Neste jogo de exemplo, os objetos de sombreador são m_vertexShader, m_vertexShaderFlat e m_pixelShader, m_pixelShaderFlat.
    • O sombreador de vértice processa os primitivos e a iluminação básica, e o sombreador de pixel (às vezes chamado de sombreador de fragmento) processa as texturas e quaisquer efeitos por pixel.
    • Há duas versões desses sombreadores (regulares e planos) para renderizar diferentes primitivos. A razão pela qual temos versões diferentes é que as versões planas são muito mais simples e não fazem realces especulares ou quaisquer efeitos de iluminação por pixel. Eles são usados para as paredes e tornam a renderização mais rápida em dispositivos de baixa potência.

GameRenderer.h

Agora vamos examinar o código no objeto de classe de renderizador do jogo de exemplo.

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

Construtor

Em seguida, vamos examinar o construtor GameRenderer do jogo de exemplo e compará-lo com o construtor Sample3DSceneRenderer fornecido no modelo de aplicativo 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();
}

Criar e carregar recursos gráficos do DirectX

No jogo de exemplo (e no modelo de aplicativo DirectX 11 (Universal Windows) do Visual Studio), a criação e o carregamento de recursos de jogo são implementados usando esses dois métodos chamados do construtor GameRenderer:

Método CreateDeviceDependentResources

No modelo de aplicativo DirectX 11, esse método é usado para carregar o sombreador de vértice e pixel de forma assíncrona, criar o sombreador e o buffer constante, criar uma malha com vértices que contêm informações de posição e cor.

No jogo de exemplo, essas operações dos objetos de cena são divididas entre os métodos CreateGameDeviceResourcesAsync e FinalizeCreateGameDeviceResources.

Para este jogo de exemplo, o que entra nesse método?

  • Variáveis instanciadas (m_gameResourcesLoaded = false e m_levelResourcesLoaded = false) que indicam se os recursos foram carregados antes de avançar para a renderização, já que os estamos carregando de forma assíncrona.
  • Como a renderização de HUD e sobreposição estão em objetos de classe separados, chame os métodos GameHud::CreateDeviceDependentResources e GameInfoOverlay::CreateDeviceDependentResources aqui.

Aqui está o código para 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();
}

Abaixo está uma lista dos métodos usados para criar e carregar recursos.

  • CreateDeviceDependentResources
    • CreateGameDeviceResourcesAsync (adicionado)
    • FinalizeCreateGameDeviceResources (adicionado)
  • CreateWindowSizeDependentResources

Antes de mergulhar nos outros métodos usados para criar e carregar recursos, vamos primeiro criar o renderizador e ver como ele se encaixa no loop do jogo.

Criar o renderizador

O GameRenderer é criado no construtor do GameMain. Ele também chama os dois outros métodos, CreateGameDeviceResourcesAsync e FinalizeCreateGameDeviceResources, que são adicionados para ajudar a criar e carregar recursos.

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

    ...
}

Método CreateGameDeviceResourcesAsync

CreateGameDeviceResourcesAsync é chamado do método construtor GameMain no loop create_task , pois estamos carregando recursos do jogo de forma assíncrona.

CreateDeviceResourcesAsync é um método que é executado como um conjunto separado de tarefas assíncronas para carregar os recursos do jogo. Como se espera que ele seja executado em um thread separado, ele só tem acesso aos métodos de dispositivo Direct3D 11 (aqueles definidos em ID3D11Device) e não aos métodos de contexto do dispositivo (os métodos definidos em ID3D11DeviceContext), portanto, ele não executa nenhuma renderização.

O método FinalizeCreateGameDeviceResources é executado no thread principal e tem acesso aos métodos de contexto do dispositivo Direct3D 11.

Em princípio:

  • Use apenas métodos ID3D11Device em CreateGameDeviceResourcesAsync porque eles são de thread livre, o que significa que eles podem ser executados em qualquer thread. Também é esperado que eles não sejam executados no mesmo thread em que o GameRenderer foi criado.
  • Não use métodos em ID3D11DeviceContext aqui porque eles precisam ser executados em um único thread e no mesmo thread que GameRenderer.
  • Use esse método para criar buffers constantes.
  • Use esse método para carregar texturas (como os arquivos .dds) e informações do sombreador (como os arquivos .cso) nos sombreadores.

Este método é usado para:

  • Crie os 4 buffers constantes: m_constantBufferNeverChanges, m_constantBufferChangeOnResize, m_constantBufferChangesEveryFrame m_constantBufferChangesEveryPrim
  • Criar um objeto sampler-state que encapsula informações de amostragem para uma textura
  • Crie um grupo de tarefas que contenha todas as tarefas assíncronas criadas pelo método. Ele aguarda a conclusão de todas essas tarefas assíncronas e, em seguida, chama FinalizeCreateGameDeviceResources.
  • Crie um carregador usando o Basic Loader. Adicione as operações de carregamento assíncrono do carregador como tarefas ao grupo de tarefas criado anteriormente.
  • Métodos como BasicLoader::LoadShaderAsync e BasicLoader::LoadTextureAsync são usados para carregar:
    • objetos de sombreador compilados (VertextShader.cso, VertexShaderFlat.cso, PixelShader.cso e PixelShaderFlat.cso). Para obter mais informações, acesse Vários formatos de arquivo de sombreador.
    • texturas específicas do jogo (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;
    }
}

Método FinalizeCreateGameDeviceResources

O método FinalizeCreateGameDeviceResources é chamado depois que todas as tarefas de recursos de carga que estão no método CreateGameDeviceResourcesAsync são concluídas .

  • Inicialize constantBufferNeverChanges com as posições de luz e a cor. Carrega os dados iniciais nos buffers constantes com uma chamada de método de contexto de dispositivo para ID3D11DeviceContext::UpdateSubresource.
  • Como os recursos carregados de forma assíncrona concluíram o carregamento, é hora de associá-los aos objetos de jogo apropriados.
  • Para cada objeto de jogo, crie a malha e o material usando as texturas que foram carregadas. Em seguida, associe a malha e o material ao objeto do jogo.
  • Para o objeto de jogo de destinos, a textura composta de anéis coloridos concêntricos, com um valor numérico na parte superior, não é carregada de um arquivo de textura. Em vez disso, ele é gerado processualmente usando o código em TargetTexture.cpp. A classe TargetTexture cria os recursos necessários para desenhar a textura em um recurso fora da tela no momento da inicialização. A textura resultante é então associada aos objetos de jogo de destino apropriados.

FinalizeCreateGameDeviceResources e CreateWindowSizeDependentResources compartilham partes semelhantes de código para estes:

  • Use SetProjParams para garantir que a câmera tenha a matriz de projeção correta. Para obter mais informações, acesse Câmera e espaço de coordenadas.
  • Manipule a rotação da tela postando a multiplicação da matriz de rotação 3D pela matriz de projeção da câmera. Em seguida, atualize o buffer constante ConstantBufferChangeOnResize com a matriz de projeção resultante.
  • Defina a variável global booleana m_gameResourcesLoaded para indicar que os recursos agora estão carregados nos buffers, prontos para a próxima etapa. Lembre-se de que inicializamos essa variável pela primeira vez como FALSE no método construtor do GameRenderer, por meio do método GameRenderer::CreateDeviceDependentResources.
  • Quando esse m_gameResourcesLoaded é TRUE, a renderização dos objetos de cena pode ocorrer. Isso foi abordado no artigo Estrutura de renderização I: Introdução à renderização , no método 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;
}

Método CreateWindowSizeDependentResource

Os métodos CreateWindowSizeDependentResources são chamados sempre que o tamanho da janela, a orientação, a renderização habilitada para estéreo ou a resolução são alterados. No jogo de exemplo, ele atualiza a matriz de projeção em ConstantBufferChangeOnResize.

Os recursos de tamanho de janela são atualizados desta maneira:

  • A estrutura do aplicativo obtém um dos vários eventos possíveis que indicam uma alteração no estado da janela.
  • Em seguida, o loop do jogo principal é informado sobre o evento e chama CreateWindowSizeDependentResources na instância da classe principal (GameMain), que chama a implementação CreateWindowSizeDependentResources na classe do renderizador do jogo (GameRenderer).
  • O trabalho principal desse método é garantir que os visuais não fiquem confusos ou inválidos devido a uma alteração nas propriedades da janela.

Para este jogo de exemplo, várias chamadas de método são as mesmas que o método FinalizeCreateGameDeviceResources. Para obter o passo a passo do código, vá para a seção anterior.

Os ajustes de renderização do tamanho do HUD e da janela de sobreposição do jogo são abordados em Adicionar uma interface de usuário.

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

Próximas etapas

Este é o processo básico para implementar a estrutura de renderização gráfica de um jogo. Quanto maior o jogo, mais abstrações você teria que colocar em prática para lidar com hierarquias de tipos de objetos e comportamentos de animação. Você precisa implementar métodos mais complexos para carregar e gerenciar ativos, como malhas e texturas. Em seguida, vamos aprender como adicionar uma interface de usuário.