Como criar gráficos 3D usando o DirectX no MetaTrader 5

30 abril 2020, 07:34
MetaQuotes
0
2 375

A computação gráfica tridimensional fornece as impressões de objetos tridimensionais em uma tela plana. Tais objetos, bem como a posição do visualizador, podem mudar com o tempo. Consequentemente, a imagem bidimensional também deve ser alterada para criar a ilusão de profundidade da imagem, ou seja, ela deve suportar rotação, zoom, mudanças na iluminação e assim por diante. A MQL5 permite criar e gerenciar gráficos de computador diretamente da plataforma MetaTrader 5 usando as funções DirectX. Observe que sua placa de vídeo deve suportar o DX 11 e o Shader Model 5.0 para que as funções funcionem.


Modelagem de Objetos

Para desenhar um objeto tridimensional em um espaço plano, deve-se obter primeiro um modelo desse objeto nas coordenadas X, Y e Z. Isso significa que cada ponto na superfície do objeto deve ser descrito especificando suas coordenadas. Idealmente, seria necessário descrever um número infinito de pontos na superfície do objeto para preservar a qualidade da imagem durante o dimensionamento. Na prática, os modelos 3D são descritos usando uma malha composta por polígonos. Uma malha mais detalhada com um número maior de polígonos fornece um modelo mais realista. No entanto, são necessários mais recursos computacionais para calcular esse modelo e renderizar os gráficos 3D.

Um modelo de bule como uma malha de polígono

Um modelo de bule como uma malha de polígono.

A divisão dos polígonos em triângulos apareceu há muito tempo, quando os primeiros gráficos computacionais precisavam rodar em placas gráficas fracas. O triângulo permite a descrição exata da posição de uma pequena parte da superfície, bem como o cálculo dos parâmetros relacionados, como luzes e reflexos de luz. A coleção desses pequenos triângulos permite a criação de uma imagem tridimensional realista do objeto. A seguir, o polígono e o triângulo serão usados como sinônimos, pois é muito mais fácil imaginar um triângulo do que um polígono com N vértices.


Cubo composto de triângulos.

Um modelo tridimensional de um objeto pode ser criado descrevendo as coordenadas de cada vértice do triângulo, o que permite o cálculo adicional de coordenadas para cada ponto do objeto, mesmo que o objeto se mova ou a posição do visualizador se altere. Assim, nós lidamos com os vértices, as arestas que os conectam e a face que é formada pelas arestas. Se a posição de um triângulo é conhecida, nós podemos criar uma normal para a face usando as leis da álgebra linear (uma normal é um vetor que é perpendicular à superfície). Isso permite calcular como o rosto será iluminado e como a luz será refletida a partir dele.


Exemplos de objetos simples com vértices, arestas, faces e normais. A normal representa a seta vermelha.

Um objeto do modelo pode ser criado de diferentes maneiras. A topologia descreve como os polígonos formam a malha 3D. Uma boa topologia permite usar o número mínimo de polígonos para descrever um objeto e pode facilitar o movimento e a rotação do objeto.

Modelo de esfera em duas topologias

Modelo de esfera em duas topologias.

O efeito do volume é criado usando luzes e sombras nos polígonos do objeto. Assim, o objetivo da computação gráfica em 3D é calcular a posição de cada ponto de um objeto, calcular as luzes e sombras e exibi-lo na tela.

Criando uma forma

Vamos escrever um programa simples que cria um cubo. Utilizaremos a classe CCanvas3D da biblioteca de gráficos 3D.

A classe CCanvas3DWindow, que renderiza uma janela 3D, possui um número mínimo de membros e métodos. Nós adicionaremos gradualmente novos métodos com uma explicação dos conceitos de gráficos 3D implementados nas funções para trabalhar com o DirectX.

//+------------------------------------------------------------------+
//| Janela do aplicativo                                             |
//+------------------------------------------------------------------+
class CCanvas3DWindow
  {
protected:
   CCanvas3D         m_canvas;
   //--- dimensões da tela
   int               m_width;
   int               m_height;
   //--- objeto Cubo
   CDXBox            m_box;

public:
                     CCanvas3DWindow(void) {}
                    ~CCanvas3DWindow(void) {m_box.Shutdown();}
   //-- criação da cena
   virtual bool      Create(const int width,const int height){}
   //--- cálculo da cena
   void              Redraw(){}
   //--- processando os eventos do gráfico
   void              OnChartChange(void) {}
  };

A criação de uma cena começa com a criação de uma tela. Em seguida, os seguintes parâmetros são definidos para a matriz de projeção:

  1. Um ângulo de visão de 30 graus (M_PI/6), do qual observamos a cena 3D
  2. Proporção como uma proporção de largura e altura
  3. Distância para o plano de recorte próximo (0.1f) e distante (100.f)

Isso significa que apenas os objetos entre essas duas paredes virtuais (0.1f e 100.f) serão renderizados na matriz de projeção. Além disso, o objeto deve cair no ângulo de visão horizontal de 30 graus. Observe que as distâncias e todas as coordenadas em computação gráfica são virtuais. O que importa são as relações entre distâncias e tamanhos, mas não os valores em absoluto.

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
      //--- salva as dimensões da tela
      m_width=width;
      m_height=height;
      //--- cria uma tela para renderizar uma cena 3D
      ResetLastError();
      if(!m_canvas.CreateBitmapLabel("3D Sample_1",0,0,m_width,m_height,COLOR_FORMAT_ARGB_NORMALIZE))
        {
         Print("Error creating canvas: ",GetLastError());
         return(false);
         }
      //--- define os parâmetros da matriz de projeção - ângulo de visão, proporção de tela, distância dos planos de corte próximo e distante
      m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
      //--- cria o cubo - passa para ele o gerenciador de recursos, parâmetros da cena e as coordenadas dos dois cantos opostos do cubo
      if(!m_box.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),DXVector3(-1.0,-1.0,5.0),DXVector3(1.0,1.0,7.0)))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- adiciona o cubo à cena
      m_canvas.ObjectAdd(&m_box);
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Após a criação da matriz de projeção, nós podemos prosseguir com a construção do objeto 3D — um cubo baseado na classe CDXBox. Para criar um cubo, basta indicar dois vetores apontando para os cantos opostos do cubo. Observando a criação do cubo no modo de depuração, você pode ver o que está acontecendo na DXComputeBox(): a criação de todos os vértices do cubo (suas coordenadas são gravadas na matriz 'vértices'), bem como a divisão das bordas do cubo em triângulos que são enumerados e salvos na matriz 'indiсes'. No total, o cubo possui 8 vértices, 6 faces divididas em 12 triângulos e 36 índices que enumeram os vértices desses triângulos.

Embora o cubo tenha apenas 8 vértices, 24 vetores são criados para descrevê-los, pois um conjunto separado de vértices com uma normal deve ser especificada para cada uma das 6 faces. A direção da normal afetará o cálculo da iluminação para cada face. A ordem na qual os vértices de um triângulo são listados no índice determina quais de seus lados serão visíveis. A ordem na qual os vértices e índices são preenchidos é mostrada no código DXUtils.mqh:

   for(int i=20; i<24; i++)
      vertices[i].normal=DXVector4(0.0,-1.0,0.0,0.0);

As coordenadas de textura para o mapeamento de textura para cada face são descritas no mesmo código:

//--- texture coordinates
   for(int i=0; i<faces; i++)
     {
      vertices[i*4+0].tcoord=DXVector2(0.0f,0.0f);
      vertices[i*4+1].tcoord=DXVector2(1.0f,0.0f);
      vertices[i*4+2].tcoord=DXVector2(1.0f,1.0f);
      vertices[i*4+3].tcoord=DXVector2(0.0f,1.0f);
      }

Cada um dos 4 vetores da face define um dos 4 ângulos para o mapeamento de textura. Isso significa que uma estrutura de forma quadrada será mapeada para cada face do cubo para renderizar a textura. Obviamente, isso é necessário apenas se a textura estiver definida.


Cálculo e Renderização de cenas

Todos os cálculos devem ser realizados novamente sempre que a cena 3D for alterada. Aqui está a ordem dos cálculos necessários:

  • Calcular o centro de cada objeto nas coordenadas do mundo
  • Calcular a posição de cada elemento do objeto, ou seja, de cada vértice
  • Determinar a profundidade do pixel e sua visibilidade para o visualizador
  • Calcular a posição de cada pixel no polígono especificado por seus vértices
  • Definir a cor de cada pixel no polígono de acordo com a textura especificada
  • Calcular a direção do pixel de luz e seu reflexo
  • Aplicar a luz difusa a cada pixel
  • Converter todas as coordenadas do mundo em coordenadas da câmera
  • Converter as coordenadas da câmera nas coordenadas da matriz de projeção
Todas essas operações são executadas no método Render do objeto CCanvas3D. Após a renderização, a imagem calculada é transferida da matriz de projeção para a tela chamando o método Update.
   //+------------------------------------------------------------------+
   //| Atualização da cena                                              |
   //+------------------------------------------------------------------+
   void              Redraw()
     {
      //--- calcula a cena 3D
      m_canvas.Render(DX_CLEAR_COLOR|DX_CLEAR_DEPTH,ColorToARGB(clrBlack));
      //--- atualiza a imagem na tela de acordo com a cena atual
      m_canvas.Update();
      }

No nosso exemplo, o cubo é criado apenas uma vez e não muda mais. Portanto, o quadro na tela só precisará ser alterado se houver alterações no gráfico, como o redimensionamento do gráfico. Nesse caso, as dimensões da tela são ajustadas às dimensões atuais do gráfico, a matriz de projeção é redefinida e uma imagem na tela é atualizada.

   //+------------------------------------------------------------------+
   //| Processa o evento de alteração do gráfico                        |
   //+------------------------------------------------------------------+
   void              OnChartChange(void)
     {
      //--- obtém as dimensões atuais do gráfico
      int w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
      int h=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS);
      //--- atualiza as dimensões da tela de acordo com o tamanho do gráfico
      if(w!=m_width || h!=m_height)
        {
         m_width =w;
         m_height=h;
         //--- redimensiona a tela
         m_canvas.Resize(w,h);
         DXContextSetSize(m_canvas.DXContext(),w,h);
         //--- atualiza a matriz de projeção de acordo com os tamanhos das telas
         m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
         //--- recalcula a cena 3D e renderiza ela na tela
         Redraw();
         }
      }

Iniciamos o EA "Step1 Create Box.mq5". Você verá um quadrado branco em um fundo preto. Por padrão, a cor branca é definida para os objetos durante a criação. A iluminação ainda não foi definida.

Um cubo branco e seu layout no espaço

Um cubo branco e seu layout no espaço

O eixo X é orientado para a direita, o Y é orientado para cima e o Z é orientado para dentro da cena 3D. Esse sistema de coordenadas é chamado de left-handed ou orientado para a esquerda.

O centro do cubo está no ponto com as seguintes coordenadas X=0, Y=0, Z=6. A posição da qual olhamos para o cubo está no centro das coordenadas, que é o valor padrão. Se você quiser alterar a posição a partir da qual a cena 3D é exibida, defina explicitamente as coordenadas apropriadas usando a função ViewPositionSet().

Para concluir a operação do programa, pressione "Escape".


Rotação de Objetos em torno do Eixo Z e o Ponto-de-observador

Para animar a cena, permita a rotação do cubo em torno do eixo Z. Para fazer isso, adicionamos um timer — com base em seus eventos, o cubo será girado no sentido anti-horário.

Criamos uma matriz de rotação para ativar a rotação em torno do eixo Z em um determinado ângulo usando o método DXMatrixRotationZ(). Em seguida, passamos ele como parâmetro para o método TransformMatrixSet(). Isso mudará a posição do cubo no espaço 3D. Mais uma vez, chamamos a função Redraw() para atualizar a imagem na tela.

   //+------------------------------------------------------------------+
   //| Manipulador do timer                                             |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- variáveis para calcular o ângulo de rotação
      static ulong last_time=0;
      static float angle=0;
      //--- obtém a hora atual
      ulong current_time=GetMicrosecondCount();
      //--- calcula o delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- aumenta o ângulo de rotação do cubo ao redor do eixo Z
      angle+=deltatime;
      //--- lembra da hora
      last_time=current_time;
      //--- define o ângulo de rotação do cubo ao redor do eixo Z
      DXMatrix rotation;
      DXMatrixRotationZ(rotation,angle);
      m_box.TransformMatrixSet(rotation);
      //--- recalcula a cena 3D e renderiza ela na tela
      Redraw();
      }

Após o seu início, você verá um quadrado branco rotativo.

O cubo gira em torno do eixo Z no sentido anti-horário

O código fonte deste exemplo está disponível no arquivo "Step2 Rotation Z.mq5" Por favor, note que o ângulo M_PI/5 é especificado agora ao criar a cena, que é maior que o ângulo M_PI/6 do exemplo anterior. 

      //--- define os parâmetros da matriz de projeção - ângulo de visão, proporção de tela, distância dos planos de corte próximo e distante
      m_matrix_view_angle=(float)M_PI/5;
      m_canvas.ProjectionMatrixSet(m_matrix_view_angle,(float)m_width/m_height,0.1f,100.0f);
      //--- cria o cubo - passa para ele o gerenciador de recursos, parâmetros da cena e as coordenadas dos dois cantos opostos do cubo

No entanto, a dimensão do cubo na tela é visualmente menor. Quanto menor o ângulo de visão que indicamos ao definir a matriz de projeção, maior será a parte do quadro, que é ocupada pelo objeto. Isso pode ser comparado ao funcionamento de um telescópio: o objeto é maior, embora o ângulo de visão seja menor.


Gerenciamento de Posição da Câmera

A classe CCanvas3D possui três métodos para definir os parâmetros importantes da cena 3D, que são interconectados:

Todos esses parâmetros são usados em combinação — isso significa que, se você deseja definir algum desses parâmetros na cena 3D, os outros dois parâmetros também devem ser inicializados. Isso deve ser feito pelo menos no estágio de geração da cena. Isso é mostrado no exemplo a seguir, no qual a borda superior do quadro oscila para a esquerda e para a direita. A oscilação é implementada adicionando as três linhas de código a seguir no método Create():

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
....       
      //--- adiciona o cubo à cena
      m_canvas.ObjectAdd(&m_box);
      //--- define os parâmetros da cena
      m_canvas.ViewUpDirectionSet(DXVector3(0,1,0));  // define o vetor de direção para cima, ao longo do eixo Y  
      m_canvas.ViewPositionSet(DXVector3(0,0,0));     // define o ponto-de-observação do centro das coordenadas
      m_canvas.ViewTargetSet(DXVector3(0,0,6));       // define o ponto-de-observação no centro do cubo      
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Modificamos o método OnTimer() para fazer o vetor horizontal girar para a esquerda e direita.

   //+------------------------------------------------------------------+
   //| Manipulador do timer                                             |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- variáveis para calcular o ângulo de rotação
      static ulong last_time=0;
      static float max_angle=(float)M_PI/30;
      static float time=0;
      //--- obtém a hora atual
      ulong current_time=GetMicrosecondCount();
      //--- calcula o delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- aumenta o ângulo de rotação do cubo ao redor do eixo Z
      time+=deltatime;
      //--- lembra da hora
      last_time=current_time;
      //--- define o ângulo de rotação em torno do eixo Z
      DXVector3 direction=DXVector3(0,1,0);     // direção inicial do topo
      DXMatrix rotation;                        // vetor de rotação      
      //--- calcula a matriz de rotação 
      DXMatrixRotationZ(rotation,float(MathSin(time)*max_angle));
      DXVec3TransformCoord(direction,direction,rotation);
      m_canvas.ViewUpDirectionSet(direction);   // define a nova direção do topo
      //--- recalcula a cena 3D e renderiza ela na tela
      Redraw();
      }

Salvamos o exemplo como "Step3 ViewUpDirectionSet.mq5" e executamos ele. Você verá a imagem de um cubo oscilante, embora ele esteja realmente imóvel. Este efeito é obtido quando a própria câmera gira para a esquerda e para a direita.

A direção da parte superior oscila a esquerda e direita

A direção da parte superior oscila a esquerda e direita

Lembre-se de que existe uma conexão entre as coordenadas do alvo, da câmera e da direção do topo. Assim, para controlar a posição da câmera, você também deve especificar a direção do topo e as coordenadas do alvo, ou seja, o ponto-de-observação.


Gerenciamento de Cores de Objetos

Vamos modificar nosso código e colocar o cubo no centro das coordenadas, enquanto movemos a câmera.

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- cria o cubo - passa para ele o gerenciador de recursos, parâmetros da cena e as coordenadas dos dois cantos opostos do cubo
      if(!m_box.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),DXVector3(-1.0,-1.0,-1.0),DXVector3(1.0,1.0,1.0)))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- define a cor 
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- adiciona o cubo à cena
      m_canvas.ObjectAdd(&m_box);
      //--- define as posições para a câmera, o objetivo e a direção da parte superior
      m_canvas.ViewUpDirectionSet(DXVector3(0.0,1.0,0.0));  // define o vetor de direção para cima, ao longo do eixo Y
      m_canvas.ViewPositionSet(DXVector3(3.0,2.0,-5.0));    // posiciona a câmera à direita, na parte superior e na frente do cubo
      m_canvas.ViewTargetSet(DXVector3(0,0,0));             // define a direção de observação ao centro do cubo
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Além disso, pintamos o cubo de azul. A cor é definida no formato de cor RGB com um canal alfa (o canal alfa é indicado por último), embora os valores sejam normalizados para um. Assim, um valor 1 significa 255 e 0,5 significa 127.

Adicionamos a rotação ao redor do eixo X e salvamos as alterações como "Step4 Box Color.mq5".

Vista do cubo em rotação para cima e a direita.

Vista do cubo em rotação para cima e a direita.


Rotação e Movimento

Os objetos podem ser movidos e girados em três direções por vez. Todas as alterações dos objetos são implementadas usando matrizes. Cada um deles, ou seja, rotação, movimento e transformação, pode ser calculado separadamente. Vamos mudar o exemplo: a visão da câmera agora é de cima e de frente.

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
  ...
      m_canvas.ProjectionMatrixSet(m_matrix_view_angle,(float)m_width/m_height,0.1f,100.0f);
      //--- posiciona a câmera no topo e na frente do centro de coordenadas
      m_canvas.ViewPositionSet(DXVector3(0.0,2.0,-5.0));
      m_canvas.ViewTargetSet(DXVector3(0.0,0.0,0.0));
      m_canvas.ViewUpDirectionSet(DXVector3(0.0,1.0,0.0));      
      //--- cria o cubo - passa para ele o gerenciador de recursos, parâmetros da cena e as coordenadas dos dois cantos opostos do cubo
      if(!m_box.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),DXVector3(-1.0,-1.0,-1.0),DXVector3(1.0,1.0,1.0)))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- define a cor do cubo
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- calcula a posição do cubo e a matriz de transferência
      DXMatrix rotation,translation;
      //--- gira o cubo sequencialmente ao longo dos eixos X, Y e Z
      DXMatrixRotationYawPitchRoll(rotation,(float)M_PI/4,(float)M_PI/3,(float)M_PI/6);
      //--- move o cubo para a direita/baixo/dentro
      DXMatrixTranslation(translation,1.0,-2.0,5.0);
      //--- obtém a matriz de transformação como um produto de rotação e transferência
      DXMatrix transform;
      DXMatrixMultiply(transform,rotation,translation);
      //--- define a matriz de transformação 
      m_box.TransformMatrixSet(transform);      
      //--- adiciona o cubo à cena
      m_canvas.ObjectAdd(&m_box);    
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Criamos sequencialmente as matrizes de rotação e transferência, aplicamos a matriz de transformação resultante e renderizamos o cubo. Salvamos as alterações em "Step5 Translation.mq5" e executamos ele.

Rotação e movimento de um cubo

Rotação e movimento de um cubo

A câmera está parada e está apontada para o centro das coordenadas levemente de cima. O cubo foi girado em três direções e foi deslocado para a direita, para baixo e para dentro da cena.


Trabalhando com a Iluminação

Para obter uma imagem tridimensional realista, é necessário calcular a iluminação de cada ponto na superfície do objeto. Isso é feito usando o Modelo de Iluminação de Phong, que calcula a intensidade da cor dos três componentes de iluminação a seguir: ambiente, difuso e especular. Os seguintes parâmetros são usados aqui:

  • DirectionLight — a direção da iluminação direcional é definida no CCanvas3D
  • AmbientLight — a cor e a intensidade da iluminação ambiente são definidas no CCanvas3D
  • DiffuseColor — o componente de iluminação difusa calculado é definido no CDXMesh e em suas classes filho
  • EmissionColor — o componente de iluminação de fundo é definido no CDXMesh e em suas classes filho
  • SpecularColor — o componente especular é definido no CDXMesh e em suas classes filho

Modelo de Iluminação de Phong
Modelo de Iluminação de Phong


O modelo de iluminação é implementado em shaders padrão, os parâmetros do modelo são definidos no CCanvas3D e os parâmetros do objeto são definidos no CDXMesh e em suas classes filho. Modificamos o exemplo da seguinte maneira:

  1. Retornamos o cubo ao centro de coordenadas.
  2. Definimos a cor branca.
  3. Adicionamos uma fonte direcional de cor amarela que ilumina a cena de cima para baixo.
  4. Definimos a cor azul para a iluminação não direcional.
      //--- define a cor amarela da fonte e direciona-a de cima para baixo
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- define a cor azul para a luz ambiente 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));          
      //--- cria o cubo - passa para ele o gerenciador de recursos, parâmetros da cena e as coordenadas dos dois cantos opostos do cubo
      if(!m_box.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),DXVector3(-1.0,-1.0,-1.0),DXVector3(1.0,1.0,1.0)))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- define a cor branca do cubo
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0)); 
      //--- adiciona brilho verde ao cubo (emissão)
      m_box.EmissionColorSet(DXColor(0.0,1.0,0.0,0.2f)); 

Observe que a posição da fonte de luz direcionada não está definida no Canvas3D, enquanto apenas a direção na qual a luz se espalha é dada. A fonte da luz direcional é considerada uma distância infinita e um fluxo de luz estritamente paralelo ilumina a cena.

m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));

Aqui, o vetor de propagação da luz é apontado ao longo do eixo Y na direção negativa, ou seja, de cima para baixo. Além disso, se você definir parâmetros para a fonte de luz direcionada (LightColorSet e LightDirectionSet), também deverá especificar a cor da luz ambiente (AmbientColorSet). Por padrão, a cor da luz ambiente é definida como branca com intensidade máxima e, portanto, todas as sombras serão brancas. Isso significa que os objetos na cena serão iluminados com branco pela iluminação ambiente, enquanto a luz direcional da fonte será interrompida pela luz branca.

      //--- define a cor amarela da fonte e direciona-a de cima para baixo
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- define a cor azul para a luz ambiente 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));  // deve ser especificado

A animação gif abaixo mostra como a imagem muda quando nós adicionamos iluminação. O código fonte do exemplo está disponível no arquivo "Step6 Add Light.mq5".

O cubo branco com emissão verde sob uma fonte de luz amarela, com luz ambiente azul

O cubo branco com emissão verde sob uma fonte de luz amarela, com luz ambiente azul

Tente desativar os métodos de cores no código acima para ver como ele funciona.


Animação

A animação implica uma mudança nos parâmetros e objetos da cena ao longo do tempo. Quaisquer propriedades disponíveis podem ser alteradas dependendo da hora ou dos eventos. Define o timer para 10 milissegundos — este evento afetará a atualização da cena:

int OnInit()
  {
...
//--- cria a tela
   ExtAppWindow=new CCanvas3DWindow();
   if(!ExtAppWindow.Create(width,height))
      return(INIT_FAILED);
//--- define o timer
   EventSetMillisecondTimer(10);
//---
   return(INIT_SUCCEEDED);
   }

Adiciona o manipulador de eventos apropriado ao CCanvas3DWindow. Nós precisamos alterar os parâmetros do objeto (como rotação, movimento e zoom) e a direção da iluminação:

   //+------------------------------------------------------------------+
   //| Manipulador do timer                                             |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {    
      static ulong last_time=0;
      static float time=0;       
      //--- obtém a hora atual
      ulong current_time=GetMicrosecondCount();
      //--- calcula o delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- aumenta o valor do tempo decorrido
      time+=deltatime;
      //--- lembra da hora
      last_time=current_time;
      //--- calcula a posição do cubo e a matriz de rotação
      DXMatrix rotation,translation,scale;
      DXMatrixRotationYawPitchRoll(rotation,time/11.0f,time/7.0f,time/5.0f);
      DXMatrixTranslation(translation,(float)sin(time/3),0.0,0.0);
      //--- calcula a compressão/extensão do cubo ao longo dos eixos
      DXMatrixScaling(scale,1.0f+0.5f*(float)sin(time/1.3f),1.0f+0.5f*(float)sin(time/1.7f),1.0f+0.5f*(float)sin(time/1.9f));
      //--- multiplica as matrizes para obter a transformação final
      DXMatrix transform;
      DXMatrixMultiply(transform,scale,rotation);
      DXMatrixMultiply(transform,transform,translation);
      //--- define a matriz de transformação
      m_box.TransformMatrixSet(transform);
      //--- calcula a rotação da fonte de luz em torno do eixo Z
      DXMatrixRotationZ(rotation,deltatime);
      DXVector3 light_direction;
      //--- obtém a direção atual da fonte de luz
      m_canvas.LightDirectionGet(light_direction);
      //--- calcula a nova direção da fonte de luz e define ela
      DXVec3TransformCoord(light_direction,light_direction,rotation);
      m_canvas.LightDirectionSet(light_direction);
      //--- recalcula a cena 3D e desenha ela na tela
      Redraw();
      }

Observe que as alterações do objeto são aplicadas sobre os valores iniciais, como se sempre lidássemos com o estado inicial do cubo e aplicássemos todas as operações relacionadas à rotação/movimento/compressão do zero, o que significa que o estado atual do cubo não é salvo. No entanto, a direção da fonte de luz é alterada por incrementos deltatime a partir do valor atual.

Um cubo em rotação com iluminação dinâmica

Um cubo em rotação com a direção da fonte de luz que muda dinamicamente.

O resultado é uma animação 3D bem complexa. O código de exemplo está disponível no arquivo "Step7 Animation.mq5".


Controle da Posição da Câmera Usando o Mouse

Vamos considerar o último elemento de animação nos gráficos 3D, uma reação às ações do usuário. Adicionamos o gerenciamento da câmera usando o mouse em nosso exemplo. Primeiro, aderimos aos eventos do mouse e criamos os manipuladores correspondentes:

int OnInit()
  {
...
//--- define o timer
   EventSetMillisecondTimer(10);
//--- ativa o recebimento de eventos do mouse: cliques em movimento e botão
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,1);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,1)
//---
   return(INIT_SUCCEEDED);
   }
void OnDeinit(const int reason)
  {
//--- Remove o timer
   EventKillTimer();
//--- desativa o recebimento de eventos do mouse
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,0);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,0);
//--- remove o objeto
   delete ExtAppWindow;
//--- retorna o gráfico ao modo de exibição usual com os gráficos de preços
   ChartSetInteger(0,CHART_SHOW,true);
   }
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- evento de alteração do gráfico
   if(id==CHARTEVENT_CHART_CHANGE)
      ExtAppWindow.OnChartChange();
//--- evento de movimento do mouse
   if(id==CHARTEVENT_MOUSE_MOVE)
      ExtAppWindow.OnMouseMove((int)lparam,(int)dparam,(uint)sparam);
//--- evento de rolagem da roda do mouse
   if(id==CHARTEVENT_MOUSE_WHEEL)
      ExtAppWindow.OnMouseWheel(dparam);

No CCanvas3DWindow, criamos o manipulador de eventos de movimento do mouse. Ele mudará os ângulos de direção da câmera quando o mouse for movido com o botão esquerdo pressionado:

   //+------------------------------------------------------------------+
   //| Processando os movimentos do mouse                               |
   //+------------------------------------------------------------------+
   void              OnMouseMove(int x,int y,uint flags)
     {
      //--- botão esquerdo do mouse
      if((flags&1)==1)
        {
         //--- não há informações sobre a posição anterior do mouse
         if(m_mouse_x!=-1)
           {
            //--- atualiza o ângulo da câmera ao mudar de posição
            m_camera_angles.y+=(x-m_mouse_x)/300.0f;
            m_camera_angles.x+=(y-m_mouse_y)/300.0f;
            //--- define o ângulo vertical no intervalo entre (-Pi/2, Pi2)
            if(m_camera_angles.x<-DX_PI*0.49f)
               m_camera_angles.x=-DX_PI*0.49f;
            if(m_camera_angles.x>DX_PI*0.49f)
               m_camera_angles.x=DX_PI*0.49f;
            //--- atualiza a posição da câmera
            UpdateCameraPosition();
            }
         //--- salva a posição do mouse
         m_mouse_x=x;
         m_mouse_y=y;
         }
      else
        {
         //--- redefine a posição salva se o botão esquerdo do mouse não for pressionado
         m_mouse_x=-1;
         m_mouse_y=-1;
         }
      }

Aqui está o manipulador de eventos da roda do mouse, que altera a distância entre a câmera e o centro da cena:

   //+------------------------------------------------------------------+
   //| Processando os eventos da roda do mouse                          |
   //+------------------------------------------------------------------+
   void              OnMouseWheel(double delta)
     {
      //--- atualiza a distância entre a câmera e o centro em uma rolagem do mouse
      m_camera_distance*=1.0-delta*0.001;
      //--- define a distância no intervalo entre [3,50]
      if(m_camera_distance>50.0)
         m_camera_distance=50.0;
      if(m_camera_distance<3.0)
         m_camera_distance=3.0;
      //--- atualiza a posição da câmera
      UpdateCameraPosition();
      }

Ambos os manipuladores chamam o método UpdateCameraPosition() para atualizar a posição da câmera de acordo com os parâmetros atualizados:

   //+------------------------------------------------------------------+
   //| Atualiza a posição da câmera                                     |
   //+------------------------------------------------------------------+
   void              UpdateCameraPosition(void)
     {
      //--- a posição da câmera levando em consideração a distância ao centro das coordenadas
      DXVector4 camera=DXVector4(0.0f,0.0f,-(float)m_camera_distance,1.0f);
      //--- rotação da câmera em torno do eixo X
      DXMatrix rotation;
      DXMatrixRotationX(rotation,m_camera_angles.x);
      DXVec4Transform(camera,camera,rotation);
      //--- rotação da câmera em torno do eixo Y
      DXMatrixRotationY(rotation,m_camera_angles.y);
      DXVec4Transform(camera,camera,rotation);
      //--- coloca a câmera na posição
      m_canvas.ViewPositionSet(DXVector3(camera));
      }

O código fonte está disponível no arquivo "Step8 Mouse Control.mq5".

Controle da Posição da Câmera Usando o Mouse

Controle da posição da câmera usando o mouse.


Aplicando Texturas

Uma textura é uma imagem bitmap aplicada à superfície de um polígono para representar os padrões ou materiais. O uso de texturas permite reproduzir pequenos objetos na superfície, o que exigiria muitos recursos se os criássemos usando os polígonos. Por exemplo, isso pode ser uma imitação de pedra, madeira, solo e outros materiais.

CDXMesh e suas classes filho permitem especificar a textura. No pixel shader padrão, essa textura é usada junto com o DiffuseColor. Removemos a animação do objeto e aplicamos uma textura de pedra. Ele deve estar localizado na pasta MQL5\Files do diretório de trabalho da plataforma:

   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- define a cor branca para a iluminação não direcional
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0));

      //--- adiciona a textura para desenhar as faces do cubo
      m_box.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- adiciona o cubo à cena
      m_canvas.ObjectAdd(&m_box);
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Um cubo com uma textura de pedra

Um cubo com uma textura de pedra.


Criando objetos Personalizados

Todos os objetos consistem em vértices (DXVector3), que são conectados às primitivas usando índices. A primitiva mais comum é um triângulo. Um objeto 3D básico é criado através da criação de uma lista de vértices que contêm pelo menos coordenadas (mas também pode conter muitos dados adicionais, como a normal, cor etc.), o tipo de primitiva na qual elas são combinadas e um lista de índices de vértices pelos quais eles serão combinados em primitivas.


A Biblioteca padrão possui o tipo de vértice DXVertex, que contém sua coordenada, a normal para o cálculo de iluminação, coordenadas de textura e cor. O sombreador de vértice padrão funciona com esse tipo de vértice.

struct DXVertex
  {
   DXVector4         position;  // coordenadas do vértice
   DXVector4         normal;    // vetor normal
   DXVector2         tcoord;    // coordenada da face para aplicar a textura
   DXColor           vcolor;    // cor
  };


O tipo auxiliar MQL5\Include\Canvas\DXDXUtils.mqh contém um conjunto de métodos para gerar a geometria (vértices e índices) das primitivas básicas e carregar a geometria 3D de arquivos .OBJ.

Adicionamos a criação de uma esfera e um torus, aplicamos a mesma textura de pedra:

   virtual bool      Create(const int width,const int height)
     {
 ...     
      //--- vértices e índices para objetos criados manualmente
      DXVertex vertices[];
      uint indices[];
      //--- prepara vértices e índices para a esfera
      if(!DXComputeSphere(0.3f,50,vertices,indices))
         return(false);
      //--- define a cor branca para os vértices
      DXColor white=DXColor(1.0f,1.0f,1.0f,1.0f);
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- cria o objeto de esfera
      if(!m_sphere.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- define a cor difusa para a esfera
      m_sphere.DiffuseColorSet(DXColor(0.0,1.0,0.0,1.0));
      //--- define a cor especular branca
      m_sphere.SpecularColorSet(white);
      m_sphere.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- adiciona a esfera a uma cena
      m_canvas.ObjectAdd(&m_sphere);
      //--- prepara as vértices e índices para o torus
      if(!DXComputeTorus(0.3f,0.1f,50,vertices,indices))
         return(false);
      //--- define a cor branca para os vértices
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- cria o objeto torus
      if(!m_torus.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- define a cor difusa para o torus
      m_torus.DiffuseColorSet(DXColor(0.0,0.0,1.0,1.0));
      m_torus.SpecularColorSet(white);
      m_torus.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- adiciona o torus a uma cena
      m_canvas.ObjectAdd(&m_torus);      
      //--- redesenha a cena
      Redraw();
      //--- sucesso
      return(true);
      }

Adiciona animação para os novos objetos:

   void              OnTimer(void)
     {
...
      m_canvas.LightDirectionSet(light_direction);
      //--- órbita da esfera
      DXMatrix translation;
      DXMatrixTranslation(translation,1.1f,0,0);
      DXMatrixRotationY(rotation,time);
      DXMatrix transform;
      DXMatrixMultiply(transform,translation,rotation);
      m_sphere.TransformMatrixSet(transform);
      //--- órbita do torus com rotação em torno do seu eixo
      DXMatrixRotationX(rotation,time*1.3f);
      DXMatrixTranslation(translation,-2,0,0);
      DXMatrixMultiply(transform,rotation,translation);
      DXMatrixRotationY(rotation,time/1.3f);
      DXMatrixMultiply(transform,transform,rotation);
      m_torus.TransformMatrixSet(transform);           
      //--- recalcula a cena 3D e desenha ela na tela
      Redraw();
      }


Salvamos as alterações como Three Objects.mq5 e executamos ele.

Figuras que rotacionam na órbita do cubo.

Figuras que rotacionam na órbita do cubo.


Superfície 3D Baseada em Dados

Vários gráficos são geralmente usados para criar relatórios e analisar dados, como gráficos lineares, histogramas, diagramas de pizza etc. A MQL5 oferece uma conveniente biblioteca gráfica, que no entanto só pode criar gráficos 2D.

A classe CDXSurface permite visualizar uma superfície usando dados personalizados armazenados em uma matriz bidimensional. Vamos ver o exemplo da seguinte função matemática

z=sin(2.0*pi*sqrt(x*x+y*y))

Criamos um objeto para desenhar a superfície e uma matriz para armazenar os dados:

   virtual bool      Create(const int width,const int height)
     {
...
      //--- prepara uma matriz para armazenar os dados
      m_data_width=m_data_height=100;
      ArrayResize(m_data,m_data_width*m_data_height);
      for(int i=0;i<m_data_width*m_data_height;i++)
         m_data[i]=0.0;
      //--- cria um objeto de superfície
      if(!m_surface.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),m_data,m_data_width,m_data_height,2.0f,
                           DXVector3(-2.0,-0.5,-2.0),DXVector3(2.0,0.5,2.0),DXVector2(0.25,0.25),
                           CDXSurface::SF_TWO_SIDED|CDXSurface::SF_USE_NORMALS,CDXSurface::CS_COLD_TO_HOT))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- cria textura e reflexão
      m_surface.SpecularColorSet(DXColor(1.0,1.0,1.0,1.0));
      m_surface.TextureSet(m_canvas.DXDispatcher(),"checker.bmp");
      //--- adiciona a superfície à cena
      m_canvas.ObjectAdd(&m_surface);
      //--- sucesso
      return(true);
      }

A superfície será desenhada dentro de uma caixa com uma base de 4x4 e uma altura de 1. As dimensões da textura são 0.25x0.25.

  • SF_TWO_SIDED indica que a superfície será desenhada acima e abaixo da superfície, caso a câmera se mova sob a superfície.
  • SF_USE_NORMALS indica que os cálculos das normais serão usados para calcular as reflexões da superfície causadas pela fonte de luz direcional.
  • CS_COLD_TO_HOT define a coloração do mapa de calor da superfície de azul para vermelho com uma transição entre verde e amarelo.

Para animar a superfície, adicionamos o tempo abaixo do sinal senoidal e atualizamos ele através do timer.

   void              OnTimer(void)
     {
      static ulong last_time=0;
      static float time=0;
      //--- obtém a hora atual
      ulong current_time=GetMicrosecondCount();
      //--- calcula o delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- aumenta o valor do tempo decorrido
      time+=deltatime;
      //--- lembra da hora
      last_time=current_time;
      //--- calculamos os valores da superfície levando em consideração as mudanças de tempo
      for(int i=0; i<m_data_width; i++)
        {
         double x=2.0*i/m_data_width-1;
         int offset=m_data_height*i;
         for(int j=0; j<m_data_height; j++)
           {
            double y=2.0*j/m_data_height-1;
            m_data[offset+j]=MathSin(2.0*M_PI*sqrt(x*x+y*y)-2*time);
            }
         }
      //--- atualizamos os dados para desenhar a superfície
      if(m_surface.Update(m_data,m_data_width,m_data_height,2.0f,
                          DXVector3(-2.0,-0.5,-2.0),DXVector3(2.0,0.5,2.0),DXVector2(0.25,0.25),
                          CDXSurface::SF_TWO_SIDED|CDXSurface::SF_USE_NORMALS,CDXSurface::CS_COLD_TO_HOT))
        {
         //--- recalcula a cena 3D e desenha ela na tela
         Redraw();
         }
      }

O código fonte está disponível em 3D Surface.mq5, o programa de exemplo é mostrado no vídeo.




Neste artigo, nós consideramos os recursos das Funções DirectX na criação de formas geométricas simples e gráficos 3D animados para a análise visual de dados. Exemplos mais complexos podem ser encontrados no diretório de instalação da plataforma MetaTrader 5: Expert Advisors "Correlation Matrix 3D" e "Math 3D Morpher", além do script "Remnant 3D". 

A MQL5 permite resolver tarefas importantes de negociação algorítmica sem usar pacotes de terceiros:

  • Otimizar estratégias de negociação complexas que contêm muitos parâmetros de entrada
  • Obter resultados de otimização
  • Visualizar os dados obtidos usando imagens tridimensionais de uma maneira mais conveniente
Use a funcionalidade de ponta para visualizar os dados de bolsa e desenvolver estratégias de negociação na MetaTrader 5 — agora com gráficos em 3D!


Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7708

Arquivos anexados |
Step4_Box_Color.mq5 (15.25 KB)
Step6_Add_Light.mq5 (14.27 KB)
Step7_Animation.mq5 (18.83 KB)
Step9_Texture.mq5 (23.16 KB)
Three_Objects.mq5 (27.9 KB)
3D_Surface.mq5 (24.48 KB)
MQL5.zip (199.48 KB)
Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIII): solicitações de negociação pendentes, fechamento de posições por condições Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIII): solicitações de negociação pendentes, fechamento de posições por condições

Continuamos a trabalhar na funcionalidade da biblioteca para negociar usando solicitações pendentes. Nós já implementamos o envio de solicitações pendentes segundo condições para abrir posições e definir ordens pendentes. Hoje criaremos um recurso para fechamento parcial, total e por meio da posição oposta, tudo isso segundo condições.

Implementando OLAP na negociação (Parte 3): analisando cotações para desenvolver estratégias de negociação Implementando OLAP na negociação (Parte 3): analisando cotações para desenvolver estratégias de negociação

Neste artigo, continuaremos a estudar a abordagem OLAP aplicada à negociação, bem como a expandir os recursos apresentados nos dois primeiros artigos. Desta vez, analisaremos cotações de maneira operacional. Formularemos e testaremos uma hipótese sobre estratégias de negociação baseadas em indicadores históricos agregados. Apresentaremos EAs para estudos de padrões de barras e negociação adaptativa.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIV): ordens de negociação pendentes - exclusão de ordens, modificação de ordens/posições por condições Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIV): ordens de negociação pendentes - exclusão de ordens, modificação de ordens/posições por condições

Neste artigo, concluiremos a descrição do conceito de solicitações de negociação pendentes e criaremos uma funcionalidade para excluir ordens pendentes e modificar ordens/posições de acordo com as condições definidas. Assim, teremos toda uma funcionalidade com a qual poderemos criar estratégias personalizadas simples, mais precisamente alguma lógica para o EA se comportar quando ocorrerem as condições especificadas pelo usuário.

Trabalhando com as funções de rede ou MySQL sem DLL: Parte I - Conector Trabalhando com as funções de rede ou MySQL sem DLL: Parte I - Conector

A MetaTrader 5 recebeu algumas funções de rede recentemente. Isso abriu grandes oportunidades para os programadores que desenvolvem produtos para o Mercado. Agora eles podem implementar coisas que antes exigiam bibliotecas dinâmicas. Neste artigo, nós vamos considerá-los usando a implementação do MySQL como exemplo.