English Русский 中文 Español 日本語 Português
preview
DirectX-Tutorial (Teil I): Zeichnen des ersten Dreiecks

DirectX-Tutorial (Teil I): Zeichnen des ersten Dreiecks

MetaTrader 5Integration | 26 April 2022, 08:06
414 1
Rorschach
Rorschach

Inhaltsverzeichnis

  1. Einführung
  2. DirectX-API
    1. Geschichte von DirectX
    2. Direct3D
    3. Device
    4. Device Context
    5. Swap Chain
    6. Eingabe-Layout
    7. Topologie der Primitiven
    8. HLSL
  3. Grafik-Pipeline
  4. 3D-Grafiken
    1. Primitive
    2. Vertexes
    3. Farbe
  5. Abfolge von Aktionen in MQL
  6. Praxis
    1. Übersicht der Klassen
    2. Vertex-Array
    3. Initialisierung
    4. Erstellen einer Leinwand
    5. DirectX-Initialisierung
    6. Bildanzeige
    7. Freigeben von Ressourcen
    8. Shader
    9. OnStart
  7. Schlussfolgerung
  8. Referenzen und Links

Einführung

Ein Initiationsritus oder Initialisierungsritus ist das, was einem in den Sinn kommt, wenn man versteht, dass man so viel Code schreiben und riesige C++-Strukturen füllen muss, um mit DirectX auch nur ein primitives Dreieck zu zeichnen. Ganz zu schweigen von komplexeren Dingen, wie Texturen, Transformationsmatrizen und Schatten. Glücklicherweise hat sich MetaQuotes darum gekümmert: Sie haben die ganze Routine versteckt und nur die nötigsten Funktionen übrig gelassen. Dies hat jedoch einen Nebeneffekt: Jeder, der sich nicht mit DirectX auskennt, kann nicht das ganze Bild sehen und verstehen, warum und wie das alles geschieht. Es muss noch viel Code in MQL geschrieben werden.

Ohne zu verstehen, was sich unter der Haube befindet, ist DirectX verwirrend: "Warum ist es so schwierig und verwirrend, kann man es nicht einfacher machen?" Und das ist nur der erste Schritt. Im weiteren Verlauf lernen wir die Shader-Sprache HLSL und die Besonderheiten der Grafikkartenprogrammierung kennen. Um all diese Verwirrungen zu vermeiden, schlage ich vor, die interne Struktur von DirectX zu betrachten, ohne dabei zu sehr ins Detail zu gehen. Dann werden wir ein kleines Skript in MQL schreiben, das ein Dreieck auf dem Bildschirm anzeigt.


DirectX-API

Geschichte von DirectX

DirectX ist ein Satz von APIs (Application Programming Interface) für die Arbeit mit Multimedia und Video auf Microsoft-Plattformen. Es wurde in erster Linie für die Entwicklung von Spielen entwickelt, aber im Laufe der Zeit begannen die Entwickler, es auch für technische und mathematische Software zu verwenden. DirectX ermöglicht die Arbeit mit Grafiken, Sound, Eingaben und Netzwerken, ohne auf Low-Level-Funktionen zugreifen zu müssen. Die API erschien als Alternative zum plattformübergreifenden OpenGL. Bei der Entwicklung von Windows 95 führte Microsoft wesentliche Änderungen ein, die die Entwicklung von Anwendungen und Spielen erschwerten und somit die Popularität dieses Betriebssystems beeinträchtigen könnten. DirectX wurde als Lösung geschaffen, um mehr Programmierer dazu zu bringen, Spiele für Windows zu entwickeln. Die Entwicklung von DirectX wurde von Craig Eisler, Alex St. John und Eric Engstrom.

  • September 1995. Erste Veröffentlichung. Es war eine recht primitive Version, die als Zusatz zur Windows-API lief. Sie fand nicht viel Beachtung. Die Entwickler verwendeten hauptsächlich DOS, für das das neue Betriebssystem höhere Systemanforderungen stellte. Außerdem gab es zu diesem Zeitpunkt bereits OpenGL. Es war nicht klar, ob Microsoft DirectX weiterhin unterstützen würde.
  • Juni 1996. Die Veröffentlichung von Version 2.
  • September 1996. Version 3.
  • August 1997. Anstelle der vierten Version veröffentlicht Microsoft die Version 5. Mit dieser Version wurde es einfacher, Code zu schreiben, und die Programmierer begannen, ihr Aufmerksamkeit zu schenken.
  • August 1998. Version 6. Die Arbeit wurde weiter vereinfacht.
  • September 1999. Version 7. Es wurde möglich, Vertex-Puffer im Videospeicher zu erstellen, was einen großen Vorteil gegenüber OpenGL darstellt.
  • November 2000. Version 8. Ein entscheidender Moment. Bis zu diesem Zeitpunkt versuchte DirectX aufzuholen, aber Version 8 überholte die Industrie. Microsoft begann, mit Grafikkartenherstellern zusammenzuarbeiten. Vertex- und Pixel-Shader erscheinen. Die Entwicklung erforderte nur einen Personal Computer, im Gegensatz zu OpenGL, das eine Workstation erforderte.
  • Dezember 2002. Version 9. DirectX ist zum Industriestandard geworden. Die Shader-Sprache HLSL erscheint. Wahrscheinlich war dies die langlebigste Version von DirectX. Wie Socket 775...
  • November 2006. Version 10. Im Gegensatz zu Version 9 hatte diese eine Bindung an das Betriebssystem Vista, das nicht sehr beliebt war. Diese Faktoren wirkten sich negativ auf den Erfolg von Version 10 aus. Microsoft fügte einen Geometrie-Shader hinzu.
  • Oktober 2009. Version 11. Hinzufügung von Tessellation, Compute Shader, verbesserte Arbeit mit Multi-Core-Prozessoren.
  • Juli 2015. Version 12. Low-Level-API. Die Version bot eine noch bessere Kompatibilität mit Multi-Core-Prozessoren, die Möglichkeit, die Ressourcen mehrerer Grafikkarten von verschiedenen Herstellern zu kombinieren, Raytracing.


Direct3D

Direct3D ist eine von vielen Komponenten der größeren DirectX-API; sie ist für die Grafik zuständig und dient als Vermittler zwischen Anwendungen und dem Grafikkartentreiber. Direct3D basiert auf COM (Component Object Model). COM ist ein Application Binary Interface (ABI) Standard, der 1993 von Microsoft eingeführt wurde. Er wird zur Erstellung von Objekten bei der Interprozesskommunikation (IPC) in verschiedenen Programmiersprachen verwendet. COM erschien als eine Lösung, die darauf abzielte, einen sprachunabhängigen Weg zur Implementierung von Objekten zu bieten, die außerhalb ihrer Erstellungsumgebung verwendet werden können. COM ermöglicht die Wiederverwendung von Objekten, ohne ihre interne Implementierung zu kennen, da sie gut definierte Schnittstellen bieten, die von der Implementierung getrennt sind. COM-Objekte sind für ihre eigene Erstellung und Zerstörung unter Verwendung der Referenzzählung verantwortlich.

Schnittstellen

Schnittstellen


Device

Alles in Direct3D beginnt mit Device. Es wird verwendet, um Ressourcen (Puffer, Textur, Shader, Statusobjekte) und die Aufzählung der Fähigkeiten von Grafikadaptern zu erstellen. Device ist ein virtueller Adapter, der sich auf dem System des Nutzers befindet. Bei dem Adapter kann es sich entweder um eine echte Grafikkarte oder ihre Software-Emulation handeln. Am häufigsten werden Hardware-Geräte verwendet, da sie die höchste Leistung bieten. Device bietet eine einheitliche Schnittstelle zu all diesen Adaptern und verwendet sie, um Grafiken auf einen oder mehrere Ausgänge zu rendern.

Direct3D

Device


Device Context

Device Context ist für alles zuständig, was mit Rendering zu tun hat. Dazu gehören die Konfiguration der Pipeline und die Erstellung von Befehlen für das Rendering. Device Context erschien in der elften Version von DirectX — vorher wurde das Rendering von Device implementiert. Es gibt zwei Arten von Context: Immediate Context und Deferred Context.

Der Immediate Context ermöglicht den Zugriff auf Daten auf der Grafikkarte und die sofortige Ausführung einer Befehlsliste auf dem Gerät. Jedes Device hat nur einen Immediate Context. Es kann jeweils nur ein Thread auf ihn zugreifen. Um den Zugriff für mehrere Threads zu ermöglichen, sollte die Synchronisierung verwendet werden.

Der Deferred Context fügt der Befehlsliste Befehle hinzu, die später im Immediate Context ausgeführt werden. Somit durchlaufen alle Befehle letztendlich den Immediate Context. Deferred Context ist mit einem gewissen Overhead verbunden, so dass die Vorteile seiner Verwendung nur bei der Parallelisierung ressourcenintensiver Aufgaben sichtbar werden. Sie können mehrere Deferred Contexts erstellen und auf jeden von einem separaten Thread aus zugreifen. Um jedoch von mehreren Threads aus auf denselben Deferred Context zugreifen zu können, ist eine Synchronisierung erforderlich, genau wie beim Immediate Context.


Swap Chain

Swap Chain wurde entwickelt, um einen oder mehrere Rückpuffer zu erstellen. In diesen Puffern werden gerenderte Bilder gespeichert, bis sie auf dem Bildschirm angezeigt werden. Der vordere und der hintere Puffer funktionieren wie folgt. Der vordere Puffer ist das, was Sie auf dem Bildschirm sehen. Der hintere Puffer ist das Bild, auf das gerendert wird. Dann werden die Puffer vertauscht: Der vordere wird zum hinteren, und der hintere kommt in den vorderen. Und der ganze Vorgang wird immer wieder wiederholt. So sehen wir immer das Bild, während das nächste "hinter den Kulissen" gerendert wird.

Swapchain

Swap Chain

Device, Device Context und Swap Chain sind die Hauptkomponenten, die zum Rendern eines Bildes benötigt werden.


Eingabe-Layout

Das Eingabe-Layout informiert die Pipeline über die Struktur des Vertex-Puffers (Vertex = Scheitelpunkt). Für unsere Zwecke benötigen wir nur die Koordinaten, weshalb wir einfach das Array der Scheitelpunkte vom Typ float4 übergeben können, ohne eine spezielle Struktur zu verwenden. float4 ist eine Struktur, die aus vier Float-Variablen besteht.

struct float4
  {
   float x;
   float y;
   float z;
   float w;
  };

Betrachten wir zum Beispiel eine komplexere Vertex-Struktur, die aus einer Koordinate und zwei Farben besteht:

struct Vertex
  {
   float4 Pos;
   float4 Color0;
   float4 Color1;
  };

Input layout in MQL für diese Struktur sieht wie folgt aus:

DXVertexLayout layout[3] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT},
                            {"COLOR", 0, DX_FORMAT_R32G32B32A32_FLOAT},
                            {"COLOR", 1, DX_FORMAT_R32G32B32A32_FLOAT}};

Jedes Element des Arrays "layout" beschreibt das entsprechende Element der Vertex-Struktur.

  • Das erste Element der Struktur DXVertexLayout ist der semantische Name. Er wird verwendet, um die Elemente der Vertex-Struktur mit den Elementen der Struktur im Vertex-Shader abzubilden. "POSITION" bedeutet, dass der Wert für die Koordinaten zuständig ist, "COLOR" wird für die Farbe verwendet.
  • Das zweite Element ist der semantische Index. Wenn wir mehrere Parameter desselben Typs übergeben müssen, zum Beispiel zwei Farbwerte, wird der erste mit Index 0 und der zweite mit Index 1 übergeben.
  • Das letzte Element beschreibt den Typ, in dem der Wert in der Vertex-Struktur dargestellt wird. DX_FORMAT_R32G32B32A32_FLOAT bedeutet wörtlich, dass es sich um eine RGBA-Farbe handelt, die durch einen 32-Bit-Fließkommawert für jede Komponente dargestellt wird. Dies kann verwirrend sein. Dieser Typ kann verwendet werden, um Koordinaten zu übergeben — er liefert Informationen über vier 32-Bit-Fließkommazahlen, genau wie float4 in der Vertex-Struktur.


Topologie der Primitiven

Der Vertex-Puffer speichert Informationen über die Punkte, aber wir wissen nicht, wie sie im Primitiv relativ zueinander angeordnet sind. Dafür wird die Primitive Topology verwendet. Point List bedeutet, dass der Puffer einzelne Punkte speichert. Line Strip stellt den Puffer als verbundene Punkte dar, die eine Polylinie bilden. Je zwei Punkte in der Linienliste beschreiben eine einzelne Linie. Triangle Strip und Triangle List legen die Reihenfolge der Punkte für Dreiecke fest, ähnlich wie bei Linien.

Topologie

Topologie

HLSL

High Level Shading Language (HLSL) ist eine C-ähnliche Sprache zum Schreiben von Shadern. Shader wiederum sind Programme, die für die Ausführung auf einer Grafikkarte konzipiert sind. Die Programmierung in allen GPGPU-Sprachen ist sehr ähnlich und hat eine spezifische Eigenschaft, die mit dem Design von Grafikkarten zusammenhängt. Wenn Sie Erfahrung mit OpenCL, Cuda oder OpenGL haben, werden Sie HLSL sehr schnell verstehen. Wenn Sie jedoch nur Programme für CPUs erstellt haben, kann es schwierig sein, auf ein neues Paradigma umzusteigen. Oft werden die Optimierungsmethoden, die traditionell für den Prozessor verwendet werden, nicht funktionieren. Für den Prozessor wäre es zum Beispiel richtig, die "if"-Anweisung zu verwenden, um unnötige Berechnungen zu vermeiden oder den optimalen Algorithmus auszuwählen. Auf dem Grafikprozessor hingegen kann dies die Programmausführungszeit erhöhen. Um das Maximum herauszuholen, müssen Sie möglicherweise die Anzahl der beteiligten Register zählen. Die drei wichtigsten Grundsätze für eine hohe Leistung bei der Programmierung von Grafikkarten sind: Parallelität, Durchsatz und Auslastung.


Grafik-Pipeline

Die Pipeline dient dazu, eine 3D-Szene in eine 2D-Darstellung zu konvertieren. Die Pipeline ist ein Spiegelbild der internen Struktur der Grafikkarte. Das folgende Diagramm zeigt, wie der Datenstrom vom Eingang der Pipeline bis zu ihrem Ausgang durch alle Stufen (stages) fließt. Das Oval zeigt Stufen, die mit der HLSL-Sprache programmiert werden — Shader — und das Rechteck zeigt feste Stufen. Einige von ihnen sind optional und können einfach übersprungen werden.

Grafik-Pipeline

Grafik-Pipeline

  • Die Input-Assembler-Stage empfängt Daten aus den Vertex- und Indexpuffern und bereitet sie für den Vertex-Shader vor.

  • Die Vertex-Shader-Stage führt Operationen mit Scheitelpunkten durch. Programmierbare Stufen. Obligatorisch in der Pipeline.
  • Die Hull-Shader-Stage ist für die Ebene der Tesselierung zuständig. Programmierbare Stufen. Optional.
  • Die Tessellatorstufe erzeugt kleinere Primitive. Feste Stufen. Optional.
  • Domain Shader-Stage berechnet die endgültigen Scheitelpunktwerte (vertex) nach der Tesselierung. Programmierbare Stufen. Optional.
  • Geometry Shader-Stage wendet verschiedene Transformationen auf Primitive (Punkte, Linien, Dreiecke) an. Programmierbare Stufen. Optional.
  • Stream-Output-Stage überträgt Daten an den GPU-Speicher, von wo aus sie zurück in die Pipeline gesendet werden können. Feste Stufen. Optional.
  • Die Rasterizer-Stage schneidet alles ab, was nicht in den Anwendungsbereich fällt, und bereitet die Daten für den Pixel-Shader vor. Feste Stufen.
  • Pixel-Shader-Stage führt Pixel-Operationen durch. Programmierbare Stufen. Obligatorisch in der Pipeline.

  • Output-Merger-Stage formt das endgültige Bild. Feste Stufen.

Ein weiterer erwähnenswerter Shader ist der Compute Shader (DirectCompute), der eine separate Pipeline darstellt. Dieser Shader ist für allgemeine Berechnungen konzipiert, ähnlich wie OpenCL und Cuda. Programmierbare Stufen. Optional.

Die DirectX-Implementierung von MetaQuotes umfasst weder DirectCompute noch die Tesselation-Stage. Wir haben also nur drei Shader: Vertex, Geometry und Pixel.


3D-Grafiken

Primitive

Das Rendering von Primitiven ist der Hauptzweck der Grafik-API. Moderne Grafikkarten sind für das schnelle Rendern einer großen Anzahl von Dreiecken ausgelegt. Beim derzeitigen Entwicklungsstand der Computergrafik ist die effektivste Methode zum Zeichnen von 3D-Objekten die Erstellung einer Fläche aus Polygonen. Eine Fläche kann durch die Angabe von nur drei Punkten beschrieben werden. 3D-Modellierungssoftware verwendet oft Rechtecke, aber die Grafikkarte erzwingt trotzdem die Umwandlung der Polygone in Dreiecke.

Netz (mesh)

Netz aus Dreiecken

Vertexes

Um ein Dreieck in Direct3D zu rendern, müssen drei Scheitelpunkte angegeben werden. Es mag den Anschein haben, dass ein Scheitelpunkt die Position eines Punktes im Raum ist, aber in Direct3D ist es etwas mehr als das. Zusätzlich zur Scheitelpunktposition können wir Farbe, Texturkoordinaten und Normalen übergeben. Im Allgemeinen werden Matrixtransformationen verwendet, um Koordinaten zu normalisieren. Um die Sache nicht zu kompliziert zu machen, ist zu berücksichtigen, dass die Koordinaten der Eckpunkte in der Phase der Rasterung entlang der X- und Y-Achse im Bereich [-1; 1] liegen müssen; entlang der Z-Achse zwischen 0 und 1.


Farbe

Die Farbe in der Computergrafik hat drei Komponenten: Rot, Grün und Blau. Dies hängt mit den strukturellen Merkmalen des menschlichen Auges zusammen. Display-Pixel bestehen ebenfalls aus drei Unterpixeln dieser Farben. MQL hat die Funktion ColorToARGB, um Webfarben in das ARGB-Format umzuwandeln, das neben der Farbe auch die Transparenz speichert. Die Farbe kann normalisiert sein, wenn die Komponenten im Bereich [0;1] liegen, und nicht normalisiert: zum Beispiel haben die Komponenten für eine 32-Bit-Farbe Werte von 0 bis 255 (2^8-1). Die meisten modernen Bildschirme arbeiten mit 32-Bit-Farben.


Abfolge von Aktionen in MQL

Um ein Bild mit DirectX in MQL anzuzeigen, müssen Sie folgendes tun:

  1. Erstellen eines Bitmap Label oder Bitmap Objekt mit ObjectCreate.
  2. Erstellen einer dynamischen grafischen Ressource mit ResourceCreate.
  3. Verknüpfen einer Ressource mit dem Objekt mittels ObjectSetString mit dem Parameter OBJPROP_BMPFILE.
  4. Erstellen einer Datei für Shader (oder speichern Sie Shader in einer String-Variablen).
  5. Schreiben eines Vertex- und Pixel-Shaders in HLSL.
  6. Verbinden der Shader-Datei mit #resource "Dateiname.hlsl" as string variable_name;
  7. Beschreiben des Formats der Vertex-Punkte in einem Array vom Typ DXVertexLayout
  8. Kontext erstellen — DXContextCreate.
  9. Den Vertex-Shader erstellen — DXShaderCreate mit dem DX_SHADER_VERTEX Parameter.
  10. Erstellen des Pixel-Shaders - DXShaderCreate mit dem DX_SHADER_PIXEL Parameter.
  11. Den Vertex-Puffer erstellen — DXBufferCreate mit dem DX_BUFFER_VERTEX Parameter.
  12. Falls erforderlich, einen Indexpuffer erstellen — DXBufferCreate mit dem DX_BUFFER_INDEX Parameter.
  13. Übergabe des Vertex-Formats — DXShaderSetLayout.
  14. Festlegen der Topologie von Primitiven fest — DXPrimiveTopologySet.
  15. Einstellen der Vertex- und Pixel-Shader — DXShaderSet.
  16. Festlegen der Vertex- (und Index-, falls vorhanden) Puffer — DXBufferSet.
  17. Löschen des Tiefenpuffers — DXContextClearDepth.
  18. Falls erforderlich, den Farbpuffer löschen — DXContextClearColors
  19. Senden eines Rendering-Befehls — DXDraw (oder DXDrawIndexed, wenn ein Indexpuffer angegeben ist)
  20. Übergeb des Ergebnisses an die Grafikressource — DXContextGetColors
  21. Aktualisieren der Grafikressource — ResourceCreate
  22. Vergessen Sie nicht, das Diagramm zu aktualisieren — ChartRedraw
  23. Nach der Verwendung aufräumen — DXRelease
  24. Löschen der Grafikressource — ResourceFree
  25. Löschen des Grafikobjekts — ObjectDelete

Sind Sie noch bei mir? In der Tat ist alles einfacher, als es scheint. Sie werden es noch sehen.


Praxis

Übersicht der Klassen

Der gesamte Prozess der Verwendung von DirectX kann in mehrere Phasen unterteilt werden: Erstellung einer Leinwand (canvas), Initialisierung des device, Schreiben von Vertex- und Pixel-Shadern, Anzeige des resultierenden Bildes auf dem Bildschirm, Freigabe von Ressourcen. Die Klasse wird wie folgt aussehen:

class DXTutorial
  {
private:
   int               m_width;
   int               m_height;
   uint              m_image[];
   string            m_canvas;
   string            m_resource;

   int               m_dx_context;
   int               m_dx_vertex_shader;
   int               m_dx_pixel_shader;
   int               m_dx_buffer;

   bool              InitCanvas();
   bool              InitDevice(float4 &vertex[]);
   void              Deinit();

public:

   void              DXTutorial() { m_dx_context = 0; m_dx_vertex_shader = 0; m_dx_pixel_shader = 0; m_dx_buffer = 0; }
   void             ~DXTutorial() { Deinit(); }

   bool              Init(float4 &vertex[], int width, int height);
   bool              Draw();
  };

Private Mitglieder:

  • m_width und m_height — Breite und Höhe der Leinwand. Diese Mitglieder werden bei der Erstellung des Bitmap-Label-Objekts, der dynamischen Grafikressource und des Grafikkontexts verwendet. Ihre Werte werden während der Initialisierung gesetzt, aber es ist auch möglich, ihre Werte manuell zu setzen.
  • m_image — ein Array, das bei der Erstellung einer Grafikressource verwendet wird. Das Ergebnis der DirectX-Operation wird darin übergeben.
  • m_canvas — der Name des grafischen Objekts, m_resource — der Name der Grafikressource. Wird bei der Initialisierung und Deinitialisierung verwendet.
      DirectX-Handles:
  • m_dx_context — das wichtigste, das Grafikkontext-Handle. Nimmt an allen DirectX-Operationen teil. Es wird initialisiert, wenn der Grafikkontext erstellt wird.
  • m_dx_vertex_shader — Handle des Vertex-Shaders. Er wird beim Setzen des Vertex-Markups, beim Binden an den Grafikkontext und beim Deinitialisieren verwendet. Wird beim Kompilieren des Vertex-Shaders initialisiert.
  • m_dx_pixel_shader — Pixel-Shader-Handle. Es wird beim Binden an den Grafikkontext und bei der Deinitialisierung verwendet. Wird bei der Kompilierung des Pixel-Shaders initialisiert.
  • m_dx_buffer — Vertex-Puffer-Handle. Es wird beim Binden an den Grafikkontext und bei der Deinitialisierung verwendet. Er wird initialisiert, wenn der Vertex-Puffer erstellt wird.

      Initialisierungs- und Deinitialisierungsmethoden:

  • InitCanvas() — erzeugt einen Canvas zur Darstellung des Bildes. Das Bitmap-Label-Objekt und die dynamische Grafikressource werden verwendet. Der Hintergrund wird mit Schwarz ausgefüllt. Gibt den Fortschritt der Operation zurück.
  • InitDevice() — initialisiert DirectX. Erzeugt einen Grafikkontext, Vertex- und Pixel-Shader und einen Vertex-Puffer. Legt den Typ der Primitive fest und markiert die Scheitelpunkte. Nimmt ein Array von Scheitelpunkten als Eingabe. Gibt den Fortschritt der Operation zurück.
  • Deinit() — gibt verwendete Ressourcen frei. Löscht den Grafikkontext, die Vertex- und Pixel-Shader, den Vertex-Puffer, das Bitmap-Label-Objekt und die dynamische Grafikressource.

Öffentliche Mitglieder:

  • DXTutorial() — Konstruktor. Setzt DirectX-Handles auf 0.
  • ~DXTutorial() — Destruktor. Ruft die Methode Deinit() auf.
  • Init() — Vorbereitung auf die Arbeit. Nimmt ein Array von Scheitelpunkten und eine optionale Höhe und Breite als Eingabe. Validiert die empfangenen Daten, ruft InitCanvas() und InitDevice() auf. Gibt den Fortschritt der Operation zurück.
  • Draw() — zeigt das Bild auf dem Bildschirm an. Löscht Farb- und Tiefenpuffer, gibt das Bild in eine Grafikressource aus. Gibt den Fortschritt der Operation zurück.


Vertex-Array

Da Scheitelpunkte nur Informationen über Koordinaten enthalten, verwenden wir der Einfachheit halber eine Struktur mit 4 Float-Variablen. X, Y, Z sind Koordinaten im dreidimensionalen Raum. W ist eine Hilfskonstante, die gleich 1 sein muss und für Matrixoperationen verwendet wird.

struct float4
  {
   float             x;
   float             y;
   float             z;
   float             w;
  };

Ein Dreieck benötigt 3 Scheitelpunkte, also verwenden wir ein Array der Größe 3.

float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}};


Initialisierung

Wir übergeben das Array der Eckpunkte und die Leinwandgröße an das Objekt und überprüfen die Eingabedaten. Wenn die übergebene Breite oder Höhe kleiner als eins ist, dann wird der Parameter auf 500 Pixel gesetzt. Die Größe des Vertex-Arrays muss 3 betragen. Als Nächstes wird jeder Scheitelpunkt in einer Schleife überprüft. X- und Y-Koordinaten müssen im Bereich [-1;1] liegen, Z muss gleich 0 sein - es wird zwangsweise auf diesen Wert zurückgesetzt. W muss 1 sein und wird ebenfalls zwangsweise zurückgesetzt. Aufruf der Canvas- und DirectX-Initialisierungsfunktionen.

bool DXTutorial::Init(float4 &vertex[], int width = 500, int height = 500)
  {
   if(width <= 0)
     {
      m_width = 500;
      Print("Warning: width changed to 500");
     }
   else
     {
      m_width = width;
     }

   if(height <= 0)
     {
      m_height = 500;
      Print("Warning: height changed to 500");
     }
   else
     {
      m_height = height;
     }

   if(ArraySize(vertex) != 3)
     {
      Print("Error: 3 vertex are needed for a triangle");
      return(false);
     }

   for(int i = 0; i < 3; i++)
     {
      if(vertex[i].w != 1)
        {
         vertex[i].w = 1.0f;
         Print("Warning: vertex.w changed to 1");
        }

      if(vertex[i].z != 0)
        {
         vertex[i].z = 0.0f;
         Print("Warning: vertex.z changed to 0");
        }

      if(fabs(vertex[i].x) > 1 || fabs(vertex[i].y) > 1)
        {
         Print("Error: vertex coordinates must be in the range [-1;1]");
         return(false);
        }
     }

   ResetLastError();

   if(!InitCanvas())
     {
      return(false);
     }

   if(!InitDevice(vertex))
     {
      return(false);
     }

   return(true);
  }


Erstellen einer Leinwand

Die Funktion InitCanvas() erzeugt ein Bitmap-Label-Objekt, dessen Koordinaten in Pixel angegeben werden. Dann wird eine dynamische Grafikressource an dieses Objekt gebunden, in die das Bild aus DirectX ausgegeben wird.

bool DXTutorial::InitCanvas()
  {
   m_canvas = "DXTutorialCanvas";
   m_resource = "::DXTutorialResource";
   int area = m_width * m_height;

   if(!ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0))
     {
      Print("Error: failed to create an object to draw");
      return(false);
     }

   if(!ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100))
     {
      Print("Warning: failed to move the object horizontally");
     }

   if(!ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100))
     {
      Print("Warning: failed to move the object vertically");
     }

   if(ArrayResize(m_image, area) != area)
     {
      Print("Error: failed to resize the array for the graphical resource");
      return(false);
     }

   if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area)
     {
      Print("Warning: failed to initialize array for graphical resource");
     }

   if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("Error: failed to create a resource to draw");
      return(false);
     }

   if(!ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource))
     {
      Print("Error: failed to bind resource to object");
      return(false);
     }

   return(true);
  }

Schauen wir uns den Code genauer an.

m_canvas = "DXTutorialCanvas";

Wir geben den Namen für die grafische Ressource "DXTutorialCanvas" an und

m_resource = "::DXTutorialResource";

geben den Namen für die dynamische grafische Ressource "::DXTutorialResource" an.

int area = m_width * m_height;

Da die Methode das Produkt aus Breite und Höhe mehrmals benötigt, berechnen wir es im Voraus und speichern das Ergebnis.

ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0)

Wir erzeugen ein Bitmap-Label-Objekt mit dem Namen "DXTutorialCanvas".

ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)

Verschieben des Objekts um 100 Pixel nach rechts von der linken oberen Ecke des Diagramms.

ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)

Verschieben des Objekts um 100 Pixel von der linken oberen Ecke des Diagramms nach unten.

ArrayResize(m_image, area)

Ändern der Größe des zu zeichnenden Arrays.

ArrayInitialize(m_image, ColorToARGB(clrBlack))

Füllen des Arrays mit schwarzer Farbe. Die Farben im Array müssen im ARGB-Format gespeichert sein. Wir verwenden der Einfachheit halber die Standardfunktion ColorToARGB, um die Farbe in das gewünschte Format zu konvertieren.

ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)

Erstellen einer dynamischen, grafischen Ressource mit dem Namen "::DXTutorialResource", mit der Breite m_width und der Höhe m_height. Wir geben die Verwendung einer Farbe mit Transparenz durch COLOR_FORMAT_ARGB_NORMALIZE an. Dazu verwenden wir das Array m_image als Datenquelle.

ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)

Verknüpfen des Objekts und die Ressource. Bisher haben wir die Größe des Objekts nicht angegeben, da es sich automatisch an die Größe der Ressource anpasst.


DirectX-Initialisierung

Kommen wir nun zum interessantesten Teil.

bool DXTutorial::InitDevice(float4 &vertex[])
  {
   DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }};
   string shader_error = "";

   m_dx_context = DXContextCreate(m_width, m_height);
   if(m_dx_context == INVALID_HANDLE)
     {
      Print("Error: failed to create graphics context: ", GetLastError());
      return(false);
     }

   m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error);
   if(m_dx_vertex_shader == INVALID_HANDLE)
     {
      Print("Error: failed to create vertex shader: ", GetLastError());
      Print("Shader compilation error: ", shader_error);
      return(false);
     }

   m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error);
   if(m_dx_pixel_shader == INVALID_HANDLE)
     {
      Print("Error: failed to create pixel shader: ", GetLastError());
      Print("Shader compilation error: ", shader_error);
      return(false);
     }

   m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);
   if(m_dx_buffer == INVALID_HANDLE)
     {
      Print("Error: failed to create vertex buffer: ", GetLastError());
      return(false);
     }

   if(!DXShaderSetLayout(m_dx_vertex_shader, layout))
     {
      Print("Error: failed to set vertex layout: ", GetLastError());
      return(false);
     }

   if(!DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST))
     {
      Print("Error: failed to set primitive type: ", GetLastError());
      return(false);
     }

   if(!DXShaderSet(m_dx_context, m_dx_vertex_shader))
     {
      Print("Error, failed to set vertex shader: ", GetLastError());
      return(false);
     }

   if(!DXShaderSet(m_dx_context, m_dx_pixel_shader))
     {
      Print("Error: failed to set pixel shader: ", GetLastError());
      return(false);
     }

   if(!DXBufferSet(m_dx_context, m_dx_buffer))
     {
      Print("Error: failed to set buffer to render: ", GetLastError());
      return(false);
     }

   return(true);
  }

Lassen Sie uns den Code analysieren.

DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }};

Diese Zeile beschreibt das Format der Scheitelpunkte. Diese Information wird benötigt, damit die Grafikkarte das Eingabefeld mit den Scheitelpunkten korrekt verarbeiten kann. In diesem Fall ist die Größe des Arrays gleich 1, da die Scheitelpunkte nur Positionsinformationen speichern. Wenn wir jedoch Informationen über die Farbe der Eckpunkte hinzufügen, benötigen wir eine weitere Array-Zelle. "POSITION" bedeutet, dass sich die Informationen auf Koordinaten beziehen. 0 ist ein semantischer Index. Wenn wir zwei verschiedene Koordinaten in einem Scheitelpunkt übergeben müssen, können wir den Index 0 für die erste und 1 für die zweite Koordinate verwenden. DX_FORMAT_R32G32B32A32_FLOAT - Format der Informationsdarstellung. In diesem Fall handelt es sich um vier 32-Bit-Gleitkommazahlen.

string shader_error = "";

Diese Variable speichert Shader-Kompilierungsfehler.

m_dx_context = DXContextCreate(m_width, m_height);

Erzeugen eines Grafikkontextes mit der Breite von m_width und der Höhe von m_height. Merken Sie sich den Handle.

m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error);

Erstellen des Vertex-Shader und speichern des Handle. DX_SHADER_VERTEX indiciert der Shader-Typ - vertex. String shader speichert den Quellcode der Vertex- und Pixel-Shader, aber es wird empfohlen, sie in separaten Dateien zu speichern und sie als Ressourcen einzubinden. "VShader" ist der Name des Einstiegspunktes (Funktion 'main' in normalen Programmen). Wenn ein Shader-Kompilierungsfehler auftritt, werden zusätzliche Informationen in shader_error geschrieben. Wenn zum Beispiel der Einstiegspunkt "VSha" angeben wurde, wird die Variable den folgenden Text enthalten: "error X3501: 'VSha': entrypoint not found".

m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error);

Das Gleiche gilt für den Pixel-Shader: hier den entsprechenden Typ und den Einstiegspunkt angeben.

m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);

Einen Puffer erstellen und den Handle speichern. Angeben, dass es ein Vertex-Puffer ist. Übergeben eines Arrays von Scheitelpunkten.

DXShaderSetLayout(m_dx_vertex_shader, layout)

Übergeben der Informationen über das Layout der Scheitelpunkte.

DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)

Setzt den Typ der Primitive "Liste der Dreiecke".

DXShaderSet(m_dx_context, m_dx_vertex_shader)

Übergiben der Informationen über den Vertex-Shader.

DXShaderSet(m_dx_context, m_dx_pixel_shader)

Übergiben der Informationen über den Pixel-Shader.

DXBufferSet(m_dx_context, m_dx_buffer)

Übergiben der Informationen über den Puffer.


Bildanzeige

DirectX gibt das Bild in einem Array aus. Auf der Grundlage dieses Arrays wird eine Grafikressource erstellt.

bool DXTutorial::Draw()
  {
   DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f};

   if(!DXContextClearColors(m_dx_context, dx_color))
     {
      Print("Error: failed to clear the color buffer: ", GetLastError());
      return(false);
     }

   if(!DXContextClearDepth(m_dx_context))
     {
      Print("Error: failed to clear the depth buffer: ", GetLastError());
      return(false);
     }

   if(!DXDraw(m_dx_context))
     {
      Print("Error: failed to draw vertices of the vertex buffer: ", GetLastError());
      return(false);
     }

   if(!DXContextGetColors(m_dx_context, m_image))
     {
      Print("Error: unable to get image from the graphics context: ", GetLastError());
      return(false);
     }

   if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("Error: failed to create a resource to draw");
      return(false);
     }

   return(true);
  }

Lassen Sie uns die Methode genauer analysieren.

DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f};

Die Variable dx_color vom Typ DXVector wird erzeugt. Ihr wird die rote Farbe mit halber Transparenz zugewiesen. RGBA-Format mit Werten von 0 bis 1 float.

DXContextClearColors(m_dx_context, dx_color)

Ausfüllen des Puffers mit der Farbe dx_color. .

DXContextClearDepth(m_dx_context)

Löschen des Tiefenpuffers.

DXDraw(m_dx_context)

Senden einer Renderingaufgabe an DirectX.

DXContextGetColors(m_dx_context, m_image)

Rückgabe des Ergebnisses in das Array m_image.

ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)

Aktualisieren die dynamische Grafikressource.


Freigeben von Ressourcen

DirectX erfordert die manuelle Freigabe von Ressourcen. Außerdem ist es notwendig, das grafische Objekt und die Ressource zu löschen. Prüfen wir, ob wir die Ressourcen freigeben müssen und rufen dann DXRelease auf. Die dynamische grafische Ressource wird durch ResourceFree gelöscht. Das Grafikobjekt wird durch ObjectDelete freigegeben.

void DXTutorial::Deinit()
  {
   if(m_dx_pixel_shader > 0 && !DXRelease(m_dx_pixel_shader))
     {
      Print("Error: failed to release the pixel shader handle: ", GetLastError());
     }

   if(m_dx_vertex_shader > 0 && !DXRelease(m_dx_vertex_shader))
     {
      Print("Error: failed to release the vertex shader handle: ", GetLastError());
     }

   if(m_dx_buffer > 0 && !DXRelease(m_dx_buffer))
     {
      Print("Error: failed to release the vertex buffer handle: ", GetLastError());
     }

   if(m_dx_context > 0 && !DXRelease(m_dx_context))
     {
      Print("Error: failed to release the graphics context handle: ", GetLastError());
     }

   if(!ResourceFree(m_resource))
     {
      Print("Error: failed to delete the graphics resource");
     }

   if(!ObjectDelete(0, m_canvas))
     {
      Print("Error: failed to delete graphical object");
     }
  }


Shader

Shader werden in der Shader-Zeichenkette gespeichert. Bei großen Mengen ist es jedoch besser, sie in separaten externen Dateien zu speichern und sie als Ressourcen einzubinden.

string shader = "float4 VShader( float4 Pos : POSITION ) : SV_POSITION  \r\n"
                "  {                                                    \r\n"
                "   return Pos;                                         \r\n"
                "  }                                                    \r\n"
                "                                                       \r\n"
                "float4 PShader( float4 Pos : SV_POSITION ) : SV_TARGET \r\n"
                "  {                                                    \r\n"
                "   return float4( 0.0f, 1.0f, 0.0f, 1.0f );            \r\n"
                "  }                                                    \r\n";

Ein Shader ist ein Programm für eine Grafikkarte. In DirectX wird es in der C-ähnlichen Sprache HLSL geschrieben. Float4 im Shader ist ein integrierter Datentyp, im Gegensatz zu unserer Struktur. VShader ist in diesem Fall ein Vertex-Shader, während PShader ein Pixel-Shader ist. POSITION - semantische Angabe, dass es sich bei den Eingabedaten um Koordinaten handelt; die Bedeutung ist dieselbe wie bei DXVertexLayout. SV_POSITION - ebenfalls semantisch, wird aber für den Ausgabewert verwendet. Das Präfix SV_ zeigt an, dass es sich um einen Systemwert handelt. SV_TARGET - semantisch, zeigt an, dass der Wert in die Textur oder den Pixelbuffer geschrieben wird. Was passiert hier also. Die Koordinaten werden in den Vertex-Shader eingegeben, der sie unverändert an die Ausgabe weitergibt. Der Pixel-Shader (aus der Rasterisierungsphase) erhält die interpolierten Werte, für die die Farbe auf grün gesetzt wird.


OnStart

In der Funktion wird eine Instanz der DXTutorial-Klasse erzeugt. Es wird die Funktion Init aufgerufen, der das Array der Vertices übergeben wird. Dann wird die Draw Funktion aufgerufen. Danach endet die Ausführung des Skripts.

void OnStart()
  {
   float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}};
   DXTutorial dx;
   if(!dx.Init(vertex)) return;
   ChartRedraw();
   Sleep(1000);
   if(!dx.Draw()) return;
   ChartRedraw();
   Sleep(1000);
  }


Schlussfolgerung

In diesem Artikel haben wir die Geschichte von DirectX betrachtet. Wir haben versucht zu verstehen, was es ist und welchen Zweck es erfüllt. Wir haben auch die interne Struktur der API betrachtet. Wir haben uns die Pipeline angesehen, die auf modernen Grafikkarten Vertices in Pixel umwandelt. Außerdem enthält der Artikel eine Liste von Aktionen, die für die Arbeit mit DirectX erforderlich sind, und ein kleines Beispiel in MQL. Schließlich haben wir unser erstes Dreieck gerendert! Herzlichen Glückwunsch! Aber es gibt noch viele andere neue und interessante Dinge, die man für einen vollwertigen Betrieb mit DirectX lernen muss. Dazu gehören neben den Vertices auch die Übertragung anderer Daten, die Shader-Programmiersprache HLSL, verschiedene Transformationen mit Matrizen, Texturen, Normalen und zahlreiche Spezialeffekte.


Referenzen und Links

  1. Wikipedia.
  2. Microsoft documentation.


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/10425

Letzte Kommentare | Zur Diskussion im Händlerforum (1)
Tong Shi Yang
Tong Shi Yang | 29 Apr. 2022 in 19:56
MetaQuotes:

Der neue Artikel DirectX Tutorial (Teil 1): Zeichnen des ersten Dreiecks wurde veröffentlicht.

Autor: Rorschach

Grafiken in der DoEasy-Bibliothek (Teil 97): Unabhängige Handhabung der Bewegung von Formularobjekten Grafiken in der DoEasy-Bibliothek (Teil 97): Unabhängige Handhabung der Bewegung von Formularobjekten
In diesem Artikel werde ich die Implementierung des unabhängigen Ziehens von beliebigen Formularobjekten mit der Maus betrachten. Außerdem werde ich die Bibliothek um Fehlermeldungen und neue Deal-Eigenschaften ergänzen, die zuvor in das Terminal und MQL5 implementiert wurden.
Lernen Sie, wie Sie ein Handelssystem mit Hilfe des MACD entwickelt Lernen Sie, wie Sie ein Handelssystem mit Hilfe des MACD entwickelt
In diesem Artikel lernen wir ein neues Instrument aus unserer Serie kennen: Wir lernen, wie man ein Handelssystem auf der Grundlage eines der beliebtesten technischen Indikatoren, dem Moving Average Convergence Divergence (MACD), entwickelt.
Charts interessanter machen: Hinzufügen eines Hintergrunds Charts interessanter machen: Hinzufügen eines Hintergrunds
Viele Arbeitsplätze enthalten ein repräsentatives Bild, das etwas über den Benutzer aussagt. Diese Bilder machen die Arbeitsumgebung schöner und spannender. Sehen wir uns an, wie man die Charts durch Hinzufügen eines Hintergrunds interessanter gestalten kann.
Datenwissenschaft und maschinelles Lernen (Teil 02): Logistische Regression Datenwissenschaft und maschinelles Lernen (Teil 02): Logistische Regression
Die Klassifizierung von Daten ist für einen Algo-Händler und einen Programmierer von entscheidender Bedeutung. In diesem Artikel werden wir uns auf einen logistischen Klassifizierungsalgorithmus konzentrieren, der uns wahrscheinlich helfen kann, die Ja- oder Nein-Stimmen, die Höhen und Tiefen, Käufe und Verkäufe zu identifizieren.