Framework di rendering I: Introduzione al rendering

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.

Finora abbiamo illustrato come strutturare un gioco UWP (Universal Windows Platform) e come definire una macchina a stati per gestire il flusso del gioco. Cerchiamo ora di imparare a sviluppare il framework di rendering. Vediamo come il gioco di esempio esegue il rendering della scena del gioco tramite Direct3D 11.

Direct3D 11 contiene una serie di API che forniscono accesso alle funzionalità avanzate di hardware grafico ad alte prestazioni che è possibile utilizzare per creare grafica 3D per applicazioni a elevato utilizzo di grafica, come ad esempio i giochi.

Rendering della grafica del gioco sullo schermo significa fondamentalmente eseguire il rendering di una sequenza di frame sullo schermo. In ogni frame è necessario eseguire il rendering degli oggetti visibili nella scena, in base alla visuale.

Per eseguire il rendering di un frame, è necessario passare le informazioni necessarie della scena all'hardware in modo che possano essere visualizzate sullo schermo. Se si desidera non visualizzare nulla sullo schermo, si dovrà avviare il rendering non appena inizia l'esecuzione del gioco.

Obiettivi

Configurare un framework di rendering di base per visualizzare l'output grafico per un gioco DirectX UWP. È possibile suddividerlo generalmente nei seguenti tre passaggi.

  1. Stabilire una connessione all'interfaccia grafica.
  2. Creare le risorse necessarie per disegnare la grafica.
  3. Visualizzare la grafica eseguendo il rendering del frame.

In questo argomento viene spiegato il rendering della grafica, che illustra i passaggi 1 e 3.

Framework di rendering II: Rendering del gioco illustra il passaggio 2: come configurare il framework di rendering, e come vengono preparati i dati prima che il rendering possa verificarsi.

Operazioni preliminari

È consigliabile acquisire familiarità con i concetti di base relativi alla grafica e al rendering. Se non si ha familiarità con Direct3D e rendering, vedere Termini e concetti per una breve descrizione dei termini di grafica e rendering utilizzati in questo argomento.

La classe GameRenderer rappresenta il renderer per questo gioco di esempio. È responsabile della creazione e della gestione di tutti gli oggetti Direct3D 11 e Direct2D utilizzati per generare le visuali del gioco. Mantiene inoltre un riferimento all'oggetto Simple3DGame utilizzato per recuperare l'elenco di oggetti dei quali eseguire il rendering, nonché lo stato del gioco per l'heads-up display (HUD).

In questa parte dell'esercitazione, ci concentreremo sul rendering degli oggetti 3D nel gioco.

Stabilire una connessione all'interfaccia grafica

Per informazioni sull'accesso all'hardware per il rendering, vedere l'argomento Definire il framework dell'app UWP del gioco.

Il metodo App::Initialize

La funzione std::make_shared, come illustrato di seguito, viene utilizzata per creare uno shared_ptr in DX::DeviceResources, che fornisce anche accesso al dispositivo.

In Direct3D 11, viene utilizzato un dispositivo per allocare e distruggere oggetti, eseguire il rendering di primitive e comunicare con la scheda grafica tramite il driver grafico.

void Initialize(CoreApplicationView const& applicationView)
{
    ...

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

Visualizzare la grafica eseguendo il rendering del frame

La scena di gioco deve eseguire il rendering quando il gioco viene avviato. Le istruzioni per il rendering iniziano nel metodo GameMain::Run, come illustrato di seguito.

Il flusso semplice è questo.

  1. Update
  2. Render
  3. Present

Il metodo GameMain::Run

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible) // if the window is visible
        {
            switch (m_updateState)
            {
            ...
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

Update

Vedere l'argomento Gestione del flusso di gioco per maggiori informazioni sull'aggiornamento degli stati del gioco nel metodo GameMain::Update.

Render

Il rendering viene implementato chiamando il metodo GameRenderer::Render da GameMain::Run.

Se è abilitato il rendering stereo, sono disponibili due passaggi di rendering, uno per l'occhio sinistro e uno per quello destro. In ogni passaggio di rendering, associamo il target di rendering e la vista depth-stencil al dispositivo. Cancelliamo inoltre la vista depth-stencil in un secondo momento.

Nota

È possibile ottenere il rendering stereo tramite altri metodi, ad esempio lo stereo a passaggio singolo utilizzando l'instanziamento dei vertici o gli shader di geometria. Il metodo di rendering a doppio passaggio è un modo più lento ma più pratico per ottenere il rendering stereo.

Quando il gioco è in esecuzione e le risorse vengono caricate, aggiorniamo la matrice di proiezione, una volta per ogni passaggio di rendering. Gli oggetti sono leggermente diversi da ogni visuale. Successivamente, configuriamo la pipeline di rendering grafico.

Nota

Per maggiori informazioni sulla modalità di caricamento delle risorse, vedere Creare e caricare risorse grafiche DirectX.

In questo gioco di esempio, il renderer è progettato per utilizzare un layout di vertici standard in tutti gli oggetti. Questo semplifica la progettazione dello shader e consente di modificare facilmente gli shader, indipendentemente dalla geometria degli oggetti.

Il metodo GameRenderer::Render

Impostiamo il contesto Direct3D per utilizzare un layout dei vertici di input. Gli oggetti layout di input descrivono il modo in cui i dati del buffer dei vertici vengono trasmessi nella pipeline di rendering.

Successivamente, impostiamo il contesto Direct3D per utilizzare i buffer costanti definiti in precedenza, utilizzati dallo stadio di pipeline dello shader di vertici e dallo stadio di pipeline dello shader di pixel.

Nota

Vedere Framework di rendering II: Rendering del gioco per maggiori informazioni sulla definizione dei buffer costanti.

Poiché lo stesso layout di input e serie di buffer costanti viene utilizzato per tutti gli shader presenti nella pipeline, viene configurato una sola volta per ogni frame.

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    int renderingPasses = 1;
    if (stereoEnabled)
    {
        renderingPasses = 2;
    }

    for (int i = 0; i < renderingPasses; i++)
    {
        // Iterate through the number of rendering passes to be completed.
        // 2 rendering passes if stereo is enabled.
        if (i > 0)
        {
            // Doing the Right Eye View.
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetViewRight() };

            // Resets render targets to the screen.
            // OMSetRenderTargets binds 2 things to the device.
            // 1. Binds one render target atomically to the device.
            // 2. Binds the depth-stencil view, as returned by the GetDepthStencilView method, to the device.
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-omsetrendertargets

            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());

            // Clears the depth stencil view.
            // A depth stencil view contains the format and buffer to hold depth and stencil info.
            // For more info about depth stencil view, go to: 
            // https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-stencil-view--dsv-
            // A depth buffer is used to store depth information to control which areas of 
            // polygons are rendered rather than hidden from view. To learn more about a depth buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-buffers
            // A stencil buffer is used to mask pixels in an image, to produce special effects. 
            // The mask determines whether a pixel is drawn or not,
            // by setting the bit to a 1 or 0. To learn more about a stencil buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/stencil-buffers

            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // Direct2D -- discussed later
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmapRight());
        }
        else
        {
            // Doing the Mono or Left Eye View.
            // As compared to the right eye:
            // m_deviceResources->GetBackBufferRenderTargetView instead of GetBackBufferRenderTargetViewRight
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetView() };

            // Same as the Right Eye View.
            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // d2d -- Discussed later under Adding UI
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmap());
        }

        const float clearColor[4] = { 0.5f, 0.5f, 0.8f, 1.0f };

        // Only need to clear the background when not rendering the full 3D scene since
        // the 3D world is a fully enclosed box and the dynamics prevents the camera from
        // moving outside this space.
        if (i > 0)
        {
            // Doing the Right Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetViewRight(), clearColor);
        }
        else
        {
            // Doing the Mono or Left Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetView(), clearColor);
        }

        // Render the scene objects
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            if (stereoEnabled)
            {
                // When doing stereo, it is necessary to update the projection matrix once per rendering pass.

                auto orientation = m_deviceResources->GetOrientationTransform3D();

                ConstantBufferChangeOnResize changesOnResize;
                // Apply either a left or right eye projection, which is an offset from the middle
                XMStoreFloat4x4(
                    &changesOnResize.projection,
                    XMMatrixMultiply(
                        XMMatrixTranspose(
                            i == 0 ?
                            m_game->GameCamera().LeftEyeProjection() :
                            m_game->GameCamera().RightEyeProjection()
                            ),
                        XMMatrixTranspose(XMLoadFloat4x4(&orientation))
                        )
                    );

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

            // Update variables that change once per frame.
            ConstantBufferChangesEveryFrame constantBufferChangesEveryFrameValue;
            XMStoreFloat4x4(
                &constantBufferChangesEveryFrameValue.view,
                XMMatrixTranspose(m_game->GameCamera().View())
                );
            d3dContext->UpdateSubresource(
                m_constantBufferChangesEveryFrame.get(),
                0,
                nullptr,
                &constantBufferChangesEveryFrameValue,
                0,
                0
                );

            // Set up the graphics pipeline. This sample uses the same InputLayout and set of
            // constant buffers for all shaders, so they only need to be set once per frame.
            // For more info about the graphics or rendering pipeline, see
            // https://learn.microsoft.com/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

            // IASetInputLayout binds an input-layout object to the input-assembler (IA) stage. 
            // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
            // Set up the Direct3D context to use this vertex layout. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
            d3dContext->IASetInputLayout(m_vertexLayout.get());

            // VSSetConstantBuffers sets the constant buffers used by the vertex shader pipeline stage.
            // Set up the Direct3D context to use these constant buffers. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-vssetconstantbuffers

            ID3D11Buffer* constantBufferNeverChanges{ m_constantBufferNeverChanges.get() };
            d3dContext->VSSetConstantBuffers(0, 1, &constantBufferNeverChanges);
            ID3D11Buffer* constantBufferChangeOnResize{ m_constantBufferChangeOnResize.get() };
            d3dContext->VSSetConstantBuffers(1, 1, &constantBufferChangeOnResize);
            ID3D11Buffer* constantBufferChangesEveryFrame{ m_constantBufferChangesEveryFrame.get() };
            d3dContext->VSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            ID3D11Buffer* constantBufferChangesEveryPrim{ m_constantBufferChangesEveryPrim.get() };
            d3dContext->VSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);

            // Sets the constant buffers used by the pixel shader pipeline stage. 
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-pssetconstantbuffers

            d3dContext->PSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            d3dContext->PSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);
            ID3D11SamplerState* samplerLinear{ m_samplerLinear.get() };
            d3dContext->PSSetSamplers(0, 1, &samplerLinear);

            for (auto&& object : m_game->RenderObjects())
            {
                // The 3D object render method handles the rendering.
                // For more info, see Primitive rendering below.
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        // Start of 2D rendering
        ...
    }
}

Rendering delle primitive

Quando si esegue il rendering della scena, si scorrono in ciclo tutti gli oggetti di cui è necessario eseguire il rendering. I passaggi seguenti vengono ripetuti per ciascun oggetto (primitive).

  • Aggiornare il buffer costante (m_constantBufferChangesEveryPrim) con la matrice di trasformazione globale del modello e le informazioni sui materiali.
  • Il m_constantBufferChangesEveryPrim contiene parametri per ciascun oggetto. Include la matrice di trasformazione da locale a globale (object-to-world), nonché le proprietà dei materiali, ad esempio il colore e l'esponente speculare per i calcoli di illuminazione.
  • Impostare il contesto Direct3D per utilizzare il layout dei vertici di input per i dati dell'oggetto mesh da trasmettere nello stadio di input-assembler (IA) della pipeline di rendering.
  • Impostare il contesto Direct3D per utilizzare un buffer di indice nello stadio IA. Specificare le informazioni delle primitive: tipo, ordine dati.
  • Inviare una chiamata di disegno (draw) per disegnare la primitiva indicizzata e non istanziata. Il metodo GameObject::Render aggiorna il buffer costante delle primitive con i dati specifici di una determinata primitiva. Ciò comporta una chiamata DrawIndexed sul contesto per disegnare la geometria di ciascuna primitiva. In particolare, questa chiamata di disegno accoda comandi e dati all'unità di elaborazione grafica (GPU), come parametrizzato dai dati del buffer costante. Ciascuna chiamata di disegno esegue lo shader di vertici una volta per ogni vertice e quindi lo shader di pixel una volta per ogni pixel di ogni triangolo nella primitiva. Le trame fanno parte dello stato utilizzato dallo shader di pixel per eseguire il rendering.

Ecco i motivi per l'uso di più buffer costanti.

  • Il gioco utilizza più buffer costanti, ma deve solo aggiornare questi buffer una sola volta per ogni primitiva. Come accennato in precedenza, i buffer costanti sono come input per gli shader eseguiti per ogni primitiva. Alcuni dati sono statici (m_constantBufferNeverChanges); alcuni dati sono costanti nel frame (m_constantBufferChangesEveryFrame), ad esempio la posizione della telecamera; e alcuni dati sono specifici della primitiva, ad esempio il suo colore e le trame (m_constantBufferChangesEveryPrim).
  • Il renderer del gioco separa questi input in buffer costanti diversi per ottimizzare la larghezza di banda di memoria utilizzata dalla CPU e dalla GPU. Questo approccio consente anche di ridurre al minimo la quantità di dati di cui la GPU deve tenere traccia. La GPU ha una grande coda di comandi e ogni volta che il gioco chiama Draw, tale comando viene accodato insieme ai dati ad esso associati. Quando il gioco aggiorna il buffer costante delle primitive ed emette il comando Draw successivo, il driver grafico aggiunge questo comando successivo e i dati associati alla coda. Se il gioco disegna 100 primitive, potrebbe avere potenzialmente 100 copie dei dati del buffer costante nella coda. Per ridurre al minimo la quantità di dati inviati dal gioco alla GPU, il gioco utilizza un buffer costante delle primitive separato che contiene solo gli aggiornamenti per ogni primitiva.

Il metodo GameObject::Render

void GameObject::Render(
    _In_ ID3D11DeviceContext* context,
    _In_ ID3D11Buffer* primitiveConstantBuffer
    )
{
    if (!m_active || (m_mesh == nullptr) || (m_normalMaterial == nullptr))
    {
        return;
    }

    ConstantBufferChangesEveryPrim constantBuffer;

    // Put the model matrix info into a constant buffer, in world matrix.
    XMStoreFloat4x4(
        &constantBuffer.worldMatrix,
        XMMatrixTranspose(ModelMatrix())
        );

    // Check to see which material to use on the object.
    // If a collision (a hit) is detected, GameObject::Render checks the current context, which 
    // indicates whether the target has been hit by an ammo sphere. If the target has been hit, 
    // this method applies a hit material, which reverses the colors of the rings of the target to 
    // indicate a successful hit to the player. Otherwise, it applies the default material 
    // with the same method. In both cases, it sets the material by calling Material::RenderSetup, 
    // which sets the appropriate constants into the constant buffer. Then, it calls 
    // ID3D11DeviceContext::PSSetShaderResources to set the corresponding texture resource for the 
    // pixel shader, and ID3D11DeviceContext::VSSetShader and ID3D11DeviceContext::PSSetShader 
    // to set the vertex shader and pixel shader objects themselves, respectively.

    if (m_hit && m_hitMaterial != nullptr)
    {
        m_hitMaterial->RenderSetup(context, &constantBuffer);
    }
    else
    {
        m_normalMaterial->RenderSetup(context, &constantBuffer);
    }

    // Update the primitive constant buffer with the object model's info.
    context->UpdateSubresource(primitiveConstantBuffer, 0, nullptr, &constantBuffer, 0, 0);

    // Render the mesh.
    // See MeshObject::Render method below.
    m_mesh->Render(context);
}

Il metodo MeshObject::Render

void MeshObject::Render(_In_ ID3D11DeviceContext* context)
{
    // PNTVertex is a struct. stride provides us the size required for all the mesh data
    // struct PNTVertex
    //{
    //  DirectX::XMFLOAT3 position;
    //  DirectX::XMFLOAT3 normal;
    //  DirectX::XMFLOAT2 textureCoordinate;
    //};
    uint32_t stride{ sizeof(PNTVertex) };
    uint32_t offset{ 0 };

    // Similar to the main render loop.
    // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
    ID3D11Buffer* vertexBuffer{ m_vertexBuffer.get() };
    context->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);

    // IASetIndexBuffer binds an index buffer to the input-assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetindexbuffer.
    context->IASetIndexBuffer(m_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0);

    // Binds information about the primitive type, and data order that describes input data for the input assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetprimitivetopology.
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // Draw indexed, non-instanced primitives. A draw API submits work to the rendering pipeline.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-drawindexed.
    context->DrawIndexed(m_indexCount, 0, 0);
}

Il metodo DeviceResources::Present

Chiamiamo il metodo DeviceResources::Present per visualizzare il contenuto che abbiamo inserito nei buffer.

Utilizziamo il termine catena di scambio per una raccolta di buffer utilizzati per la visualizzazione di frame all'utente. Ogni volta che un'applicazione presenta un nuovo frame per la visualizzazione, il primo buffer nella catena di scambio assume il posto del buffer visualizzato. Questo processo viene denominato scambio o inversione. Per maggiori informazioni, vedere Catene di scambio.

  • Il metodo Present dell'interfaccia IDXGISwapChain1 istruisce DXGI per bloccare fino a quando non viene eseguita la sincronizzazione verticale (VSync), mettendo l'applicazione in sospensione fino alla VSync successiva. In questo modo non si sprecano cicli di rendering dei frame che non verranno mai visualizzati sullo schermo.
  • Il metodo DiscardView dell'interfaccia ID3D11DeviceContext3 elimina il contenuto del target di rendering. Si tratta di un'operazione valida solo quando il contenuto esistente verrà completamente sovrascritto. Se vengono utilizzati rettangoli sporchi o di scorrimento, allora questa chiamata deve essere rimossa.
  • Utilizzando lo stesso metodo DiscardView, rimuovere il contenuto del depth-stencil.
  • Il metodo HandleDeviceLost viene utilizzato per gestire lo scenario del dispositivo da rimuovere. Se il dispositivo è stato rimosso da una disconnessione o da un aggiornamento del driver, è allora necessario ricreare tutte le risorse del dispositivo. Per maggiori informazioni, vedere Gestire gli scenari rimossi di un dispositivo in Direct3D 11.

Suggerimento

Per ottenere una frequenza di frame uniforme, è necessario accertarsi che la quantità di lavoro per il rendering di un frame si adatti al tempo tra VSync.

// Present the contents of the swap chain to the screen.
void DX::DeviceResources::Present()
{
    // The first argument instructs DXGI to block until VSync, putting the application
    // to sleep until the next VSync. This ensures we don't waste any cycles rendering
    // frames that will never be displayed to the screen.
    HRESULT hr = m_swapChain->Present(1, 0);

    // Discard the contents of the render target.
    // This is a valid operation only when the existing contents will be entirely
    // overwritten. If dirty or scroll rects are used, this call should be removed.
    m_d3dContext->DiscardView(m_d3dRenderTargetView.get());

    // Discard the contents of the depth stencil.
    m_d3dContext->DiscardView(m_d3dDepthStencilView.get());

    // If the device was removed either by a disconnection or a driver upgrade, we 
    // must recreate all device resources.
    if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
    {
        HandleDeviceLost();
    }
    else
    {
        winrt::check_hresult(hr);
    }
}

Passaggi successivi

In questo argomento viene illustrato il rendering della grafica sullo schermo e viene fornita una breve descrizione per alcuni dei termini di rendering utilizzati (di seguito). Altre informazioni sul rendering nell'argomento Rendering framework II: Rendering del gioco e su come preparare i dati necessari prima del rendering.

Termini e concetti

Scena di gioco semplice

Una scena di gioco semplice è costituita da pochi oggetti con diverse sorgenti di luce.

La forma di un oggetto è definita da una serie di coordinate X, Y, Z nello spazio. La posizione di rendering effettiva nel mondo dei giochi può essere determinata applicando una matrice di trasformazione alle coordinate X, Y, Z posizionali. Potrebbe anche avere una serie di coordinate di trama, U e V, che specificano il modo in cui un materiale viene applicato all'oggetto. Ciò definisce le proprietà di superficie dell'oggetto e consente di vedere se un oggetto ha una superficie grezza (come una palla da tennis) o una superficie lucida liscia (come una palla da bowling).

Le informazioni sulla scena e sull'oggetto vengono utilizzate dal framework di rendering per ricreare la scena frame dopo frame, rendendola attiva sul monitor di visualizzazione.

Pipeline di rendering

La pipeline di rendering è il processo mediante il quale le informazioni della scena 3D vengono convertite in un'immagine visualizzata sullo schermo. In Direct3D 11 questa pipeline è programmabile. È possibile adattare gli stadi per supportare le proprie esigenze di rendering. Gli stadi che presentano core di shader comuni sono programmabili utilizzando il linguaggio di programmazione HLSL. È nota anche come pipeline di rendering grafico o semplicemente pipeline.

Per agevolare la creazione di questa pipeline, è necessario avere familiarità con questi dettagli.

Per maggiori informazioni, vedere Informazioni sulla pipeline di rendering di Direct3D 11 e Pipeline grafica.

HLSL

HLSL è il linguaggio degli shader di livello alto per DirectX. Mediante HLSL è possibile creare shader programmabili simili al linguaggio C per la pipeline Direct3D. Per altre informazioni, vedere HLSL.

Shader

Uno shader può essere considerato come una serie di istruzioni che determinano la modalità di visualizzazione della superficie di un oggetto durante il rendering. Quelli programmati con HLSL sono noti come shader HLSL. I file di codice sorgente per gli shader [HLSL])(#hlsl) hanno l'estensione file .hlsl. Questi shader possono essere compilati in fase di compilazione o in fase di esecuzione e impostati in fase di esecuzione nello stadio appropriato della pipeline. Un oggetto shader compilato ha l'estensione file .cso.

Gli shader Direct3D 9 possono essere progettati usando il modello di shader 1, il modello di shader 2 e il modello di shader 3; gli shader Direct3D 10 possono essere progettati solo sul modello di shader 4. Gli shader Direct3D 11 possono essere progettati sul modello di shader 5. Direct3D 11.3 e Direct3D 12 possono essere progettati sul modello di shader 5.1 mentre i Direct3D 12 possono essere progettati anche sul modello di shader 6.

Shader di vertici e shader di pixel

I dati entrano nella pipeline grafica come flusso di primitive e vengono elaborati dai vari shader, ad esempio shader di vertici e shader di pixel.

Gli shader di vertici elaborano i vertici, in genere eseguendo operazioni quali trasformazioni, rivestimento e illuminazione. Gli shader di pixel consentono tecniche di ombreggiatura avanzate, quali l'illuminazione per pixel e la post-elaborazione. Combinano variabili costanti, dati di trama, valori interpolati per vertice e altri dati per produrre output per pixel.

Stadi shader

Una sequenza di questi vari shader definiti per elaborare questo flusso di primitive è nota come stadi di shader in una pipeline di rendering. Gli stadi effettivi dipendono dalla versione di Direct3D, ma in genere includono gli stadi di vertici, geometria e pixel. Esistono anche altri stadi, ad esempio gli shader hull e domain per la tassellatura e lo shader di calcolo. Tutti questi stadi sono completamente programmabili tramite HLSL. Per maggiori informazioni, vedere Pipeline grafica.

Vari formati di file per shader

Ecco le estensioni file per i codici per shader.

  • Un file con estensione .hlsl contiene il codice sorgente [HLSL])(#hlsl).
  • Un file con estensione .cso contiene un oggetto shader compilato.
  • Un file con estensione .h è un file di intestazione, ma in un contesto di codice shader questo file di intestazione definisce una matrice di byte che contiene dati dello shader.
  • Un file con estensione .hlsli contiene il formato dei buffer costanti. Nel gioco di esempio il file è Shaders>ConstantBuffers.hlsli.

Nota

È possibile incorporare uno shader caricando un file .cso in fase di esecuzione o aggiungendo un file .h nel proprio codice eseguibile. Ma non verrebbero utilizzati entrambi per lo stesso shader.

Comprensione più approfondita di DirectX

Direct3D 11 è un serie di API che possono aiutarci a creare grafica per applicazioni a elevato utilizzo di grafica, quali i giochi, in cui vogliamo avere una buona scheda grafica per elaborare un calcolo intensivo. Questa sezione spiega brevemente i concetti di programmazione grafica di Direct3D 11: risorsa, sottorisorsa, dispositivo e contesto di dispositivo.

Risorsa

È possibile considerare le risorse (note anche come risorse del dispositivo) come informazioni su come eseguire il rendering di un oggetto, ad esempio trama, posizione o colore. Le risorse forniscono dati alla pipeline e definiscono il rendering durante la scena. Le risorse possono essere caricate dai supporti di gioco o create in modo dinamico in fase di esecuzione.

Una risorsa è infatti un'area della memoria accessibile tramite la pipeline Direct3D. Affinché la pipeline possa accedere alla memoria in modo efficiente, i dati inseriti al suo interno (quali geometria di input, risorse shader e trame) devono essere archiviati in una risorsa. Esistono due tipi di risorse da cui derivano tutte le risorse Direct3D: buffer o trama. Per ogni stadio della pipeline possono essere attive fino a 128 risorse. Per maggiori informazioni, vedere Risorse.

Sottorisorsa

Il termine sottorisorsa fa riferimento a un sottoinsieme di una risorsa. Direct3D può fare riferimento a un'intera risorsa oppure può fare riferimento a sottoinsiemi di una risorsa. Per maggiori informazioni, vedere Sottorisorsa.

Depth-stencil

Una risorsa depth-stencil contiene il formato e il buffer per contenere informazioni di profondità e stencil. Viene creato utilizzando una risorsa trama. Per maggiori informazioni su come creare una risorsa depth-stencil, vedere Configurazione della funzionalità depth-stencil. Accediamo alla risorsa depth-stencil tramite la vista depth-stencil implementata usando l'interfaccia ID3D11DepthStencilView.

Le informazioni di profondità indicano quali aree dei poligoni sono dietro le altre, in modo da poter determinare quali sono quelle nascoste. Le informazioni sugli stencil indicano quali pixel sono mascherati. Possono essere utilizzate per produrre effetti speciali in quanto determinano se un pixel viene disegnato o meno; impostano il bit su 1 o 0.

Per maggiori informazioni, vedere Vista depth-stencil, buffer di profondità e buffer di stencil.

Target di rendering

Un target di rendering è una risorsa in cui è possibile scrivere alla fine di un passaggio di rendering. Viene in genere creato mediante il metodo ID3D11Device::CreateRenderTargetView utilizzando il buffer posteriore della catena di scambio (che è anche una risorsa) come parametro di input.

Ogni target di rendering deve avere anche una vista depth-stencil corrispondente perché quando utilizziamo OMSetRenderTargets per impostare il target di rendering prima di utilizzarlo, è necessaria anche una vista depth-stencil. Accediamo alla risorsa del target di rendering tramite la vista target di rendering implementata utilizzando l'interfaccia ID3D11RenderTargetView.

Dispositivo

È possibile immaginare un dispositivo come modo per allocare e distruggere oggetti, eseguire il rendering di primitive e comunicare con la scheda grafica tramite il driver grafico.

Per una spiegazione più precisa, un dispositivo Direct3D è il componente di rendering di Direct3D. Un dispositivo incapsula e archivia lo stato di rendering, esegue le trasformazioni e le operazioni di illuminazione e rasterizza un'immagine su una superficie. Per maggiori informazioni, vedere Dispositivi

Un dispositivo è rappresentato dall'interfaccia ID3D11Device. In altre parole, l'interfaccia ID3D11Device rappresenta un adattatore di visualizzazione virtuale e viene utilizzata per creare risorse di proprietà di un dispositivo.

Esistono versioni diverse di ID3D11Device. ID3D11Device5 è la versione più recente e aggiunge nuovi metodi a quelli presenti in ID3D11Device4. Per maggiori informazioni su come Direct3D comunica con l'hardware sottostante, vedere Architettura WDDM (Windows Device Driver Model).

Ogni applicazione deve avere almeno un dispositivo; la maggior parte delle applicazioni ne crea solo uno. Creare un dispositivo per uno dei driver hardware installati sulla macchina chiamando D3D11CreateDevice o D3D11CreateDeviceAndSwapChain e specificando il tipo di driver con il flag D3D_DRIVER_TYPE. Ogni dispositivo può utilizzare uno o più contesti di dispositivo, a seconda della funzionalità desiderata. Per maggiori informazioni, vedere Funzione D3D11CreateDevice.

Contesto del dispositivo

Un contesto di dispositivo viene utilizzato per impostare lo stato della pipeline e generare comandi di rendering mediante le risorse di proprietà di un dispositivo.

Direct3D 11 implementa due tipi di contesti di dispositivo, uno per il rendering immediato e l'altro per il rendering differito; entrambi i contesti sono rappresentati con un'interfaccia ID3D11DeviceContext.

Le interfacce ID3D11DeviceContext hanno versioni diverse; ID3D11DeviceContext4 aggiunge nuovi metodi a quelli presenti in ID3D11DeviceContext3.

ID3D11DeviceContext4 è introdotta in Windows 10 Creators Update ed è la versione più recente dell'interfaccia ID3D11DeviceContext. Le applicazioni destinate a Windows 10 Creators Update e versioni successive devono utilizzare questa interfaccia anziché le versioni precedenti. Per maggiori informazioni, vedere ID3D11DeviceContext4.

DX::DeviceResources

La classe DX::DeviceResources si trova nei file DeviceResources.cpp/.h e controlla tutte le risorse del dispositivo DirectX.

Buffer

Una risorsa buffer è una raccolta di dati totalmente tipizzati, raggruppati in elementi. È possibile utilizzare i buffer per archiviare un'ampia varietà di dati, tra cui vettori di posizione, vettori normali, coordinate di trama in un buffer di vertici, indici in un buffer di indici o stato del dispositivo. Gli elementi del buffer possono includere valori di dati compressi (ad esempio valori di superficie R8G8B8A8), interi singoli a 8 bit o quattro valori a virgola mobile a 32 bit.

Sono disponibili tre tipi di buffer: buffer di vertici, buffer di indici e buffer costante.

Buffer di vertici

Contiene i dati dei vertici utilizzati per definire la propria geometria. I dati dei vertici includono coordinate di posizione, dati di colore, dati delle coordinate delle trame, dati normali e così via.

Buffer di indici

Contiene offset di interi in buffer di vertici e vengono utilizzati per eseguire il rendering delle primitive in modo più efficiente. Un buffer di indici contiene una serie sequenziale di indici a 16 bit o a 32 bit; ogni indice viene utilizzato per identificare un vertice in un buffer di vertici.

Buffer costante o buffer costante shader

Consente di fornire in modo efficiente i dati dello shader alla pipeline. È possibile utilizzare buffer costanti come input per gli shader eseguiti per ogni primitiva e archiviare i risultati dello stadio di output del flusso della pipeline di rendering. Concettualmente, un buffer costante è simile a un buffer di vertici a singolo elemento.

Progettazione e implementazione di buffer

È possibile progettare buffer in base al tipo di dati, ad esempio nel nostro gioco di esempio, viene creato un buffer per i dati statici, un altro per i dati costanti nel frame e un altro per i dati specifici in una primitiva.

Tutti i tipi di buffer vengono incapsulati dall'interfaccia ID3D11Buffer ed è possibile creare una risorsa buffer chiamando ID3D11Device::CreateBuffer. Tuttavia, un buffer deve essere associato alla pipeline prima di potervi accedere. I buffer possono essere associati a più stadi della pipeline contemporaneamente per la lettura. Un buffer può anche essere associato a un singolo stadio della pipeline per la scrittura; tuttavia, lo stesso buffer non può essere associato simultaneamente sia per la lettura sia per la scrittura.

È possibile associare buffer in questi modi.

  • Nello stadio input-assembler chiamando metodi ID3D11DeviceContext quali ID3D11DeviceContext::IASetVertexBuffers e ID3D11DeviceContext::IASetIndexBuffer.
  • Nello stadio stream-output chiamando ID3D11DeviceContext::SOSetTargets.
  • Nello stadio shader chiamando metodi di shader, quali ID3D11DeviceContext::VSSetConstantBuffers.

Per maggiori informazioni, vedere Introduzione ai buffer in Direct3D 11.

DXGI

Microsoft DirectX Graphics Infrastructure (DXGI) è un sottosistema che incapsula alcune delle attività di basso livello necessarie per Direct3D. È necessario prestare particolare attenzione quando si utilizza DXGI in un'applicazione multithreading per garantire che non si verifichino situazioni di stallo. Per maggiori informazioni, vedere Multithreading e DXGI

Livello di funzionalità

Il livello di funzionalità è un concetto introdotto in Direct3D 11 per gestire la diversità delle schede video nelle macchine nuove ed esistenti. Un livello di funzionalità è una serie ben definita di funzionalità dell'unità di elaborazione grafica (GPU).

Ogni scheda video implementa un determinato livello di funzionalità DirectX a seconda delle GPU installate. Nelle versioni precedenti di Microsoft Direct3D è possibile scoprire la versione di Direct3D implementata dalla scheda video e quindi programmare di conseguenza la propria applicazione.

Con il livello di funzionalità, quando si crea un dispositivo, è possibile tentare di creare un dispositivo per il livello di funzionalità che si desidera richiedere. Se la creazione del dispositivo funziona, tale livello di funzionalità esiste, in caso contrario, l'hardware non supporta tale livello di funzionalità. È possibile provare a ricreare un dispositivo a un livello di funzionalità inferiore, oppure scegliere di uscire dall'applicazione. Ad esempio, il livello di funzionalità 12_0 richiede Direct3D 11.3 o Direct3D 12 e il modello di shader 5.1. Per maggiori informazioni, vedere Livelli di funzionalità Direct3D: panoramica per ogni livello di funzionalità.

Utilizzando i livelli di funzionalità, è possibile sviluppare un'applicazione per Direct3D 9, Microsoft Direct3D 10 o Direct3D 11 e quindi eseguirla su hardware 9, 10 o 11 (con alcune eccezioni). Per maggiori informazioni, vedere Livelli di funzionalità Direct3D.

Rendering stereo

Il rendering stereo viene utilizzato per migliorare l'illusione della profondità. Utilizza due immagini, una dall'occhio sinistro e l'altra dall'occhio destro per visualizzare una scena sullo schermo di visualizzazione.

Matematicamente, applichiamo una matrice di proiezione stereo, che è un leggero offset orizzontale a destra e a sinistra, della normale matrice di proiezione mono per ottenere questo risultato.

Abbiamo eseguito due passaggi di rendering per ottenere il rendering stereo in questo gioco di esempio.

  • Associare al target di rendering destro, applicare la proiezione destra, quindi disegnare l'oggetto della primitiva.
  • Associare al target di rendering sinistro, applicare la proiezione sinistra, quindi disegnare l'oggetto della primitiva.

Telecamera e spazio delle coordinate

Il gioco ha il codice necessario per aggiornare il mondo nel proprio sistema di coordinate (talvolta chiamato spazio globale o spazio di scena). Tutti gli oggetti, inclusa la telecamera, sono posizionati e orientati in questo spazio. Per maggiori informazioni, vedere Sistemi di coordinate.

Uno shader di vertici esegue la conversione pesante dalle coordinate del modello alle coordinate del dispositivo con l'algoritmo seguente (dove V è un vettore e M è una matrice).

V(device) = V(model) x M(model-to-world) x M(world-to-view) x M(view-to-device)

  • M(model-to-world) è una matrice di trasformazione per le coordinate del modello alle coordinate globali, nota anche come Matrice di trasformazione globale. Questo viene fornito dalla primitiva.
  • M(world-to-view) è una matrice di trasformazione per le coordinate globali alle coordinate della visuale, nota anche come Matrice di trasformazione della visuale.
    • Questo viene fornito dalla matrice di visuale della telecamera. È definito dalla posizione della telecamera insieme ai vettori di visuale (il vettore look at che punta direttamente nella scena proveniente dalla telecamera e il vettore look up che è perpendicolare verso l'alto rispetto ad esso).
    • Nel gioco di esempio, m_viewMatrix è la matrice di trasformazione della visuale, e viene calcolata utilizzando Camera::SetViewParams.
  • M(view-to-device) è una matrice di trasformazione per le coordinate della visuale alle coordinate del dispositivo, nota anche come Matrice di trasformazione della proiezione.
    • Questo viene fornito dalla proiezione della telecamera. Fornisce informazioni sulla quantità di tale spazio effettivamente visibile nella scena finale. Il campo di visualizzazione (FoV, Field of View), le proporzioni e i piani di ritaglio definiscono la matrice di trasformazione della proiezione.
    • Nel gioco di esempio, m_projectionMatrix definisce la trasformazione alle coordinate di proiezione, calcolate utilizzando Camera::SetProjParams (per la proiezione stereo, si utilizzano due matrici di proiezione, una per ogni vista dell'occhio).

Il codice shader in VertexShader.hlsl viene caricato con questi vettori e matrici dai buffer costanti ed esegue questa trasformazione per ogni vertice.

Trasformazione delle coordinate

Direct3D utilizza tre trasformazioni per modificare le coordinate del modello 3D in coordinate pixel (spazio dello schermo). Queste trasformazioni sono trasformazione globale, trasformazione della visuale e trasformazione della proiezione. Per maggiori informazioni, vedere Panoramica delle trasformazioni.

Matrice di trasformazione globale

Una trasformazione globale modifica le coordinate dallo spazio del modello, in cui i vertici vengono definiti in relazione all'origine locale di un modello, allo spazio globale, in cui i vertici vengono definiti rispetto a un'origine comune a tutti gli oggetti di una scena. In sostanza, la trasformazione globale posiziona un modello nel mondo; da qui il suo nome. Per maggiori informazioni, vedere Trasformazione globale.

Matrice di trasformazione della visuale

Una trasformazione della visuale individua il visualizzatore nello spazio globale, trasformando i vertici nello spazio della telecamera. Nello spazio della telecamera, la telecamera o il visualizzatore, si trova all'origine, guardando nella direzione z positiva. Per maggiori informazioni, andare a Trasformazione della visuale.

Matrice di trasformazione della proiezione

La trasformazione di proiezione converte il frustum di visione in una forma cuboide. Un frustum di visione è un volume 3D in una scena posizionata rispetto alla telecamera della viewport. Una viewport è un rettangolo bidimensionale (2D) in cui viene proiettata una scena 3D. Per maggiori informazioni, vedere Viewports e ritaglio

Poiché l'estremità vicina del frustum di visione è più piccola rispetto all'estremità lontana, questo ha l'effetto di espandere gli oggetti che sono vicini alla telecamera; in pratica è il modo in cui la prospettiva viene applicata alla scena. Quindi gli oggetti più vicini al giocatore appaiono più grandi; gli oggetti che si trovano più lontano appaiono più piccoli.

Matematicamente, la trasformazione di proiezione è una matrice che in genere è sia una scala che una proiezione prospettica. Funziona come l'obiettivo di una telecamera. Per maggiori informazioni, vedere Trasformazione della proiezione.

Stato del campionatore

Lo stato del campionatore determina il modo in cui i dati delle trame vengono campionati usando modalità di indirizzamento delle trame, filtri e livello di dettaglio. Il campionamento viene eseguito ogni volta che un pixel di trama (o texel) viene letto da una trama.

Una trama contiene una matrice di texel. La posizione di ogni texel è indicata da (u,v), dove u è la larghezza e v è l'altezza, e viene mappata tra 0 e 1 in base alla larghezza e all'altezza della trama. Le coordinate delle trame risultanti vengono utilizzate per risolvere un texel durante il campionamento di una trama.

Quando le coordinate della trama sono inferiori a 0 o superiori a 1, la modalità di indirizzo della trama definisce il modo in cui la coordinata della trama indirizza la posizione di un texel. Ad esempio, quando si utilizza TextureAddressMode.Clamp, qualsiasi coordinata esterna all'intervallo 0-1 viene bloccata su un valore massimo di 1 e un valore minimo di 0 prima del campionamento.

Se risulta essere troppo grande o troppo piccola per il poligono, la trama viene filtrata per adattarsi allo spazio. Un filtro di ingrandimento ingrandisce una trama, un filtro di minimizzazione riduce la trama per adattarla in un'area più piccola. L'ingrandimento delle trame ripete il texel di esempio per uno o più indirizzi che restituisce un'immagine più sfocata. La minimizzazione delle trame è più complessa perché richiede la combinazione di più valori di texel in un singolo valore. Ciò può causare aliasing o bordi frastagliati in base ai dati della trama. L'approccio più diffuso per la minimizzazione consiste nell'utilizzare una mipmap. Una mipmap è una trama a più livelli. Le dimensioni di ogni livello sono una potenza di 2 dimensioni inferiori rispetto al livello precedente fino a una trama 1x1. Quando viene utilizzata la minimizzazione, un gioco sceglie il livello mipmap più vicino alle dimensioni necessarie in fase di rendering.

La classe BasicLoader

BasicLoader è una classe di caricamento semplice che fornisce il supporto per il caricamento di shader, trame e mesh da file su disco. Fornisce sia metodi sincroni che asincroni. In questo gioco di esempio, i file BasicLoader.h/.cpp si trovano nella cartella Utilità.

Per maggiori informazioni, vedere Basic Loader.