In diesem Teil geht es jetzt darum, ein Direct3D Device zu erzeugen und den Hintergrund mit einer Farbe zu löschen. Klingt nicht nach viel, ist aber viel Arbeit.
Technisches Wissen
Bevor wir anfangen Code zu schreiben, betrachten wir erst einmal, wie wir jetzt arbeiten müssen und welche Grundkomponenten es in DirectX gibt. Betrachten wir erst einmal den funktionalen Teil und schauen uns an, wie unser Programm strukturiert ist:
Hier sind drei grün hinterlegte neue Punkte auffällig. Zum einem müssen wir in der Initialisierungsroutine DirectX Initialisieren, zum anderen in der Hauptschleife einmal unser gewünschtes Ergebnis rendern (zu deutsch: Zeichnen) und am Ende unsere Objekte releasen (zerstören).
Wir fügen also drei Methoden hinzu: bool Initialize(HINSTANCE hInstance), void Render() und void Release(), welche in der Startfunktion einfach aufgerufen werden. In der Initialisierungsfunktion rufen wir dann AssignApplicationAndCreateWindow() und InitializeDirectX10() auf.
Die InitializeDirectX10()-Funktion initialisiert alles Wichtige für Direct3D 10 auf. Doch was ist wichtig?
Für unseren Zweck gibt es drei Interfaces, die wir bedienen werden:
Interfacename | Deklarationsname | Beschreibung |
---|---|---|
ID3D10Device | gDevice | Dieses Interace verwaltet unseren Hauptkern von DirectX 10. Man könnte es als „das Arbeitspferd“ bezeichnen. |
ID3D10RenderTargetView | gRenderTarget | Man kann sich hier das Interface zum Monitorbild indirekt vorstellen, auf das wir malen. |
IDXGISwapChain | gSwapChain | Dieses Interface bestimmt so gesehen, wo unser Bild platziert wird und wann. Wir greifen hier ansatzweise auf die Hardware zu. |
Haben wir das Render Target View (RTV) erzeugt, so binden wir es an das Device. Dann erstellen wir einen Viewport, der den Zeichnungsbereich des RTV bestimmt, und binden dieses auch an das Device. Dann ist alles initialisiert.
In der Render()-Funktion werden wir dem Device anordnen, den Hintergrund von zugewiesenen RTV zu löschen. Ist dies erledigt, so schicken wir dem SwapChain eine Anweisung, das Bild auf dem Monitor zu präsentieren.
Die Release()-Funktion räumt nach dem Verlassen der Hauptschleife alle Objekte auf. Hier ist zu beachten, dass man vom Vollbildmodus in den Fenstermodus wechselt vor dem Releasen des SwapChains.
Hier noch einmal ein Bild, um die Beziehungen zu den Interfaces zu verdeutlichen: Implementation
Wie angekündigt ist unsere Hauptfunktion nun etwas abgeändert:
Code: Alles auswählen
// Initialisieren
if(!Initialize(hInstance)) return 0;
// Die Hauptschleife
while(RunWindow())
{
Render();
}
// Programm verlassen
Release();
return 0;
Code: Alles auswählen
bool Initialize(HINSTANCE hInstance)
{
if(!AssignApplicationAndCreateWindow(hInstance)) return false;
if(!InitializeDirectX10()) return false;
return true;
}
Code: Alles auswählen
bool InitializeDirectX10()
{
// Swap Chain Desc füllen (Konfiguration)
DXGI_SWAP_CHAIN_DESC dsc = {0};
dsc.BufferCount = 1;
dsc.BufferDesc.Width = 1024;
dsc.BufferDesc.Height = 768;
dsc.BufferDesc.RefreshRate.Denominator = 1; // 60Hz
dsc.BufferDesc.RefreshRate.Numerator = 60;
dsc.Windowed = WindowMode;
dsc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 32Bit + Alpha
dsc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // Als Render Target benutzen
dsc.OutputWindow = hWnd; // Unser Fenster übergeben
dsc.SampleDesc.Count = 1; // Qualität des Samplings = 1
dsc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; // Pointertausch statt kopieren
// Device erzeugen
if(FAILED(D3D10CreateDeviceAndSwapChain(NULL /*Standardadapter*/,
D3D10_DRIVER_TYPE_HARDWARE /* Hardware */,
NULL /* Kein Softwaremodul */,
0, // Keine Flags
D3D10_SDK_VERSION, // SDK-Version benutzen
&dsc, // Konfigurationsstrutktur
&gSwapChain, // Swap Chain
&gDevice))) // Device
return false;
...
Elementname | Bedeutung |
---|---|
BufferDesc {Width, Height, RefreshRate, Format, ScanlineOrdering, Scaling} | Auflösung, Bildwiederholrate (Achtung: Denominator = 1), Bildformat (gleich), Zeichenmodus und Bildposition. |
SampleDesc {Count, Quality} | Beschreibung der Kantenglättung, Count ist ist größer als 0 und Quality ist etwas von 2^n "2 hoch n", 1 oder 0. |
BufferUsage | Beschreibung der Benutzung, in unserem Falle DXGI_USAGE_RENDER_TARGET_OUTPUT. |
BufferCount | Anzahl der BackBuffer, die "Zeichenfläche" |
OutputWindow | Ausgabefenster |
SwapEffect | Wie soll der Backbuffer kopiert werden. In unserem Falle Zeigertausch der Daten: DXGI_SWAP_EFFECT_DISCARD. |
Flags | Sonstige Einstellungskonstanten |
Demnach ist R8G8B8A8 eine Komponente von Rot 1Byte, Grün 1Byte, Blau 1Byte und Alpha 1Byte. Bei Alpha handelt es sich um die Transparenz. Man kann dies alles als char[4] verstehen und ist auf dem Monitor intern tatsächlich unsigned char[4].
Dahinter stehen dann Informationen wie _UINT, _SINT, _FLOAT, _UNORM und _TYPELESS. UINT heißt unsigned int und ist je nach Bitgröße unsigned char, unsigned short oder unsigned int; _SINT ist es dann jeweils signed (Vorzeichenbehaftet); _UNORM ist als skalierte unsigned int zwischen 0 und 1 zu verstehen. _FLOAT ist dann eine Gleitkommazahl in 16Bit- oder 32Bit-Form und _TYPELESS ist Typenlos.
Wir werden DXGI_FORMAT_R8G8B8A8_UNORM verwenden, also: Jeweils 8 Bit pro Farbkomponente zwischen 0 und 1 mit Transparenz --> 32Bit + Alpha.
DXGI ist eine Bibliothek von DirectX, um auf die Hardware zuzugreifen. Zum Beispiel können wir damit prüfen, ob ein Adapter (Grafikkarte) eine Funktionalität für DirectX 10 überhaupt vorhanden ist oder oder ob überhaupt die gewünschte Auflösung verfügbar ist. Das ist jedoch nicht jetzt bestandteil unseres Wissens.
Danach initialisieren wir alles mittels D3D10CreateDeviceAndSwapChain(). Der erste Parameter erwartet einen Adapter (Grafikkarte) - lassen wir ihn NULL, so wählt Direct3D den Standardadapter. Der Zweite gibt an, wie Direct3D läuft. Wir können ihn im Hardwaremodus laufen lassen (siehe Quellcode) oder im Referenzmodus (Softwaremodus). In Direct3D 11 kommt noch der Warp-Modus hinzu - dort ist allerdings die Methode leicht verändert deklariert. Dann folgen Flags und eine Konstante zur benutzen SDK von Direct3D. Normalerweise setzt man hier D3D10_SDK_VERSION ein. Dann wird ein Zeiger auf unsere Konfigurationstruktur, unserem SwapChain und dem Device erwartet.
Die Definition FAILED() prüft, ob etwas in einer Direct3D-Funktion/-Methode schief gegangen ist von Rückgabetyp HRESULT her. Es gibt auch das Gegenstück SUCCEEDED() welches prüft, ob alles glatt ging. Geht an dieser Codestelle etwas schief, hat man
- Kein Windows Vista / Seven
- Keine Direct3D 10 fähige Grafikkarte
- Oder eine Überforderung von den Einstellungen her konfiguriert
Code: Alles auswählen
...
// Render Target View erzeugen und dem Device zuordnen
// Textur holen
ID3D10Texture2D* BackBuffer = NULL;
if(FAILED(gSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), (void**)&BackBuffer)))
return false;
// Render Target View erstellen und zuordnen
if(FAILED(gDevice->CreateRenderTargetView(BackBuffer, NULL, &gRenderTarget)))
return false;
BackBuffer->Release();
gDevice->OMSetRenderTargets(1, &gRenderTarget, NULL);
...
ID3D10Texture2D ist unser Erstkontakt mit einem Texturinterface. Es beinhaltet eine Textur (in diesem Fall einen BackBuffer). Wie wir damit erweitert arbeiten, das wird später Thema sein. Hier "missbrauchen" wir dieses Interface, um vom SwapChain einen Zeiger zur Textur für den Monitor zu bekommen und überreichen diesen Zeiger von der Textur dem RTV und damit dem Device.
Ist dies geglückt, so können wir das RTV mittels ID3D10Device::CreateRenderTargetView() erstellen. Hier wird als erstes ein Zeiger zum Backbuffer erwartet. Als zweites kann man eine Beschreibung des RTVs einfügen - was wir nicht müssen. Dann geben wir den Zeiger des Zeigers des RTVs an. Das Texturobjekt können wir danach schon getrost aufräumen lassen.
Ist dies geglückt, koppeln wir das RTV mittels ID3D10Device::OMSetRenderTargets an das Device an. Als erstes möchte es die Anzahl der RTVs wissen, dann einen Zeiger zu den RTVs (hier & da Einzelelement) und als drittes kommt ein Depth Stencil View (DSV) [Z-Buffer] ins Spiel - den wir jedoch erstmal vernachlässigen werden.
OM heißt bei der Methode übrigens Output Merger. Es gibt noch mehr. Hier ganz kurz in einer Liste was was ist:
Abkürzung | Bedeutung |
---|---|
IA | Input-Assembler-Stage: Unsere Rohdaten für Direct3D - so zum Beispiel Objektpunkte etc. |
VS | Vertex-Shader-Stage: Unser Renderer für die Objektpunkte |
GS | Geometry-Shader-Stage: Unser "Geometrie-Former" |
SO | Stream-Ouput-Stage: Die Rückgabedaten des "Geometrie-Formers" |
RS | Rasterizer-Stage: Wie etwas gezeichnet werden soll |
PS | Pixel-Shader-Stage: Wie die Pixel gerendert werden |
OM | Output-Merger-State: Wie (wo) etwas ausgegeben wird. |
Was wir jetzt noch zu guter letzt zum Initialisieren machen müssen, ist einfach einen Viewport (VP) zu erstellen und an Direct3D zu binden. Dieser VP bestimmt unseren Zeichenbereich in die Breite, Höhe und Tiefe. Dabei gibt es jeweils einen Minimalwert (Offset) und eine dazu passende Breite. Hier die Struktur und alles kompakt in Kommentaren erklärt:
Code: Alles auswählen
// View Port erzeugen und festlegen
D3D10_VIEWPORT vp = {0};
vp.TopLeftX = 0; // Obere X-Ecke in px
vp.TopLeftY = 0; // Obere Y-Ecke in px
vp.Width = 1024; // Breite der Zeichenfläche
vp.Height = 768; // Höhe der Zeichenfläche
vp.MinDepth = 0.0f; // Minimaler Tiefenwert
vp.MaxDepth = 1.0f; // Maximaler Tiefenwert
// Viewport festlegen
gDevice->RSSetViewports(1, &vp);
return true;
}
Das war es zur Initialisierung.
Das Rendering wird wirklich einfacher. Wir werden hier einfach ID3D10Device::ClearRenderTargetView() ausführen, um den Hintergrund mit einer Farbe zu löschen und einmal IDXGISwapChain::Present() um das Bild zu präsentieren.
Code: Alles auswählen
void Render()
{
// Backbuffer mit Rot löschen:
gDevice->ClearRenderTargetView(gRenderTarget, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f));
gSwapChain->Present(1, 0);
}
IDXGISwapChain::Present() zeigt unser Bild dann an. Der erste Parameter ist ein Divisor zur Präsentationsrate. Wenn wir 60Hz haben und einen Divisor von 1, sind das 60 Bilder die Sekunde. Bei 2 sind es 30 Bilder und so weiter. Bei 0 wird das Bild ohen Warten angezeigt - was dazu führt, dass man sieht, wie das Bild auf dem Monitor entsteht.
Das Releasen (Aufräumen) ist relativ simpel. Wir müssen nur darauf achten, dass wir die SwapChain vorher wieder auf Fenstermodus zurücksetzen:
Code: Alles auswählen
void ReleaseDirectX10()
{
if(gRenderTarget) gRenderTarget->Release(); // Render Target releasen
gSwapChain->SetFullscreenState(false, NULL); // Wichtig bei Vollbild
if(gSwapChain) gSwapChain->Release(); // SwapChain releasen
if(gDevice) gDevice->Release(); // Device releasen
}
void Release()
{
ReleaseDirectX10();
}
Folgende Bibliotheken werden benötigt: Windows-Standardbibliothek, d3d10.lib, d3dx10.lib und dxguid.lib
Zusammenfassung
Dieser Teil des Tutorials ist so gesehen ein wirklich harter, aber unvermeidbarer, Einstieg. Wer bis hier ohne Probleme weiß, was eigentlich passiert, der ist auf dem besten Wege Direct3D zu verstehen. Hier sollte man auch wirklich üben, bis man alles verstanden hat.
Die nächsten Tutorials werden wieder deutlich einfacher für den Einsteiger und man wird deutlich mehr Ergebnisse sehen als "nur" ein blank gelöschter Hintergrund.
Auf jeden Fall hat man schon eine Menge geschafft, wenn man auch nicht viel sieht.
Aufgaben
1. Ändere die Hintergrundfarbe zu einem Olive!
2. Spiele ein bisschen mit der Konfigurationsstruktur!
3. Lass die Hintergrundfarbe sich mit der Zeit ändern in Sinusform mit je unterschiedlichen Zeitkopplungen!