Как создать 3D-графику на DirectX в MetaTrader 5

MetaQuotes | 17 апреля, 2020

Компьютерная 3D-графика занимается тем, что отображает объекты трехмерного пространства на плоскую поверхность монитора. При этом сами объекты или позиция наблюдателя могут меняться со временем, соответственно, должна меняться и двумерная картинка, создавая иллюзию глубины изображения — поворот, приближение, изменение освещенности и так далее. MQL5 позволяет создавать и управлять компьютерной графикой прямо в терминале MetaTrader 5 с помощью функций DirectX. Для работы этих функций видеокарта пользователя должна поддерживать DX 11 и шейдеры версии 5.0.


Модель объекта

Для того чтобы нарисовать трехмерный объект на плоской поверхности, необходимо создать модель этого объекта в пространственных координатах X, Y и Z. То есть необходимо описать каждую точку на поверхности этого объекта — указать его координаты. В идеале потребуется описать бесконечное количество точек на поверхности объекта, чтобы при любом масштабировании качество картинки не терялось. На практике для описания трехмерной модели используется грубая сетка, состоящая из многоугольников — полигонов. Чем детальнее сетка, тем больше полигонов и тем реалистичнее модель. Но тем больше требуется ресурсов компьютера для расчета модели и построения 3D-графики.

Модель чайника в виде сетки полигонов

Модель чайника в виде сетки полигонов.

Изначально, когда компьютеры и видеокарты не были такими мощными как сейчас, каждый полигон делился на треугольники, так как с помощью треугольника можно однозначно описать положение небольшого участка поверхности и вычислить на нем такие необходимые параметры, как освещенность и отражение падающего света. Совокупность множества таких небольших треугольников позволяет создавать реалистичное трехмерное изображение объекта. Здесь и далее полигон и треугольник будут выступать синонимами, так как представить треугольник гораздо проще, чем полигон с N вершинами.


Куб, составленный из треугольников.

Таким образом, для создания трехмерной модели объекта достаточно описать координаты каждой вершины треугольника, чтобы затем вычислить координаты каждой точки объекта, даже если сам объект перемещается в пространстве или меняется позиция наблюдателя. Вершины треугольника называются вертексами (vertex), соединяющие их отрезки называются ребрами (edge), а поверхность, заключенная между отрезками, называется гранью (face). Зная расположения треугольника в пространстве, мы можем по законам линейной алгебры построить к ней нормаль (вектор, который выходит из поверхности и перпендикулярен ей), и таким образом вычислить, как падающий на грань свет от источника будет окрашивать поверхность и отражаться от неё.


Примеры простых объектов с вершинами, ребрами, гранями и нормалями. Нормаль - стрелка красного цвета.

Создать модель объекта можно разными способами, топология описывает то, как именно полигоны формируют 3D-модель (mesh). Правильная топология позволяет использовать минимальное количество полигонов для описания объекта и в некоторых случаях делает более простым перемещение и поворот объекта в пространстве.

Модель сферы в двух топологиях

Модель сферы в двух топологиях.

Именно игра света и тени на полигонах объекта делает его объемным, в этом и заключается задача трехмерной компьютерной графики — вычислить для каждой точки объекта его расположение в пространстве, рассчитать её цвет и освещенность, чтобы затем спроецировать на экран монитора.

Создание фигуры

Напишем простую программу, которая создает куб. Для этого используем класс CCanvas3D из библиотеки 3D-графики.

Класс CCanvas3DWindow для отрисовки 3D-окна имеет минимум членов и методов, достаточных для понимания. Далее мы будем добавлять всё новые методы с объяснением концепции 3D-графики, которая заложена в функции для работы с DirectX.

//+------------------------------------------------------------------+
//| Application window                                               |
//+------------------------------------------------------------------+
class CCanvas3DWindow
  {
protected:
   CCanvas3D         m_canvas;
   //--- размеры холста
   int               m_width;
   int               m_height;
   //--- объект Куб
   CDXBox            m_box;

public:
                     CCanvas3DWindow(void) {}
                    ~CCanvas3DWindow(void) {m_box.Shutdown();}
   //-- создание сцены
   virtual bool      Create(const int width,const int height){}
   //--- расчет сцены
   void              Redraw(){}
   //--- обработка событий графика
   void              OnChartChange(void) {}
  };

При создании сцены сначала создается холст. Затем для матрицы проекции задается:

  1. Угол зрения в 30 градусов (M_PI/6), под которым мы смотрим на 3D сцену;
  2. Соотношение сторон кадра (aspect ratio) как отношение ширины к высоте;
  3. И, наконец, расстояние до ближней (0.1f) и дальней (100.f) плоскостей отсечения.

Это означает, что в матрицу проекции будут отображаться только те объекты, которые находятся между этими двумя виртуальными стенками (0.1f и 100.f), при этом объект также должен попадать в горизонтальный угол зрения, равный 30 градусам. Расстояния и, вообще говоря, все координаты в компьютерной графике являются виртуальными. Так как важны не абсолютные величины, а соотношения между расстояниями и размерами.

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
      //--- сохраним размеры холста
      m_width=width;
      m_height=height;
      //--- создадим холст для отрисовки на нем 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);
        }
      //--- установим параметры матрицы проекции - угол зрения, отношение сторон, расстояния до ближней и дальней плоскости отсечения
      m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
      //--- создаем куб - передаем ему диспетчер ресурсов, параметры сцены  и координаты двух противоположных углов куба
      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);
        }
      //--- добавляем куб на сцену
      m_canvas.ObjectAdd(&m_box);
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

После матрицы проекции создается сам 3D-объект — это куб на основе класса CDXBox. Для создания куба необходимо и достаточно указать два вектора, указывающие на противоположные углы куба. Если вы проследите создание куба под отладкой, то обнаружите, как в методе DXComputeBox() создаются все вершины куба (их координаты записываются в массив vertices), а также сами грани куба разбиваются на треугольники, которые перечисляются и запоминаются в массиве indiсes. Итого куб имеет 8 вершин, 6 граней, которые разбиты на 12 треугольников, и 36 индексов, перечисляющих вершины этих треугольников.

Хотя куб имеет всего 8 вершин, но для описания создается 24 вектора, так как для каждой из 6 граней необходимо указать свой набор вершин, имеющих собственную нормаль. Направление нормали в дальнейшем будет влиять на расчет освещения каждой грани. Порядок перечисления вершин треугольника в индексе влияет на то, с какой стороны он будет виден. Порядок заполнения вершин и индексов можно увидеть в коде DXUtils.mqh:

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

Кроме того, там же для каждой грани описываются текстурные координаты для наложения текстуры:

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

Каждый из 4-х векторов грани задает один из 4-х углов развертки для наложения текстуры. Это означает, что при рендеринге на каждую грань куба будет натянута структура в виде квадрата для её отрисовки. Если, конечно, текстура будет задана.


Расчет и отрисовка сцены

При каждом изменении 3D-сцены необходимо заново произвести все расчеты. Это означает, что последовательно нужно вычислить:

Все эти операции производятся в методе Render объекта CCanvas3D. После рендеринга просчитанное изображение с матрицы проекции переносится на холст с помощью вызова метода Update.
   //+------------------------------------------------------------------+
   //| обновление сцены                                                 |
   //+------------------------------------------------------------------+
   void              Redraw()
     {
      //--- расчет 3D сцены
      m_canvas.Render(DX_CLEAR_COLOR|DX_CLEAR_DEPTH,ColorToARGB(clrBlack));
      //--- обновить картинку на холсте в соответствии с текущей сценой
      m_canvas.Update();
     }

В нашем примере куб создается только один раз, в дальнейшем с ним никаких изменений не происходит. Поэтому обновлять кадр на холсте придется только в том случае, если были изменения на чарте — изменение его размеров. В этом случае размеры холста подстраиваются под текущие размеры чарта, переустанавливается матрица проекции и обновляется картинка на холсте.

   //+------------------------------------------------------------------+
   //| Process chart change event                                       |
   //+------------------------------------------------------------------+
   void              OnChartChange(void)
     {
      //--- получить текущие размеры чарта
      int w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
      int h=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS);
      //--- обновить размеры холста в соответствие с размерами чарта
      if(w!=m_width || h!=m_height)
        {
         m_width =w;
         m_height=h;
         //--- изменить размеры холста
         m_canvas.Resize(w,h);
         DXContextSetSize(m_canvas.DXContext(),w,h);
         //--- обновить матрицу проекции в соответствии размерами холста
         m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
         //--- пересчитать 3D-сцену и отрисовать eё на холсте
         Redraw();
        }
     }

Запускаем советника "Step1 Create Box.mq5" и видим белый квадрат на черном фоне. По умолчанию при создании объектам выставляется белый цвет, освещение мы не указали.

Белый куб и схема его расположения в пространстве

Белый куб и схема его расположения в пространстве

При этом ось X направлена вправо, ось Y — вверх, а ось Z от нас — вглубь 3D сцены. Такая система координат называется левосторонней.

Центр куба находится в точке с координатами  X=0, Y=0, Z=6. Позиция, с которой мы наблюдаем за кубом, находится в центре координат, это значение по умолчанию. Если мы хотим сменить позицию точки зрения на 3D-сцену, то должны явно задать координаты с помощью функции ViewPositionSet().

Для завершение работы программы необходимо нажать клавишу "Escape".


Вращение объекта вокруг оси Z и угол зрения на сцену

Чтобы оживить немного сцену, придадим кубу вращение вокруг оси Z. Для этого добавим таймер, по событиям которого будем поворачивать куб против часовой стрелки.

Создадим матрицу поворота вокруг оси Z на заданный угол с помощью метода DXMatrixRotationZ() и затем передадим её в качестве параметра в метод TransformMatrixSet() — это изменит положение куба в пространстве. Для обновления изображения на холсте опять вызовем Redraw().

   //+------------------------------------------------------------------+
   //| Timer handler                                                    |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- переменные для вычисления угла поворота
      static ulong last_time=0;
      static float angle=0;
      //--- получим текуще время
      ulong current_time=GetMicrosecondCount();
      //--- вычислим дельту
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- увеличим угол поворота куб вокруг оси Z
      angle+=deltatime;
      //--- запомним время
      last_time=current_time;
      //--- устанавливаем для куба угол поворота вокруг оси Z
      DXMatrix rotation;
      DXMatrixRotationZ(rotation,angle);
      m_box.TransformMatrixSet(rotation);
      //--- пересчитать 3D-сцену и отрисовать eё на холсте
      Redraw();
     }

Запускаем и получаем вращающийся белый квадрат.

Куб вращается вокруг оси Z против часовой стрелки

Исходный код этого примера находится в файле "Step2 Rotation Z.mq5". Обратите внимание, что при создании сцены теперь указан угол M_PI/5, который больше угла=M_PI/6 из предыдущего примера. 

      //--- установим параметры матрицы проекции - угол зрения, отношение сторон, расстояния до ближней и дальней плоскости отсечения
      m_matrix_view_angle=(float)M_PI/5;
      m_canvas.ProjectionMatrixSet(m_matrix_view_angle,(float)m_width/m_height,0.1f,100.0f);
      //--- создаем куб - передаем ему диспетчер ресурсов, параметры сцены  и координаты двух противоположных углов куба

Но при этом визуально размеры куба на экране стали меньше. Чем меньше угол зрения мы указываем при задании матрицы проекции, тем большую часть кадра занимает объект. Это аналогично тому, что при взгляде через телескоп мы видим объекты крупнее, хотя сам угол зрения уменьшается.


Управление положением камеры

Класс CCanvas3D имеет 3 метода для установки важных параметров 3D сцены, которые между собой связаны:

Все эти методы используются совместно — это означает, что если вы хотите задать любой из этих параметров в 3D-сцене, то должны обязательно инициализировать два остальных параметра. Хотя бы на этапе генерации сцены. Покажем это на следующем примере, где мы будем покачивать влево-вправо верхнюю границу кадра. Для этого добавим в метод Create() 3 строчки:

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
....       
      //--- добавляем куб на сцену
      m_canvas.ObjectAdd(&m_box);
      //--- установим параметры сцены
      m_canvas.ViewUpDirectionSet(DXVector3(0,1,0));  // установим вектор направления вверх - вдоль оси Y  
      m_canvas.ViewPositionSet(DXVector3(0,0,0));     // установим взгляд из центра координат
      m_canvas.ViewTargetSet(DXVector3(0,0,6));       // направим взгляд в центр куба      
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

Изменим метод OnTimer() таким образом, чтобы он качал вектор горизонта вправо-влево.

   //+------------------------------------------------------------------+
   //| Timer handler                                                    |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- переменные для вычисления угла поворота
      static ulong last_time=0;
      static float max_angle=(float)M_PI/30;
      static float time=0;
      //--- получим текуще время
      ulong current_time=GetMicrosecondCount();
      //--- вычислим дельту
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- увеличим угол поворота куба вокруг оси Z
      time+=deltatime;
      //--- запомним время
      last_time=current_time;
      //--- устанавливаем угол поворота вокруг оси Z
      DXVector3 direction=DXVector3(0,1,0);     // начальное направление верха
      DXMatrix rotation;                        // вектор поворота      
      //--- вычислим матрицу поворота 
      DXMatrixRotationZ(rotation,float(MathSin(time)*max_angle));
      DXVec3TransformCoord(direction,direction,rotation);
      m_canvas.ViewUpDirectionSet(direction);   // установим новое направление верха
      //--- пересчитать 3D-сцену и отрисовать eё на холсте
      Redraw();
     }

Сохраняем пример под именем "Step3 ViewUpDirectionSet.mq5" и запускаем. Получаем изображение качающегося куба, хотя на самом деле он неподвижен. Такой эффект получается, когда качается влево-вправо сама камера,  на которую снимают видео.

Направление верха качается влево-вправо

Направление верха качается влево-вправо

Необходимо запомнить, что существует связь между координатами цели, камеры и направлением верха — для управления положением камеры необходимо задать также направление верха и координаты цели, на которую направлен взгляд.


Управление цветом объекта

Изменим немного наш код — поместим куб в центр координат и сместим камеру.

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- создаем куб - передаем ему диспетчер ресурсов, параметры сцены  и координаты двух противоположных углов куба
      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);
        }
      //--- установим цвет 
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- добавляем куб на сцену
      m_canvas.ObjectAdd(&m_box);
      //--- установим позиции камеры, цели и направления верха
      m_canvas.ViewUpDirectionSet(DXVector3(0.0,1.0,0.0));  // установим вектор направления вверх - вдоль оси Y
      m_canvas.ViewPositionSet(DXVector3(3.0,2.0,-5.0));    // установим камеру справа, сверху и перед кубом
      m_canvas.ViewTargetSet(DXVector3(0,0,0));             // направим взгляд в центр куба
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

Кроме того, закрасим куб в голубой цвет — цвет задается в формате RGB с альфа-каналом (альфа-канал указан последним), но при этом значения нормированы на единицу. Таким образом, значение 1 означает 255, а 0.5 — 127.

Добавим вращение вокруг оси X и сохраним изменения в "Step4 Box Color.mq5".

Вид на вращающийся куб сверх справа.

Вид на вращающийся куб сверху справа.


Вращение и перемещение

Объекты можно перемещать и вращать сразу по трем направлениям. Все изменения объектов производятся с помощью матриц, каждую из них можно вычислять по отдельности — вращение, перемещение и трансформация размеров. Изменим пример, камера теперь смотрит на куб сверху и спереди.

   //+------------------------------------------------------------------+
   //| 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);
      //--- поместим камеру сверху и перед центром координат
      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));      
      //--- создаем куб - передаем ему диспетчер ресурсов, параметры сцены  и координаты двух противоположных углов куба
      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);
        }
      //--- установим цвет куба
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- вычислим позицию куба и матрицу переноса
      DXMatrix rotation,translation;
      //--- поворачиваем куб последовательно вокруг осей X, Y и Z
      DXMatrixRotationYawPitchRoll(rotation,(float)M_PI/4,(float)M_PI/3,(float)M_PI/6);
      //-- сдвигаем куб вправо-вниз-вглубь
      DXMatrixTranslation(translation,1.0,-2.0,5.0);
      //--- получим матрицу трансформации как произведение поворота и переноса
      DXMatrix transform;
      DXMatrixMultiply(transform,rotation,translation);
      //--- установим матрицу трансформации 
      m_box.TransformMatrixSet(transform);      
      //--- добавляем куб на сцену
      m_canvas.ObjectAdd(&m_box);    
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

Последовательно создаем матрицы поворота и переноса, применяем полученную матрицу трансформации и отрисовываем куб. Сохраняем изменения в "Step5 Translation.mq5" и запускаем.

Вращение и перемещение куба

Вращение и перемещение куба

Напомним, что камера неподвижно смотрит в центр координат немного сверху. Куб был повернут в трех направления и сдвинут вправо, вниз и вглубь сцены.


Работа с освещением

Для получения реалистичного трехмерного изображения необходимо рассчитать освещение каждой точки на поверхности объекта. Для этого используется модель Фонга, которая вычисляет цветовую интенсивность трех компонент освещения — фоновой (ambient), рассеянной (diffuse) и глянцевых бликов (specular). При этом используются следующие параметры:

Модель освещения Фонга
Модель освещения Фонга


Модель освещения реализована в стандартных шейдерах, сами параметры модели задаются в CCanvas3D, а объектов — в CDXMesh и его наследниках. Внесем изменения в наш пример:

  1. Вернем куб в центр координат.
  2. Установим ему белый цвет.
  3. Добавим направленный источник желтого цвета, который светит на сцену сверху вниз.
  4. Установим синий цвет ненаправленного освещения.
      //--- установим желтый цвет источника и направим его сверху вниз
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- установим цвет окружающего освещения синим 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));          
      //--- создаем куб - передаем ему диспетчер ресурсов, параметры сцены и координаты двух противоположных углов куба
      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);
        }
      //--- установим белый цвет куба
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0)); 
      // добавим кубу зеленого свечения
      m_box.EmissionColorSet(DXColor(0.0,1.0,0.0,0.2f)); 

Обратите внимание, что в модели Canvas3D не устанавливается позиция направленного источника света, а лишь направление распространения света. Подразумевается, что источник направленного света находится на бесконечном расстоянии и на сцену падает строго параллельный поток света.

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

В данном случае вектор распространения света направлен вдоль оси Y в отрицательном направлении — то есть сверху вниз. Кроме того, если вы задаете параметры направленного источника света (LightColorSet и LightDirectionSet), то необходимо установить и цвет окружающего рассеянного освещения (AmbientColorSet). Потому что по умолчанию цвет окружающего осещения задан белым максимальной интенсивности и все тени будут белого цвета.  Это означает, что объекты на сцене будут залиты белым светом ненаправленного освещения, свет направленного источника будет перебит белым светом.

      //--- установим желтый цвет источника и направим его сверху вниз
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- установим цвет окружающего освещения синим 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));  // обязательно указать

На рисунке с помощью GIF-анимации показано последовательное изменение картинки при добавлении освещения. Исходный код примера находится в файле "Step6 Add Light.mq5".

Белый куб с зеленым свечением под желтым источником света в синем окружающем освещении

Белый куб с зеленым свечением под желтым источником света в синем окружающем освещении.

Вы можете отключать в приведенном коде методы по работе с цветом, чтобы увидеть как это работает.


Анимация

Анимация — это изменение параметров сцены и объектов во времени. Изменять можно любые доступные свойства в зависимости от времени или событий. В качестве события, управляющего обновлением сцены, заведём таймер на 10 миллисекунд:

int OnInit()
  {
...
//--- create canvas
   ExtAppWindow=new CCanvas3DWindow();
   if(!ExtAppWindow.Create(width,height))
      return(INIT_FAILED);
//--- set timer
   EventSetMillisecondTimer(10);
//---
   return(INIT_SUCCEEDED);
  }

В класс CCanvas3DWindow добавим обработчик этого события, в котором будем изменять параметры объекта (вращение, перемещение и масштабирование) и направление освещения:

   //+------------------------------------------------------------------+
   //| Timer handler                                                    |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {    
      static ulong last_time=0;
      static float time=0;       
      //--- получим текущее время
      ulong current_time=GetMicrosecondCount();
      //--- вычислим дельту
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- увеличим значение прошедшего времени
      time+=deltatime;
      //--- запомним время
      last_time=current_time;
      //--- вычислим позицию куба и матрицу поворота
      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);
      //--- вычислим сжатие/растяжение куба вдоль осей
      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));
      //--- перемножим матрицы для получения финальной трансформации
      DXMatrix transform;
      DXMatrixMultiply(transform,scale,rotation);
      DXMatrixMultiply(transform,transform,translation);
      //--- установим матрицу трансформации
      m_box.TransformMatrixSet(transform);
      //--- вычислим поворот источника света вокруг оси Z
      DXMatrixRotationZ(rotation,deltatime);
      DXVector3 light_direction;
      //--- получим текущее направление источника света
      m_canvas.LightDirectionGet(light_direction);
      //--- вычислим новое направление источника света и установим его
      DXVec3TransformCoord(light_direction,light_direction,rotation);
      m_canvas.LightDirectionSet(light_direction);
      //--- пересчитаем 3D-сцену и отрисуем на холсте
      Redraw();
     }

Обратите внимание, что изменения объекта накладываются на начальные значения — как-будто мы каждый раз берем куб из точки его создания и проделываем все операции по вращению/перемещению/растяжению с нуля, то есть память о текущем состояние не сохраняется. В то время как направление источника света мы изменяем по приращениям deltatime от текущего значения к новому.

Вращающийся куб с динамичным освещением

Вращающийся куб с динамичным изменением направления источника света.

В результате получилась очень сложная 3D анимация. Код примера находится в файле "Step7 Animation.mq5".


Управление камерой с помощью мышки

Остался последний элемент анимации в 3D графике — реакция на действия пользователя. Добавим в наш пример управление камерой с помощью мышки. Для этого подписываемся на события мыши и создаем соответствующие обработчики:

int OnInit()
  {
...
//--- установим таймер
   EventSetMillisecondTimer(10);
//--- включим получение событий мышки - перемещение и нажатие кнопок
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,1);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,1)
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
//--- удалим таймер
   EventKillTimer();
//--- отключим получение событий мышки
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,0);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,0);
//--- удаляем объект
   delete ExtAppWindow;
//--- вернем чарт к обычному режиму показа ценовых графиков
   ChartSetInteger(0,CHART_SHOW,true);
  }
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- событие изменения графика
   if(id==CHARTEVENT_CHART_CHANGE)
      ExtAppWindow.OnChartChange();
//--- событие перемещения мышки
   if(id==CHARTEVENT_MOUSE_MOVE)
      ExtAppWindow.OnMouseMove((int)lparam,(int)dparam,(uint)sparam);
//--- событие прокрутки колеса мышки
   if(id==CHARTEVENT_MOUSE_WHEEL)
      ExtAppWindow.OnMouseWheel(dparam);

Создаем в CCanvas3DWindow обработчик движения мышью, который изменяет углы направления камеры при движениях мыши с зажатой левой кнопкой мыши:

   //+------------------------------------------------------------------+
   //| Обработка движения мышки                                         |
   //+------------------------------------------------------------------+
   void              OnMouseMove(int x,int y,uint flags)
     {
      //--- левая кнопка мышки
      if((flags&1)==1)
        {
         //--- нет сведений о предыдущей позиции мышки
         if(m_mouse_x!=-1)
           {
            //--- обновить угол камеры по изменению позиции
            m_camera_angles.y+=(x-m_mouse_x)/300.0f;
            m_camera_angles.x+=(y-m_mouse_y)/300.0f;
            //--- установить вертикальный угол в диапазоне (-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;
            //--- обновить позицию камеры
            UpdateCameraPosition();
           }
         //--- сохраняем позицию мышки
         m_mouse_x=x;
         m_mouse_y=y;
        }
      else
        {
         //--- сбросим сохраненную позицию, если левая кнопка мышки не зажата
         m_mouse_x=-1;
         m_mouse_y=-1;
        }
     }

И обработчик вращения колесика, который измененяет расстояние камеры до центра сцены:

   //+------------------------------------------------------------------+
   //| Обработка событий колесика мышки                                 |
   //+------------------------------------------------------------------+
   void              OnMouseWheel(double delta)
     {
      //--- обновить удаленность камеры по вращению колесика мышки
      m_camera_distance*=1.0-delta*0.001;
      //--- установить дистанцию в диапазоне [3,50]
      if(m_camera_distance>50.0)
         m_camera_distance=50.0;
      if(m_camera_distance<3.0)
         m_camera_distance=3.0;
      //--- обновить позицию камеры
      UpdateCameraPosition();
     }

Оба обработчика вызывают метод UpdateCameraPosition() для обновления положения камеры по изменившимся параметрам:

   //+------------------------------------------------------------------+
   //| Обновляет позицию камеры                                         |
   //+------------------------------------------------------------------+
   void              UpdateCameraPosition(void)
     {
      //--- позиция камеры без поворота с учетом расстояния до центра координат
      DXVector4 camera=DXVector4(0.0f,0.0f,-(float)m_camera_distance,1.0f);
      //--- вращение камеры вокруг оси X
      DXMatrix rotation;
      DXMatrixRotationX(rotation,m_camera_angles.x);
      DXVec4Transform(camera,camera,rotation);
      //--- вращение камеры вокруг оси Y
      DXMatrixRotationY(rotation,m_camera_angles.y);
      DXVec4Transform(camera,camera,rotation);
      //--- установим камеру в позицию
      m_canvas.ViewPositionSet(DXVector3(camera));
     }

Измененный код находится в файле "Step8 Mouse Control.mq5".

Управление камерой с помощью мышки

Управление камерой с помощью мышки.


Наложение текстуры

Текстура — это растровое изображение, накладываемое на поверхность полигона для придания цвета, окраски или иллюзии рельефа. Использование текстур позволяет воспроизвести малые объекты поверхности, создание которых полигонами является чрезмерно ресурсоёмким. Например, рисунок камня или дерева, поверхности стен и почвы.

Класс CDXMesh и его наследники позволяет задавать объекту текстуру, которая в стандартном пиксельном шейдере используется совместно с его цветом рассеивания (DiffuseColor). Убираем анимирование объекта и накладываем на него текстуру камня, которая должна находиться в папке MQL5\Files рабочего каталога терминала:

   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- установим белый цвет ненаправленного освещения
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0));

      //--- добавим текстуру для отрисовки граней куба
      m_box.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- добавляем куб на сцену
      m_canvas.ObjectAdd(&m_box);
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

Куб с наложенной текстурой камня

Куб с наложенной текстурой камня.


Создание произвольных объектов

Все объекты состоят из вершин (DXVector3), и эти вершины связываются с помощью индексов в определенные примитивы. Самый распространенный примитив — трегольник. Для создания базового 3D-объекта необходимо создать список вершин, которые содержат как минимум их координаты (но могут содержать множество дополнительных данных, таких как нормаль, цвет и т.д.), тип примитивов, в которые они объединяются, и список индексов вершин, по которому они будут объединяться в примитивы.


В Стандартной библиотеке определен тип вершины DXVertex, который содержит ее координату, нормаль для расчета освещения, текстурные координаты и цвет. С этим типом вершин работает стандартный вершинный шейдер.

struct DXVertex
  {
   DXVector4         position;  // координаты вершины
   DXVector4         normal;    // вектор нормали
   DXVector2         tcoord;    // координаты грани для натягивания текстуры
   DXColor           vcolor;    // цвет
  };

Во вспомогательном файле MQL5\Include\Canvas\DXDXUtils.mqh содержится набор методов для генерации геометрии (вершин и индексов) базовых примитивов, а также загрузки 3D-геометрии из .OBJ файлов.

Добавим создание сферы и тора, наложим на них такую же текстуру камня:

   virtual bool      Create(const int width,const int height)
     {
 ...     
      //--- вершины и индексы для объектов, создаваемых вручную
      DXVertex vertices[];
      uint indices[];
      //--- подготовим вершины и индексы для сферы
      if(!DXComputeSphere(0.3f,50,vertices,indices))
         return(false);
      //--- установим вершинам белый цвет
      DXColor white=DXColor(1.0f,1.0f,1.0f,1.0f);
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- создаем объект сфера
      if(!m_sphere.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
        }
      //--- установим цвет рассеянного освещения для сферы
      m_sphere.DiffuseColorSet(DXColor(0.0,1.0,0.0,1.0));
      //--- установим белый цвет отражения от сферы
      m_sphere.SpecularColorSet(white);
      m_sphere.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- добавим сферу на сцену
      m_canvas.ObjectAdd(&m_sphere);
      //--- подготовим вершины и индексы для тора
      if(!DXComputeTorus(0.3f,0.1f,50,vertices,indices))
         return(false);
      //--- установим вершинам белый цвет
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- создадим объект тор
      if(!m_torus.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
        }
      //--- установим цвет рассеянного освещения для тора
      m_torus.DiffuseColorSet(DXColor(0.0,0.0,1.0,1.0));
      m_torus.SpecularColorSet(white);
      m_torus.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- добавим тор на сцену
      m_canvas.ObjectAdd(&m_torus);      
      //--- перерисовываем сцену
      Redraw();
      //--- succeed
      return(true);
     }

И добавим анимацию для новых объектов:

   void              OnTimer(void)
     {
...
      m_canvas.LightDirectionSet(light_direction);
      //--- орбита сферы
      DXMatrix translation;
      DXMatrixTranslation(translation,1.1f,0,0);
      DXMatrixRotationY(rotation,time);
      DXMatrix transform;
      DXMatrixMultiply(transform,translation,rotation);
      m_sphere.TransformMatrixSet(transform);
      //--- орбита тора с вращением вокруг своей оси
      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);           
      //--- пересчитаем 3D-сцену и отрисуем на холсте
      Redraw();
     }

Сохраняем изменения в "Three Objects.mq5" и запускаем.

Вращающиеся фигуры на орбите куба.

Вращающиеся фигуры на орбите куба.


3D поверхность на основе данных

Для создания отчетов и анализа данных принято использовать разнообразные графики — линейные графики, гистограммы, круговые диаграммы и т.д. MQL5 предлагает для таких целей использовать удобную графическую библиотеку, но она может отрисовывать только плоские графики.

Класс CDXSurface позволяет визуализировать в пространстве поверхность на пользовательских данных, хранящихся в двумерном массиве. Покажем, как это делается, на примере математической функции

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

Создаем объект для отрисовки поверхности и массив для хранения данных:

   virtual bool      Create(const int width,const int height)
     {
...
      //--- подготовим массив для хранения данных
      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;
      //--- создадим объект-поверхность
      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);
        }
      //--- зададим текстуру и отражение для поверхности
      m_surface.SpecularColorSet(DXColor(1.0,1.0,1.0,1.0));
      m_surface.TextureSet(m_canvas.DXDispatcher(),"checker.bmp");
      //--- добавим поверхность на сцену
      m_canvas.ObjectAdd(&m_surface);
      //--- succeed
      return(true);
     }

Поверхность будет отрисовываться в пределах параллелепипеда с основанием 4x4 и высотой в 1. Размер текстуры 0.25x0.25.

Для того чтобы анимировать поверхность, добавим под знак синуса время и будем обновлять его по таймеру.

   void              OnTimer(void)
     {
      static ulong last_time=0;
      static float time=0;
      //--- получим текущее время
      ulong current_time=GetMicrosecondCount();
      //--- вычислим дельту
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- увеличим значение прошедшего времени
      time+=deltatime;
      //--- запомним время
      last_time=current_time;
      //--- вычислим значения поверхности с учетом изменения времени
      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);
           }
        }
      //--- обновим данные для отрисовки поверхности
      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))
        {
         //--- пересчитаем 3D-сцену и отрисуем на холсте
         Redraw();
        }
     }
Исходный файл находится в "3D Surface.mq5", пример работы показан на видео.




В этой статье мы показали как функции DirectX позволяют создавать простые геометрические фигуры и анимированную 3D-графику для визуального анализа данных. Более сложные примеры вы можете найти в папке установки терминала MetaTrader 5: эксперты "Correlation Matrix 3D" и "Math 3D Morpher", а также cкрипт "Remnant 3D". 

С помощью MQL5 можно решать несколько важнейших задач алготрейдинга без обращения к сторонним пакетам:

Используйте все возможности для визуализации биржевых данных и разработки торговых стратегий в MetaTrader 5 — теперь и с помощью 3D-графики!