Share via


Comprendre le pipeline de rendu Direct3D 11

Précédemment, vous avez vu comment créer une fenêtre que vous pouvez utiliser pour dessiner dans Travailler avec des ressources d’appareil DirectX. Maintenant, vous allez apprendre à créer le pipeline graphique et à l’emplacement où vous pouvez y être connecté.

Rappelez-vous qu’il existe deux interfaces Direct3D qui définissent le pipeline graphique : ID3D11Device, qui fournit une représentation virtuelle du GPU et de ses ressources ; et ID3D11DeviceContext, qui représente le traitement graphique pour le pipeline. En règle générale, vous utilisez une instance d’ID3D11Device pour configurer et obtenir les ressources GPU dont vous avez besoin pour commencer à traiter les graphiques dans une scène, et vous utilisez ID3D11DeviceContext pour traiter ces ressources à chaque étape de nuanceur appropriée dans le pipeline graphique. Vous appelez généralement les méthodes ID3D11Device rarement, c’est-à-dire uniquement lorsque vous configurez une scène ou lorsque l’appareil change. D’autre part, vous allez appeler ID3D11DeviceContext chaque fois que vous traitez une image pour l’affichage.

Cet exemple crée et configure un pipeline graphique minimal adapté à l’affichage d’un simple cube en nuance de vertex. Il illustre approximativement le plus petit ensemble de ressources nécessaire à l’affichage. Lorsque vous lisez les informations ici, notez les limitations de l’exemple donné où vous devrez peut-être l’étendre pour prendre en charge la scène que vous souhaitez afficher.

Cet exemple couvre deux classes C++ pour les graphiques : une classe Device Resource Manager et une classe de renderer de scène 3D. Cette rubrique se concentre spécifiquement sur le convertisseur de scène 3D.

Que fait le convertisseur de cube ?

Le pipeline graphique est défini par la classe renderer de scène 3D. Le convertisseur de scène est en mesure de :

  • Définissez des mémoires tampons constantes pour stocker vos données uniformes.
  • Définissez des tampons de vertex pour contenir vos données de vertex d’objet et des tampons d’index correspondants pour permettre au nuanceur de vertex de parcourir correctement les triangles.
  • Créez des ressources de texture et des vues de ressources.
  • Chargez vos objets de nuanceur.
  • Mettez à jour les données graphiques pour afficher chaque image.
  • Restituer (dessiner) les graphiques dans la chaîne d’échange.

Les quatre premiers processus utilisent généralement les méthodes d’interface ID3D11Device pour initialiser et gérer les ressources graphiques, et les deux derniers utilisent les méthodes d’interface ID3D11DeviceContext pour gérer et exécuter le pipeline graphique.

Une instance de la classe Renderer est créée et gérée en tant que variable membre sur la classe de projet main. Le instance DeviceResources est géré en tant que pointeur partagé entre plusieurs classes, notamment la classe de projet main, la classe de fournisseur d’affichage d’application et renderer. Si vous remplacez Renderer par votre propre classe, envisagez également de déclarer et d’affecter les instance DeviceResources en tant que membre de pointeur partagé :

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

Passez simplement le pointeur dans le constructeur de classe (ou une autre méthode d’initialisation) une fois que le instance DeviceResources a été créé dans la méthode Initialize de la classe App. Vous pouvez également passer une référence weak_ptr si, au lieu de cela, vous souhaitez que votre classe main soit entièrement propriétaire des instance DeviceResources.

Créer le convertisseur de cube

Dans cet exemple, nous organisons la classe de renderer de scène avec les méthodes suivantes :

  • CreateDeviceDependentResources : appelé chaque fois que la scène doit être initialisée ou redémarrée. Cette méthode charge vos données de vertex, textures, nuanceurs et autres ressources initiales, et construit les mémoires tampons de constante et de vertex initiales. En règle générale, la plupart du travail ici est effectué avec des méthodes ID3D11Device , et non avec les méthodes ID3D11DeviceContext .
  • CreateWindowSizeDependentResources : appelé chaque fois que l’état de la fenêtre change, par exemple lorsque le redimensionnement se produit ou lorsque l’orientation change. Cette méthode reconstruit les matrices de transformation, telles que celles de votre appareil photo.
  • Mise à jour : généralement appelée à partir de la partie du programme qui gère l’état de jeu immédiat ; dans cet exemple, nous l’appelons simplement à partir de la classe Main . Faites lire à cette méthode toutes les informations d’état du jeu qui affectent le rendu, telles que les mises à jour de la position des objets ou des images d’animation, ainsi que toutes les données de jeu globales telles que les niveaux de lumière ou les modifications apportées à la physique du jeu. Ces entrées sont utilisées pour mettre à jour les mémoires tampons constantes et les données d’objet par image.
  • Render : généralement appelé à partir de la partie du programme qui gère la boucle de jeu ; dans ce cas, il est appelé à partir de la classe Main . Cette méthode construit le pipeline graphique : elle lie les nuanceurs, lie les mémoires tampons et les ressources aux phases de nuanceur et appelle le dessin pour l’image actuelle.

Ces méthodes constituent le corps des comportements pour le rendu d’une scène avec Direct3D à l’aide de vos ressources. Si vous étendez cet exemple avec une nouvelle classe de rendu, déclarez-la sur la classe de projet main. Voici donc ce qui suit :

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

devient cela :

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Là encore, notez que cet exemple suppose que les méthodes ont les mêmes signatures dans votre implémentation. Si les signatures ont changé, passez en revue la boucle principale et apportez les modifications en conséquence.

Examinons plus en détail les méthodes de rendu des scènes.

Créer des ressources dépendantes de l’appareil

CreateDeviceDependentResources regroupe toutes les opérations d’initialisation de la scène et de ses ressources à l’aide d’appels ID3D11Device . Cette méthode suppose que l’appareil Direct3D vient d’être initialisé (ou a été recréé) pour une scène. Il recrée ou recharge toutes les ressources graphiques spécifiques à une scène, telles que les nuanceurs de vertex et de pixels, les tampons de vertex et d’index pour les objets, et toutes les autres ressources (par exemple, sous forme de textures et de vues correspondantes).

Voici un exemple de code pour 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();
}

Chaque fois que vous chargez des ressources à partir d’un disque, des ressources telles que des fichiers ou des textures d’objet de nuanceur compilé (CSO ou .cso), faites-le de manière asynchrone. Cela vous permet de maintenir d’autres tâches en cours en même temps (comme d’autres tâches d’installation), et comme la boucle main n’est pas bloquée, vous pouvez continuer à afficher quelque chose visuellement intéressant pour l’utilisateur (comme une animation de chargement pour votre jeu). Cet exemple utilise l’API Concurrency::Tasks disponible à partir de Windows 8 ; notez la syntaxe lambda utilisée pour encapsuler les tâches de chargement asynchrone. Ces lambdas représentent les fonctions appelées off-thread, de sorte qu’un pointeur vers l’objet de classe actuel (ceci) est capturé explicitement.

Voici un exemple de chargement du bytecode du nuanceur :

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

Voici un exemple de création de tampons de vertex et d’index :

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

Cet exemple ne charge pas de maillages ou de textures. Vous devez créer les méthodes de chargement des types de maillage et de texture spécifiques à votre jeu, et les appeler de manière asynchrone.

Renseignez également les valeurs initiales de vos mémoires tampons constantes par scène. Les exemples de mémoire tampon constante par scène incluent des lumières fixes ou d’autres éléments et données de scène statiques.

Implémenter la méthode CreateWindowSizeDependentResources

Les méthodes CreateWindowSizeDependentResources sont appelées chaque fois que la taille, l’orientation ou la résolution de la fenêtre change.

Les ressources de taille de fenêtre sont mises à jour comme suit : le processus de message statique obtient l’un des événements possibles indiquant un changement d’état de la fenêtre. Votre boucle main est ensuite informée de l’événement et appelle CreateWindowSizeDependentResources sur le instance de classe main, qui appelle ensuite l’implémentation CreateWindowSizeDependentResources sur la classe de renderer de scène.

Le travail principal de cette méthode consiste à s’assurer que les visuels ne deviennent pas confus ou non valides en raison d’une modification des propriétés de la fenêtre. Dans cet exemple, nous mettons à jour les matrices de projet avec un nouveau champ d’affichage (FOV) pour la fenêtre redimensionnée ou réorientée.

Nous avons déjà vu le code pour la création de ressources de fenêtre dans DeviceResources : il s’agissait de la chaîne d’échange (avec mémoire tampon arrière) et de la vue cible de rendu. Voici comment le convertisseur crée des transformations dépendantes des proportions :

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

Si votre scène a une disposition spécifique de composants qui dépend du rapport d’aspect, il s’agit de l’endroit où les réorganiser pour qu’ils correspondent à ce rapport d’aspect. Vous pouvez également modifier la configuration du comportement de post-traitement ici.

Implémenter la méthode Update

La méthode Update est appelée une fois par boucle de jeu : dans cet exemple, elle est appelée par la méthode du même nom de la classe main. Son objectif est simple : mettre à jour la géométrie de la scène et l’état du jeu en fonction du temps écoulé (ou des étapes de temps écoulés) depuis l’image précédente. Dans cet exemple, nous faisons simplement pivoter le cube une fois par image. Dans une scène de jeu réelle, cette méthode contient beaucoup plus de code pour la vérification de l’état du jeu, la mise à jour des mémoires tampons constantes par image (ou d’autres dynamiques), des mémoires tampons de géométrie et d’autres ressources en mémoire en conséquence. Étant donné que la communication entre l’UC et le GPU entraîne une surcharge, veillez à ne mettre à jour que les mémoires tampons qui ont réellement changé depuis la dernière trame. Vos mémoires tampons constantes peuvent être regroupées ou fractionnées, selon les besoins, pour rendre cela plus efficace.

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

Dans ce cas, la rotation met à jour la mémoire tampon constante avec une nouvelle matrice de transformation pour le cube. La matrice sera multipliée par sommet pendant la phase de nuanceur de vertex. Étant donné que cette méthode est appelée avec chaque image, il s’agit d’un bon emplacement pour agréger toutes les méthodes qui mettent à jour vos mémoires tampons de constante et de vertex dynamiques, ou pour effectuer d’autres opérations qui préparent les objets de la scène pour la transformation par le pipeline graphique.

Implémenter la méthode Render

Cette méthode est appelée une fois par boucle de jeu après l’appel de Update. Comme Update, la méthode Render est également appelée à partir de la classe main. Il s’agit de la méthode dans laquelle le pipeline graphique est construit et traité pour l’image à l’aide de méthodes sur le instance ID3D11DeviceContext. Cela aboutit à un appel final à ID3D11DeviceContext::D rawIndexed. Il est important de comprendre que cet appel (ou d’autres appels Draw* similaires définis sur ID3D11DeviceContext) exécute réellement le pipeline. Plus précisément, c’est lorsque Direct3D communique avec le GPU pour définir l’état de dessin, exécute chaque étape de pipeline et écrit les résultats des pixels dans la ressource de mémoire tampon cible de rendu pour l’affichage par la chaîne d’échange. Étant donné que la communication entre l’UC et le GPU entraîne une surcharge, combinez plusieurs appels de dessin en un seul si vous le pouvez, en particulier si votre scène comporte un grand nombre d’objets rendus.

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

Il est recommandé de définir les différentes étapes de pipeline graphique sur le contexte dans l’ordre. En règle générale, l’ordre est le suivant :

  • Actualisez les ressources de mémoire tampon constante avec de nouvelles données si nécessaire (à l’aide des données de Update).
  • Assembly d’entrée (IA) : c’est là que nous attachons les tampons de vertex et d’index qui définissent la géométrie de la scène. Vous devez attacher chaque vertex et mémoire tampon d’index pour chaque objet de la scène. Étant donné que cet exemple n’a que le cube, il est assez simple.
  • Nuanceur de vertex (VS) : attachez tous les nuanceurs de vertex qui transforment les données dans les mémoires tampons de vertex et attachez des tampons constants pour le nuanceur de vertex.
  • Nuanceur de pixels (PS) : joignez tous les nuanceurs de pixels qui effectueront des opérations par pixel dans la scène rastérisée, et attachez des ressources d’appareil pour le nuanceur de pixels (mémoires tampons constantes, textures, etc.).
  • Fusion de sortie (OM) : il s’agit de l’étape où les pixels sont fusionnés, une fois les nuanceurs terminés. Il s’agit d’une exception à la règle, car vous attachez vos gabarits de profondeur et vos cibles de rendu avant de définir l’une des autres étapes. Vous pouvez avoir plusieurs gabarits et cibles si vous avez des nuanceurs de vertex et de pixels supplémentaires qui génèrent des textures telles que des cartes d’ombres, des cartes de hauteur ou d’autres techniques d’échantillonnage . Dans ce cas, chaque passe de dessin nécessite la ou les cibles appropriées définies avant d’appeler une fonction de dessin.

Ensuite, dans la dernière section (Utiliser des nuanceurs et des ressources de nuanceur), nous allons examiner les nuanceurs et expliquer comment Direct3D les exécute.

À suivre

Utiliser des nuanceurs et des ressources de nuanceur