English Русский 中文 Español Deutsch 日本語
preview
Tutorial DirectX (Parte I): Desenhando o primeiro triângulo

Tutorial DirectX (Parte I): Desenhando o primeiro triângulo

MetaTrader 5Integração | 22 junho 2022, 09:23
480 3
Rorschach
Rorschach

Índice

  1. Introdução
  2. API DirectX
    1. A história do DirectX
    2. Direct3D
    3. Device
    4. Device Context
    5. Swap Chain
    6. Input Layout
    7. Topologia Primitiva
    8. HLSL
  3. Pipeline Gráfica
  4. Gráficos 3D
    1. Primitivas
    2. Vértices
    3. Cor
  5. Sequência de ações em MQL
  6. Prática
    1. Visão geral da classe
    2. Matriz de vértices
    3. Inicialização
    4. Criando uma Tela
    5. Inicialização do DirectX
    6. Exibição da imagem
    7. Liberando os recursos
    8. Sombreadores
    9. OnStart
  7. Conclusão
  8. Referências e Links

Introdução

Um rito de iniciação, ou rito de inicialização, é o que vem à mente quando você entende que precisa escrever tanto código e preencher enormes estruturas em C++, para desenhar até mesmo um triângulo primitivo usando o DirectX. Sem falar em coisas mais complexas, como texturas, matrizes de transformação e sombras. Felizmente, a MetaQuotes cuidou disso: eles esconderam toda a rotina, deixando apenas as funções mais necessárias. No entanto, há um efeito colateral disso: qualquer pessoa que não esteja familiarizada com o DirectX não poderá ver a imagem inteira e entender o por que e como tudo está acontecendo. Ainda há muito código a ser escrito em MQL.

Sem entender o que está por detrás, o DirectX é desconcertante: "Por que ele é tão difícil e confuso, não pode ser simplificado?" E esta é apenas a primeira etapa. Além disso, você estuda a linguagem de sombreamento HLSL e as peculiaridades da programação da placa de vídeo. Para evitar todas essas confusões, eu sugiro considerar a estrutura interna do DirectX, embora sem entrar em detalhes. Em seguida, nós escreveremos um pequeno script em MQL que exibe um triângulo na tela.


API DirectX

A história do DirectX

O DirectX é um conjunto de APIs (Application Programming Interface) para trabalhar com multimídia e vídeo nas plataformas da Microsoft. Ele foi desenvolvido principalmente para criar jogos, mas com o tempo os desenvolvedores começaram a usá-lo em softwares de engenharia e matemática. O DirectX permite trabalhar com gráficos, som, entradas e redes sem a necessidade de acessar funções de baixo nível. A API surgiu como uma alternativa ao OpenGL, que é multiplataforma. Ao criar o Windows 95, a Microsoft implementou mudanças substanciais, que dificultaram o ambiente de desenvolvimento de aplicativos e jogos e, portanto, poderiam afetar a popularidade desse sistema operacional. O DirectX foi criado como uma solução para fazer com que mais programadores desenvolvessem jogos para o Windows. O desenvolvimento do DirectX foi iniciado por Craig Eisler, Alex St. John e Eric Engstrom.

  • Setembro de 1995. Primeira versão. Era uma versão bastante primitiva, que funcionava como um complemento para a API do Windows. Ela não chamou muita atenção. Os desenvolvedores estavam usando principalmente o DOS, que se comparado com o novo sistema operacional, ele tinha maiores requisitos de sistema. Além disso, o OpenGL já existia naquela época. Não ficou claro se a Microsoft continuaria a oferecer suporte ao DirectX.
  • Junho de 1996. O lançamento da versão 2.
  • Setembro de 1996. Versão 3.
  • Agosto de 1997. Em vez da quarta versão, a Microsoft lançou a versão 5. Ficou mais fácil escrever códigos com esta versão e os programadores começaram a prestar mais atenção nela.
  • Agosto de 1998. Versão 6. O trabalho foi mais simplificado.
  • Setembro de 1999. Versão 7. Tornou-se possível criar buffers de vértice na memória de vídeo, sendo uma grande vantagem sobre o OpenGL.
  • Novembro de 2000. Versão 8. Momento crucial. Antes deste momento, o DirectX estava tentando recuperar o atraso, mas a versão oito ultrapassou a indústria. A Microsoft começou a cooperar com os fabricantes de placas gráficas. Apareceram os vértices e sombreadores de pixel. O desenvolvimento exigia apenas um computador pessoal, ao contrário do OpenGL que exigia uma estação de trabalho.
  • Dezembro de 2002. Versão 9. O DirectX tornou-se o padrão da indústria. Apareceu a linguagem sombreadora HLSL. Provavelmente esta foi a versão mais duradoura do DirectX. Como o soquete 775...
  • Novembro de 2006. Versão 10. Ao contrário da versão 9, esta tinha uma ligação com o sistema operacional Vista, que não era popular. Esses fatores tiveram um efeito negativo no sucesso da versão 10. A Microsoft adicionou um sombreador de geometria.
  • Outubro de 2009. Versão 11. Adição do mosaico, sombreador de computação, aprimorado o trabalho com os processadores multi-núcleo.
  • Julho de 2015. Versão 12. API de baixo nível. A versão proporcionou uma compatibilidade ainda melhor com os processadores multi-núcleo, a possibilidade de combinar os recursos de várias placas de vídeo de diferentes fornecedores, ray tracing.


Direct3D

O Direct3D é um dos diversos componentes da API DirectX; ele é responsável pelos gráficos e é um intermediário entre as aplicações e o driver da placa gráfica. O Direct3D é baseado em COM (Component Object Model). COM é um padrão de interface binária de aplicação (ABI) introduzido pela Microsoft em 1993. Ele é usado para criar objetos em comunicação entre processos (IPC) em várias linguagens de programação. O COM surgiu como uma solução com o objetivo de fornecer uma maneira independente da linguagem para implementar objetos que pudessem ser usados fora de seu ambiente de criação. COM permite que objetos sejam reutilizados sem o conhecimento de sua implementação interna, pois fornecem interfaces bem definidas e separadas da implementação. Os objetos COM são responsáveis por sua própria criação e destruição usando contagem de referência.

Interfaces

Interfaces


Device

Tudo no Direct3D começa com um Device. Ele é usado para criar recursos (buffers, textura, sombreadores, objetos de estado) e a enumeração de recursos dos adaptadores gráficos. O Device é um adaptador virtual localizado no sistema do usuário. O adaptador pode ser uma placa de vídeo real ou uma emulação de software. Os dispositivos de hardware são usados com mais frequência porque fornecem o mais alto desempenho. O dispositivo fornece uma interface unificada para todos esses adaptadores e os usa para a renderização gráfica de uma ou mais saídas.

Direct3D

Device


Device Context

O Device Context (contexto do dispositivo) é responsável por qualquer coisa relacionada à renderização. Isso inclui a configuração do pipeline e a criação de comandos para a renderização. O Device Context apareceu na décima primeira versão do DirectX — antes, a renderização era implementada pelo Device. Existem dois tipos de contexto: Immediate Context (Contexto Imediato) e Deferred Context (Contexto Diferido).

O contexto imediato fornece acesso aos dados na placa de vídeo e a capacidade de executar imediatamente uma lista de comandos no dispositivo. Cada Device tem apenas um Immediate Context. Apenas uma thread pode acessá-lo por vez. A sincronização deve ser usada para permitir o acesso a várias threads.

O Deferred Context adiciona comandos à lista de comandos para serem executados posteriormente no Immediate Context. Assim, todos os comandos eventualmente passam pelo Immediate Context. O Deferred Context envolve alguma sobrecarga, portanto, os benefícios de usá-lo são visíveis apenas na paralelização de tarefas com uso intensivo de recursos. Você pode criar vários contextos adiados e acessar cada um de uma thread separada. Mas para acessar o mesmo Deferred Context de várias threads, você precisa de sincronização, assim como no Immediate Context.


Swap Chain

O Swap Chain (cadeia de troca) é projetado para criar um ou mais buffers de retorno. Esses buffers armazenam imagens renderizadas até que elas sejam exibidas na tela. Os buffers frontais e traseiros funcionam da seguinte forma. O buffer frontal é o que você está vendo na tela. O buffer traseiro é a imagem que está sendo renderizada. Em seguida, os buffers são trocados: o frontal se torna traseiro e o de trás vai para a frente. E todo o processo é repetido diversas vezes. Assim, nós sempre vemos a imagem, enquanto a próxima está sendo renderizada "nos bastidores".

Swapchain

Swap Chain

Device, Device Context e Swap Chain são os principais componentes necessários para a renderização de uma imagem.


Input Layout

O Input Layout informa ao pipeline sobre a estrutura do buffer de vértice. Nós só precisamos das coordenadas para os nossos propósitos, e é por isso que nós podemos simplesmente passar o array de vértices do tipo float4, sem usar uma estrutura especial. float4 é uma estrutura que consiste em quatro variáveis float.

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

Por exemplo, considere uma estrutura de vértices mais complexa que consiste numa coordenada e duas cores:

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

O Input Layout em MQL para esta estrutura ficará assim:

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

Cada elemento do array 'layout' descreve o elemento correspondente da estrutura Vertex.

  • O primeiro elemento da estrutura DXVertexLayout é o nome semântico. Ele é usado para mapear os elementos da estrutura Vertex com os elementos da estrutura no sombreador de vértices. "POSITION" significa que o valor é responsável pelas coordenadas, "COLOR" é usado para a cor.
  • O segundo elemento é o índice semântico. Se precisarmos passar vários parâmetros do mesmo tipo, por exemplo, dois valores de cor, o primeiro é passado com índice 0 e o segundo com índice 1.
  • O último elemento descreve o tipo que o valor é representado na estrutura Vertex. DX_FORMAT_R32G32B32A32_FLOAT significa literalmente que é uma cor RGBA representada por um valor de ponto flutuante de 32 bits para cada componente. Isso pode ser confuso. Esse tipo pode ser usado para passar coordenadas — ele fornece informações sobre quatro valores de ponto flutuante de 32 bits, assim como float4 na estrutura Vertex.


Topologia Primitiva

O buffer de vértices armazena informações sobre os pontos, mas nós não sabemos como eles estão localizados em relação uns aos outros na primitiva. É para isso que a Topologia Primitiva é usada. PointList significa que o buffer armazena pontos individuais. Line Strip representa o buffer como pontos conectados formando uma polilinha. Cada dois pontos na Line List descrevem uma única linha. Triangle Strip e Triangle List definem a ordem dos pontos para os triângulos, semelhantes às linhas.

Topologia

Topologia

HLSL

High Level Shading Language (HLSL) é uma linguagem semelhante a C para escrever sombreadores. Sombreadores, por sua vez, são programas projetados para rodar numa placa gráfica. A programação em todas as linguagens GPGPU é muito semelhante e possui um recurso específico relacionado ao design das placas gráficas. Se você tiver experiência com OpenCL, Cuda ou OpenGL, entenderá o HLSL muito rapidamente. Mas se você criou apenas programas para CPUs, pode ser difícil mudar para um novo paradigma. Muitas vezes, os métodos de otimização tradicionalmente usados para o processador não funcionarão. Como exemplo, seria correto que o processador usasse a instrução 'if' para evitar cálculos desnecessários ou para selecionar o algoritmo ideal. Mas na GPU, pelo contrário, isso pode aumentar o tempo de execução do programa. Para tirar o máximo, você pode ter que contar o número de registros envolvidos. Os três principais princípios de alto desempenho na programação de placas gráficas são: paralelismo, taxa de transferência e ocupação.


Pipeline Gráfica

O pipeline foi projetado para converter uma cena 3D numa representação de exibição 2D. O pipeline é um reflexo da estrutura interna da placa de vídeo. O diagrama abaixo mostra como o fluxo de dados flui da entrada do pipeline para a sua saída em todos os estágios. O oval mostra os estágios que são programados usando a linguagem HLSL — sombreadores, e o retângulo mostra os estágios fixos. Alguns deles são opcionais e podem ser facilmente ignorados.

Pipeline Gráfica

Pipeline Gráfica

  • O estágio do Input Assembler (Montador de Entrada) recebe os dados dos buffers do vértice e índice e os prepara para o sombreador de vértice.

  • O estágio Vertex Shader (Sombreador de Vértice) realiza as operações com os vértices. Estágio programável. Obrigatório no pipeline.
  • O estágio Hull Shader é responsável pelo nível de tesselação. Estágio programável. Opcional.
  • O estágio Tessellator cria primitivas menores. Estágio fixo. Opcional.
  • O estágio Domain Shader calcula os valores finais dos vértices após a tesselação. Estágio programável. Opcional.
  • O estágio Geometry Shader (Sombreador Geometry) aplica várias transformações nas primitivas (pontos, linhas, triângulos). Estágio programável. Opcional.
  • O estágio Stream Output (Saída de Fluxo) transfere os dados para a memória da GPU a partir da qual eles podem ser enviados de volta ao pipeline. Estágio fixo. Opcional.
  • O estágio Rasterizer (Rasterizador) corta tudo o que não cai no escopo, prepara os dados para o Sombreador de Pixel. Estágio fixo.
  • O estágio Pixel Shader (Sombreador de Pixel) executa as operações de pixel. Estágio programável. Obrigatório no pipeline.

  • O estágio Output Merger (Fusão de Saída) forma a imagem final. Estágio fixo.

Outro sombreador que vale a pena mencionar é o Compute Shader (DirectCompute), que é um pipeline separado. Este sombreador é projetado para cálculos de uso geral, semelhante ao OpenCL e Cuda. Estágio programável. Opcional.

A implementação do DirectX da MetaQuotes não inclui o DirectCompute e o estágio de tesselação. Assim, nós temos apenas três sombreadores: vértice, geometria e pixel.


Gráficos 3D

Primitivas

A renderização de primitivas é o objetivo principal da API gráfica. As placas de vídeo modernas são adaptadas para a renderização rápida de um grande número de triângulos. Na verdade, no atual estágio de desenvolvimento da computação gráfica, a maneira mais eficaz de desenhar objetos 3D é criar uma superfície a partir de polígonos. Uma superfície pode ser descrita especificando apenas três pontos. O software de modelagem 3D geralmente usa retângulos, mas a placa gráfica ainda forçará os polígonos em triângulos.

Malha

Malha de triângulos

Vértices

Três vértices devem ser especificados para a renderização de um triângulo no Direct3D. Pode parecer que um vértice é a posição de um ponto no espaço, mas no Direct3D é algo mais que isso. Além da posição do vértice, nós podemos passar as cores, coordenadas de textura e normais. Geralmente, as transformações de matrizes são geralmente usadas para normalizar as coordenadas. Para não complicar neste momento, leve em consideração o fato de que na etapa de rasterização as coordenadas dos vértices ao longo dos eixos X e Y devem estar dentro de [-1; 1]; ao longo de Z de 0 a 1.


Cor

A cor na computação gráfica tem três componentes: vermelho, verde e azul. Isso está relacionado às características estruturais do olho humano. Os pixels de exibição também consistem em três subpixels dessas cores. MQL tem a função ColorToARGB para converter as cores da web para o formato ARGB, que armazena a transparência além da cor. A cor pode ser normalizada quando os componentes estiverem no intervalo [0;1] e não normalizada: por exemplo, componentes para uma cor de 32 bits terão valores de 0 a 255 (2^8-1). A maioria dos monitores modernos funcionam com cores de 32 bits.


Sequência de ações em MQL

Para exibir uma imagem usando o DirectX em MQL, você precisa fazer o seguinte:

  1. Criar uma "Etiqueta Gráfica" ou um objeto Bitmap usando a ObjectCreate.
  2. Criar um recurso gráfico dinâmico usando a ResourceCreate.
  3. Vincular um recurso ao objeto usando a ObjectSetString com o parâmetro OBJPROP_BMPFILE.
  4. Criar um arquivo para os sombreadores (ou salvar os sombreadores numa variável de string).
  5. Escrever sombreadores de vértice e pixel em HLSL.
  6. Conectar o arquivo do sombreador usando o #resource "NomeArquivo.hlsl" as string variable_name;
  7. Descrever o formato dos vértices numa matriz do tipo DXVertexLayout
  8. Criar um contexto — DXContextCreate.
  9. Criar o sombreador de vértice — DXShaderCreate com o parâmetro DX_SHADER_VERTEX.
  10. Criar o sombreador de pixel — DXShaderCreate com o parâmetro DX_SHADER_PIXEL.
  11. Criar o buffer de vértice — DXBufferCriar com o parâmetro DX_BUFFER_VERTEX.
  12. Se necessário, criar um buffer de índice — DXBufferCriar com o parâmetro DX_BUFFER_INDEX.
  13. Passar o formato do vértice — DXShaderSetLayout.
  14. Definir a topologia das primitivas — DXPrimiveTopologySet.
  15. Vincular os sombreadores de vértice e pixel — DXShaderSet.
  16. Vincular o buffer de vértice (e índice, se houver) — DXBufferSet.
  17. Limpar o buffer de profundidade — DXContextClearDepth.
  18. Se necessário, limpar o buffer de cores — DXContextClearColors
  19. Enviar um comando de renderização — DX Draw (ou DXDrawIndexado se um buffer de índice for especificado)
  20. Passar o resultado para o recurso gráfico — DXContextGetColors
  21. Atualizar o recurso gráfico — ResourceCreate
  22. Não se esqueça de atualizar o gráfico — ChartRedraw
  23. Limpar após o uso — DX Release
  24. Excluir o recurso gráfico —ResourceFree
  25. Excluir o objeto gráfico —ObjectDelete

Você ainda está comigo? Na verdade, tudo é mais fácil do que parece. Você vai ver mais adiante.


Prática

Visão geral da classe

Todo o processo de utilização do DirectX pode ser dividido em várias etapas: criação de uma tela, inicialização do dispositivo, escrita do sombreadores de vértice e pixel, exibição da imagem resultante na tela, liberação dos recursos. A classe ficará assim:

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

Membros privados:

  • m_width and m_height — largura e altura da tela. Esses membros são usados ao criar o objeto "Etiqueta Gráfica", recurso gráfico dinâmico e o contexto gráfico. Seus valores são definidos durante a inicialização, mas também é possível definir seus valores manualmente.
  • m_image — uma matriz usada ao criar um recurso gráfico. O resultado da operação do DirectX é passado para ele.
  • m_canvas — o nome do objeto gráfico, m_resource — o nome do recurso gráfico. Usado durante a inicialização e desinicialização.
      Manipuladores do DirectX:
  • m_dx_context — o mais importante, o identificador de contexto gráfico. Participa de todas as operações do DirectX. Ele é inicializado quando o contexto gráfico é criado.
  • m_dx_vertex_shader — manipulador do sombreador de vértice. Ele é usado ao definir a marcação do vértice, vincular ao contexto gráfico e desinicialização. Inicializado na compilação do sombreador de vértice.
  • m_dx_pixel_shader — manipulador do sombreador de pixel. Ele é usado ao vincular ao contexto gráfico e na desinicialização. Inicializado na compilação do sombreador de pixel.
  • m_dx_buffer — identificador de buffer do vértice. Ele é usado ao vincular ao contexto gráfico e na desinicialização. Ele é inicializado quando o buffer de vértice é criado.

      Métodos de inicialização e desinicialização:

  • InitCanvas() — cria uma tela para exibir a imagem. O objeto "Etiqueta Gráfica" e o recurso gráfico dinâmico são usados. O fundo é preenchido com preto. Retorna o andamento da operação.
  • InitDevice() — inicializa o DirectX. Cria um contexto gráfico, sombreadores de vértice e pixel e um buffer de vértice. Define o tipo de primitivas e marca os vértices. Recebe um array de vértices como entrada. Retorna o andamento da operação.
  • Deinit() — libera os recursos usados. Exclui o contexto gráfico, sombreadores de vértice e pixel, buffer de vértice, objeto "Etiqueta Gráfica" e o recurso gráfico dinâmico.

Membros públicos:

  • DXTutorial() — construtor. Define os identificadores do DirectX como 0.
  • ~DXTutorial() — destrutor. Chama o método Deinit().
  • Init() — preparação para o trabalho. Recebe uma matriz de vértices e uma altura e largura opcionais como entrada. Valida os dados recebidos, chama a InitCanvas() e InitDevice(). Retorna o andamento da operação.
  • Draw() — exibe a imagem na tela. Limpa os buffers de cor e profundidade, envia a imagem para um recurso gráfico. Retorna o andamento da operação.


Matriz de vértices

Como os vértices contêm apenas informações sobre as coordenadas, por simplicidade nós usaremos uma estrutura contendo 4 variáveis flutuantes. X, Y, Z são as coordenadas no espaço tridimensional. W é uma constante auxiliar, deve ser igual a 1, usada para as operações com matrizes.

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

Um triângulo precisa de 3 vértices, então nós usamos uma matriz de tamanho igual a 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}};


Inicialização

Passamos a matriz de vértices e o tamanho da tela para o objeto. Verificamos os dados de entrada. Se a largura ou altura passada for menor que um, o parâmetro será definido como 500 pixels. O tamanho da matriz de vértices deve ser 3. Em seguida, cada vértice é verificado num loop. As coordenadas X e Y devem estar no intervalo [-1;1], Z deve ser igual a 0 - se for forçosamente redefinido para este valor. W deve ser 1, também se redefine forçadamente. Chamamos as funções de inicialização de tela e o 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);
  }


Criando uma Tela

A função InitCanvas() cria um objeto do tipo "Etiqueta Gráfica" cujas coordenadas são definidas em pixels. Em seguida, um recurso gráfico dinâmico é vinculado a esse objeto, no qual a imagem do DirectX será enviada.

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 considerar o código com mais detalhes.

m_canvas = "DXTutorialCanvas";

Especificamos o nome do recurso gráfico "DXTutorialCanvas".

m_resource = "::DXTutorialResource";

Especificamos o nome do recurso gráfico dinâmico "::DXTutorialResource".

int area = m_width * m_height;

O método precisará do produto da largura e da altura várias vezes, então nós calculamos com antecedência e salvamos o resultado.

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

Criamos um objeto do tipo "Etiqueta Gráfica" chamado "DXTutorialCanvas".

ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)

Movemos o objeto 100 pixels para a direita a partir do canto superior esquerdo do gráfico.

ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)

Movemos o objeto 100 pixels para baixo a partir do canto superior esquerdo do gráfico.

ArrayResize(m_image, area)

Redimensionamos a matriz para desenhar.

ArrayInitialize(m_image, ColorToARGB(clrBlack))

Preenchemos a matriz com a cor preta. As cores na matriz devem ser armazenadas no formato ARGB. Por conveniência, usamos a função padrão ColorToARGB para converter a cor para o formato necessário.

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

Criamos um recurso gráfico dinâmico chamado "::DXTutorialResource", com a largura de m_width e com a altura de m_height. Indicamos o uso de uma cor com transparência através da COLOR_FORMAT_ARGB_NORMALIZE. Usamos a matriz m_imagem como fonte de dados.

ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)

Associamos o objeto e o recurso. Anteriormente, nós não especificamos o tamanho do objeto, pois ele se ajustará automaticamente ao tamanho do recurso.


Inicialização do DirectX

Vamos para a parte mais interessante.

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

Vamos analisar o código.

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

Esta linha descreve o formato dos vértices. Essas informações são necessárias para que a placa gráfica manipule corretamente a matriz de entrada dos vértices. Neste caso o tamanho do array é igual a 1, pois os vértices armazenam apenas as informações de posição. Mas se nós adicionarmos as informações sobre a cor do vértice, nós precisaremos de outra célula do array. "POSITION" significa que a informação está relacionada com as coordenadas. 0 é um índice semântico. Se nós precisarmos passar duas coordenadas diferentes num vértice, nós podemos usar o índice 0 para o primeiro e 1 para o segundo. DX_FORMAT_R32G32B32A32_FLOAT - formato de representação das informações. Nesse caso, quatro números de ponto flutuante de 32 bits.

string shader_error = "";

Esta variável armazenará os erros de compilação do sombreador.

m_dx_context = DXContextCreate(m_width, m_height);

Criamos um contexto gráfico com a largura de m_width e a altura de m_height. Lembre-se do manipulador.

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

Criamos o sombreador de vértice e salvamos o identificador. DX_SHADER_VERTEX indica o tipo de sombreador - vértice. A String shader armazena o código-fonte dos sombreadores de vértice e pixel, mas é recomendável armazená-los em arquivos separados e incluí-los como recursos. "VShader" é o nome do ponto de entrada (função 'main' em programas normais). Se ocorrer um erro de compilação do sombreador, informações adicionais serão gravadas no shader_error. Por exemplo, se você especificar o ponto de entrada "VSha", a variável conterá o seguinte texto: "error X3501: 'VSha': entrypoint not found".

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

O mesmo diz respeito ao sombreador de pixel: specify here the appropriate type and entry point.

m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);

Criamos um buffer e salvamos o identificador. Indicamos que ele é um buffer de vértice. Passamos um array de vértices.

DXShaderSetLayout(m_dx_vertex_shader, layout)

Passamos informações sobre o layout dos vértices.

DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)

Definimos a "lista de triângulos" como o tipo de primitivas.

DXShaderSet(m_dx_context, m_dx_vertex_shader)

Passamos informações sobre o sombreador de vértice.

DXShaderSet(m_dx_context, m_dx_pixel_shader)

Passamos informações sobre o sombreador de pixel.

DXBufferSet(m_dx_context, m_dx_buffer)

Passamos informações sobre o buffer.


Exibição da imagem

O DirectX envia a imagem para uma matriz. Um recurso gráfico é criado com base nessa matriz.

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 analisar o método com mais detalhes.

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

A variável dx_color do tipo DXVector é criada. A cor vermelha com meia transparência é atribuída a ele. Formato RGBA com valores float de 0 a 1.

DXContextClearColors(m_dx_context, dx_color)

Preenchemos o buffer com a cor dx_color.

DXContextClearDepth(m_dx_context)

Limpamos o buffer de profundidade.

DXDraw(m_dx_context)

Enviamos uma tarefa de renderização para o DirectX.

DXContextGetColors(m_dx_context, m_image)

Retornamos o resultado para a matriz m_image.

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

Atualizamos o recurso gráfico dinâmico.


Liberando os recursos

O DirectX requer que os recursos sejam liberados manualmente. Além disso, é necessário excluir o objeto gráfico e o recurso. Verificamos se nós precisamos liberar os recursos e depois chamamos o DXRelease. O recurso gráfico dinâmico é excluído por meio da ResourceFree. O objeto gráfico é liberado através da 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

Sombreamentos serão armazenados na string shader. Mas com grandes volumes, é melhor colocá-los em arquivos externos separados e conectá-los 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";

Um shader é um programa para uma placa gráfica. No DirectX ele é escrito na linguagem HLSL do tipo C. O float4 no sombreador é um tipo de dado embutido, ao contrário de nossa estrutura. O VShader é um sombreador de vértice neste caso, enquanto o PShader é um sombreador de pixel. POSITION - semântica indicando que os dados de entrada são as coordenadas; o significado é o mesmo que em DXVertexLayout. SV_POSITION - também semântico, mas é usado para o valor de saída. O prefixo SV_ indica que este é um valor do sistema. SV_TARGET - semântica, indica que o valor será escrito na textura ou no buffer de pixel. Então o que está acontecendo aqui. As coordenadas são inseridas no sombreador de vértice, que as passa inalteradas para a saída. O sombreador de pixel (do estágio de rasterização) recebe os valores interpolados, para os quais a cor é definida como verde.


OnStart

Uma instância da classe DXTutorial é criada na função. A função Init é chamada, para a qual o array de vértices é passado. Então a função Draw é chamada. Depois disso, a execução do script termina.

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


Conclusão

Neste artigo, nós consideramos a história do DirectX. Nós tentamos entender o que ele é e a sua finalidade. Nós também consideramos a estrutura interna da API. Nós vimos o pipeline que converte os vértices em pixels em placas gráficas modernas. Além disso, o artigo fornece uma lista de ações necessárias para trabalhar com o DirectX e um pequeno exemplo em MQL. Finalmente, nós renderizamos nosso primeiro triângulo! Parabéns! Mas há muitas outras coisas novas e interessantes para aprender para uma operação completa com o DirectX. Isso inclui a transferência de outros dados além dos vértices, a linguagem de programação do sombreador HLSL, várias transformações usando matrizes, texturas, normais e vários efeitos especiais.


Referências e Links

  1. Wikipedia.
  2. Documentação da Microsoft.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/10425

Últimos Comentários | Ir para discussão (3)
Daniel Jose
Daniel Jose | 22 jun 2022 em 14:40
O artigo ficou muito bom e muito bem escrito. Mas gostaria de deixar como dica para futuros artigos, você disponibilizar em anexo o código fonte usado ne explicação, para quem deseja ver a coisa funcionando de fato, assim o interesse no artigo será ainda maior ... LEMBRE-SE : Muitos estão começando a aprender MQL5, e não vão conseguir de fato acompanhar a explicação a ponto de fazer um código funcional, o que acabara por desestimular a pessoa a aprender a programar em MQL5 ... principalmente que tem muito pouca experiência em programação. Fica ai a dica ... 😁👍
Rorschach
Rorschach | 2 ago 2022 em 19:58
Daniel Jose #:
O artigo ficou muito bom e muito bem escrito. Mas gostaria de deixar como dica para futuros artigos, você disponibilizar em anexo o código fonte usado ne explicação, para quem deseja ver a coisa funcionando de fato, assim o interesse no artigo será ainda maior ... LEMBRE-SE : Muitos estão começando a aprender MQL5, e não vão conseguir de fato acompanhar a explicação a ponto de fazer um código funcional, o que acabara por desestimular a pessoa a aprender a programar em MQL5 ... principalmente que tem muito pouca experiência em programação. Fica ai a dica ... 😁👍
Thanks for the feedback. All code is presented in the article, nothing has been cut.
Jose Roque Do Carmo Junior
Jose Roque Do Carmo Junior | 29 set 2023 em 14:12
Excelente artigo. Parabéns!!! Terá uma part II ensinando como carregar imagens no directX, texturas e matriz de transformação?
Analisando as razões pelas quais alguns EAs fracassam Analisando as razões pelas quais alguns EAs fracassam
Neste artigo, analisaremos dados de moedas e tentaremos entender com isso por que os Expert Advisors podem mostrar bons resultados em alguns intervalos e, ao mesmo tempo, ter um desempenho ruim em outros.
Usando a classe CCanvas em aplicativos MQL Usando a classe CCanvas em aplicativos MQL
Neste artigo falaremos sobre o uso da classe CCanvas em aplicações MQL, com uma descrição detalhada e exemplos, para que o usuário tenha uma compreensão básica de como usar esta ferramenta
Conselhos de um programador profissional (Parte III): Registro de Logs. Conectando-se ao sistema Seq de coleta e análise de logs Conselhos de um programador profissional (Parte III): Registro de Logs. Conectando-se ao sistema Seq de coleta e análise de logs
Implementação da classe Logger para unificar e estruturar as mensagens que são impressas no log da guia Experts na caixa de ferramentas. Conexão com o sistema Seq de coleta e análise de logs. Monitoramento de mensagens de log online.
Desenvolvendo um EA de negociação do zero (Parte 20): Um novo sistema de ordens (III) Desenvolvendo um EA de negociação do zero (Parte 20): Um novo sistema de ordens (III)
Vamos continuar a implementação do novo sistema de ordens . A criação deste sistema é algo que demanda um bom domínio do MQL5, além de entender como de fato a plataforma MetaTrader 5 funciona e os recursos que ela nos fornece.