Freigeben über


Grundlegendes zur Direct3D 11-Renderingpipeline

Zuvor haben Sie sich angesehen, wie Sie ein Fenster erstellen, das Sie zum Zeichnen in Arbeiten mit DirectX-Geräteressourcen verwenden können. Nun erfahren Sie, wie Sie die Grafikpipeline erstellen und wo Sie sich in sie einfügen können.

Sie werden sich erinnern, dass es zwei Direct3D-Schnittstellen gibt, die die Grafikpipeline definieren: ID3D11Device, die eine virtuelle Darstellung der GPU und ihrer Ressourcen bereitstellt; und ID3D11DeviceContext, die die Grafikverarbeitung für die Pipeline darstellt. In der Regel verwenden Sie eine instance id3D11Device, um die GPU-Ressourcen zu konfigurieren und abzurufen, die Sie benötigen, um mit der Verarbeitung der Grafiken in einer Szene zu beginnen, und Sie verwenden ID3D11DeviceContext, um diese Ressourcen bei jeder geeigneten Shaderphase in der Grafikpipeline zu verarbeiten. Id3D11Gerätemethoden rufen Sie in der Regel selten auf, d. h. nur, wenn Sie eine Szene einrichten oder wenn sich das Gerät ändert. Andererseits rufen Sie id3D11DeviceContext jedes Mal auf, wenn Sie einen Frame für die Anzeige verarbeiten.

In diesem Beispiel wird eine minimale Grafikpipeline erstellt und konfiguriert, die zum Anzeigen eines einfachen drehenden, vertexschattierten Würfels geeignet ist. Es zeigt etwa den kleinsten Satz von Ressourcen, die für die Anzeige erforderlich sind. Wenn Sie die Informationen hier lesen, beachten Sie die Einschränkungen des angegebenen Beispiels, bei denen Sie es möglicherweise erweitern müssen, um die Szene zu unterstützen, die Sie rendern möchten.

In diesem Beispiel werden zwei C++-Klassen für Grafiken behandelt: eine Geräteressourcen-Manager-Klasse und eine 3D-Szenenrendererklasse. Dieses Thema konzentriert sich speziell auf den 3D-Szenenrenderer.

Was macht der Cuberenderer?

Die Grafikpipeline wird durch die 3D-Szenenrendererklasse definiert. Der Szenenrenderer kann Folgendes ausführen:

  • Definieren Sie Konstantenpuffer, um Ihre einheitlichen Daten zu speichern.
  • Definieren Sie Vertexpuffer, um Ihre Objektvertexdaten zu speichern, und entsprechende Indexpuffer, damit der Vertexshader die Dreiecke ordnungsgemäß durchlaufen kann.
  • Erstellen sie Texturressourcen und Ressourcenansichten.
  • Laden Sie Ihre Shaderobjekte.
  • Aktualisieren Sie die Grafikdaten, um jeden Frame anzuzeigen.
  • Rendern (Zeichnen) der Grafiken in der Swapchain.

Die ersten vier Prozesse verwenden in der Regel die ID3D11Device-Schnittstellenmethoden zum Initialisieren und Verwalten von Grafikressourcen, und die letzten beiden verwenden die ID3D11DeviceContext-Schnittstellenmethoden , um die Grafikpipeline zu verwalten und auszuführen.

Eine instance der Renderer-Klasse wird erstellt und als Membervariable für die Standard-Projektklasse verwaltet. Die DeviceResources-instance wird als freigegebener Zeiger über mehrere Klassen hinweg verwaltet, einschließlich der Standard Projektklasse, der App-Ansichtsanbieterklasse und des Renderers. Wenn Sie Renderer durch Ihre eigene Klasse ersetzen, sollten Sie auch die DeviceResources-instance als freigegebenen Zeigermember deklarieren und zuweisen:

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

Übergeben Sie den Zeiger einfach an den Klassenkonstruktor (oder eine andere Initialisierungsmethode), nachdem die DeviceResources-instance in der Initialize-Methode der App-Klasse erstellt wurde. Sie können auch einen weak_ptr Verweis übergeben, wenn Sie stattdessen möchten, dass Ihre Standard-Klasse die DeviceResources-instance vollständig besitzt.

Erstellen des Cuberenderers

In diesem Beispiel organisieren wir die Szenenrendererklasse mit den folgenden Methoden:

  • CreateDeviceDependentResources: Wird immer aufgerufen, wenn die Szene initialisiert oder neu gestartet werden muss. Diese Methode lädt Ihre anfänglichen Vertexdaten, Texturen, Shader und andere Ressourcen und erstellt die anfänglichen Konstanten- und Vertexpuffer. In der Regel erfolgt die meiste Arbeit hier mit ID3D11Device-Methoden , nicht mit ID3D11DeviceContext-Methoden .
  • CreateWindowSizeDependentResources: Wird immer aufgerufen, wenn sich der Fensterzustand ändert, z. B. wenn die Größe geändert wird oder wenn sich die Ausrichtung ändert. Mit dieser Methode werden Transformationsmatrizen neu erstellt, z. B. die für Ihre Kamera.
  • Update: Wird in der Regel aus dem Teil des Programms aufgerufen, der den sofortigen Spielzustand verwaltet; In diesem Beispiel rufen wir sie einfach aus der Main-Klasse auf. Lassen Sie diese Methode aus allen Spielzustandsinformationen lesen, die sich auf das Rendering auswirken, z. B. Aktualisierungen der Objektposition oder Animationsframes, sowie aus allen globalen Spieldaten wie Lichtstärken oder Änderungen an der Spielphysik. Diese Eingaben werden verwendet, um die Konstantenpuffer und Objektdaten pro Frame zu aktualisieren.
  • Rendern: Wird in der Regel aus dem Teil des Programms aufgerufen, der die Spielschleife verwaltet; In diesem Fall wird sie von der Main-Klasse aufgerufen. Diese Methode erstellt die Grafikpipeline: Sie bindet Shader, bindet Puffer und Ressourcen an Shaderphasen und ruft die Zeichnung für den aktuellen Frame auf.

Diese Methoden umfassen den Text der Verhaltensweisen zum Rendern einer Szene mit Direct3D unter Verwendung Ihrer Ressourcen. Wenn Sie dieses Beispiel mit einer neuen Renderingklasse erweitern, deklarieren Sie sie in der Standard Projektklasse. So geht folgendes:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

zu

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Beachten Sie erneut, dass in diesem Beispiel davon ausgegangen wird, dass die Methoden in Ihrer Implementierung über die gleichen Signaturen verfügen. Wenn sich die Signaturen geändert haben, überprüfen Sie die Main-Schleife , und nehmen Sie die Änderungen entsprechend vor.

Sehen wir uns die Methoden zum Rendern von Szenen genauer an.

Erstellen geräteabhängiger Ressourcen

CreateDeviceDependentResources konsolidiert alle Vorgänge zum Initialisieren der Szene und ihrer Ressourcen mithilfe von ID3D11Device-Aufrufen . Bei dieser Methode wird davon ausgegangen, dass das Direct3D-Gerät gerade für eine Szene initialisiert (oder neu erstellt wurde). Alle szenenspezifischen Grafikressourcen, z. B. die Vertex- und Pixelshader, die Vertex- und Indexpuffer für Objekte und alle anderen Ressourcen (z. B. Texturen und die zugehörigen Ansichten), werden neu erstellt oder neu geladen.

Beispielcode für 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();
}

Jedes Mal, wenn Sie Ressourcen vom Datenträger laden , z. B. Kompilierte Shaderobjektdateien (CSO oder .cso)-Dateien oder Texturen, tun dies asynchron. Dadurch können Sie andere Aufgaben gleichzeitig am Laufen halten (wie andere Setupaufgaben), und da die Standard-Schleife nicht blockiert ist, können Sie weiterhin etwas visuell Interessantes für den Benutzer anzeigen (z. B. eine Ladeanimation für Ihr Spiel). In diesem Beispiel wird die Concurrency::Tasks-API verwendet, die ab Windows 8 verfügbar ist. Beachten Sie die Lambdasyntax, die zum Kapseln von asynchronen Ladetasks verwendet wird. Diese Lambdas stellen die Funktionen dar, die als off-thread bezeichnet werden, sodass ein Zeiger auf das aktuelle Klassenobjekt (dieses) explizit erfasst wird.

Hier sehen Sie ein Beispiel für das Laden des Shader-Bytecodes:

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

Hier sehen Sie ein Beispiel für das Erstellen von Vertex- und Indexpuffern:

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 diesem Beispiel werden keine Gitter oder Texturen geladen. Sie müssen die Methoden zum Laden der Gitter- und Texturtypen erstellen, die für Ihr Spiel spezifisch sind, und sie asynchron aufrufen.

Füllen Sie auch hier Die Anfangswerte für Ihre Konstantenpuffer pro Szene auf. Beispiele für einen konstanten Puffer pro Szene sind feste Lichter oder andere statische Szenenelemente und -daten.

Implementieren der CreateWindowSizeDependentResources-Methode

CreateWindowSizeDependentResources-Methoden werden bei jeder Änderung der Fenstergröße, -ausrichtung oder -auflösung aufgerufen.

Fenstergrößenressourcen werden wie folgt aktualisiert: Die statische Meldung proc erhält eines von mehreren möglichen Ereignissen, die auf eine Änderung des Fensterzustands hinweisen. Ihre Standard-Schleife wird dann über das Ereignis informiert und ruft CreateWindowSizeDependentResources für die Standard-Klasse instance auf, die dann die CreateWindowSizeDependentResources-Implementierung für die Szenenrendererklasse aufruft.

Die primäre Aufgabe dieser Methode besteht darin, sicherzustellen, dass die Visuals aufgrund einer Änderung der Fenstereigenschaften nicht verwechselt oder ungültig werden. In diesem Beispiel aktualisieren wir die Projektmatrizen mit einem neuen Sichtfeld (FOV) für das geänderte oder neu ausgerichtete Fenster.

Wir haben bereits den Code zum Erstellen von Fensterressourcen in DeviceResources gesehen. Dabei handelt es sich um die Swapchain (mit Hintergrundpuffer) und die Renderzielansicht. Hier erfahren Sie, wie der Renderer seitenverhältnisabhängige Transformationen erstellt:

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

Wenn Ihre Szene ein bestimmtes Layout von Komponenten aufweist, das vom Seitenverhältnis abhängt, ist dies der Ort, an dem sie an dieses Seitenverhältnis anordnen können. Möglicherweise möchten Sie auch hier die Konfiguration des Nachverarbeitungsverhaltens ändern.

Implementieren der Update-Methode

Die Update-Methode wird einmal pro Spielschleife aufgerufen. In diesem Beispiel wird sie von der gleichnamigen Methode der Standard Klasse aufgerufen. Es hat einen einfachen Zweck: Die Szenengeometrie und den Spielzustand basierend auf der verstrichenen Zeit (oder den verstrichenen Zeitschritten) seit dem vorherigen Frame zu aktualisieren. In diesem Beispiel drehen wir den Cube einfach einmal pro Frame. In einer realen Spielszene enthält diese Methode viel mehr Code zum Überprüfen des Spielzustands, zum Aktualisieren von Konstantenpuffern pro Frame (oder anderen dynamischen) Konstantenpuffern, Geometriepuffern und anderen In-Memory-Ressourcen entsprechend. Da bei der Kommunikation zwischen CPU und GPU Mehraufwand entsteht, sollten Sie sicherstellen, dass Sie nur Puffer aktualisieren, die sich tatsächlich seit dem letzten Frame geändert haben. Ihre Konstantenpuffer können bei Bedarf gruppiert oder aufgeteilt werden, um dies effizienter zu gestalten.

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 diesem Fall aktualisiert Rotieren den Konstantenpuffer mit einer neuen Transformationsmatrix für den Cube. Die Matrix wird während der Vertex-Shaderphase pro Vertex multipliziert. Da diese Methode mit jedem Frame aufgerufen wird, ist dies ein guter Ort, um alle Methoden zu aggregieren, die Ihre dynamischen Konstanten- und Vertexpuffer aktualisieren, oder um andere Vorgänge auszuführen, die die Objekte in der Szene für die Transformation durch die Grafikpipeline vorbereiten.

Implementieren der Render-Methode

Diese Methode wird einmal pro Spielschleife aufgerufen, nachdem Update aufgerufen wurde. Wie Update wird auch die Render-Methode aus der Standard-Klasse aufgerufen. Dies ist die Methode, bei der die Grafikpipeline für den Frame mithilfe von Methoden im ID3D11DeviceContext-instance erstellt und verarbeitet wird. Dies führt zu einem endgültigen Aufruf von ID3D11DeviceContext::D rawIndexed. Es ist wichtig zu verstehen, dass dieser Aufruf (oder andere ähnliche Draw* -Aufrufe, die für ID3D11DeviceContext definiert sind) die Pipeline tatsächlich ausführt. Dies ist insbesondere der Fall, wenn Direct3D mit der GPU kommuniziert, um den Zeichnungszustand festzulegen, jede Pipelinephase ausführt und die Pixelergebnisse zur Anzeige durch die Swapchain in die Renderzielpufferressource schreibt. Da bei der Kommunikation zwischen CPU und GPU ein Mehraufwand entsteht, kombinieren Sie nach Möglichkeit mehrere Draw-Aufrufe in einem einzelnen, insbesondere wenn Ihre Szene viele gerenderte Objekte enthält.

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

Es empfiehlt sich, die verschiedenen Grafikpipelinephasen für den Kontext in der richtigen Reihenfolge festzulegen. In der Regel lautet die Reihenfolge:

  • Aktualisieren Sie konstanten Pufferressourcen nach Bedarf mit neuen Daten (mithilfe von Daten aus Update).
  • Eingabeassembly (IA): Hier werden die Vertex- und Indexpuffer angefügt, die die Szenengeometrie definieren. Sie müssen jeden Scheitelpunkt und Indexpuffer für jedes Objekt in der Szene anfügen. Da dieses Beispiel nur den Cube enthält, ist es ziemlich einfach.
  • Vertexshader (VS): Fügen Sie alle Vertex-Shader an, die die Daten in den Vertexpuffern transformieren und Konstantenpuffer für den Vertexshader anfügen.
  • Pixelshader (PS): Fügen Sie alle Pixel-Shader an, die pixelspezifische Vorgänge in der rasterten Szene ausführen, und fügen Sie Geräteressourcen für den Pixelshader an (Konstantenpuffer, Texturen usw.).
  • Output Merger (OM): Dies ist die Phase, in der Pixel gemischt werden, nachdem die Shader abgeschlossen sind. Dies ist eine Ausnahme von der Regel, da Sie Ihre Tiefenschablonen und Renderziele anfügen, bevor Sie eine der anderen Phasen festlegen. Möglicherweise verfügen Sie über mehrere Schablonen und Ziele, wenn Sie über zusätzliche Vertex- und Pixelshader verfügen, die Texturen wie Schattenkarten, Höhenzuordnungen oder andere Samplingverfahren generieren. In diesem Fall muss jeder Zeichnungsdurchlauf die entsprechenden Ziele festlegen, bevor Sie eine Zeichnungsfunktion aufrufen.

Als Nächstes sehen wir uns im letzten Abschnitt (Arbeiten mit Shadern und Shaderressourcen) die Shader an und erläutern, wie Direct3D sie ausführt.

Als Nächstes

Arbeiten mit Shadern und Shaderressourcen