Compartilhar via


Entender o pipeline de renderização do Direct3D 11

Anteriormente, você examinava como criar uma janela que pode ser usada para desenhar em Trabalhar com recursos de dispositivo DirectX. Agora, você aprenderá a criar o pipeline de gráficos e onde pode conectá-lo.

Você lembrará que há duas interfaces Direct3D que definem o pipeline de gráficos: ID3D11Device, que fornece uma representação virtual da GPU e seus recursos; e ID3D11DeviceContext, que representa o processamento gráfico para o pipeline. Normalmente, você usa uma instância de ID3D11Device para configurar e obter os recursos de GPU necessários para começar a processar os elementos gráficos em uma cena e usar ID3D11DeviceContext para processar esses recursos em cada estágio de sombreador apropriado no pipeline de gráficos. Normalmente, você chama os métodos ID3D11Device com pouca frequência, ou seja, somente quando você configura uma cena ou quando o dispositivo é alterado. Por outro lado, você chamará ID3D11DeviceContext sempre que processar um quadro para exibição.

Este exemplo cria e configura um pipeline gráfico mínimo adequado para exibir um cubo simples com sombreamento de vértice. Ele demonstra aproximadamente o menor conjunto de recursos necessários para exibição. Ao ler as informações aqui, observe as limitações do exemplo especificado em que talvez seja necessário estendê-la para dar suporte à cena que deseja renderizar.

Este exemplo aborda duas classes C++ para elementos gráficos: uma classe de gerenciador de recursos de dispositivo e uma classe de renderizador de cena 3D. Este tópico se concentra especificamente no renderizador de cena 3D.

O que o renderizador de cubo faz?

O pipeline de gráficos é definido pela classe de renderizador de cena 3D. O renderizador de cena é capaz de:

  • Defina buffers constantes para armazenar seus dados uniformes.
  • Defina buffers de vértice para manter os dados de vértice do objeto e os buffers de índice correspondentes para permitir que o sombreador de vértice ande os triângulos corretamente.
  • Criar recursos de textura e exibições de recursos.
  • Carregue seus objetos de sombreador.
  • Atualize os dados gráficos para exibir cada quadro.
  • Renderize (desenhe) os elementos gráficos para a cadeia de troca.

Os quatro primeiros processos normalmente usam os métodos de interface ID3D11Device para inicializar e gerenciar recursos gráficos, e os dois últimos usam os métodos de interface ID3D11DeviceContext para gerenciar e executar o pipeline de gráficos.

Uma instância da classe Renderizador é criada e gerenciada como uma variável de membro na classe de projeto main. A instância DeviceResources é gerenciada como um ponteiro compartilhado em várias classes, incluindo a classe de projeto main, a classe app view-provider e Renderer. Se você substituir o Renderizador por sua própria classe, considere declarar e atribuir a instância DeviceResources como um membro de ponteiro compartilhado também:

std::shared_ptr<DX::DeviceResources> m_deviceResources;

Basta passar o ponteiro para o construtor de classe (ou outro método de inicialização) depois que a instância DeviceResources for criada no método Initialize da classe App . Você também pode passar uma referência de weak_ptr se, em vez disso, quiser que sua classe main possua completamente a instância DeviceResources.

Criar o renderizador de cubo

Neste exemplo, organizamos a classe de renderizador de cena com os seguintes métodos:

  • CreateDeviceDependentResources: chamado sempre que a cena deve ser inicializada ou reiniciada. Esse método carrega seus dados de vértice, texturas, sombreadores e outros recursos iniciais e constrói os buffers de constante e vértice iniciais. Normalmente, a maior parte do trabalho aqui é feita com métodos ID3D11Device , não métodos ID3D11DeviceContext .
  • CreateWindowSizeDependentResources: chamado sempre que o estado da janela é alterado, como quando ocorre o redimensionamento ou quando a orientação é alterada. Esse método recria matrizes de transformação, como aquelas para sua câmera.
  • Atualização: normalmente chamada da parte do programa que gerencia o estado imediato do jogo; neste exemplo, basta chamá-lo da classe Main . Faça com que esse método leia de qualquer informação de estado de jogo que afete a renderização, como atualizações na posição do objeto ou quadros de animação, além de quaisquer dados globais do jogo, como níveis de luz ou alterações na física do jogo. Essas entradas são usadas para atualizar os buffers constantes por quadro e os dados do objeto.
  • Renderização: normalmente chamado da parte do programa que gerencia o loop de jogo; nesse caso, ele é chamado da classe Main . Esse método constrói o pipeline de gráficos: ele associa sombreadores, associa buffers e recursos a estágios de sombreador e invoca o desenho para o quadro atual.

Esses métodos compõem o corpo de comportamentos para renderizar uma cena com Direct3D usando seus ativos. Se você estender este exemplo com uma nova classe de renderização, declare-o na classe de projeto main. Portanto, isso:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

se torna isso:

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Novamente, observe que este exemplo pressupõe que os métodos tenham as mesmas assinaturas em sua implementação. Se as assinaturas tiverem sido alteradas, examine o loop Principal e faça as alterações adequadamente.

Vamos dar uma olhada nos métodos de renderização de cena em mais detalhes.

Criar recursos dependentes do dispositivo

CreateDeviceDependentResources consolida todas as operações para inicializar a cena e seus recursos usando chamadas ID3D11Device . Esse método pressupõe que o dispositivo Direct3D acabou de ser inicializado (ou foi recriado) para uma cena. Ele recria ou recarrega todos os recursos gráficos específicos da cena, como os sombreadores de vértice e pixel, os buffers de vértice e índice para objetos e quaisquer outros recursos (por exemplo, como texturas e suas exibições correspondentes).

Aqui está o código de exemplo para CreateDeviceDependentResources:

void Renderer::CreateDeviceDependentResources()
{
    // Compile shaders using the Effects library.
    auto CreateShadersTask = Concurrency::create_task(
            [this]( )
            {
                CreateShaders();
            }
        );

    // Load the geometry for the spinning cube.
    auto CreateCubeTask = CreateShadersTask.then(
            [this]()
            {
                CreateCube();
            }
        );
}

void Renderer::CreateWindowSizeDependentResources()
{
    // Create the view matrix and the perspective matrix.
    CreateViewAndPerspective();
}

Sempre que você carrega recursos do disco , recursos como arquivos ou texturas de objeto de sombreador compilado (CSO ou .cso) fazem isso de forma assíncrona. Isso permite que você mantenha outros trabalhos funcionando ao mesmo tempo (como outras tarefas de instalação) e, como o loop main não está bloqueado, você pode continuar exibindo algo visualmente interessante para o usuário (como uma animação de carregamento para o jogo). Este exemplo usa a API Concurrency::Tasks que está disponível a partir de Windows 8; observe a sintaxe lambda usada para encapsular tarefas de carregamento assíncronas. Essas lambdas representam as funções chamadas off-thread, portanto, um ponteiro para o objeto de classe atual (isso) é capturado explicitamente.

Aqui está um exemplo de como você pode carregar o código de byte do sombreador:

HRESULT hr = S_OK;

// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();

// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;

size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];

fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
    bytes,
    bytesRead,
    nullptr,
    &m_pVertexShader
    );

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

delete bytes;


bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
    bytes,
    bytesRead,
    nullptr,
    m_pPixelShader.GetAddressOf()
    );

delete bytes;

CD3D11_BUFFER_DESC cbDesc(
    sizeof(ConstantBufferStruct),
    D3D11_BIND_CONSTANT_BUFFER
    );

hr = device->CreateBuffer(
    &cbDesc,
    nullptr,
    m_pConstantBuffer.GetAddressOf()
    );

fclose(vShader);
fclose(pShader);

Aqui está um exemplo de como criar buffers de vértice e índice:

HRESULT Renderer::CreateCube()
{
    HRESULT hr = S_OK;

    // Use the Direct3D device to load resources into graphics memory.
    ID3D11Device* device = m_deviceResources->GetDevice();

    // Create cube geometry.
    VertexPositionColor CubeVertices[] =
    {
        {DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  0,   0,   0),},
        {DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  0,   0,   1),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  0,   1,   0),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  0,   1,   1),},

        {DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  1,   0,   0),},
        {DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  1,   0,   1),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  1,   1,   0),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  1,   1,   1),},
    };
    
    // Create vertex buffer:
    
    CD3D11_BUFFER_DESC vDesc(
        sizeof(CubeVertices),
        D3D11_BIND_VERTEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA vData;
    ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
    vData.pSysMem = CubeVertices;
    vData.SysMemPitch = 0;
    vData.SysMemSlicePitch = 0;

    hr = device->CreateBuffer(
        &vDesc,
        &vData,
        &m_pVertexBuffer
        );

    // Create index buffer:
    unsigned short CubeIndices [] = 
    {
        0,2,1, // -x
        1,2,3,

        4,5,6, // +x
        5,7,6,

        0,1,5, // -y
        0,5,4,

        2,6,7, // +y
        2,7,3,

        0,4,6, // -z
        0,6,2,

        1,3,7, // +z
        1,7,5,
    };

    m_indexCount = ARRAYSIZE(CubeIndices);

    CD3D11_BUFFER_DESC iDesc(
        sizeof(CubeIndices),
        D3D11_BIND_INDEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA iData;
    ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
    iData.pSysMem = CubeIndices;
    iData.SysMemPitch = 0;
    iData.SysMemSlicePitch = 0;
    
    hr = device->CreateBuffer(
        &iDesc,
        &iData,
        &m_pIndexBuffer
        );

    return hr;
}

Este exemplo não carrega nenhuma malha ou textura. Você deve criar os métodos para carregar os tipos de malha e textura específicos do seu jogo e chamá-los de forma assíncrona.

Preencha os valores iniciais para os buffers constantes por cena aqui também. Exemplos de buffer constante por cena incluem luzes fixas ou outros elementos de cena estáticos e dados.

Implementar o método CreateWindowSizeDependentResources

Os métodos CreateWindowSizeDependentResources são chamados sempre que o tamanho da janela, a orientação ou a resolução são alterados.

Os recursos de tamanho de janela são atualizados da seguinte maneira: o proc de mensagem estática obtém um dos vários eventos possíveis que indicam uma alteração no estado da janela. O loop main é informado sobre o evento e chama CreateWindowSizeDependentResources na instância da classe main, que chama a implementação CreateWindowSizeDependentResources na classe de renderizador de cena.

O principal trabalho desse método é verificar se os objetos visuais não ficaram confusos ou se tornaram inválidos devido a uma alteração nas propriedades da janela. Neste exemplo, atualizamos as matrizes do projeto com um novo campo de exibição (FOV) para a janela redimensionada ou reorientada.

Já vimos o código para criar recursos de janela em DeviceResources - que era a cadeia de troca (com buffer de fundo) e renderizava a exibição de destino. Veja como o renderizador cria transformações dependentes de taxa de proporção:

void Renderer::CreateViewAndPerspective()
{
    // Use DirectXMath to create view and perspective matrices.

    DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
    DirectX::XMVECTOR at  = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
    DirectX::XMVECTOR up  = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.view,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixLookAtRH(
                eye,
                at,
                up
                )
            )
        );

    float aspectRatioX = m_deviceResources->GetAspectRatio();
    float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.projection,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixPerspectiveFovRH(
                2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
                aspectRatioX,
                0.01f,
                100.0f
                )
            )
        );
}

Se a cena tiver um layout específico de componentes que dependem da taxa de proporção, esse será o local para reorganizá-los para corresponder a essa taxa de proporção. Talvez você queira alterar a configuração do comportamento pós-processamento aqui também.

Implementar o método Update

O método Update é chamado uma vez por loop de jogo – neste exemplo, ele é chamado pelo método da classe main de mesmo nome. Ele tem uma finalidade simples: atualizar a geometria da cena e o estado do jogo com base na quantidade de tempo decorrido (ou etapas de tempo decorrido) desde o quadro anterior. Neste exemplo, simplesmente giramos o cubo uma vez por quadro. Em uma cena de jogo real, esse método contém muito mais código para verificar o estado do jogo, atualizar buffers constantes por quadro (ou outros dinâmicos), buffers de geometria e outros ativos na memória adequadamente. Como a comunicação entre a CPU e a GPU incorre em sobrecarga, certifique-se de atualizar apenas os buffers que realmente foram alterados desde o último quadro – os buffers constantes podem ser agrupados ou divididos, conforme necessário, para tornar isso mais eficiente.

void Renderer::Update()
{
    // Rotate the cube 1 degree per frame.
    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.world,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixRotationY(
                DirectX::XMConvertToRadians(
                    (float) m_frameCount++
                    )
                )
            )
        );

    if (m_frameCount == MAXUINT)  m_frameCount = 0;
}

Nesse caso, Girar atualiza o buffer constante com uma nova matriz de transformação para o cubo. A matriz será multiplicada por vértice durante o estágio do sombreador de vértice. Como esse método é chamado com cada quadro, esse é um bom lugar para agregar todos os métodos que atualizam a constante dinâmica e os buffers de vértice ou para executar quaisquer outras operações que preparem os objetos na cena para transformação pelo pipeline de gráficos.

Implementar o método Render

Esse método é chamado uma vez por loop de jogo depois de chamar Update. Assim como Update, o método Render também é chamado da classe main. Esse é o método em que o pipeline de gráficos é construído e processado para o quadro usando métodos na instância ID3D11DeviceContext . Isso culmina em uma chamada final para ID3D11DeviceContext::D rawIndexed. É importante entender que essa chamada (ou outras chamadas semelhantes de Draw* definidas em ID3D11DeviceContext) realmente executa o pipeline. Especificamente, isso ocorre quando o Direct3D se comunica com a GPU para definir o estado de desenho, executa cada estágio de pipeline e grava os resultados do pixel no recurso de buffer de destino de renderização para exibição pela cadeia de troca. Como a comunicação entre a CPU e a GPU incorre em sobrecarga, combine várias chamadas de desenho em uma única se puder, especialmente se a cena tiver muitos objetos renderizados.

void Renderer::Render()
{
    // Use the Direct3D device context to draw.
    ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();

    ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
    ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();

    context->UpdateSubresource(
        m_pConstantBuffer.Get(),
        0,
        nullptr,
        &m_constantBufferData,
        0,
        0
        );

    // Clear the render target and the z-buffer.
    const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
    context->ClearRenderTargetView(
        renderTarget,
        teal
        );
    context->ClearDepthStencilView(
        depthStencil,
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
        1.0f,
        0);

    // Set the render target.
    context->OMSetRenderTargets(
        1,
        &renderTarget,
        depthStencil
        );

    // Set up the IA stage by setting the input topology and layout.
    UINT stride = sizeof(VertexPositionColor);
    UINT offset = 0;

    context->IASetVertexBuffers(
        0,
        1,
        m_pVertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

    context->IASetIndexBuffer(
        m_pIndexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );
    
    context->IASetPrimitiveTopology(
        D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
        );

    context->IASetInputLayout(m_pInputLayout.Get());

    // Set up the vertex shader stage.
    context->VSSetShader(
        m_pVertexShader.Get(),
        nullptr,
        0
        );

    context->VSSetConstantBuffers(
        0,
        1,
        m_pConstantBuffer.GetAddressOf()
        );

    // Set up the pixel shader stage.
    context->PSSetShader(
        m_pPixelShader.Get(),
        nullptr,
        0
        );

    // Calling Draw tells Direct3D to start sending commands to the graphics device.
    context->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}

É uma boa prática definir os vários estágios do pipeline de gráficos no contexto em ordem. Normalmente, o pedido é:

  • Atualizar recursos de buffer constante com novos dados conforme necessário (usando dados de Atualização).
  • Assembly de entrada (IA): é aqui que anexamos os buffers de vértice e índice que definem a geometria da cena. Você precisa anexar cada vértice e buffer de índice para cada objeto na cena. Como este exemplo tem apenas o cubo, é muito simples.
  • VS (sombreador de vértice): anexe todos os sombreadores de vértice que transformarão os dados nos buffers de vértice e anexarão buffers constantes para o sombreador de vértice.
  • Sombreador de pixel (PS): anexe todos os sombreadores de pixel que executarão operações por pixel na cena rasterizada e anexe recursos do dispositivo para o sombreador de pixel (buffers constantes, texturas e assim por diante).
  • Fusão de saída (OM): esse é o estágio em que os pixels são misturados, depois que os sombreadores são concluídos. Essa é uma exceção à regra, pois você anexa os estênceis de profundidade e renderiza os destinos antes de definir qualquer um dos outros estágios. Você poderá ter vários estênceis e destinos se tiver sombreadores de vértice e pixel adicionais que geram texturas, como mapas de sombra, mapas de altura ou outras técnicas de amostragem. Nesse caso, cada passagem de desenho precisará do(s) destino(s) apropriado definido(s) antes de chamar uma função de desenho.

Em seguida, na seção final (Trabalhar com sombreadores e recursos de sombreador), examinaremos os sombreadores e discutiremos como o Direct3D os executa.

A seguir

Trabalhar com sombreadores e recursos de sombreador