Freigeben über


Hinzufügen visueller Inhalte zum Marble Maze-Beispiel

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, die 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

Der Beispielcode, der diesem Dokument entspricht, befindet sich im DirectX Marble Maze-Spielbeispiel.

  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 ein möglichst breites Spektrum an 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, um sie während der Laufzeit so effizient wie möglich 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 Mesh-Format 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 Renderinglogik, indem Sie zuerst alle Szenenobjekte aktualisieren, bevor Sie sie 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. Ein vertikales leeres ist die Zeit zwischen dem Abschluss der Zeichnung eines Frames auf dem Monitor und dem 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 neu bei DirectX-Grafiken sind, empfehlen wir Ihnen, DirectX: Erste Schritte zu lesen, um sich mit den grundlegenden Konzepten zur Erstellung eines UWP-DirectX-Spiels vertraut zu machen. Während Sie dieses Dokument lesen und den Quellcode von Marble Maze durcharbeiten, können Sie sich auf die folgenden Ressourcen beziehen, um ausführlichere 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 mit hoher Leistung und qualitativ hochwertigem Rendering für 2D-Geometrie, Bitmaps und Text.
  • DirectWrite: Beschreibt DirectWrite, das hochwertige Textrendering unterstützt.
  • Windows-Imageerstellungskomponente: Beschreibt WIC, eine erweiterbare Plattform, die api auf niedriger Ebene für digitale Bilder bereitstellt.

Feature-Ebenen

Direct3D 11 führt ein Paradigma mit der Bezeichnung "Feature Levels"ein. Eine Featureebene ist ein gut definierter Satz von GPU-Funktionen. Verwenden Sie Featureebenen, um Ihr Spiel auf früheren Versionen von Direct3D-Hardware auszurichten. Marble Maze unterstützt die Feature-Ebene 9.1, da es keine erweiterten Funktionen aus höheren Ebenen erfordert. 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 Featureebenen finden Sie unter Direct3D 11 unter "Downlevel Hardware".

Initialisieren von Direct3D und Direct2D

Ein Gerät stellt den Anzeigeadapter 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 Ressourcen. Marble Maze definiert die MarbleMazeMain-Klasse zum Behandeln von spielspezifischen Ressourcen, 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 sowie 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-Fabriken

Die DeviceResources::CreateDeviceIndependentResources--Methode erstellt die Fabriken für Direct2D, DirectWrite und WIC. In DirectX-Grafiken sind Fabriken die Ausgangspunkte zum Erstellen von Grafikressourcen. Marble Maze gibt D2D1_FACTORY_TYPE_SINGLE_THREADED an, da es alle Zeichenoperationen im Hauptthread ausführt.

// 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 den Direct3D-Anzeigeadapter darstellt. Da Marble Maze die Featureebene 9.1 und höher unterstützt, gibt die DeviceResources::CreateDeviceResources-Methode die Ebenen 9.1 bis 11.1 im FeatureLevels-Array an. Direct3D geht die Liste in der Reihenfolge durch und gibt der Anwendung die erste verfügbare Featureebene. Daher werden die D3D_FEATURE_LEVEL Arrayeinträge von der höchsten zur niedrigsten Ebene aufgeführt, sodass die App die höchste verfügbare Featureebene erhält. Die DeviceResources::CreateDeviceResources--Methode ruft das Direct3D 11.1-Gerät ab, indem das Von D3D11CreateDevicezurü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 Videospeicherflächen zwischen Grafiklaufzeiten. Marble Maze verwendet das zugrunde liegende DXGI-Gerät des Direct3D-Geräts, um ein 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 Overview and Direct2D and Direct3D Interoperability Overview.

Zuordnen von Direct3D zur Ansicht

Die DeviceResources::CreateWindowSizeDependentResources Methode erstellt die Grafikressourcen, die von einer bestimmten Fenstergröße abhängen, wie die Swapchain sowie die Direct3D- und Direct2D-Renderziele. Eine wichtige Möglichkeit, dass sich eine DirectX-UWP-App von einer Desktop-App unterscheidet, ist, wie die Swapchain dem Ausgabefenster zugeordnet ist. Eine Swap-Chain ist dafür verantwortlich, den Puffer anzuzeigen, auf den das Gerät für die Darstellung auf dem Monitor rendert. 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 Ausgabe des Geräts mit der Ansicht zu verknüpfen. Das folgende Beispiel zeigt den Teil der DeviceResources::CreateWindowSizeDependentResources-Methode, die 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 dem vertikalen Leerzeichen wird im Abschnitt Darstellen der Szene in diesem Dokument ausführlicher 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

Der Begriff Ansicht 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. Das Einrichten der App-Ansicht wird in der Marble Maze-Anwendungsstrukturbeschrieben. Im Direct3D-Kontext hat der Begriff "Ansicht" 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 Ansichtstransformation oder -transformationsmatrix 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 View 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 ist, um Texturen und Shader zu laden. 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 Fenster-Events reagieren. Dieser Prozess wird in Laden von Spielressourcen im Hintergrund in diesem Handbuch ausführlicher erläutert.

Laden der 2D-Überlagerung und Benutzeroberfläche

In Marble Maze ist die Überlagerung das Bild, das oben auf dem Bildschirm angezeigt wird. Das Overlay erscheint immer vor der Szene. In Marble Maze enthält die Überlagerung das Windows-Logo und die Textzeichenfolge DirectX Marble Maze-Spielbeispiel. Die Überlagerung wird von der Klasse SampleOverlay verwaltet, die in SampleOverlay.hdefiniert 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 besteht darin, dass die Klasse SampleOverlay- beim Initialisieren ihre Inhalte in ein ID2D1Bitmap1--Objekt zeichnet oder zwischenspeichert. Zum Zeitpunkt des Zeichnens 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 UI-Elemente:

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

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 UI-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 , die die Benutzeroberfläche verwaltet, nur eine Sammlung von ElementBase-Objekten , die 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 wird mit HighScoreTable das Verhalten der Highscore-Tabelle definiert. 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-Grafikverarbeitung erfolgt durch Shader, sei es durch Modelltransformationen und Szenenbeleuchtung oder durch komplexere Geometrieverarbeitung, von Skinning bis Tessellation von Charakteren. Weitere Informationen zum Shader-Programmiermodell finden Sie unter HLSL.

Marble Maze verwendet Vertex- und Pixelshader. Ein Vertex-Shader arbeitet immer mit einem Eingabevertex und erzeugt einen Ausgangsvertex. 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 nach dem Laden der Überlagerung. Die Entwurfszeitversionen dieser Shader werden in BasicVertexShader.hlsl- und BasicPixelShader.hlsl-definiert. Während der Renderphase wendet Marble Maze diese Shader sowohl auf den Ball als auch auf das Labyrinth an.

Das Marble Maze-Projekt enthält sowohl .hlsl-Versionen (Entwurfszeitformat) als auch .cso-Versionen (Laufzeitformat) der Shaderdateien. Beim Erstellen verwendet Visual Studio den fxc.exe Effekt-Compiler, um die HLSL-Quelldatei in einen CSO-Binärshader 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 Weltkoordinatenraum transformiert, der auch vom Pixel-Shader 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 ein weiches Kegellicht nachzuahmen, das über dem Labyrinth schwebt 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 läuft gegen Null, da die Oberflächennormale senkrecht zum Licht wird, und der Umgebungsanteil verringert sich, da die Normale vom Licht weg zeigt. Punkte, die näher an der Murmel (und damit näher am Zentrum des Scheinwerferlichts) liegen, 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 der weiße Marmor das Scheinwerferlicht diffus auf andere Objekte in der Szene reflektieren. 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 abgetasteten 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 zum Laden der Shaders. Im folgenden Beispiel wird der Vertex-Shader geladen. Das Laufzeitformat für diesen Shader ist BasicVertexShader.cso. Die m_vertexShader-Mitgliedsvariable 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 eine ID3D11InputLayout--Objekt. Das Eingabelayoutobjekt 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 Primitiven oder Vertices 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 Objektraum 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 vierkomponentige 32-Bit-unsigned-Ganzzahl.

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. Das Element TANGENT wird beispielsweise 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 zum Bump Mapping finden Sie unter Bump Mapping (Direct3D 9).

 

Weitere Informationen zur Eingabe-Assemblierungsschritt finden Sie unter Input-Assembler Stage und Einstieg in die Input-Assembler Phase.

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

Erstellung 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. Im Beispiel wird eine D3D11_BUFFER_DESC Struktur erzeugt, die das D3D11_BIND_CONSTANT_BUFFER Flag verwendet, um die Nutzung als Konstantenpuffer festzulegen. 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 konstanten Puffer, 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 Konstantenpuffer dem Shadercode zugeordnet werden, vergleichen Sie die ConstantBuffer--Struktur in MarbleMazeMain.h mit dem ConstantBuffer--Konstantenpuffer, der vom Vertex-Shader in BasicVertexShader.hlsldefiniert wird.

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 sie den Konstantenpuffer aktiviert. 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 Registrieren.

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. Netze werden in der Regel in 3D-Erstellungssoftware erstellt und in Dateien verwaltet, die vom 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 Meshes. Diese Klasse wird in SDKMesh.h deklariert. SDKMesh bietet Methoden zum Laden, Rendern und Löschen von Mesh-Daten.

Von Bedeutung

Marble Maze verwendet das SDK-Mesh-Format und stellt die SDKMesh Klasse nur zur Veranschaulichung bereit. Obwohl das SDK-Mesh-Format zum Lernen 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 Methode MarbleMazeMain::LoadDeferredResources die Methode SDKMesh::Create verwendet, um Mesh-Daten für das Labyrinth und 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, sollten Sie darauf achten, dass die Mesh-Geometrie für das Physiksystem gelesen wird, wenn die Meshes 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 zum Laden der Kollisionsgeometrie aus einer SDK-Mesh-Datei finden Sie im MarbleMazeMain::ExtractTrianglesFromMesh Methode im Quellcode.

Spielzustand wird aktualisiert

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

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-Sammlung.

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 Status des Zustandsautomaten (dem 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, um anzuzeigen, dass die Kamera zurückgesetzt werden muss, sodass sich diese direkt über der Murmel befindet. Die Kamera wird zurückgesetzt, wenn das Spiel gestartet wird oder die Murmel durch das Labyrinth fällt. Wenn das Hauptmenü oder die Highscore-Anzeige aktiv ist, wird die Kamera an einer konstanten Position eingestellt. Andernfalls verwendet Marble Maze den timeDelta Parameter, um die Position der Kamera zwischen ihrer aktuellen Position und der Zielposition 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 folgen oder zu jagen.

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. Abgesehen von der Hauptdiagonale, deren Elemente alle Einsen sind, ist die Identitätsmatrix eine quadratische Matrix, die aus Nullen besteht. Die Modellmatrix der Murmel basiert auf der Multiplikation ihrer Positionsmatrix mit ihrer Rotationsmatrix.

// 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 Stanzansichten.
  3. Bereiten Sie die Vertex- und Pixelshader für die Zeichnung vor.
  4. Rendern Sie die 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 das Overlay.

Vorbereiten der Renderziele

Bevor Sie die Szene rendern, müssen Sie den Tiefenschablonenpuffer für das aktuelle Renderziel festlegen. Wenn Ihre Szene nicht garantiert jedes Pixel auf dem Bildschirm abdeckt, löschen Sie auch die Render- und Stencil-Ansichten. Marble Maze löscht die Render- und Stencil-Ansichten in jedem 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 die aktuellen 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 ID3D11RenderTargetView und ID3D11DepthStencilView Schnittstellen 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 unter Output-Merger Phase.

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.

Von Bedeutung

Marble Maze verwendet ein Paar von Vertex- und Pixel-Shadern 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 Methode MarbleMazeMain::Render die Methode ID3D11DeviceContext::IASetInputLayout 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 den Vertex-Shader bzw. den Pixel-Shader 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 haben, verwendet es die ID3D11DeviceContext::UpdateSubresource Methode, 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 die Modell- und Ansichtskomponenten der Struktur ConstantBuffer in der Methode MarbleMazeMain::Update aktualisiert werden. Die MarbleMazeMain::Render-Methode ruft dann die ID3D11DeviceContext::VSSetConstantBuffers- und ID3D11DeviceContext::PSSetConstantBuffers-Methoden auf, um diesen konstanten Puffer als den 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 zum Rendern vorzubereiten.

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 empfohlen, sie in einem Spiel in Produktionsqualität zu verwenden. Beachten Sie jedoch, dass die SDKMesh::RenderMesh-Methode, die von SDKMesh::Renderaufgerufen wird, die Methoden ID3D11DeviceContext::IASetVertexBuffers und ID3D11DeviceContext::IASetIndexBuffer verwendet, um die aktuellen Vertex- und Indexpuffer festzulegen, die das Mesh 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.

Darstellen der Benutzeroberfläche und des Overlays

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 sie die Benutzeroberfläche und die Überlagerung darstellt.

// 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ächen-Elemente zu zeichnen. Diese Methode legt den Zeichnungszustand fest, zeichnet alle aktiven Benutzerschnittstellelemente und stellt dann den vorherigen Zeichnungszustand wieder her.

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 Overlaybitmap 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ückkehrt, ruft die Spielschleife die DX::DeviceResources::Present-Methode auf, um das gerenderte Bild an den Monitor oder das Display zu senden. Die DX::DeviceResources::Present-Methode ruft IDXGISwapChain::Present auf, um den Präsentationsvorgang 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 zum IDXGISwapChain::P resent, 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 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 Hinzufügen von Eingaben und Interaktivität zum Marble Maze-Beispiel, um Informationen zu einigen der wichtigsten Methoden zu erhalten, die Sie beim Arbeiten mit Eingabegeräten berücksichtigen sollten. In diesem Dokument wird erläutert, wie Marble Maze Toucheingaben, Beschleunigungsmesser, Gamecontroller und Mauseingaben unterstützt.