Meine Werkzeuge
Namensräume
Varianten

DirectX 11 Jumpstart/Das erste Dreieck

Aus indiedev
Wechseln zu: Navigation, Suche
DirectX 11 Jumpstart
Das erste Dreieck
Autor Glatzemann
Programmier­sprache C++
Kategorie DirectX
Diskussion Thread im Forum
Lizenz indiedev article license
Originalartikel: MitOhneHaare.de


Nachdem im letzten Teil dieser Serie erfolgreich DirectX initialisiert wurde, möchte ich nun einen Scrhitt weiter gehen. Wir werden daher in diesem Teil der DirectX 11 Jumpstart Reihe Direct3D verwenden um unser erstes Dreieck zu rendern. Das klingt erstmal nicht sonderlich spektakulär, aber dies ist die Grundlage sowohl für 2D- als auch für 3D-Grafiken.

Bitte beachtet, dass das Design der Klassen die ich hier vorstelle noch nicht vollständig abgeschlossen ist. Teilweise verzichte ich auf gewisse Abstraktionen um es für euch etwas einfacher zu machen. Der BackBuffer ist beispielsweise so ein Kandidat. Um komfortabel und fehlerfrei arbeiten zu können bietet es sich an, den BackBuffer zu kapseln, allerdings führt dies natürlich zu einer erhöhten Komplexität unter der selbstverständlich die Übersichtlichkeit leidet. Ich werde auf solche Themen im weiteren Verlauf der Reihe eingehen und regelmäßig solche Dinge refactoren und damit verbessern.


Inhaltsverzeichnis

Vorbereitung

Für diesen Teil solltet ihr euch ein neues Projekt anlegen, so wie wir es in den letzten Teilen bereits besprochen hatten. Dazu könnt ihr folgende Dateien aus dem vorhergehenden Teil übernehmen und so als Basis verwenden.

  • D3DRenderView.h
  • D3DRenderView.cpp
  • main.h
  • main.cpp
  • View.h
  • View.cpp

Zusätzlich solltet ihr eine SampleGame04View.h und SampleGame04.cpp anlegen und in der WinMain entsprechend aufrufen, die dann wie folgt aussieht.


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	int returnValue;
	View* view;
	D3DRenderView* renderView = new SampleGame04View();

	view = new View(renderView);
	view->Initialize(hInstance, nCmdShow, WindowProc);
	returnValue = view->Run();
	view->Shutdown();
	delete view;
	
	delete renderView;

	return returnValue;
}

Ein Programmstart an dieser Stelle sollte nun dazu führen, dass wir den gleichen Stand wie im letzten Teil dieser Reihe erhalten, nur halt mit einem neuen Projekt. Sollte es hier noch zu Problemen kommen ist es ratsam, die vorherigen Teile nochmal durchzuarbeiten oder im Forum konkrete Fragen zu stellen.

Der Bauplan

Fangen wir wie immer mit dem Bauplan an. Im Rahmen dessen werde ich kurz erklären, was wir alles besprechen werden und bei der eigentlichen Implementierung werde ich genauer erklären, was und warum wir es machen. Der Bauplan gehört selbstverständlich in die Include-Datei SampleGame04View.h und sieht wie folgt aus.


#ifndef _SAMPLEGAME_04_VIEW_H_
#define _SAMPLEGAME_04_VIEW_H_

#include "D3DRenderView.h"

struct VERTEX
{
	FLOAT X, Y, Z;		// position
	D3DXCOLOR Color;	// color
};

class SampleGame04View : public D3DRenderView
{
public:
	SampleGame04View() {};

	void Initialize(HWND hWnd);
	void Shutdown();

	void RenderFrame();

private:
	ID3D11VertexShader *pVS;
	ID3D11PixelShader *pPS;
	ID3D11InputLayout *pLayout;
	ID3D11Buffer *pVBuffer;
};

#endif

Wir werden unser eigenes Vertex-Format für einen Vertex Buffer aufbauen (Zeilen 27 und 28). Dieser Vertex Buffer wird dann mit den Vertices eines Dreiecks befüllt und dann durch unseren ersten Vertex- und Pixel-Shader (Zeilen 25 und 26) gerendert.

Das Vertex-Format

In den Zeilen 06 bis 10 erzeugen wir einen eigenen Datentyp, der das Format unserer Vertices beschreibt. Dies ist notwendig um strukturiert auf die Daten zugreifen zu können, die später im Vertex-Buffer auf der Grafikkarte liegen werden. Ein Vertex-Buffer ist dabei einfach ein Speicherbereich in dem Daten stehen. Diese Daten bestehen dabei schlicht und einfach aus einzelnen Bytes. Wie diese Daten zu interpretieren sind, müssen wir der Grafikkarte mitteilen. Dies erfolgt durch ein sogenanntes InputLayout. Zum besseren Verständnis: Bis einschliesslich DirectX 9 wurde das Pendant dazu übrigens Vertex Declaration genannt.

Was genau ein Vertex Buffer ist und wie dieser in DirectX angelegt und verwendet wird, darüber habe ich einen eigenständigen Artikel [1] geschrieben. Diesen solltet ihr euch durchlesen, da ich im weiteren Verlauf zwar einige Dinge daraus nochmals erklären werde, aber deutlich weniger detailliert.

Die Implementierung

Wir öffnen und bearbeiten nun die Datei SampleGame04View.cpp und beginnen mit der Implementierung. Was dem aufmerksamen Leser sicherlich direkt auffällt ist die Tatsache, dass wir zwei Methoden aus dem Basis-Render-View D3DRenderView überladen und zwar Initialize und Shutdown. Beginnen wir mit dem Rahmen der Initialize-Methode.

Initialisierung


void SampleGame04View::Initialize(HWND hWnd)
{
	D3DRenderView::Initialize(hWnd);

}

In Zeile 3 rufen wird die Initialize-Methode der Basisklasse auf. Das ist wichtig, da dort DirectX initialisiert wird und erst danach ein Device und ein DeviceContext zur Verfügung stehen. Diese werden wir im weiteren Verlauf benötigen.

Da wir ein Dreieck rendern möchten, benötigen wir erstmal Shader. Der Vertex Shader wird dabei die einzelnen Vertices unseres Dreiecks in eine für die Grafikpipeline verwendbare Form bringen und der Pixel Shader wird die einzelnen Bildpunkte im BackBuffer so setzen bzw. besser gesagt einfärben, wie wir es uns wünschen.

Dazu verwenden wir den folgenden Sourcecode, den wir ans Ende der Methode Initialize packen.


	ID3D10Blob *VS, *PS;
	ID3D10Blob* l_pBlob_Errors = NULL;
	LPVOID l_pError = NULL;

	D3DX11CompileFromFile(L"shaders.hlsl", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, &l_pBlob_Errors, 0);
	D3DX11CompileFromFile(L"shaders.hlsl", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, &l_pBlob_Errors, 0);

	device->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
	device->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);

	deviceContext->VSSetShader(pVS, 0, 0);
	deviceContext->PSSetShader(pPS, 0, 0);

In den Zeilen 1 bis 3 deklarieren wir ein paar Variablen, die wir für den weiteren Verlauf benötigen. Nichts besonderes soweit. In den Zeilen 5 und 6 benutzen wir dann eine Funktion aus der D3DX11-Toolbox: D3DX11CompileFromFile. Dies lädt einen HLSL Shader aus einer Textdatei und kompiliert diesen, so dass er von der Grafikkarte verwendet werden kann. Dabei können wir diverse Parameter angeben, wie z.B. den Namen der Datei (erster Parameter: shaders.hlsl), den Namen der Funktion innerhalb der Shader-Datei (dritter Parameter) und das Shader-Modell (vierter Parameter).

Der HLSL-Compiler von DirectX erzeugt ein Byte-Array und legt das Ergebnis in den Blob's VS und PS (für Vertex- und PixelShader) ab. Dies dauert eine gewisse Zeit und ermöglicht eine einfache Änderung des Shader-Codes. Während wir unser Spiel entwickeln ist dies sehr praktisch, aber später wenn wir das Spiel veröffentlichen wollen wir uns natürlich sowohl die Zeit sparen, als auch verhindern, dass der Spieler den Shader abändern kann. Die Richtlinien für Metro Style Games unter Windows 8 erfordern es sogar, dass im fertigen Produkt ausschliesslich mit kompilierten Shadern gearbeitet wird, da der HLSL-Compiler nicht mit dem Spiel ausgeliefert werden darf. Wie wir das machen, werde ich in diesem Tutorial nicht zeigen, denn das würde den Rahmen sprengen und ist auch erstmal nicht so wichtig, da wir noch einen weiten Weg vor uns haben, bis wir ein Spiel veröffentlichen können.

In den Zeilen 8 und 9 laden wir den kompilierten Shader auf die Grafikkarte. Die beiden Create-Methoden legen dabei einen Zeiger auf den Shader in die Variablen pVS und pPS ab, die wir in der Header-Datei als private Member-Variablen deklariert haben.

In den Zeilen 11 und 12 teilen wir der Grafikkarte mit, dass für alle nachfolgenden Render-Vorgänge unser Pixel- und Vertex-Shader verwendet werden soll. Diese Angabe ist so lange gültig, bis etwas anderes eingestellt wird. Für dieses einfache Beispiel reicht es aus, dass wir das einmalig in der Initialize-Methode machen. Wir verwenden nur einen Shader und auch rendern wir nur ein einziges Dreieck. Wenn mehrere Effekte und damit Shader verwendent werden sollen, dann ist es sinnvoll dies unmittelbar vor dem jeweiligen Befehl zum rendern durchzuführen, damit sichergestellt ist, dass der richtige Shader eingestellt wurde.

Selbstverständlich benötigen wir auch den Shader. Dazu legen wir die Datei shaders.hlsl an und kopieren den folgenden Text in die Datei:


struct VOut
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

VOut VShader(float4 position : POSITION, float4 color : COLOR)
{
    VOut output;

    output.position = position;
    output.color = color;

    return output;
}


float4 PShader(float4 position : SV_POSITION, float4 color : COLOR) : SV_TARGET
{
    return color;
}

Die Datei sollte sich im gleichen Verzeichnis befinden wie die Source-Dateien. Wenn wir später unser Programm ohne Visual Studio starten wollen (also durch Doppelklick auf die erzeugte .EXE-Datei) müssen wir diese Datei in den Debug- bzw. Release-Ordner kopieren, in dem sich auch die ausführbare Datei befindet.

Die Shader, die wir damit erzeugen haben praktisch die einfachste Form, die möglich ist. Der Vertex-Shader leitet einfach sowohl die Positionen und Farben aus dem Vertex-Buffer an den Pixel-Shader weiter und der Pixel-Shader schreibt einfach die Farbe, die ihm übergeben wurde. Der Code sollte einigermaßen verständlich sein. Zum jetzigen Zeitpunkt ist dieser aber auch noch nicht so wichtig. Wir wollen schliesslich erstmal nur ein Dreieck rendern.

Zurück zu unserem C++-Sourcecode. Der nächste Schritt wäre nun die Erzeugung eines Input-Layout das den Vertex Buffer mit dem Vertex Shader verbindet. Wir teilen der Grafikkarte also einfach mit, wie die Daten aus dem Vertex Buffer vom Vertex Shader interpretiert werden sollen. Den Code packen wir wieder in die Initialize-Methode.


	D3D11_INPUT_ELEMENT_DESC ied[] = 
	{
		{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0,  0, D3D11_INPUT_PER_VERTEX_DATA, 0},
		{"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
	};

	device->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
	
	deviceContext->IASetInputLayout(pLayout);

Eine relativ genaue Beschreibung was dort gemacht wird habe ich im bereits verlinkten Artikel über den Vertex Buffer bereitgestellt. Wir teilen dem Shader im Grunde genommen einfach mit, dass das erste Element im Vertex Buffer eine Positionsangabe ist, die 12 Byte lang ist, darauf folgt eine Farbangabe und das ganze wiederholt sich für jedes Element, das sich im Buffer befindet. Daraus erzeugen wird ein Input-Layout und setzen es als das Aktive im aktuellen Kontext.

Der nächste Schritt ist die Erzeugung der Vertex Daten für unser Dreieck im Arbeitsspeicher. Dies erfolgt mit folgendem Code:


	VERTEX OurVertices[] =
	{
		{ 0.00f,  0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
		{ 0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
		{-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)},
	};

Wie ich bereits erwähnt hatte, möchte ich aus Vereinfachungsgründen zunächst auf Transformationen usw. verzichten. Daher geben wir die Koordinaten unseres Dreiecks im sogenannten Screen Space an. Dies sind Bildschirmkoordinaten die auf allen Achsen im Bereich von -1.0 bis 1.0 verlaufen. Das Dreieck befindet sich somit in der Mitte des Bildschirms und ist 0.9 Einheiten Breit und 1.0 Einheiten hoch. Jeder Vertex bekommt eine eigene Farbe, damit wir die Ecken einfach den einzelnen Vertices zuordnen können.

Hinweis
Hinweis

Ein einfacher Trick zum Debuggen von im Code erzeugten Vertex-Daten ist das einfärben einzelner Vertices um eine optische Überprüfung zu haben.


Die Reihenfolge ist übrigens extrem wichtig und zwar aus einem ganz einfachen Grund: Wenn diese immer gleich ist, dann kann ich eine wichtige Information daraus gewinnen und zwar, von welcher Richtung das Dreieck betrachtet wird. Schaue ich von vorne auf das Dreieck, so sind die Vertices im Uhrzeigersinn angeordnet. Schaue ich mir dieses Dreieck jedoch von hinten an, so ist die Reihenfolge genau entgegengesetzt. Dies ist eine sehr, sehr wichtige Information für die Grafikkarte, die für eine wichtige Optimierung verwendet wird. Über die Vertex-Reihenfolge realisiert die Grafikkarte das sogenannte Backface Culling, bei dem alle Dreiecke entfernt werden, die man von hinten sieht, da diese normalerweise unsichtbar sind. Wird eine Kugel aus Dreiecken erzeugt, so kann dadurch fast die Hälfter aller Dreiecke ausgelassen werden, ohne das sichtbare Ergebnis zu beeinflussen.

Man kann diese Reihenfolge übrigens beeinflussen. Dies geschieht mit dem sogenannten CullMode des RasterizerStates. In bestimmten Fällen (bei Schattenwurf oder zum rendern von Lichtkegeln z.B.) kann es notwendig sein, daß die Richtung umgekehrt werden muss. In diesem Tutorial und auch sonst wird aber meistens mit der Default-Einstellung gearbeitet. Daran sollten wir uns auch halten und dies verinnerlichen, denn dann werden wir keine Probleme bekommen. Nicht einfach zu findende Fehler und unschöne Effekte entstehen nämlich immer dann, wenn man beide Richtungen kombiniert und Teile der Geometrie dadurch falsch oder garnicht dargestellt werden.

Der letzte Schritt der Intialisierung ist die Erzeugung des Vertex Buffer, sowie das hochladen der Vertex-Daten in den Speicher der Grafikkarte. Der Code dazu sollte aus dem Vertex Buffer Artikel bereits bekannt sein und ist dort ganz gut erklärt.


	D3D11_BUFFER_DESC bd;
	ZeroMemory(&bd, sizeof(bd));

	bd.Usage = D3D11_USAGE_DYNAMIC;
	bd.ByteWidth = sizeof(VERTEX) * 3;
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	device->CreateBuffer(&bd, NULL, &pVBuffer);

	D3D11_MAPPED_SUBRESOURCE ms;
	deviceContext->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);
	memcpy(ms.pData, OurVertices, sizeof(OurVertices));
	deviceContext->Unmap(pVBuffer, NULL);

Damit ist die Initialisierung abgeschlossen.

Aufräumen

Nachdem wird nun initialisiert haben, müssen wir auch wieder aufräumen, wenn das Programm beendet wird. Dazu überschreiben wir die Shutdown-Methode.


void SampleGame04View::Shutdown()
{
	pVBuffer->Release();

	pLayout->Release();

	pVS->Release();
	pPS->Release();

	D3DRenderView::Shutdown();
}

Wir geben den Vertex Buffer frei (Zeile 3), gefolgt vom Input Layout (Zeile 5) und machen mit Vertex- und Pixel-Shader weiter (Zeile 7 und 8). Der letzte Befehl ruft die Shutdown-Methode aus der Basisklasse auf. Es ist wichtig, dass dies als letztes geschieht, da dort DirectX freigegeben wird. Würden wir dies zuerst machen, so wären die Zeiger auf die zuvor freigegebenen Elemente unter Umständen ungültig und es könnte zum Absturz kommen.

Rendern

Das Rendern erfolgt - wie bereits ausführlich in den vorherigen Teilen ausgeführt - in der RenderFrame-Methode, die wir dazu natürlich etwas erweitern müssen.


void SampleGame04View::RenderFrame()
{
	D3DRenderView::deviceContext->ClearRenderTargetView(D3DRenderView::backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));

	UINT stride = sizeof(VERTEX);
	UINT offset = 0;
	deviceContext->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);

	deviceContext->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	deviceContext->Draw(3, 0);

	swapchain->Present(0, 0);
}

Die Zeilen 3 und 13 sind der Code, den wir im vorherigen Teil dieser Reihe entwickelt haben. In den Zeilen 5 bis 7 teilen wir nun der Grafikkarte mit, welcher Vertex Buffer zum Rendern verwendet werden soll. Genau wie die Angaben zu Input Layout und den Shadern gilt diese Einstellung solange, bis etwas anderes eingestellt wird. Die Parameter von IASetVertexbuffers sind hier ganz gut erklärt.

In Zeile 9 teilen wir der Grafikkarte mit, dass wir Dreiecke rendern wollen und in Zeile 11 rendern wir dann unsere 3 Vertices die unser gewünschtes Dreieck bilden.

Abschluss

Wenn wir nun unser Programm mit F5 starten, dann sollte das folgende Bild erscheinen.

Dx11 jumpstart first triangle.png

Und das war es auch schon in diesem Teil. Wir haben unser erstes Dreieck mit DirectX gerendert und damit schon große Fortschritte gemacht. Sicherlich fehlen noch einige wichtige Dinge, auf die ich in den nächsten Teilen dieser Reihe eingehen werde, aber erstmal haben wir einen Etappensieg errungen.

Ich hoffe, daß ihr meinen Ausführungen folgen konntet und etwas interessantes gelernt habt. Falls ihr noch Fragen habt, möchte ich euch bitten über die Diskussionsfunktion (Link links oben) im Forum mit mir Kontakt aufzunehmen. Dort beantworten alle User der Community und natürlich auch ich gerne eure Fragen.

Querverweise

  1. Erzeugung und Verwendung von Vertex Buffern mit DirectX 11

Sourcecode

Hier möchte ich nun nochmal den gesamten Source-Code der Datei SampleGame04View.cpp zusammenhängend darstellen, damit die Übersicht gewahrt bleibt.


#include "SampleGame04View.h"

void SampleGame04View::Initialize(HWND hWnd)
{
	D3DRenderView::Initialize(hWnd);

	ID3D10Blob *VS, *PS;
	ID3D10Blob* l_pBlob_Errors = NULL;
	LPVOID l_pError = NULL;

	D3DX11CompileFromFile(L"shaders.hlsl", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, &l_pBlob_Errors, 0);
	D3DX11CompileFromFile(L"shaders.hlsl", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, &l_pBlob_Errors, 0);

	device->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
	device->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);

	deviceContext->VSSetShader(pVS, 0, 0);
	deviceContext->PSSetShader(pPS, 0, 0);

	D3D11_INPUT_ELEMENT_DESC ied[] = 
	{
		{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0,  0, D3D11_INPUT_PER_VERTEX_DATA, 0},
		{"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
	};

	device->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
	
	deviceContext->IASetInputLayout(pLayout);

	VERTEX OurVertices[] =
	{
		{ 0.00f,  0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
		{ 0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
		{-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)},
	};

	D3D11_BUFFER_DESC bd;
	ZeroMemory(&bd, sizeof(bd));

	bd.Usage = D3D11_USAGE_DYNAMIC;
	bd.ByteWidth = sizeof(VERTEX) * 3;
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	device->CreateBuffer(&bd, NULL, &pVBuffer);

	D3D11_MAPPED_SUBRESOURCE ms;
	deviceContext->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);
	memcpy(ms.pData, OurVertices, sizeof(OurVertices));
	deviceContext->Unmap(pVBuffer, NULL);
}

void SampleGame04View::Shutdown()
{
	pVBuffer->Release();

	pLayout->Release();

	pVS->Release();
	pPS->Release();

	D3DRenderView::Shutdown();
}

void SampleGame04View::RenderFrame()
{
	D3DRenderView::deviceContext->ClearRenderTargetView(D3DRenderView::backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));

	UINT stride = sizeof(VERTEX);
	UINT offset = 0;
	deviceContext->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);

	deviceContext->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	deviceContext->Draw(3, 0);

	swapchain->Present(0, 0);
}

Navigation
Tutorials und Artikel
Community Project
Werkzeuge