English Русский 中文 Deutsch 日本語 Português
preview
Tutorial de DirectX (Parte I): Dibujamos el primer triángulo

Tutorial de DirectX (Parte I): Dibujamos el primer triángulo

MetaTrader 5Integración | 9 mayo 2022, 07:23
567 0
Rorschach
Rorschach

Contenido

  1. Introducción
  2. DirectX API
    1. Historia de DirectX
    2. Direct3D
    3. Device
    4. Device Context
    5. Swap Chain
    6. Input Layout
    7. Primitive Topology
    8. HLSL
  3. Tubería de gráficos
  4. Gráficos 3D
    1. Primitivas
    2. Vértices
    3. Color
  5. Secuencia de acciones en MQL
  6. Práctica
    1. Descripción general de la clase
    2. Matriz de vértices
    3. Inicialización
    4. Creando un lienzo para el dibujado
    5. Inicializando DirectX
    6. Mostrando la imagen
    7. Liberando recursos
    8. Sombreadores
    9. OnStart
  7. Conclusión
  8. Referencias y enlaces

Introducción

Un rito de iniciación, o, como yo prefiero, un rito de inicialización. Esta es la frase que nos viene a la mente al ver cuánto código hay y cuánto necesitamos escribir, rellenando enormes estructuras en C++ para dibujar al menos un triángulo primitivo usando DirectX. Eso por no mencionar cosas más complejas como texturas, matrices de transformación, sombras y similares. Afortunadamente, MetaQuotes se encargó hace tiempo de esta cuestión, ocultando toda la rutina y dejándonos solo las funciones realmente necesarias. No obstante, debido a ello, quienes no están familiarizados con DirectX tienen otros problemas: no hay integridad en la imagen, y no está claro qué, por qué y cómo está sucediendo. El código a escribir en MQL aún posee cierta redundancia.

Sin entender lo que sucede bajo el capó de DirectX, nos quedamos perplejos: "¿por qué tantas dificultades, por qué es tan confuso, no se puede simplificar?" Y esta es solo la primera etapa. Nadie ha eliminado el estudio del lenguaje de sombreado HLSL y las peculiaridades de la programación de las tarjetas de vídeo. Para evitar todas estas confusiones, vamos a analizar junto a los lectores (sin rompernos la cabeza) la estructura interna de DirectX. Después escribiremos un pequeño script en MQL que mostrará un triángulo en la pantalla.


DirectX API

Historia de DirectX

DirectX es un conjunto de API (interfaz de programación de aplicaciones) para trabajar con multimedia y vídeo en plataformas de Microsoft. Fue desarrollado principalmente para crear juegos, pero con el tiempo comenzó a usarse en software matemático y de ingeniería. DirectX nos permite trabajar con gráficos, sonido, entradas y red sin necesidad de acceder a funciones de bajo nivel. La API apareció como una alternativa a OpenGL multiplataforma. Al crear Windows 95, se introdujeron cambios bastante importantes que podían afectar a la popularidad del futuro sistema operativo entre los creadores de programas y juegos. Para facilitar el desarrollo de un nuevo sistema operativo, se creó DirectX. DirectX fue fundado por Craig Eisler, Alex Saint John y Eric Engstrom.

  • Septiembre de 1995. Primera versión. Era una versión bastante primitiva, en su mayor parte un complemento de la API de Windows, y no llamó mucho la atención. La tendencia era DOS, en comparación con el cual, el nuevo sistema operativo demandaba mayores requisitos del sistema. Además, ya existía OpenGL. No había certeza de que Microsoft continuara ofreciendo soporte a DirectX.
  • Junio de 1996. Lanzamiento de la segunda versión.
  • Septiembre de 1996. Tercera versión.
  • Agosto de 1997. La cuarta versión nunca salió: apareció directamente la quinta. Como resultaba más sencillo escribir código para él, recibió algo de atención por parte de los programadores.
  • Agosto de 1998. Sexta versión. El trabajo se simplificó aún más.
  • Septiembre de 1999. Séptima versión. La capacidad de crear búferes de vértices en la memoria de vídeo se convirtió en una gran ventaja sobre OpenGL.
  • Noviembre de 2000. Octava versión. Momento crucial. Antes de esta, DirectX cumplía el papel de rezagado, pero en la versión 8 dio alcance al resto de la industria. Microsoft comenzó a cooperar con los fabricantes de tarjetas de vídeo. Aparecieron sombreadores de vértices y píxeles. Para el desarrollo bastaba una computadora personal, a diferencia de OpenGL, que requería una estación de trabajo.
  • Diciembre de 2002. Novena versión. DirectX se convirtió en el estándar de la industria. Apareció el lenguaje de sombreado HLSL. Fue, probablemente, la versión más longeva de DirectX. Como el socket 775... Aunque estoy divagando.
  • Noviembre de 2006. Décima versión. A diferencia de la novena versión, en esta se implementó la vinculación al sistema operativo Vista, que, a su vez, no era popular. Estos factores tuvieron un efecto negativo en el éxito de décima versión. Se añadió un sombreador de geometría.
  • Octubre de 2009. Undécima versión. Se añadió la teselación, el sombreador de computación, el trabajo mejorado con procesadores multinúcleo.
  • Julio de 2015. Duodécima versión. API de bajo nivel. Se mejoró la compatibilidad con procesadores de núcleos múltiples, la capacidad de combinar los recursos de varias tarjetas de vídeo de diferentes proveedores y el trazado de rayos.


Direct3D

Direct3D es uno de los muchos componentes de la mayor API DirectX, es responsable de los gráficos y actúa como intermediario entre las aplicaciones y el driver de la tarjeta gráfica. Direct3D se basa en COM (modelo de objetos componentes). COM es un estándar de interfaz binaria (ABI) para componentes de software, introducido por Microsoft en 1993. Se usa para crear objetos en la comunicación entre procesos (IPC) en una gran cantidad de lenguajes de programación. COM apareció como una solución capaz de proporcionar un método independiente del lenguaje para implementar objetos que podrían usarse fuera de su entorno de creación. COM permite que los objetos se reutilicen sin conocer su implementación interna, pues ofrecen interfaces bien definidas que están separadas de la implementación. Los objetos COM son responsables de su propia creación y destrucción mediante el recuento de referencias.

interfaces

Interfaces


Device

Todo en Direct3D comienza con Device (dispositivo). La creación de recursos (búferes, texturas, sombreadores, objetos de estado) y la enumeración de las capacidades de los adaptadores de gráficos tienen lugar con su ayuda. El dispositivo es un adaptador virtual ubicado en el sistema del usuario. El adaptador puede ser una tarjeta de vídeo real o su emulación de software. Los dispositivos de hardware se usan con mayor frecuencia porque ofrecen el mayor rendimiento. El dispositivo proporciona una interfaz unificada para todos estos adaptadores y los usa para representar gráficos en una o más salidas.

Direct3D

Device


Device Context

Device Context (contexto del dispositivo) se encarga de todo lo relacionado con el renderizado. Hablamos tanto de la configuración de la tubería como de la creación de comandos para renderizar. Device Context apareció en la undécima versión de DirectX, antes de que Device se encargara del renderizado. Hay dos tipos de contexto: Immediate Context (contexto inmediato) y Deferred Context (contexto diferido).

El contexto inmediato ofrece acceso a los datos en la tarjeta de vídeo y la capacidad de ejecutar inmediatamente una lista de comandos en el dispositivo. Cada Device tiene solo un Immediate Context. Solo un hilo puede acceder a él a la vez. Para acceder desde varios hilos, debemos utilizar la sincronización.

Deferred Context añade comandos a la lista de comandos para la ejecución diferida en Immediate Context. Por lo tanto, todos los comandos eventualmente pasan por Immediate Context. Deferred Context añade algunos gastos generales, los beneficios de usarlo aparecen solo al paralelizar tareas intensivas que requieren un uso intensivo del procesador. Podemos crear múltiples Deferred Context y acceder a cada uno desde un hilo aparte. Pero para acceder al mismo Deferred Context desde múltiples hilos, al igual que en el caso de Immediate Context, necesitamos sincronización.


Swap Chain

Swap Chain (cadena de intercambio) está diseñada para crear uno o más búferes traseros (back). Estos búferes contienen imágenes renderizadas hasta que se muestran en la pantalla. El funcionamiento del búfer delantero (delantero) y un búfer trasero ocurrirá de la forma siguiente. En la pantalla, se nos muestra el búfer delantero, mientras se está procesando el búfer trasero. Luego se intercambian ambos, el delantero se vuelve trasero, y al contrario. Y el proceso completo se repite una y otra vez. Por consiguiente, siempre veremos la imagen terminada, mientras que "detrás de escena" se dibuja la siguiente.

Swapchain

Swap Chain

Device, Device Context y Swap Chain son los componentes principales necesarios para representar una imagen.


Input Layout

Input layout le dice a la tubería qué estructura es el búfer de vértices. Para nuestros propósitos, basta con las coordenadas, por lo que podemos prescindir de la estructura especial transmitiendo una matriz de vértices float4. float4 es una estructura que consta de cuatro variables de tipo float.

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

Por ejemplo, vamos a analizar una estructura de vértice más compleja que consta de una coordenada y dos colores:

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

Input layout en MQL para ella tendrá el aspecto siguiente:

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

Cada elemento de la matriz layout describe el elemento correspondiente de la estructura Vertex.

  • El primer elemento de la estructura DXVertexLayout es el nombre semántico. Sirve para comparar los elementos de la estructura Vertex con los elementos de la estructura en el sombreador de vértices. "POSITION" indica que el valor es responsable de las coordenadas, y "COLOR", del color.
  • El segundo elemento es un índice semántico. Si necesitamos transmitir varios parámetros del mismo tipo, por ejemplo, dos valores de color, transmitiremos el primero con el índice 0, y el segundo con el índice 1.
  • El último elemento describe el tipo en el que se representa el valor en la estructura Vertex. DX_FORMAT_R32G32B32A32_FLOAT indica literalmente que este es un color RGBA representado por un valor de punto flotante de 32 bits para cada componente. Esto puede resultar confuso. Podemos usar este tipo para transmitir coordenadas, lo principal es que ofrece información sobre cuatro valores de punto flotante de 32 bits, al igual que float4 en la estructura Vertex.


Primitive Topology

El búfer de vértices almacena información sobre los puntos, pero no sabemos cómo se ubican entre sí en la primitiva. Para eso tenemos Primitive Topology. Point List nos indica que los puntos individuales se almacenan en el búfer. Line Strip representa la zona de influencia como puntos conectados secuencialmente entre sí, formando una línea quebrada. En Line List, cada dos puntos describen una sola línea. Triangle Strip y Triangle List establecen un orden para los puntos de los triángulos similar a las líneas.

Topology

Topología

HLSL

El lenguaje de sombreado de alto nivel es un lenguaje similar a C para escribir sombreadores. Los sombreadores, a su vez, son programas diseñados para ejecutarse en una tarjeta de vídeo. La programación en todos los lenguajes GPGPU es muy similar y tiene sus propias peculiaridades relacionadas con el diseño de tarjetas de vídeo. Si el lector tiene experiencia con OpenCL, Cuda u OpenGL, se acostumbrará a HLSL muy rápidamente. Pero si solo ha escrito programas para procesadores centrales, al principio le será difícil cambiar a un nuevo paradigma. Con frecuencia, los métodos de optimización familiares para el procesador no funcionarán. Como ejemplo, sería correcto que el procesador usara la declaración if para ahorrarse los cálculos innecesarios o para seleccionar el algoritmo óptimo. Pero en la GPU, esto puede, por el contrario, aumentar el tiempo de ejecución del programa. Para sacar el máximo de partido, puede que tengamos que contar el número de registros involucrados. Los tres principios fundamentales del alto rendimiento al programar tarjetas de vídeo son: paralelismo, ancho de banda y carga.


Tubería de gráficos

La tubería está diseñada para convertir una escena 3D en una representación de visualización 2D. La tubería es un reflejo de la estructura interna de la tarjeta de vídeo. El siguiente esquema muestra cómo fluyen los datos desde la entrada de la tubería hasta su salida a través de todas las etapas. Con un óvalo se marcan las etapas programadas con el lenguaje HLSL, los sombreadores; con un rectángulo, las etapas fijas. Algunas de ellas son opcionales y se pueden omitir sin consecuencias.

Graphics Pipeline

Tubería de gráficos

  • Input Assembler stage: recibe los datos de los búferes de índice y vértice y se prepara para el sombreador de vértice.

  • Vertex Shader stage: sombreador de vértices. Realiza operaciones en los vértices. Etapa programable. Debe estar presente en la tubería.
  • Hull Shader stage: sombreador de superficie. Responsable del nivel de teselado. Etapa programable. No requerido.
  • Tessellator stage: crea primitivas más pequeñas. Etapa fija. No requerido.
  • Domain Shader stage: sombreador de dominio. Calcula los valores de los vértices finales después de la teselación. Etapa programable. No requerido.
  • Geometry Shader stage: sombreador geométrico. Aplica diferentes transformaciones a las primitivas (puntos, líneas, triángulos). Etapa programable. No requerido.
  • Stream Output stage: transmite los datos a la memoria de GPU, desde donde pueden ser reenviados a la tubería. Etapa fija. No requerido.
  • Rasterizer stage: corta todo lo que no entra en el ámbito, prepara los datos para el sombreador de píxeles. Etapa fija.
  • Pixel Shader stage: sombreador de píxeles. Realiza operaciones con píxeles. Etapa programable. Debe estar presente en la tubería.

  • Output Merger stage: forma la imagen final. Etapa fija.

Asimismo, merece la pena recordar Compute Shader (DirectCompute), que representa una tubería aparte. Este sombreador se encarga de calcular la designación final, es un análogo de OpenCL y Cuda. Etapa programable. No requerido.

La implementación de MetaQuotes de DirectX no incluye DirectCompute ni la etapa de teselación. Por consiguiente, solo tenemos disponibles tres sombreadores: el de vértices, el geométrico y el de píxeles.


Gráficos 3D

Primitivas

La representación de primitivas es el objetivo principal de la API de gráficos. Las tarjetas de vídeo modernas están adaptadas para dibujar rápidamente un gran número de triángulos. El hecho es que en la etapa actual de desarrollo de los gráficos por computadora, la forma más efectiva de dibujar objetos 3D es crear su superficie a partir de polígonos. En este caso, para describir un plano, bastará con especificar tres puntos. El uso de triángulos es común en el software de modelado 3D, pero, aun así, la tarjeta gráfica dividirá forzosamente a los polígonos en triángulos.

Mesh

Red de triángulos

Vértices

Para representar un triángulo en Direct3D, se deben especificar tres vértices. Puede parecer que un vértice es la posición de un punto en el espacio, pero en Direct3D es algo más. Además de la posición de los vértices, podemos transmitir datos de color, coordenadas de textura, normales. De cara al futuro, diremos que las transformaciones de matrices generalmente se usan para normalizar coordenadas. Sin embargo, para no complicarnos antes de tiempo, consideraremos que en la etapa de rasterización en los ejes X e Y, las coordenadas de los vértices deberán estar dentro de [-1; 1], y en Z, de 0 a 1.


Color

En los gráficos por computadora, el color tiene tres componentes: rojo, verde y azul. Esto se debe a las características estructurales de la retina humana. Los píxeles del monitor también constan de tres subpíxeles de estos colores. Hay una función ColorToARGB en MQL para convertir colores web a formato ARGB, donde, además de los colores, se almacena la información sobre la transparencia. El color puede ser normalizado cuando los componentes están en el rango [0;1], y no normalizado, por ejemplo, para un color de 32 bits, los componentes adoptarán valores de 0 a 255 (2^8-1). La mayoría de los monitores modernos funcionan con color de 32 bits.


Secuencia de acciones en MQL

Para mostrar la imagen con la ayuda de DirectX en MQL, deberemos hacer lo siguiente:

  1. Crear un objeto "Etiqueta gráfica" o "Dibujo" con la ayuda de ObjectCreate.
  2. Crear un recurso gráfico dinámico con la ayuda de ResourceCreate.
  3. Vincular el recurso a un objeto usando ObjectSetString con el parámetro OBJPROP_BMPFILE.
  4. Crear un archivo para sombreadores (o guardar los sombreadores en una variable de tipo string).
  5. Escribir sombreadores de vértices y píxeles en HLSL.
  6. Conectar el archivo con sombreadores usando #resource "FileName.hlsl" as string variable_name;
  7. Describir el formato de los vértices en una matriz de tipo DXVertexLayout
  8. Crear un contexto: DXContextCreate.
  9. Crear un sombreador de vértices — DXShaderCreate con el parámetro DX_SHADER_VERTEX.
  10. Crear un sombreador de píxeles — DXShaderCreate con el parámetro DX_SHADER_PIXEL.
  11. Crear un búfer de vértices — DXBufferCreate con el parámetro DX_BUFFER_VERTEX.
  12. Si es necesario, crear un búfer de índice — DXBufferCreate con el parámetro DX_BUFFER_INDEX.
  13. Transmitir información sobre el formato del vértice — DXShaderSetLayout.
  14. Establecer la topología de las primitivas — DXPrimiveTopologySet.
  15. Vincular los sombreadores de vértices y píxeles — DXShaderSet.
  16. Vincular el búfer de vértice (y el de índice, si lo hay) — DXBufferSet.
  17. Borrar el búfer de profundidad — DXContextClearDepth.
  18. Si es necesario, borrar el búfer de color — DXContextClearColors
  19. Enviar el comando de dibujado — DXDraw (o DXDrawIndexed si el búfer de índice está configurado)
  20. Transmitir el resultado al recurso gráfico — DXContextGetColors
  21. Actualizar el recurso gráfico — ResourceCreate
  22. No olvidar actualizar el gráfico — ChartRedraw
  23. Después de usarlo, hay que asegurarse de dejar todo limpio y ordenado — DXRelease
  24. Eliminar un recurso gráfico — ResourceFree
  25. Eliminar un objeto gráfico — ObjectDelete

¿Sigue conmigo? De hecho, todo resulta más sencillo de lo que parece. Podrá convencerse de ello a continuación. Y esto es mucho menos de lo que podemos hacer con las ventajas simples.


Práctica

Descripción general de la clase

Podemos dividir el proceso completo de trabajo con DirectX en varias etapas: crear un lienzo para dibujar, inicializar el dispositivo, escribir los sombreadores de vértices y píxeles, mostrar la imagen resultante en la pantalla y liberar recursos. La clase tendrá el aspecto siguiente:

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();
  };

Miembros privados:

  • m_width y m_height indican la anchura y la altura del lienzo. Se usan al crear el objeto "Etiqueta gráfica", el recurso de gráficos dinámicos y el contexto de gráficos. Sus valores se establecen durante la inicialización, pero también podemos indicar sus valores manualmente.
  • m_image — matriz utilizada al crear un recurso gráfico. Precisamente a ella se transmite el resultado del funcionamiento de DirectX.
  • m_canvas — nombre del objeto gráfico, m_resource — nombre del recurso gráfico. Se utilizan durante la inicialización y la desinicialización.
      Controlador de DirectX:
  • m_dx_context — el controlador de contexto gráfico más importante. Interviene en todas las operaciones con DirectX. Se inicializa al crear el contexto de gráficos.
  • m_dx_vertex_shader — controlador de sombreador de vértices. Se usa al configurar el marcado de vértices, al vincular al contexto de gráficos y al desinicializar. Se inicializa al compilarse el sombreador de vértices.
  • m_dx_pixel_shader — controlador de sombreado de píxeles. Se utiliza al realizar la vinculación a un contexto de gráficos y al realizar la desinicialización. Se inicializa al compilarse el sombreador de píxeles.
  • m_dx_buffer — controlador del búfer de vértices. Se utiliza al realizar la vinculación a un contexto de gráficos y al realizar la desinicialización. Se inicializa al crearse el búfer de vértices.

      Métodos de inicialización y desinicialización:

  • InitCanvas() — crea un lienzo para mostrar una imagen. Se utiliza el objeto "Etiqueta gráfica" y el recurso gráfico dinámico. El fondo se rellena en color negro. Retorna el estado del progreso de la operación.
  • InitDevice() — se inicializa DirectX. Se crean un contexto de gráficos, sombreadores de vértices y píxeles y un búfer de vértices. Se establecen el tipo de primitivas y el marcado de vértices. Toma una matriz de vértices como entrada. Retorna el estado del progreso de la operación.
  • Deinit() — borra los recursos usados. Se eliminan el contexto de gráficos, los sombreadores de vértices y píxeles, el búfer de vértices, el objeto Etiqueta gráfica y el recurso de gráficos dinámicos.

Miembros públicos:

  • DXTutorial() — es el constructor. Los identificadores de DirectX se establecen en 0.
  • ~DXTutorial() — es el destructor. Se llama al método Deinit().
  • Init() — preparación para el trabajo. Toma como entrada una matriz de vértices y una altura y anchura opcionales. Comprueba la corrección de los datos obtenidos, llama a InitCanvas() e InitDevice(). Retorna el estado del progreso de la operación.
  • Draw() — muestra una imagen en la pantalla. Borra los búferes de color y profundidad, y envía la imagen a un recurso gráfico. Retorna el estado del progreso de la operación.


Matriz de vértices

Como los vértices contienen solo información sobre las coordenadas, usaremos por simplicidad una estructura que contenga 4 variables de tipo float. Coordenadas X, Y, Z en el espacio tridimensional: W es una constante auxiliar, debe ser igual a 1, y es necesaria para las operaciones matriciales.

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

Para conseguir un triángulo, necesitamos 3 vértices, por lo que usaremos una matriz de tamaño 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}};


Inicialización

Transmitimos al objeto una matriz de vértices y el tamaño del lienzo. Comprobamos los datos de entrada. Si la anchura o altura transmitidas es menor a uno, el parámetro se establecerá en 500 píxeles. El tamaño de la matriz de vértices debe ser igual a 3. Luego, en un ciclo, comprobamos cada vértice. Las coordenadas X e Y deben estar en el rango [-1;1], Z debe ser igual a 0, y restablece a la fuerza a este valor. W debe ser igual a 1, también se reinicia forzosamente. Llamamos a las funciones de inicialización del lienzo y DirectX.

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


Creando un lienzo para el dibujado

En la función InitCanvas(), se crea un objeto "Etiqueta gráfica", cuyas coordenadas se especifican en píxeles. Luego, a este objeto se vincula un recurso gráfico dinámico en el que se mostrará la imagen de DirectX.

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

Vamos a estudiar el código más en profundidad.

m_canvas = "DXTutorialCanvas";

Asignamos el nombre al objeto gráfico "DXTutorialCanvas".

m_resource = "::DXTutorialResource";

Establecemos el nombre del recurso gráfico dinámico "::DXTutorialResource".

int area = m_width * m_height;

El método necesitará el producto de la anchura y la altura varias veces, por lo que haremos los cálculos de antemano y guardaremos el resultado.

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

Creamos un objeto "Etiqueta gráfica" llamado "DXTutorialCanvas".

ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)

Desplazamos el objeto 100 píxeles hacia la derecha desde la esquina superior izquierda del gráfico.

ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)

Desplazamos el objeto 100 píxeles hacia abajo desde la esquina superior izquierda del gráfico.

ArrayResize(m_image, area)

Cambiamos el tamaño de la matriz para el dibujado.

ArrayInitialize(m_image, ColorToARGB(clrBlack))

Rellenamos la matriz en color negro. Los colores de la matriz deben almacenarse en formato ARGB. Para mayor comodidad, usamos la función estándar ColorToARGB para la conversión de color.

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

Creamos un recurso gráfico dinámico llamado "::DXTutorialResource", con una anchura m_width y una altura m_height. Indicamos el uso de color con transparencia a través de COLOR_FORMAT_ARGB_NORMALIZE. Usamos la matriz m_image como fuente de datos.

ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)

Vinculamos un objeto y un recurso. Antes, no especificábamos el tamaño del objeto: ahora se ajustarán automáticamente al tamaño del recurso.


Inicializando DirectX

Vamos a pasar a lo más interesante.

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

Analicemos el código.

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

Aquí se describe el formato de los vértices. Esta información es necesaria para que la tarjeta de vídeo procese correctamente la matriz de vértices en la entrada. En este caso, el tamaño de la matriz será 1, porque los vértices solo almacenan información sobre la posición. Pero si también añadimos información sobre el color del vértice, necesitaremos una celda de matriz más. "POSITION" indica que la información está relacionada con coordenadas. 0 es el índice semántico. Si necesitamos transmitir dos coordenadas distintas en un vértice, podemos indicar el índice 0 para la primera y el índice 1 para la segunda. DX_FORMAT_R32G32B32A32_FLOAT - formato en el que se presenta la información. En este caso, cuatro números de coma flotante de 32 bits.

string shader_error = "";

En esta variable se guardarán los errores de compilación del sombreador.

m_dx_context = DXContextCreate(m_width, m_height);

Creamos un contexto de gráficos con una anchura m_width y una altura m_height. Recordamos el controlador.

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

Creamos un sombreador de vértices y guardamos el controlador. DX_SHADER_VERTEX indica el tipo de sombreador: de vértices. La línea sombreador almacena el código fuente de los sombreadores de vértices y píxeles, pero le recomendamos almacenarlos en archivos separados e incluirlos como recursos. "VShader" es el nombre del punto de entrada (función principal en los programas normales). Cuando un sombreador compila un error, shader_error contendrá información adicional. Por ejemplo, si indicamos el punto de entrada "VSha", la variable contendrá el siguiente texto: "error X3501: 'VSha': entrypoint not found".

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

Lo mismo sucede con el sombreador de píxeles, solo que indicamos el tipo y el punto de entrada correspondientes.

m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);

Creamos un búfer y guardamos el identificador. Indicamos que el búfer es un búfer de vértices. Transmitimos una matriz de vértices.

DXShaderSetLayout(m_dx_vertex_shader, layout)

Transmitimos la información sobre el marcado de vértices.

DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)

Establecemos el tipo de primitivas "lista de triángulos".

DXShaderSet(m_dx_context, m_dx_vertex_shader)

Transmitimos la información sobre el sombreador de vértices.

DXShaderSet(m_dx_context, m_dx_pixel_shader)

Transmitimos la información sobre el sombreador de píxeles.

DXBufferSet(m_dx_context, m_dx_buffer)

Transmitimos la información sobre el búfer.


Mostrando la imagen

DirectX muestra la imagen en una matriz. Usando como base esta matriz, se crea un recurso gráfico.

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

Vamos a analizar este método con más detalle.

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

Se crea una variable dx_color de tipo DXVector. Se le asigna un color rojo con transparencia media. El formato RGBA con valores float de 0 a 1.

DXContextClearColors(m_dx_context, dx_color)

Rellenamos el búfer en el color dx_color.

DXContextClearDepth(m_dx_context)

Limpiamos el búfer de profundidad.

DXDraw(m_dx_context)

Enviamos a DirectX la tarea a dibujar.

DXContextGetColors(m_dx_context, m_image)

Obtenemos el resultado del funcionamiento en la matriz m_image.

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

Actualizamos el recurso gráfico dinámico.


Liberando recursos

DirectX requiere la liberación manual de recursos. También debemos eliminar el gráfico y el recurso. Comprobamos la necesidad de liberar recursos, luego llamamos a la función DXRelease. El recurso gráfico dinámico se elimina con ResourceFree. El objeto gráfico se libera con ObjectDelete.

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");
     }
  }


Sombreadores

Guardaremos los sombreadores en la línea shader. Pero con grandes volúmenes, será mejor ponerlos en archivos externos separados e incluirlos como recursos.

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";

Un sombreador es un programa para tarjetas de vídeo. DirectX está escrito en un lenguaje HLSL similar a C. float4 en el sombreador es un tipo de datos incorporado, a diferencia de nuestra estructura. VShader en este caso representa el sombreador de vértices y PShader representa el sombreador de píxeles. POSITION  es una semántica que indica que la entrada representa coordenadas, el sentido es igual que en DXVertexLayout. SV_POSITION es la misma semántica, pero del valor de salida. El prefijo SV_ indica que el valor es un valor de sistema. SV_TARGET es una semántica, indica que el valor se escribirá en una textura o un búfer de píxeles. Bien, qué sucede aquí. El sombreador de vértices recibe coordenadas como entrada y las transmite sin cambios a la salida. El sombreador de píxeles (de la etapa de rasterización) recibe los valores interpolados, para los cuales el color se establece en verde.


OnStart

La función crea una instancia de la clase DXTutorial. Se llama a la función Init a la que se transmite una matriz de vértices. Luego se llama a la función Draw. Después de ello, el script finaliza la ejecución.

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


Conclusión

En el artículo, nos hemos familiarizado con la historia de DirectX. Además, hemos entendido qué es y por qué lo necesitamos. Asimismo, hemos analizado la estructura interna de la API. También hemos aprendido cómo es la canalización para convertir vértices en píxeles en las tarjetas de vídeo modernas, familiarizános además con la lista de acciones necesarias para trabajar con DirectX. Como colofón, hemos estudiado un pequeño ejemplo en MQL. ¡Y finalmente, hemos mostrado nuestro primer triángulo! ¡Enhorabuena, ha superado el rito de iniciación! Pero no se relaje, aún hay muchas cosas nuevas e interesantes por delante que necesita saber para exprimir a tope DirectX. Esto incluye la transmisión de datos adicionales a los vértices y el lenguaje de programación de sombreadores HLSL, así como varias transformaciones usando matrices, texturas, normales y numerosos efectos especiales.


Referencias y enlaces

  1. Wikipedia.
  2. Documentación de Microsoft.


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/10425

Múltiples indicadores en un gráfico (Parte 05): Convirtamos el MetaTrader 5 en un sistema RAD (I) Múltiples indicadores en un gráfico (Parte 05): Convirtamos el MetaTrader 5 en un sistema RAD (I)
A pesar de no saber programar, muchas personas son bastante creativas y tienen grandes ideas, pero la falta de conocimientos o de entendimiento sobre la programación les impide hacer algunas cosas. Aprenda a crear un Chart Trade, pero utilizando la propia plataforma MT5, como si fuera un IDE.
Gráficos en la biblioteca DoEasy (Parte 97): Procesamiento independiente del desplazamiento de los objetos de formulario Gráficos en la biblioteca DoEasy (Parte 97): Procesamiento independiente del desplazamiento de los objetos de formulario
En el presente artículo, analizaremos la implementación del desplazamiento independiente de cualquier objeto de formulario con el ratón, y también complementaremos la biblioteca con mensajes de error y nuevas propiedades de transacciones previamente introducidos en el terminal y MQL5.
Consejos de un programador profesional (Parte III): Registro Conexión al sistema de recopilación y análisis de logs Seq Consejos de un programador profesional (Parte III): Registro Conexión al sistema de recopilación y análisis de logs Seq
Implementación de la clase Logger para unificar (estructurar) los mensajes mostrados en el diario del experto. Conexión al sistema de recopilación y análisis de logs Seq. Supervisión de los mensajes en el modo online.
Indicadores múltiplos em um gráfico (Parte 04): Iniciando pelo EA Indicadores múltiplos em um gráfico (Parte 04): Iniciando pelo EA
En artículos anteriores, expliqué cómo crear un indicador con múltiples subventanas, lo que se vuelve interesante cuando usamos un indicador personalizado. Aquí entenderemos cómo añadir múltiples ventanas en un EA.