Informazioni sulla pipeline di rendering direct3D 11

In precedenza si è visto come creare una finestra che è possibile usare per il disegno in Usare le risorse del dispositivo DirectX. Si apprenderà ora come creare la pipeline grafica e dove è possibile collegarsi.

Si ricorderà che sono disponibili due interfacce Direct3D che definiscono la pipeline grafica: ID3D11Device, che fornisce una rappresentazione virtuale della GPU e delle relative risorse; e ID3D11DeviceContext, che rappresenta l'elaborazione grafica per la pipeline. In genere, si usa un'istanza di ID3D11Device per configurare e ottenere le risorse GPU necessarie per avviare l'elaborazione della grafica in una scena e si usa ID3D11DeviceContext per elaborare tali risorse in ogni fase appropriata dello shader nella pipeline grafica. In genere si chiamano i metodi ID3D11Device raramente, ovvero solo quando si configura una scena o quando il dispositivo cambia. D'altra parte, chiamerai ID3D11DeviceContext ogni volta che elabora un frame per la visualizzazione.

In questo esempio viene creata e configurata una pipeline grafica minima adatta per la visualizzazione di un semplice cubo rotante con vertex shaded. Illustra approssimativamente il set di risorse più piccolo necessario per la visualizzazione. Come si legge qui le informazioni, prendere nota delle limitazioni dell'esempio specificato in cui potrebbe essere necessario estenderlo per supportare la scena di cui si vuole eseguire il rendering.

Questo esempio illustra due classi C++ per la grafica: una classe device resource manager e una classe renderer della scena 3D. Questo argomento è incentrato in particolare sul renderer della scena 3D.

Che cosa fa il renderer del cubo?

La pipeline grafica è definita dalla classe renderer della scena 3D. Il renderer della scena è in grado di:

  • Definire buffer costanti per archiviare i dati uniformi.
  • Definire i vertex buffer per contenere i dati dei vertici dell'oggetto e i buffer di indice corrispondenti per consentire al vertex shader di camminare correttamente i triangoli.
  • Creare risorse trama e viste delle risorse.
  • Caricare gli oggetti shader.
  • Aggiornare i dati grafici per visualizzare ogni frame.
  • Eseguire il rendering (disegnare) la grafica nella catena di scambio.

I primi quattro processi usano in genere i metodi di interfaccia ID3D11Device per inizializzare e gestire le risorse grafiche e gli ultimi due usano i metodi di interfaccia ID3D11DeviceContext per gestire ed eseguire la pipeline grafica.

Un'istanza della classe Renderer viene creata e gestita come variabile membro nella classe di progetto principale. L'istanza DeviceResources viene gestita come puntatore condiviso tra diverse classi, tra cui la classe di progetto principale , la classe del provider di visualizzazioni app e renderer. Se si sostituisce Renderer con la propria classe, è consigliabile dichiarare e assegnare anche l'istanza DeviceResources come membro puntatore condiviso:

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

È sufficiente passare il puntatore al costruttore di classe (o ad altro metodo di inizializzazione) dopo la creazione dell'istanza deviceResources nel metodo Initialize della classe App . È anche possibile passare un riferimento weak_ptr se, invece, si vuole che la classe principale sia completamente proprietaria dell'istanza DeviceResources .

Creare il renderer del cubo

In questo esempio viene organizzata la classe renderer della scena con i metodi seguenti:

  • CreateDeviceDependentResources: viene chiamato ogni volta che la scena deve essere inizializzata o riavviata. Questo metodo carica i dati iniziali dei vertici, trame, shader e altre risorse e costruisce i buffer costanti e vertici iniziali. In genere, la maggior parte del lavoro qui viene eseguita con i metodi ID3D11Device , non ID3D11DeviceContext .
  • CreateWindowSizeDependentResources: viene chiamato ogni volta che lo stato della finestra cambia, ad esempio quando si verifica il ridimensionamento o quando l'orientamento cambia. Questo metodo ricompila le matrici di trasformazione, ad esempio quelle per la fotocamera.
  • Aggiornamento: in genere chiamato dalla parte del programma che gestisce lo stato immediato del gioco; in questo esempio viene semplicemente chiamato dalla classe Main . Fare in modo che questo metodo legga da qualsiasi informazione sullo stato del gioco che influisce sul rendering, ad esempio gli aggiornamenti alla posizione dell'oggetto o ai fotogrammi di animazione, oltre a tutti i dati globali del gioco, ad esempio i livelli di luce o le modifiche alla fisica del gioco. Questi input vengono usati per aggiornare i buffer costanti per frame e i dati dell'oggetto.
  • Rendering: in genere chiamato dalla parte del programma che gestisce il ciclo del gioco; in questo caso, viene chiamato dalla classe Main . Questo metodo costruisce la pipeline grafica: associa gli shader, associa buffer e risorse alle fasi dello shader e richiama il disegno per il frame corrente.

Questi metodi costituiscono il corpo dei comportamenti per il rendering di una scena con Direct3D usando gli asset. Se si estende questo esempio con una nuova classe di rendering, dichiararla nella classe di progetto principale. Quindi:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

diventa:

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Anche in questo esempio si presuppone che i metodi abbiano le stesse firme nell'implementazione. Se le firme sono state modificate, esaminare il ciclo Main e apportare le modifiche di conseguenza.

Verranno ora esaminati in modo più dettagliato i metodi di rendering della scena.

Creare risorse dipendenti dal dispositivo

CreateDeviceDependentResources consolida tutte le operazioni per inizializzare la scena e le relative risorse usando chiamate ID3D11Device . Questo metodo presuppone che il dispositivo Direct3D sia stato appena inizializzato (o ricreato) per una scena. Ricrea o ricarica tutte le risorse grafiche specifiche della scena, ad esempio i vertex e pixel shader, i vertex e i buffer di indice per gli oggetti e qualsiasi altra risorsa(ad esempio, come trame e visualizzazioni corrispondenti).

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

Ogni volta che si caricano risorse dal disco, ad esempio file o trame dell'oggetto shader compilato (CSO o cso), eseguire questa operazione in modo asincrono. In questo modo puoi continuare a lavorare contemporaneamente (come altre attività di configurazione) e perché il ciclo principale non è bloccato puoi continuare a visualizzare qualcosa visivamente interessante per l'utente (ad esempio un'animazione di caricamento per il tuo gioco). Questo esempio usa l'API Concurrency::Tasks disponibile a partire da Windows 8. Si noti la sintassi lambda usata per incapsulare le attività di caricamento asincrone. Queste espressioni lambda rappresentano le funzioni chiamate off-thread, quindi viene acquisito in modo esplicito un puntatore all'oggetto classe corrente (questo).

Ecco un esempio di come caricare il bytecode dello shader:

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

Ecco un esempio di come creare vertex buffer e index buffer:

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

In questo esempio non vengono caricate mesh o trame. È necessario creare i metodi per caricare i tipi di mesh e trama specifici del gioco e chiamarli in modo asincrono.

Popolare anche i valori iniziali per i buffer costanti per scena. Esempi di buffer costante per scena includono luci fisse o altri dati e elementi statici della scena.

Implementare il metodo CreateWindowSizeDependentResources

I metodi CreateWindowSizeDependentResources vengono chiamati ogni volta che cambiano le dimensioni, l'orientamento o la risoluzione della finestra.

Le risorse delle dimensioni della finestra vengono aggiornate in questo modo: il processo di messaggio statico ottiene uno dei diversi eventi possibili che indicano una modifica dello stato della finestra. Il ciclo principale viene quindi informato sull'evento e chiama CreateWindowSizeDependentResources nell'istanza della classe principale, che chiama quindi l'implementazione CreateWindowSizeDependentResources nella classe renderer della scena.

Il processo principale di questo metodo consiste nel assicurarsi che gli oggetti visivi non diventino confusi o non validi a causa di una modifica delle proprietà della finestra. In questo esempio le matrici di progetto vengono aggiornate con un nuovo campo di visualizzazione (FOV) per la finestra ridimensionata o riorientata.

È già stato visto il codice per la creazione di risorse finestra in DeviceResources , ovvero la catena di scambio (con buffer nascosto) e la visualizzazione di destinazione di rendering. Ecco come il renderer crea trasformazioni dipendenti dalle proporzioni:

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 la scena ha un layout specifico di componenti che dipende dalle proporzioni, questa è la posizione in cui riorganizzare tali componenti in modo che corrispondano a tali proporzioni. È anche possibile modificare la configurazione del comportamento post-elaborazione.

Implementare il metodo Update

Il metodo Update viene chiamato una volta per ogni ciclo di gioco, in questo esempio viene chiamato dal metodo della classe main con lo stesso nome. Ha uno scopo semplice: aggiornare la geometria della scena e lo stato del gioco in base alla quantità di tempo trascorso (o passaggi temporali trascorsi) dal frame precedente. In questo esempio viene semplicemente ruotato il cubo una volta per ogni fotogramma. In una scena di gioco reale, questo metodo contiene molto più codice per controllare lo stato del gioco, aggiornare di conseguenza buffer costanti per fotogrammi (o altri buffer dinamici), buffer geometry e altri asset in memoria. Poiché la comunicazione tra la CPU e la GPU comporta un sovraccarico, assicurarsi di aggiornare solo i buffer effettivamente modificati dall'ultimo fotogramma. I buffer costanti possono essere raggruppati o suddivisi in base alle esigenze per rendere più efficiente questa operazione.

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

In questo caso , Ruota aggiorna il buffer costante con una nuova matrice di trasformazione per il cubo. La matrice verrà moltiplicata per vertice durante la fase del vertex shader. Poiché questo metodo viene chiamato con ogni fotogramma, questo è un buon posto per aggregare tutti i metodi che aggiornano i buffer costanti e vertex dinamici o per eseguire qualsiasi altra operazione che prepara gli oggetti nella scena per la trasformazione tramite la pipeline grafica.

Implementare il metodo Render

Questo metodo viene chiamato una volta per ogni ciclo di gioco dopo aver chiamato Update. Come Update, il metodo Render viene chiamato anche dalla classe main. Questo è il metodo in cui la pipeline grafica viene costruita ed elaborata per il frame usando i metodi nell'istanza ID3D11DeviceContext . Questo culmina in una chiamata finale a ID3D11DeviceContext::D rawIndexed. È importante comprendere che questa chiamata (o altre chiamate Draw* simili definite in ID3D11DeviceContext) esegue effettivamente la pipeline. In particolare, questo accade quando Direct3D comunica con la GPU per impostare lo stato di disegno, esegue ogni fase della pipeline e scrive i risultati pixel nella risorsa buffer di destinazione di rendering per la visualizzazione dalla catena di scambio. Poiché la comunicazione tra la CPU e la GPU comporta un sovraccarico, combinare più chiamate di disegno in un'unica, se possibile, soprattutto se la scena include molti oggetti di cui è stato eseguito il rendering.

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

È consigliabile impostare le varie fasi della pipeline grafica nel contesto in ordine. In genere, l'ordine è:

  • Aggiornare le risorse del buffer costante con nuovi dati in base alle esigenze (usando i dati di Update).
  • Assembly di input (IA): questo è il punto in cui vengono collegati i vertex buffer e index buffer che definiscono la geometria della scena. È necessario collegare ogni vertex buffer e buffer di indice per ogni oggetto nella scena. Poiché questo esempio ha solo il cubo, è piuttosto semplice.
  • Vertex shader (VS): collegare qualsiasi vertex shader che trasformerà i dati nei vertex buffer e allega buffer costanti per il vertex shader.
  • Pixel shader (PS): collegare qualsiasi pixel shader che eseguirà operazioni per pixel nella scena rasterizzata e allegare le risorse del dispositivo per il pixel shader (buffer costanti, trame e così via).
  • Unione di output (OM): questa è la fase in cui i pixel vengono mescolati, al termine degli shader. Si tratta di un'eccezione alla regola, perché è possibile collegare gli stencil di profondità e le destinazioni di rendering prima di impostare una qualsiasi delle altre fasi. È possibile che siano presenti più stencil e destinazioni se sono disponibili altri vertex shader e pixel shader che generano trame come mappe shadow, mappe di altezza o altre tecniche di campionamento. In questo caso, ogni passaggio di disegno richiederà il set di destinazioni appropriate prima di chiamare una funzione di disegno.

Nella sezione finale (Usare shader e risorse shader), si esamineranno gli shader e si esaminerà il modo in cui Direct3D li esegue.

Successivo

Usare shader e risorse shader