Freigeben über


Hinzufügen von visuellem Inhalt zum Marble Maze-Beispielbildpunkt

In diesem Dokument wird beschrieben, wie das Marble Maze-Spiel Direct3D und Direct2D in der App-Umgebung für die universelle Windows-Plattform (UWP) verwendet, damit Sie die Muster erlernen und anpassen können, wenn Sie mit Ihren eigenen Spielinhalten arbeiten. Informationen dazu, wie visuelle Spielkomponenten in die gesamte Anwendungsstruktur von Marble Maze passen, finden Sie unter Marble Maze-Anwendungsstruktur.

Wir haben diese grundlegenden Schritte befolgt, während wir die visuellen Aspekte von Marble Maze entwickelt haben:

  1. Erstellen Sie ein grundlegendes Framework, das die Direct3D- und Direct2D-Umgebungen initialisiert.
  2. Verwenden Sie Bild- und Modellbearbeitungsprogramme, um die 2D- und 3D-Ressourcen zu entwerfen, die im Spiel angezeigt werden.
  3. Stellen Sie sicher, dass 2D- und 3D-Ressourcen ordnungsgemäß geladen und im Spiel angezeigt werden.
  4. Integrieren Sie Vertex- und Pixelshader, welche die visuelle Qualität der Spielressourcen verbessern.
  5. Integrieren Sie Spiellogik, z. B. Animationen und Benutzereingaben.

Wir haben uns auch zuerst auf das Hinzufügen von 3D-Ressourcen und dann auf 2D-Ressourcen konzentriert. Wir haben uns beispielsweise auf die Kernspiellogik konzentriert, bevor wir das Menüsystem und den Timer hinzugefügt haben.

Außerdem mussten wir einige dieser Schritte während des Entwicklungsprozesses mehrmals durchlaufen. Wenn wir beispielsweise Änderungen an den Gitter- und Murmelmodellen vorgenommen haben, mussten wir auch einen Teil des Shadercodes ändern, der diese Modelle unterstützt.

Hinweis

Den diesem Dokument entsprechenden Beispielcode finden Sie im DirectX-Beispiel anhand des Spiels Marble Maze.

  Hier sind einige der wichtigsten Punkte, die in diesem Dokument erläutert werden, wenn Sie mit DirectX- und visuellen Spielinhalten arbeiten, nämlich wenn Sie die DirectX-Grafikbibliotheken initialisieren, Szenenressourcen laden und die Szene aktualisieren und rendern:

  • Das Hinzufügen von Spielinhalten umfasst in der Regel viele Schritte. Diese Schritte erfordern häufig auch Iterationen. Spieleentwickler konzentrieren sich häufig zuerst auf das Hinzufügen von 3D-Spielinhalten und dann auf das Hinzufügen von 2D-Inhalten.
  • Erreichen Sie mehr Kunden, und bieten Sie ihnen ein großartiges Erlebnis, indem Sie die größtmögliche Grafikhardware unterstützen.
  • Trennen Sie Entwurfszeit- und Laufzeitformate sauber. Strukturieren Sie Ihre Entwurfszeitressourcen, um die Flexibilität zu maximieren und schnelle Iterationen für Inhalte zu ermöglichen. Formatieren und komprimieren Sie Ihre Ressourcen so effizient wie möglich zur Laufzeit, um sie zu laden und zu rendern.
  • Sie erstellen die Direct3D- und Direct2D-Geräte in einer UWP-App ähnlich wie in einer klassischen Windows-Desktop-App. Ein wichtiger Unterschied besteht darin, wie die Swapchain dem Ausgabefenster zugeordnet ist.
  • Stellen Sie beim Entwerfen ihres Spiels sicher, dass das von Ihnen ausgewählte Gitterformat Ihre wichtigsten Szenarien unterstützt. Wenn ihr Spiel beispielsweise Kollisionen erfordert, stellen Sie sicher, dass Sie Kollisionsdaten aus Ihren Gittern abrufen können.
  • Trennen Sie die Spiellogik von der Rendering-Logik, indem Sie zuerst alle Szenenobjekte aktualisieren, bevor Sie diese rendern.
  • Normalerweise zeichnen Sie Ihre 3D-Szenenobjekte und dann alle 2D-Objekte, die vor der Szene angezeigt werden.
  • Synchronisieren Sie die Zeichnung mit dem vertikalen Leeren, um sicherzustellen, dass ihr Spiel keine Zeit mit dem Zeichnen von Frames verbringt, die nie tatsächlich auf dem Bildschirm angezeigt werden. Eine vertikale Leerstelle ist die Zeit zwischen der Fertigstellung der Zeichnung eines Frames auf dem Monitor und dem Start für den nächsten Frame.

Erste Schritte mit DirectX-Grafiken

Bei der Planung des Spiels für die universelle Windows-Plattform (UWP) von Marble Maze haben wir C++ und Direct3D 11.1 ausgewählt, da sie hervorragende Auswahlmöglichkeiten für die Erstellung von 3D-Spielen sind, die eine maximale Kontrolle über Rendering und hohe Leistung erfordern. DirectX 11.1 unterstützt Hardware von DirectX 9 bis DirectX 11 und kann Ihnen daher helfen, mehr Kunden effizienter zu erreichen, da Sie Code für jede der früheren DirectX-Versionen nicht neu schreiben müssen.

Marble Maze verwendet Direct3D 11.1 zum Rendern der 3D-Spielressourcen, nämlich der Murmel und des Labyrinths. Marble Maze verwendet auch Direct2D, DirectWrite und Windows Imaging Component (WIC), um die 2D-Spielressourcen wie die Menüs und den Timer zu zeichnen.

Für die Spieleentwicklung ist eine Planung erforderlich. Wenn Sie mit DirectX-Grafiken noch nicht vertraut sind, empfehlen wir Ihnen, DirectX zu lesen: Machen Sie sich mit den grundlegenden Konzepten der Erstellung eines UWP-DirectX-Spiels vertraut. Beim Durchlesen dieses Dokuments und Durchgehen des Marble Maze-Quellcodes können Sie sich auf die folgenden Ressourcen beziehen, um detailliertere Informationen zu DirectX-Grafiken zu erhalten:

  • Direct3D 11-Grafiken: Beschreibt Direct3D 11, eine leistungsfähige, hardwarebeschleunigte 3D-Grafik-API zum Rendern von 3D-Geometrien auf der Windows-Plattform.
  • Direct2D: Beschreibt Direct2D, eine hardwarebeschleunigte 2D-Grafik-API, die hohe Leistung und hochwertiges Rendering für 2D-Geometrie, Bitmaps und Text bietet.
  • DirectWrite: Beschreibt DirectWrite, womit das Rendern von Text in hoher Qualität unterstützt wird.
  • Windows-Bilderstellungskomponente: Beschreibt WIC, eine erweiterbare Plattform, die eine Low-Level-API für digitale Bilder bietet.

Feature-Ebenen

In Direct3D 11 wird ein Paradigma mit der Bezeichnung Feature-Ebenen eingeführt. Eine Feature-Ebene ist ein gut definierter Satz von GPU-Funktionen. Verwenden Sie Feature-Ebenen, um ihr Spiel für die Ausführung auf früheren Versionen von Direct3D-Hardware zu verwenden. Marble Maze unterstützt Feature-Ebene 9.1, da keine erweiterten Features von den höheren Ebenen benötigt werden. Es wird empfohlen, die größtmögliche Bandbreite an Hardware zu unterstützen und Ihre Spielinhalte so zu skalieren, dass Ihre Kunden, die über High- oder Low-End-Computer verfügen, eine großartige Erfahrung haben. Weitere Informationen zu Feature-Ebenen finden Sie unter Direct3D 11 auf Downlevel-Hardware.

Initialisieren von Direct3D und Direct2D

Ein Gerät stellt die Grafikkarte dar. Sie erstellen die Direct3D- und Direct2D-Geräte in einer UWP-App ähnlich wie in einer klassischen Windows-Desktop-App. Der Hauptunterschied besteht darin, wie Sie die Direct3D-Swapchain mit dem Fenstersystem verbinden.

Die DeviceResources-Klasse ist eine Grundlage für die Verwaltung von Direct3D und Direct2D. Diese Klasse behandelt allgemeine Infrastruktur, nicht spielspezifische Objekte. Marble Maze definiert die MarbleMazeMain-Klasse zum Behandeln von spielspezifischen Objekten, die über einen Verweis auf ein DeviceResources-Objekt verfügen, um ihm Zugriff auf Direct3D und Direct2D zu gewähren.

Während der Initialisierung erstellt der DeviceResources-Konstruktor geräteunabhängige Ressourcen und die Direct3D- und Direct2D-Geräte.

// Initialize the Direct3D resources required to run. 
DX::DeviceResources::DeviceResources() :
    m_screenViewport(),
    m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1),
    m_d3dRenderTargetSize(),
    m_outputSize(),
    m_logicalSize(),
    m_nativeOrientation(DisplayOrientations::None),
    m_currentOrientation(DisplayOrientations::None),
    m_dpi(-1.0f),
    m_deviceNotify(nullptr)
{
    CreateDeviceIndependentResources();
    CreateDeviceResources();
}

Die DeviceResources-Klasse trennt diese Funktionalität, sodass sie leichter reagieren kann, wenn sich die Umgebung ändert. Beispielsweise wird die CreateWindowSizeDependentResources-Methode aufgerufen, wenn sich die Fenstergröße ändert.

Initialisieren der Direct2D-, DirectWrite- und WIC-Tools

Die DeviceResources::CreateDeviceIndependentResources-Methode erstellt die Tools für Direct2D, DirectWrite und WIC. In DirectX-Grafiken sind Tools die Ausgangspunkte zum Erstellen von Grafikressourcen. Marble Maze gibt D2D1\_FACTORY\_TYPE\_SINGLE\_THREADED an, weil dies der Ausführung aller Zeichnungen im Hauptthread dient.

// These are the resources required independent of hardware. 
void DX::DeviceResources::CreateDeviceIndependentResources()
{
    // Initialize Direct2D resources.
    D2D1_FACTORY_OPTIONS options;
    ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));

#if defined(_DEBUG)
    // If the project is in a debug build, enable Direct2D debugging via SDK Layers.
    options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

    // Initialize the Direct2D Factory.
    DX::ThrowIfFailed(
        D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED,
            __uuidof(ID2D1Factory2),
            &options,
            &m_d2dFactory
            )
        );

    // Initialize the DirectWrite Factory.
    DX::ThrowIfFailed(
        DWriteCreateFactory(
            DWRITE_FACTORY_TYPE_SHARED,
            __uuidof(IDWriteFactory2),
            &m_dwriteFactory
            )
        );

    // Initialize the Windows Imaging Component (WIC) Factory.
    DX::ThrowIfFailed(
        CoCreateInstance(
            CLSID_WICImagingFactory2,
            nullptr,
            CLSCTX_INPROC_SERVER,
            IID_PPV_ARGS(&m_wicFactory)
            )
        );
}

Erstellen der Direct3D- und Direct2D-Geräte

Die DeviceResources::CreateDeviceResources-Methode ruft D3D11CreateDevice auf, um das Geräteobjekt zu erstellen, das die Direct3D-Grafikkarte darstellt. Da Marble Maze die Feature-Ebene 9.1 und höher unterstützt, gibt die DeviceResources::CreateDeviceResources-Methode die Ebenen 9.1 bis 11.1 im FeatureLevels-Array an. Direct3D führt die Liste in der Reihenfolge und gibt der App die erste verfügbare Feature-Ebene. Daher werden die D3D_FEATURE_LEVEL-Arrayeinträge von der höchsten zum niedrigsten aufgelistet, sodass die App die höchste verfügbare Feature-Ebene erhält. Die DeviceResources::CreateDeviceResources-Methode ruft das Direct3D 11.1-Gerät ab, indem das von D3D11CreateDevice zurückgegebene Direct3D 11-Gerät abgefragt wird.

// This flag adds support for surfaces with a different color channel ordering
// than the API default. It is required for compatibility with Direct2D.
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

#if defined(_DEBUG)
    if (DX::SdkLayersAvailable())
    {
        // If the project is in a debug build, enable debugging via SDK Layers 
        // with this flag.
        creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
    }
#endif

// This array defines the set of DirectX hardware feature levels this app will support.
// Note the ordering should be preserved.
// Don't forget to declare your application's minimum required feature level in its
// description.  All applications are assumed to support 9.1 unless otherwise stated.
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1
};

// Create the Direct3D 11 API device object and a corresponding context.
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;

HRESULT hr = D3D11CreateDevice(
    nullptr,                    // Specify nullptr to use the default adapter.
    D3D_DRIVER_TYPE_HARDWARE,   // Create a device using the hardware graphics driver.
    0,                          // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
    creationFlags,              // Set debug and Direct2D compatibility flags.
    featureLevels,              // List of feature levels this app can support.
    ARRAYSIZE(featureLevels),   // Size of the list above.
    D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for UWP apps.
    &device,                    // Returns the Direct3D device created.
    &m_d3dFeatureLevel,         // Returns feature level of device created.
    &context                    // Returns the device immediate context.
    );

if (FAILED(hr))
{
    // If the initialization fails, fall back to the WARP device.
    // For more information on WARP, see:
    // https://go.microsoft.com/fwlink/?LinkId=286690
    DX::ThrowIfFailed(
        D3D11CreateDevice(
            nullptr,
            D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
            0,
            creationFlags,
            featureLevels,
            ARRAYSIZE(featureLevels),
            D3D11_SDK_VERSION,
            &device,
            &m_d3dFeatureLevel,
            &context
            )
        );
}

// Store pointers to the Direct3D 11.1 API device and immediate context.
DX::ThrowIfFailed(
    device.As(&m_d3dDevice)
    );

DX::ThrowIfFailed(
    context.As(&m_d3dContext)
    );

Die DeviceResources::CreateDeviceResources-Methode erstellt dann das Direct2D-Gerät. Direct2D verwendet Microsoft DirectX Graphics Infrastructure (DXGI), um mit Direct3D zu arbeiten. DXGI ermöglicht die gemeinsame Nutzung von Videospeicheroberflächen zwischen Grafiklaufzeiten. Marble Maze verwendet das zugrunde liegende DXGI-Gerät vom Direct3D-Gerät, um das Direct2D-Gerät aus der Direct2D-Factory zu erstellen.

// Create the Direct2D device object and a corresponding context.
ComPtr<IDXGIDevice3> dxgiDevice;
DX::ThrowIfFailed(
    m_d3dDevice.As(&dxgiDevice)
    );

DX::ThrowIfFailed(
    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
    );

DX::ThrowIfFailed(
    m_d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        &m_d2dContext
        )
    );

Weitere Informationen zu DXGI und Interoperabilität zwischen Direct2D und Direct3D finden Sie unter DXGI-Übersicht und Direct2D and Direct3D Interoperability-Übersicht.

Zuordnen von Direct3D zur Ansicht

Die DeviceResources::CreateWindowSizeDependentResources-Methode erstellt die Grafikressourcen, die von einer bestimmten Fenstergröße wie der Swapchain und den Direct3D- und Direct2D-Renderzielen abhängen. Eine wichtige Möglichkeit, dass sich eine DirectX-UWP-App von einer Desktop-App unterscheidet, ist, wie die Swapchain dem Ausgabefenster zugeordnet ist. Eine Swapchain ist für die Anzeige des Puffers verantwortlich, auf dem das Gerät auf dem Monitor gerendert wird. Die Anwendungsstruktur von Marble Maze beschreibt, wie sich das Fenstersystem für eine UWP-App von einer Desktop-App unterscheidet. Da eine UWP-App nicht mit HWND-Objekten funktioniert, muss Marble Maze die IDXGIFactory2::CreateSwapChainForCoreWindow-Methode verwenden, um die Geräteausgabe der Ansicht zuzuordnen. Das folgende Beispiel zeigt den Teil der DeviceResources::CreateWindowSizeDependentResources-Methode, welche die Swapchain erstellt.

// Obtain the final swap chain for this window from the DXGI factory.
DX::ThrowIfFailed(
    dxgiFactory->CreateSwapChainForCoreWindow(
        m_d3dDevice.Get(),
        reinterpret_cast<IUnknown*>(m_window.Get()),
        &swapChainDesc,
        nullptr,
        &m_swapChain
        )
    );

Um den Stromverbrauch zu minimieren, was für akkubetriebene Geräte wie Laptops und Tablets wichtig ist, ruft die DeviceResources::CreateWindowSizeDependentResources-Methode die IDXGIDevice1::SetMaximumFrameLatency-Methode auf, um sicherzustellen, dass das Spiel erst nach dem vertikalen Leerzeichen gerendert wird. Die Synchronisierung mit der vertikalen Austastung wird im Abschnitt Darstellen der Szene dieses Dokuments detaillierter beschrieben.

// Ensure that DXGI does not queue more than one frame at a time. This both 
// reduces latency and ensures that the application will only render after each
// VSync, minimizing power consumption.
DX::ThrowIfFailed(
    dxgiDevice->SetMaximumFrameLatency(1)
    );

Die DeviceResources::CreateWindowSizeDependentResources-Methode initialisiert Grafikressourcen auf eine Weise, die für die meisten Spiele funktioniert.

Hinweis

Die Begriffsansicht hat in der Windows-Runtime eine andere Bedeutung als in Direct3D. In der Windows-Runtime bezieht sich eine Ansicht auf die Sammlung der Benutzeroberflächeneinstellungen für eine App, einschließlich des Anzeigebereichs und des Eingabeverhaltens sowie des Threads, der für die Verarbeitung verwendet wird. Sie geben die Konfiguration und Einstellungen an, die Sie beim Erstellen einer Ansicht benötigen. Der Vorgang zum Einrichten der App-Ansicht wird in der Marble Maze-Anwendungsstruktur beschrieben. In Direct3D hat die Begriffsansicht mehrere Bedeutungen. Eine Ressourcenansicht definiert die Unterressourcen, auf die eine Ressource zugreifen kann. Wenn beispielsweise ein Texturobjekt einer Shaderressourcenansicht zugeordnet ist, kann dieser Shader später auf die Textur zugreifen. Ein Vorteil einer Ressourcenansicht besteht darin, dass Sie Daten auf unterschiedliche Weise in verschiedenen Phasen der Renderingpipeline interpretieren können. Weitere Informationen zu Ressourcenansichten finden Sie unter Ressourcenansichten. Bei Verwendung im Kontext einer Ansichtstransformations- oder Ansichtstransformationsmatrix bezieht sich die Ansicht auf die Position und Ausrichtung der Kamera. Eine Ansichtstransformation verschiebt Objekte in der Welt um die Position und Ausrichtung der Kamera. Weitere Informationen zu Ansichtstransformationen finden Sie unter Ansicht Transform (Direct3D 9). Wie Marble Maze Ressourcen- und Matrixansichten verwendet, wird in diesem Thema ausführlicher beschrieben.

 

Laden von Szenenressourcen

Marble Maze verwendet die BasicLoader-Klasse, die in BasicLoader.h deklariert wird, zum Laden von Texturen und Shadern. Marble Maze verwendet die SDKMesh-Klasse, um die 3D-Gitter für das Labyrinth und die Murmel zu laden.

Um eine reaktionsfähige App sicherzustellen, lädt Marble Maze Szenenressourcen asynchron oder im Hintergrund. Wenn Ressourcen im Hintergrund geladen werden, kann Ihr Spiel auf Fensterereignisse reagieren. Dieser Prozess wird ausführlicher in den Laden von Spielressourcen im Hintergrund in diesem Handbuch erläutert.

Laden der 2D-Überlagerung und Benutzeroberfläche

In Marble Maze ist die Überlagerung das Bild, das oben auf dem Bildschirm angezeigt wird. Die Überlagerung wird immer vor der Szene angezeigt. In Marble Maze enthält die Überlagerung das Windows-Logo und die Textzeichenfolge DirectX-Beispielspiel Marble Maze. Die Verwaltung der Überlagerung wird von der SampleOverlay-Klasse ausgeführt, die unter SampleOverlay.h definiert ist. Obwohl wir das Overlay als Teil der Direct3D-Beispiele verwenden, können Sie diesen Code anpassen, um alle Bilder anzuzeigen, die vor der Szene angezeigt werden.

Ein wichtiger Aspekt der Überlagerung ist, dass die SampleOverlay-Klasse während der Initialisierung, da sich der Inhalt nicht ändert, in einem ID2D1Bitmap1-Objekt zeichnet oder zwischenspeichert. Zur Zeichenzeit muss die SampleOverlay-Klasse nur die Bitmap auf den Bildschirm zeichnen. Auf diese Weise müssen teure Routinen wie z. B. die Textzeichnung nicht für jeden Rahmen ausgeführt werden.

Die Benutzeroberfläche (UI) besteht aus 2D-Komponenten, z. B. Menüs und Head-up-Displays (HUDs), die vor der Szene angezeigt werden. Marble Maze definiert die folgenden Benutzeroberfläche-Elemente:

  • Menüelemente, mit denen der Benutzer das Spiel starten oder Highscores anzeigen kann.
  • Ein Timer, der drei Sekunden nach unten zählt, bevor die Wiedergabe beginnt.
  • Ein Timer, der die verstrichene Wiedergabezeit verfolgt.
  • Eine Tabelle, in der die schnellsten Endzeiten aufgelistet sind.
  • Text Pausiert, der angezeigt wird, wenn das Spiel pausiert wird.

Marble Maze definiert spielspezifische UI-Elemente in UserInterface.h. Marble Maze definiert die ElementBase-Klasse als Basistyp für alle UI-Elemente. Die ElementBase-Klasse definiert Attribute wie Größe, Position, Ausrichtung und Sichtbarkeit eines Benutzeroberfläche-Elements. Außerdem wird gesteuert, wie Elemente aktualisiert und gerendert werden.

class ElementBase
{
public:
    virtual void Initialize() { }
    virtual void Update(float timeTotal, float timeDelta) { }
    virtual void Render() { }

    void SetAlignment(AlignType horizontal, AlignType vertical);
    virtual void SetContainer(const D2D1_RECT_F& container);
    void SetVisible(bool visible);

    D2D1_RECT_F GetBounds();

    bool IsVisible() const { return m_visible; }

protected:
    ElementBase();

    virtual void CalculateSize() { }

    Alignment       m_alignment;
    D2D1_RECT_F     m_container;
    D2D1_SIZE_F     m_size;
    bool            m_visible;
};

Durch die Bereitstellung einer allgemeinen Basisklasse für UI-Elemente benötigt die UserInterface-Klasse, welche die Benutzeroberfläche verwaltet, nur eine Sammlung von ElementBase-Objekten, welche die Benutzeroberflächenverwaltung vereinfacht und einen wiederverwendbaren Benutzeroberflächen-Manager bereitstellt. Marble Maze definiert Typen, die von ElementBase abgeleitet werden, die spielspezifische Verhaltensweisen implementieren. Beispielsweise definiert HighScoreTable das Verhalten für die Highscore-Tabelle. Weitere Informationen zu diesen Typen finden Sie im Quellcode.

Hinweis

Da XAML es Ihnen ermöglicht, komplexere Benutzeroberflächen zu erstellen, z. B. in Simulations- und Strategiespielen, überlegen Sie, ob Sie XAML zum Definieren der Benutzeroberfläche verwenden möchten. Informationen zum Entwickeln einer Benutzeroberfläche in XAML in einem DirectX-UWP-Spiel finden Sie unter Erweitern des Spielbeispiels, das sich auf das DirectX 3D-Shooterbeispiel bezieht.

 

Laden von Shadern

Marble Maze verwendet die BasicLoader::LoadShader-Methode, um einen Shader aus einer Datei zu laden.

Shader sind die grundlegende Einheit der GPU-Programmierung in Spielen heute. Nahezu alle 3D-Grafikverarbeitungen werden durch Shader gesteuert, unabhängig davon, ob es sich um Modelltransformation und Szenenbeleuchtung oder komplexere Geometrieverarbeitung handelt, von der Charakter-Skin- bis zur Tessellation. Weitere Informationen zum Shader-Programmierungsmodell finden Sie unter HLSL.

Marble Maze verwendet Vertex- und Pixelshader. Ein Vertex-Shader arbeitet immer auf einem Eingabevertex und erzeugt einen Scheitelpunkt als Ausgabe. Ein Pixelshader verwendet numerische Werte, Texturdaten, interpolierte Vertexwerte und andere Daten, um eine Pixelfarbe als Ausgabe zu erzeugen. Da ein Shader jeweils ein Element transformiert, können Grafikhardware, die mehrere Shaderpipelinen bereitstellt, Gruppen von Elementen parallel verarbeiten. Die Anzahl der parallelen Pipelines, die für die GPU verfügbar sind, kann erheblich größer sein als die Anzahl, die für die CPU verfügbar ist. Daher können auch einfache Shader den Durchsatz erheblich verbessern.

Die MarbleMazeMain::LoadDeferredResources-Methode lädt einen Vertex-Shader und einen Pixelshader, nachdem die Überlagerung geladen wurde. Die Entwurfstzeitversionen dieser Shader werden in BasicVertexShader.hlsl und BasicPixelShader.hlsl definiert. Marble Maze wendet diese Shader sowohl auf den Ball als auch das Labyrinth während der Renderingphase an.

Das Marble Maze-Projekt enthält sowohl HLSL-Versionen (Entwurfszeitformat) als auch CSO-Versionen (Laufzeitformat) der Shaderdateien. Zum Zeitpunkt der Erstellung verwendet Visual Studio den Effekt-Compiler fxc.exe, um Ihre .hlsl-Quelldatei in einen binären .cso-Shader zu kompilieren. Weitere Informationen zum Effektcompilertool finden Sie unter Effect-Compiler Tool.

Der Vertex-Shader verwendet die bereitgestellten Modell-, Ansichts- und Projektionsmatrizen, um die Eingabegeometrie zu transformieren. Positionsdaten aus der Eingabegeometrie werden zweimal transformiert und ausgegeben: Einmal im Bildschirmbereich, der zum Rendern erforderlich ist, und wieder im Weltbereich, damit der Pixelshader Beleuchtungsberechnungen durchführen kann. Der Oberflächennormalvektor wird in den Weltraum transformiert, der auch vom Pixelshader für die Beleuchtung verwendet wird. Die Texturkoordinaten werden unverändert an den Pixelshader übergeben.

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

Der Pixelshader empfängt die Ausgabe des Vertex-Shaders als Eingabe. Dieser Shader führt Beleuchtungsberechnungen durch, um einen weichen Blickpunkt nachzuahmen, der über das Labyrinth bewegt wird und an der Position der Murmel ausgerichtet ist. Beleuchtung ist am stärksten für Oberflächen, die direkt auf das Licht zeigen. Die diffuse Komponente geht gegen Null, wenn die Oberflächennormale senkrecht zum Licht steht, und der Umgebungsterm nimmt ab, wenn die Normale vom Licht weg zeigt. Punkte näher an der Murmel (und daher näher an der Mitte des Spotlights) werden stärker beleuchtet. Die Beleuchtung wird jedoch für Punkte unterhalb der Murmel moduliert, um einen weichen Schatten zu simulieren. In einer realen Umgebung würde ein Objekt wie die weiße Murmel das Spotlight auf andere Objekte in der Szene diffus wiedergeben. Dies wird für die Oberflächen angenähert, die sich im Blick auf die helle Hälfte der Murmel befinden. Die zusätzlichen Beleuchtungsfaktoren liegen im relativen Winkel und Abstand zur Murmel. Die resultierende Pixelfarbe ist eine Zusammensetzung der gesampelten Textur mit dem Ergebnis der Beleuchtungsberechnungen.

float4 main(sPSInput input) : SV_TARGET
{
    float3 lightDirection = float3(0, 0, -1);
    float3 ambientColor = float3(0.43, 0.31, 0.24);
    float3 lightColor = 1 - ambientColor;
    float spotRadius = 50;

    // Basic ambient (Ka) and diffuse (Kd) lighting from above.
    float3 N = normalize(input.norm);
    float NdotL = dot(N, lightDirection);
    float Ka = saturate(NdotL + 1);
    float Kd = saturate(NdotL);

    // Spotlight.
    float3 vec = input.worldPos - marblePosition;
    float dist2D = sqrt(dot(vec.xy, vec.xy));
    Kd = Kd * saturate(spotRadius / dist2D);

    // Shadowing from ball.
    if (input.worldPos.z > marblePosition.z)
        Kd = Kd * saturate(dist2D / (marbleRadius * 1.5));

    // Diffuse reflection of light off ball.
    float dist3D = sqrt(dot(vec, vec));
    float3 V = normalize(vec);
    Kd += saturate(dot(-V, N)) * saturate(dot(V, lightDirection))
        * saturate(marbleRadius / dist3D);

    // Final composite.
    float4 diffuseTexture = Texture.Sample(Sampler, input.tex);
    float3 color = diffuseTexture.rgb * ((ambientColor * Ka) + (lightColor * Kd));
    return float4(color * lightStrength, diffuseTexture.a);
}

Warnung

Der kompilierte Pixelshader enthält 32 arithmetische Anweisungen und 1 Texturanweisung. Dieser Shader sollte auf Desktopcomputern oder Tablets mit höherer Leistung gut funktionieren. Einige Computer können diesen Shader jedoch möglicherweise nicht verarbeiten und dennoch eine interaktive Framerate bereitstellen. Berücksichtigen Sie die typische Hardware Ihrer Zielgruppe, und entwerfen Sie Ihre Shader, um die Funktionen dieser Hardware zu erfüllen.

 

Die MarbleMazeMain::LoadDeferredResources-Methode verwendet die BasicLoader::LoadShader-Methode, um die Shader zu laden. Im folgenden Beispiel wird der Vertex-Shader geladen. Das Laufzeitformat für diesen Shader lautet BasicVertexShader.cso. Die m_vertexShader-Membervariable ist ein ID3D11VertexShader-Objekt.

BasicLoader^ loader = ref new BasicLoader(m_deviceResources->GetD3DDevice());

D3D11_INPUT_ELEMENT_DESC layoutDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
m_vertexStride = 44; // must set this to match the size of layoutDesc above

Platform::String^ vertexShaderName = L"BasicVertexShader.cso";
loader->LoadShader(
    vertexShaderName,
    layoutDesc,
    ARRAYSIZE(layoutDesc),
    &m_vertexShader,
    &m_inputLayout
    );

Die m_inputLayout-Membervariable ist ein ID3D11InputLayout-Objekt. Das Eingabe-Layoutobjekt kapselt den Eingabezustand der Eingabeassemblerphase (IA). Eine Aufgabe der IA-Phase besteht darin, Shader effizienter zu machen, indem systemgenerierte Werte verwendet werden, die auch als Semantikbezeichnet werden, um nur die Grundtypen oder Scheitelpunkte zu verarbeiten, die noch nicht verarbeitet wurden.

Verwenden Sie die ID3D11Device::CreateInputLayout-Methode, um ein Eingabelayout aus einem Array von Eingabeelementbeschreibungen zu erstellen. Das Array enthält ein oder mehrere Eingabeelemente; Jedes Eingabeelement beschreibt ein Vertexdatenelement aus einem Vertexpuffer. Der gesamte Satz von Eingabeelementbeschreibungen beschreibt alle Vertexdatenelemente aus allen Vertexpuffern, die an die IA-Phase gebunden werden.

layoutDesc im obigen Codeausschnitt zeigt die Layoutbeschreibung, die Marble Maze verwendet. In der Layoutbeschreibung wird ein Vertexpuffer beschrieben, der vier Vertexdatenelemente enthält. Die wichtigen Teile der einzelnen Einträge im Array sind der semantische Name, das Datenformat und der Byte-Offset. Beispielsweise gibt das POSITION-Element die Vertexposition im Objektbereich an. Sie beginnt bei Byte-Offset 0 und enthält drei Gleitkommakomponenten (insgesamt 12 Bytes). Das NORMAL-Element gibt den normalen Vektor an. Sie beginnt mit dem Byte-Offset 12, da sie direkt nach POSITION im Layout angezeigt wird, was 12 Byte erfordert. Das NORMAL-Element enthält eine 32-Bit-Ganzzahl mit vier Komponenten.

Vergleichen Sie das Eingabelayout mit der sVSInput-Struktur, die vom Vertex-Shader definiert wird, wie im folgenden Beispiel gezeigt. Die sVSInput-Struktur definiert die Elemente POSITION, NORMALund TEXCOORD0. Die DirectX-Laufzeit ordnet jedes Element im Layout der vom Shader definierten Eingabestruktur zu.

struct sVSInput
{
    float3 pos : POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

struct sPSInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
};

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

Die Dokumentsemantik beschreibt jede der verfügbaren Semantik ausführlicher.

Hinweis

In einem Layout können Sie zusätzliche Komponenten angeben, die nicht verwendet werden, um mehreren Shadern das gleiche Layout zu ermöglichen. Beispielsweise wird das TANGENT-Element nicht vom Shader verwendet. Sie können das TANGENT-Element verwenden, wenn Sie mit Techniken wie normaler Zuordnung experimentieren möchten. Mithilfe der normalen Zuordnung, auch als Bumpmapping bezeichnet, können Sie die Wirkung von Stoßstößen auf die Oberflächen von Objekten erstellen. Weitere Informationen zur Bumpzuordnung finden Sie unter Bump Mapping (Direct3D 9).

 

Weitere Informationen zur Eingabeassemblyphase finden Sie unter Input-Assembler Stage und Getting Started mit Input-Assembler Stage.

Der Prozess der Verwendung der Vertex- und Pixelshader zum Rendern der Szene wird im Abschnitt Rendern der Szene später in diesem Dokument beschrieben.

Erstellen des Konstantenpuffers

Der Direct3D-Puffer gruppiert eine Sammlung von Daten. Ein Konstantenpuffer ist eine Art Puffer, mit dem Sie Daten an Shader übergeben können. Marble Maze verwendet einen Konstantenpuffer, um die Modellansicht (oder die Weltansicht) und die Projektionsmatrizen für das aktive Szenenobjekt zu speichern.

Das folgende Beispiel zeigt, wie die MarbleMazeMain::LoadDeferredResources-Methode einen Konstantenpuffer erstellt, der später Matrixdaten enthält. In dem Beispiel wird eine D3D11_BUFFER_DESC-Struktur erstellt, die das D3D11_BIND_CONSTANT_BUFFER-Kennzeichen zum Angeben der Verwendung als Konstantenpuffer verwendet. In diesem Beispiel wird diese Struktur dann an die ID3D11Device::CreateBuffer-Methode übergeben. Die m_constantBuffer-Variable ist ein ID3D11Buffer-Objekt.

// Create the constant buffer for updating model and camera data.
D3D11_BUFFER_DESC constantBufferDesc = {0};

// Multiple of 16 bytes
constantBufferDesc.ByteWidth = ((sizeof(ConstantBuffer) + 15) / 16) * 16;

constantBufferDesc.Usage               = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags           = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags      = 0;
constantBufferDesc.MiscFlags           = 0;

// This will not be used as a structured buffer, so this parameter is ignored.
constantBufferDesc.StructureByteStride = 0;

DX::ThrowIfFailed(
    m_deviceResources->GetD3DDevice()->CreateBuffer(
        &constantBufferDesc,
        nullptr,    // leave the buffer uninitialized
        &m_constantBuffer
        )
    );

Die MarbleMazeMain::Update-Methode aktualisiert später ConstantBuffer-Objekte, eines für das Labyrinth und eines für die Murmel. Die MarbleMazeMain::Render-Methode bindet dann jedes ConstantBuffer-Objekt an den Konstantenpuffer, bevor jedes Objekt gerendert wird. Das folgende Beispiel zeigt die ConstantBuffer-Struktur, die sich in MarbleMazeMain.hbefindet.

// Describes the constant buffer that draws the meshes.
struct ConstantBuffer
{
    XMFLOAT4X4 model;
    XMFLOAT4X4 view;
    XMFLOAT4X4 projection;

    XMFLOAT3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

Um besser zu verstehen, wie konstante Puffer auf Shader-Code abgebildet werden, vergleichen Sie die ConstantBuffer-Struktur in MarbleMazeMain.h mit dem konstanten Puffer ConstantBuffer, der vom Vertex-Shader in BasicVertexShader.hlsl definiert ist:

cbuffer ConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
    float3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

Das Layout der ConstantBuffer-Struktur entspricht dem cbuffer-Objekt. Die cbuffer-Variable gibt das Register b0 an, was bedeutet, dass die Konstantenpufferdaten im Register 0 gespeichert werden. Die MarbleMazeMain::Render-Methode gibt das Register 0 an, wenn der Konstantenpuffer aktiviert wird. Dieser Vorgang wird weiter unten in diesem Dokument ausführlicher beschrieben.

Weitere Informationen zu Konstantenpuffern finden Sie unter Einführung in Puffer in Direct3D 11. Weitere Informationen zum Registrierungsschlüsselwort finden Sie unter Register.

Laden von Gittern

Marble Maze verwendet SDK-Mesh als Laufzeitformat, da dieses Format eine grundlegende Möglichkeit zum Laden von Gitterdaten für Beispielanwendungen bietet. Für die Produktionsverwendung sollten Sie ein Gitterformat verwenden, das die spezifischen Anforderungen Ihres Spiels erfüllt.

Die MarbleMazeMain::LoadDeferredResources-Methode lädt Gitterdaten, nachdem sie die Vertex- und Pixelshader geladen haben. Ein Gitter ist eine Sammlung von Vertexdaten, die häufig Informationen wie Positionen, normale Daten, Farben, Materialien und Texturkoordinaten enthalten. Gitter werden in der Regel in 3D-Dokumenterstellungssoftware erstellt und in Dateien verwaltet, die von Anwendungscode getrennt sind. Die Murmel und das Labyrinth sind zwei Beispiele für Gitter, die das Spiel verwendet.

Marble Maze verwendet die SDKMesh-Klasse zum Verwalten von Gittern. Diese Klasse wird in SDKMesh.h deklariert. SDKMesh stellt Methoden zum Laden, Rendern und Zerstören von Gitterdaten bereit.

Wichtig

Marble Maze verwendet das SDK-Mesh-Format und stellt nur die SDKMesh-Klasse zur Veranschaulichung bereit. Obwohl das SDK-Mesh-Format zum Erlernen und zum Erstellen von Prototypen nützlich ist, ist es ein sehr einfaches Format, das möglicherweise nicht den Anforderungen der meisten Spieleentwicklung entspricht. Es wird empfohlen, ein Gitterformat zu verwenden, das den spezifischen Anforderungen Ihres Spiels entspricht.

 

Das folgende Beispiel zeigt, wie die MarbleMazeMain::LoadDeferredResources-Methode die SDKMesh::Create-Methode verwendet, um Gitterdaten für das Labyrinth und für den Ball zu laden.

// Load the meshes.
DX::ThrowIfFailed(
    m_mazeMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\maze1.sdkmesh",
        false
        )
    );

DX::ThrowIfFailed(
    m_marbleMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\marble2.sdkmesh",
        false
        )
    );

Laden von Kollisionsdaten

Obwohl sich dieser Abschnitt nicht darauf konzentriert, wie Marble Maze die Physikalische Simulation zwischen der Murmel und dem Labyrinth implementiert, beachten Sie, dass die Gittergeometrie für das Physiksystem gelesen wird, wenn die Gitter geladen werden.

// Extract mesh geometry for the physics system.
DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_walls",
        m_collision.m_wallTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_Floor",
        m_collision.m_groundTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_floorSides",
        m_collision.m_floorTriList
        )
    );

m_physics.SetCollision(&m_collision);
float radius = m_marbleMesh.GetMeshBoundingBoxExtents(0).x / 2;
m_physics.SetRadius(radius);

Die Art und Weise, wie Kollisionsdaten geladen werden, hängt weitgehend vom verwendeten Laufzeitformat ab. Weitere Informationen dazu, wie Marble Maze die Kollisionsgeometrie aus einer SDK-Mesh-Datei lädt, finden Sie in der MarbleMazeMain::ExtractTrianglesFromMesh-Methode im Quellcode.

Aktualisieren des Spielzustands

Marble Maze trennt die Spiellogik von der Renderinglogik, indem zuerst alle Szenenobjekte aktualisiert werden, bevor sie gerendert werden.

Die Marble Maze-Anwendungsstruktur beschreibt die Hauptspielschleife. Das Aktualisieren der Szene, die Teil der Spielschleife ist, geschieht, nachdem Windows-Ereignisse und -Eingaben verarbeitet wurden und bevor die Szene gerendert wird. Die MarbleMazeMain::Update-Methode behandelt die Aktualisierung der Benutzeroberfläche und des Spiels.

Aktualisieren der Benutzeroberfläche

Die MarbleMazeMain::Update-Methode ruft die UserInterface::Update-Methode auf , um den Zustand der Benutzeroberfläche zu aktualisieren.

UserInterface::GetInstance().Update(
    static_cast<float>(m_timer.GetTotalSeconds()), 
    static_cast<float>(m_timer.GetElapsedSeconds()));

Die UserInterface::Update-Methode aktualisiert jedes Element in der UI-Auflistung.

void UserInterface::Update(float timeTotal, float timeDelta)
{
    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        (*iter)->Update(timeTotal, timeDelta);
    }
}

Klassen, die von ElementBase (definiert in UserInterface.h) abgeleitet werden, implementieren die Update-Methode, um bestimmte Verhaltensweisen auszuführen. Beispielsweise aktualisiert die StopwatchTimer::Update-Methode die verstrichene Zeit um den bereitgestellten Betrag und aktualisiert den Text, den er später anzeigt.

void StopwatchTimer::Update(float timeTotal, float timeDelta)
{
    if (m_active)
    {
        m_elapsedTime += timeDelta;

        WCHAR buffer[16];
        GetFormattedTime(buffer);
        SetText(buffer);
    }

    TextElement::Update(timeTotal, timeDelta);
}

Aktualisieren der Szene

Die MarbleMazeMain::Update-Methode aktualisiert das Spiel basierend auf dem aktuellen Zustand des Zustandsautomaten (der GameState , gespeichert in m_gameState). Wenn sich das Spiel im aktiven Zustand befindet (GameState::InGameActive), aktualisiert Marble Maze die Kamera, um die Murmel zu folgen, aktualisiert den Ansichtsmatrixteil der Konstantenpuffer und aktualisiert die Physiksimulation.

Das folgende Beispiel zeigt, wie die MarbleMazeMain::Update-Methode die Position der Kamera aktualisiert. Marble Maze verwendet die m_resetCamera-Variable für die Kennzeichnung, dass die Kamera für die Anordnung direkt über der Kugel zurückgesetzt werden muss. Die Kamera wird zurückgesetzt, wenn das Spiel gestartet wird oder die Murmel durch das Labyrinth fällt. Wenn das Hauptmenü oder der Highscore-Anzeigebildschirm aktiv ist, wird die Kamera an einer konstanten Position festgelegt. Andernfalls verwendet Marble Maze den Parameter timeDelta, um die Position der Kamera zwischen den aktuellen und den Zielpositionen zu interpolieren. Die Zielposition liegt etwas über und vor der Murmel. Die Verwendung der verstrichenen Framezeit ermöglicht es der Kamera, die Murmel schrittweise zu verfolgen oder zu verfolgen.

static float eyeDistance = 200.0f;
static XMFLOAT3A eyePosition = XMFLOAT3A(0, 0, 0);

// Gradually move the camera above the marble.
XMFLOAT3A targetEyePosition;
XMStoreFloat3A(
    &targetEyePosition, 
    XMLoadFloat3A(&marblePosition) - (XMLoadFloat3A(&g) * eyeDistance));

if (m_resetCamera)
{
    eyePosition = targetEyePosition;
    m_resetCamera = false;
}
else
{
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&eyePosition) 
            + ((XMLoadFloat3A(&targetEyePosition) - XMLoadFloat3A(&eyePosition)) 
                * min(1, static_cast<float>(m_timer.GetElapsedSeconds()) * 8)
            )
    );
}

// Look at the marble. 
if ((m_gameState == GameState::MainMenu) || (m_gameState == GameState::HighScoreDisplay))
{
    // Override camera position for menus.
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&marblePosition) + XMVectorSet(75.0f, -150.0f, -75.0f, 0.0f));

    m_camera->SetViewParameters(
        eyePosition, 
        marblePosition, 
        XMFLOAT3(0.0f, 0.0f, -1.0f));
}
else
{
    m_camera->SetViewParameters(eyePosition, marblePosition, XMFLOAT3(0.0f, 1.0f, 0.0f));
}

Das folgende Beispiel zeigt, wie die MarbleMazeMain::Update-Methode die Konstantenpuffer für die Murmel und das Labyrinth aktualisiert. Das Labyrinthmodell oder die Weltmatrix bleibt immer die Identitätsmatrix. Mit Ausnahme der Hauptdiagonale, deren Elemente alle sind, ist die Identitätsmatrix eine quadratische Matrix, die aus Nullen besteht. Die Modellmatrix der Murmel basiert auf der Positionsmatrix, die ihre Drehungsmatrix umdreht.

// Update the model matrices based on the simulation.
XMStoreFloat4x4(&m_mazeConstantBufferData.model, XMMatrixIdentity());

XMStoreFloat4x4(
    &m_marbleConstantBufferData.model, 
    XMMatrixTranspose(
        XMMatrixMultiply(
            marbleRotationMatrix, 
            XMMatrixTranslationFromVector(XMLoadFloat3A(&marblePosition))
        )
    )
);

// Update the view matrix based on the camera.
XMFLOAT4X4 view;
m_camera->GetViewMatrix(&view);
m_mazeConstantBufferData.view = view;
m_marbleConstantBufferData.view = view;

Informationen dazu, wie die MarbleMazeMain::Update-Methode die Benutzereingabe liest und die Bewegung der Murmel simuliert, finden Sie unter Hinzufügen von Eingaben und Interaktivität zum Marble Maze-Beispiel.

Rendern der Szene

Wenn eine Szene gerendert wird, sind diese Schritte in der Regel enthalten.

  1. Legen Sie den Tiefenschablonenpuffer für das aktuelle Renderziel fest.
  2. Löschen Sie die Render- und Schablonenansichten.
  3. Bereiten Sie die Vertex- und Pixelshader für die Zeichnung vor.
  4. Rendern der 3D-Objekte in der Szene.
  5. Rendern Sie alle 2D-Objekte, die vor der Szene angezeigt werden sollen.
  6. Präsentieren Sie das gerenderte Bild auf dem Monitor.

Die MarbleMazeMain::Render-Methode bindet die Renderziel- und Tiefenschablonenansichten, löscht diese Ansichten, zeichnet die Szene und zeichnet dann die Überlagerung.

Vorbereiten der Renderziele

Bevor Sie Die Szene rendern, müssen Sie den Tiefenschablonenpuffer für das aktuelle Renderziel festlegen. Wenn die Szene nicht garantiert über jedes Pixel auf dem Bildschirm gezeichnet werden kann, löschen Sie auch die Render- und Schablonenansichten. Marble Maze löscht die Render- und Schablonenansichten für jeden Frame, um sicherzustellen, dass keine sichtbaren Artefakte aus dem vorherigen Frame vorhanden sind.

Das folgende Beispiel zeigt, wie die MarbleMazeMain::Render-Methode die ID3D11DeviceContext::OMSetRenderTargets-Methode aufruft, um das Renderziel und den Tiefenschablonenpuffer als aktuelle festzulegen.

auto context = m_deviceResources->GetD3DDeviceContext();

// Reset the viewport to target the whole screen.
auto viewport = m_deviceResources->GetScreenViewport();
context->RSSetViewports(1, &viewport);

// Reset render targets to the screen.
ID3D11RenderTargetView *const targets[1] = 
    { m_deviceResources->GetBackBufferRenderTargetView() };

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

// Clear the back buffer and depth stencil view.
context->ClearRenderTargetView(
    m_deviceResources->GetBackBufferRenderTargetView(), 
    DirectX::Colors::Black);

context->ClearDepthStencilView(
    m_deviceResources->GetDepthStencilView(), 
    D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 
    1.0f, 
    0);

Die SCHNITTSTELLEN ID3D11RenderTargetView und ID3D11DepthStencilView unterstützen den Texturansichtsmechanismus, der von Direct3D 10 und höher bereitgestellt wird. Weitere Informationen zu Texturansichten finden Sie unter Texturansichten (Direct3D 10). Die OMSetRenderTargets-Methode bereitet die Ausgabezusammenführungsphase der Direct3D-Pipeline vor. Weitere Informationen zur Ausgabezusammenführungsphase finden Sie in der Ausgabezusammenführungsphase.

Vorbereiten der Vertex- und Pixelshader

Führen Sie vor dem Rendern der Szenenobjekte die folgenden Schritte aus, um die Vertex- und Pixelshader für die Zeichnung vorzubereiten:

  1. Legen Sie das Shadereingabelayout als aktuelles Layout fest.
  2. Legen Sie die Vertex- und Pixelshader als aktuelle Shader fest.
  3. Aktualisieren Sie alle Konstantenpuffer mit Daten, die Sie an die Shader übergeben müssen.

Wichtig

Marble Maze verwendet ein Vertex- und Pixelshaderpaar für alle 3D-Objekte. Wenn Ihr Spiel mehrere Shaderpaare verwendet, müssen Sie diese Schritte jedes Mal ausführen, wenn Sie Objekte zeichnen, die unterschiedliche Shader verwenden. Um den Aufwand zu verringern, der mit dem Ändern des Shaderzustands verbunden ist, empfiehlt es sich, Renderaufrufe für alle Objekte zu gruppieren, die dieselben Shader verwenden.

 

Im Abschnitt Laden von Shadern in diesem Dokument wird beschrieben, wie das Eingabelayout erstellt wird, wenn der Vertex-Shader erstellt wird. Das folgende Beispiel zeigt, wie die MarbleMazeMain::Render-Methode die ID3D11DeviceContext::IASetInputLayout-Methode verwendet, um dieses Layout als aktuelles Layout festzulegen.

m_deviceResources->GetD3DDeviceContext()->IASetInputLayout(m_inputLayout.Get());

Das folgende Beispiel zeigt, wie die MarbleMazeMain::Render-Methode die Methoden ID3D11DeviceContext::VSSetShader und ID3D11DeviceContext::PSSetShader verwendet, um die Vertex- und Pixelshader als aktuelle Shader festzulegen.

// Set the vertex shader stage state.
m_deviceResources->GetD3DDeviceContext()->VSSetShader(
    m_vertexShader.Get(),   // use this vertex shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetShader(
    m_pixelShader.Get(),    // use this pixel shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetSamplers(
    0,                          // starting at the first sampler slot
    1,                          // set one sampler binding
    m_sampler.GetAddressOf());  // to use this sampler

Nachdem MarbleMazeMain::Render die Shader und deren Eingabelayout festgelegt hat, wird die ID3D11DeviceContext::UpdateSubresource-Methode verwendet, um den Konstantenpuffer mit den Modell-, Ansichts- und Projektionsmatrizen für das Labyrinth zu aktualisieren. Die UpdateSubresource-Methode kopiert die Matrixdaten aus dem CPU-Speicher in den GPU-Speicher. Denken Sie daran, dass das Modell und die Ansichtskomponenten der ConstantBuffer-Struktur in der MarbleMazeMain::Update-Methode aktualisiert werden. Die MarbleMazeMain::Render-Methode ruft dann die METHODEN ID3D11DeviceContext::VSSetConstantBuffers und ID3D11DeviceContext::P SSetConstantBuffers auf, um diesen Konstantenpuffer als aktuellen festzulegen.

// Update the constant buffer with the new data.
m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
    m_constantBuffer.Get(),
    0,
    nullptr,
    &m_mazeConstantBufferData,
    0,
    0);

m_deviceResources->GetD3DDeviceContext()->VSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

m_deviceResources->GetD3DDeviceContext()->PSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

Die MarbleMazeMain::Render-Methode führt ähnliche Schritte aus, um die Murmel so vorzubereiten, dass sie gerendert wird.

Rendern des Labyrinths und der Murmel

Nachdem Sie die aktuellen Shader aktiviert haben, können Sie Ihre Szenenobjekte zeichnen. Die MarbleMazeMain::Render-Methode ruft die SDKMesh::Render-Methode auf, um das Labyrinthgitter zu rendern.

m_mazeMesh.Render(
    m_deviceResources->GetD3DDeviceContext(), 
    0, 
    INVALID_SAMPLER_SLOT, 
    INVALID_SAMPLER_SLOT);

Die MarbleMazeMain::Render-Methode führt ähnliche Schritte aus, um die Murmel zu rendern.

Wie weiter oben in diesem Dokument erwähnt, wird die SDKMesh-Klasse zu Demonstrationszwecken bereitgestellt, es wird jedoch nicht für die Verwendung in einem Produktionsqualitätsspiel empfohlen. Beachten Sie jedoch, dass die SDKMesh::RenderMesh-Methode, die von SDKMesh::Renderaufgerufen wird, verwendet die Methoden ID3D11DeviceContext::IASetVertexBuffers und ID3D11DeviceContext::IASetIndexBuffer , um die aktuellen Vertex- und Indexpuffer festzulegen, die das Gitter definieren, und die ID3D11DeviceContext::DrawIndexed-Methode, um die Puffer zu zeichnen. Weitere Informationen zum Arbeiten mit Vertex- und Indexpuffern finden Sie unter Einführung in Puffer in Direct3D 11.

Zeichnen der Benutzeroberfläche und Überlagerung

Nach dem Zeichnen von 3D-Szenenobjekten zeichnet Marble Maze die 2D-UI-Elemente, die vor der Szene angezeigt werden.

Die MarbleMazeMain::Render-Methode endet, indem die Benutzeroberfläche und das Overlay gezeichnet werden.

// Draw the user interface and the overlay.
UserInterface::GetInstance().Render(m_deviceResources->GetOrientationTransform2D());

m_deviceResources->GetD3DDeviceContext()->BeginEventInt(L"Render Overlay", 0);
m_sampleOverlay->Render();
m_deviceResources->GetD3DDeviceContext()->EndEvent();

Die UserInterface::Render-Methode verwendet ein ID2D1DeviceContext-Objekt, um die Benutzeroberfläche-Elemente zu zeichnen. Mit dieser Methode wird der Zeichnungszustand festgelegt, alle aktiven UI-Elemente gezeichnet und anschließend der vorherige Zeichnungszustand wiederhergestellt.

void UserInterface::Render(D2D1::Matrix3x2F orientation2D)
{
    m_d2dContext->SaveDrawingState(m_stateBlock.Get());
    m_d2dContext->BeginDraw();
    m_d2dContext->SetTransform(orientation2D);

    m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);

    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        if ((*iter)->IsVisible())
            (*iter)->Render();
    }

    // We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
    // is lost. It will be handled during the next call to Present.
    HRESULT hr = m_d2dContext->EndDraw();
    if (hr != D2DERR_RECREATE_TARGET)
    {
        DX::ThrowIfFailed(hr);
    }

    m_d2dContext->RestoreDrawingState(m_stateBlock.Get());
}

Die SampleOverlay::Render-Methode verwendet eine ähnliche Technik, um die Überlagerungsbitmap zu zeichnen.

Darstellen der Szene

Nach dem Zeichnen aller 2D- und 3D-Szenenobjekte stellt Marble Maze das gerenderte Bild auf dem Monitor dar. Es synchronisiert die Zeichnung mit dem vertikalen Leerzeichen, um sicherzustellen, dass die Zeit nicht damit verbracht wird, Frames zu zeichnen, die nicht tatsächlich auf der Anzeige angezeigt werden. Marble Maze behandelt auch Geräteänderungen, wenn sie die Szene darstellt.

Nachdem die MarbleMazeMain::Render-Methode zurückgegeben wurde, ruft die Spielschleife die DX::D eviceResources::Present-Methode auf, um das gerenderte Bild an den Monitor oder die Anzeige zu senden. Die DX::D eviceResources::Present-Methode ruft IDXGISwapChain::Present auf, um den aktuellen Vorgang auszuführen, wie im folgenden Beispiel gezeigt:

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

In diesem Beispiel ist M_swapChain ein IDXGISwapChain1-Objekt. Die Initialisierung dieses Objekts wird im Abschnitt Initialisieren von Direct3D und Direct2D in diesem Dokument beschrieben.

Der erste Parameter für IDXGISwapChain::Present, SyncInterval, gibt die Anzahl der vertikalen Leerzeichen an, die vor der Darstellung des Frames gewartet werden sollen. Marble Maze gibt 1 an, sodass sie bis zum nächsten vertikalen Leerzeichen wartet.

Die IDXGISwapChain::Present-Methode gibt einen Fehlercode zurück, der angibt, dass das Gerät entfernt wurde oder anderweitig fehlgeschlagen ist. In diesem Fall wird das Gerät von Marble Maze neu initialisiert.

// 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)
{
    HandleDeviceLost();
}
else
{
    DX::ThrowIfFailed(hr);
}

Nächste Schritte

Lesen Sie den Abschnitt Hinzufügen von Eingaben und Interaktivität zum Marble Maze-Beispiel, um Informationen zu einigen wichtigen Verfahren zu erhalten, die Sie beim Arbeiten mit Eingabegeräten beachten sollten. In diesem Dokument wird erläutert, wie Marble Maze Toucheingaben, Beschleunigungsmesser, Gamecontroller und Mauseingaben unterstützt.