English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形

如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形

MetaTrader 5积分 | 29 四月 2020, 13:07
3 457 2
MetaQuotes
MetaQuotes

三维计算机图形可在平面显示器上提供三维物体的印象。 这样的物体,以及观察者的位置可随时间变化。 相应地,二维图像也应变化,从而生成图像深度的错觉,即,它应该支持旋转、缩放、光照变化、等等。 MQL5 允许利用 DirectX 函数 在 MetaTrader 5 终端里直接创建和管理计算机图形。 请注意,您的显卡应支持 DX 11 和 Shader Model 5.0 才能正常工作。


物体建模

若要在平面空间上绘制三维物体,应首先得到 X、Y 和 Z 轴坐标上的物体模型。 这意味着物体表面上的每个点均要以特定坐标定义。 理想情况下,需要定义物体表面上的无数个点,从而在缩放图像时保持品质。 实际上,3D 建模是以多边形组成的网模来定义。 多边形端点越多,则网模越详尽,提供的模型越加真实。 然而,计算这样的模型和渲染 3D 图形需要更多的计算机资源。

以茶壶模型作为多边形网模

以茶壶模型作为多边形网模。

早期的计算机图形不得不在较弱的图形卡上运行,故将多边形切分成三角形很久以前就出现了。 三角形可精确描述小表面部件的位置,以及计算相关参数,例如光照和光线反射。 这样的小三角形的集合能够创建逼真的物体三维图像。 在下文中,多边形和三角形将归并为同义词,因为想象三角形较之拥有 N 个顶点的多边形,显然容易得多。


由三角形组成的立方体。

按照三角形每个顶点的坐标定义来创建物体的三维模型,如此,即便物体移动或观察者的位置发生变化,也可以进一步计算物体每个点的坐标。 所以,我们要处理的是顶点,连接顶点的边线,以及由边线形成的表面。 如果知道三角形的位置,则可以利用线性代数定律来创建切面法线(法线是垂直于表面的向量)。 如此即可计算出切面如何光照,以及光线如何从切面反射。


简单物体的顶点、边线、切面和法线的示例。 法线是红色箭头。

物体模型能够以不同方式来创建。 拓扑学描述了多边形如何形成 3D 网模。 良好的拓扑结构允许利用最少数量的多边形来描绘对象,并可令物体的移动和旋转更加容易。

两种拓扑中的球面模型

两种拓扑中的球面模型。

在物体多边形上利用光影来创建体积效果。 因此,3D 计算机图形的目的是计算物体每个点的位置,计算光线明暗,并将其显示在屏幕上。

造型

我们编写一个创建立方体的简单程序。 利用 3D 图形库中的 CCanvas3D 类。

渲染 3D 窗体的 CCanvas3DWindow 类拥有最少的成员和方法。 我们将逐步添加新方法,并针对操控 DirectX 函数中所实现的 3D 图形概念加以解释。

//+------------------------------------------------------------------+
//| 应用窗体                                                           |
//+------------------------------------------------------------------+
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. 纵横比(宽度与高度比率)
  3. 距剪切面近处(0.1f)到远处(100.f)的距离

这意味着仅在投影矩阵中渲染物体处于两堵虚构墙面(0.1f 和 100.f)之间的部分。 另外,物体必须落入视角的水平 30 度夹角。 请注意,距离以及计算机图形中的所有坐标都是虚构的。 重要的是距离和大小之间的相对关系,而不是绝对值。

   //+------------------------------------------------------------------+
   //| 创建                                                             |
   //+------------------------------------------------------------------+
   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();
      //--- 成功
      return(true);
      }

创建投影矩阵后,我们可以继续构建 3D 物体 — 基于 CDXBox 类的立方体。 为了创建一个立方体,指定立方体两个对角的顶点即可。 通过在调试模式下观察立方体的创建,您可以看到 DXComputeBox() 中所发生的情况:创建立方体的所有顶点(它们的坐标已写入 “vertices” 数组),以及把立方体边线切分为三角形,列举并保存在 “indiсes” 数组当中。 立方体总共有 8 个顶点,6 个切面切分为 12 个三角形,这些三角形顶点列举为 36 个索引。

尽管立方体只有 8 个顶点,但由于要为 6 个切面中的每条法线指定单独的顶点集合,所以创建了 24 个顶点来描述它们。 法线方向会影响每个切面的光照计算。 三角形顶点在索引中的列举顺序决定了可见的三角形边线。 在 DXUtils.mqh 代码里展示了顶点和索引的填充顺序:

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

在同一代码里为每个切面的纹理映射定义了纹理坐标:

//--- 纹理坐标
   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 场景时,都应重新执行所有计算。 这是所需计算的顺序:

  • 在全系坐标中计算每个物体的中心
  • 计算物体每个元素的位置,即每个顶点的位置
  • 确定像素深度,及其视野的可见性
  • 按指定顶点计算每个像素在多边形上的位置
  • 根据指定的纹理设置多边形上每个像素的颜色
  • 计算光照像素的方向,及其反射
  • 为每个像素施加散射光
  • 将全系坐标转换为相机坐标
  • 将相机坐标转换为投影矩阵上的坐标
所有这些操作都是在 CCanvas3D 实例的 Render 方法中执行的。 渲染后,调用 Update 方法将计算出的图像从投影矩阵变换到画布。
   //+------------------------------------------------------------------+
   //| 刷新场景                                                           |
   //+------------------------------------------------------------------+
   void              Redraw()
     {
      //--- 计算 3D 场景
      m_canvas.Render(DX_CLEAR_COLOR|DX_CLEAR_DEPTH,ColorToARGB(clrBlack));
      //--- 根据当前场景更新画布上的图片
      m_canvas.Update();
      }

在我们的示例中,立方体仅创建一次,且不会再更改。 所以,仅当图表中有变化,诸如调整图表大小时,才需要更改画布上的框架。 在这种情况下,画布维度将调整为当前图表维度,投影矩阵被重置,而画布上的图像亦被更新。

   //+------------------------------------------------------------------+
   //| 处理图表变化事件                                                    |
   //+------------------------------------------------------------------+
   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 场景,并在画布上渲染
         Redraw();
         }
      }

启动 "Step1 Create Box.mq5" EA。 您在黑色背景上会看到一个白色正方形。 默认情况下,创建时物体的颜色设置为白色。 尚未设置光照。

白色立方体及其空间布局

白色立方体及其空间布局

X 轴指向右侧,Y 轴指向上方,Z 轴指向 3D 场景内里。 这种坐标系称为左手系。

立方体的中心位于以下坐标 X=0,Y=0,Z=6 的点上。 从我们的观察来看,立方体位于坐标中心,这是默认值。 如果您要更改 3D 场景的观察点位置,需利用 ViewPositionSet() 函数显性设置相应的坐标。

为了结束程序操作,请按 “Escape(退出)”健。


围绕 Z 轴和视角的物体旋转

若为制作场景动画,我们需启用围绕 Z 轴的立方体旋转。 为此,添加一个计时器 — 基于其事件,立方体将以逆时针旋转。

利用 DXMatrixRotationZ() 方法创建一个按给定角度围绕 Z 轴旋转的矩阵。 然后将其作为参数传递给 TransformMatrixSet() 方法。 如此即可更改立方体在 3D 空间中的位置。 再者,调用 Redraw() 刷新画布上的图像。

   //+------------------------------------------------------------------+
   //| 计时器响应                                                         |
   //+------------------------------------------------------------------+
   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 场景,并在画布上渲染
      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 类拥有三个设置重要 3D 场景参数的方法,这些方法相互关联:

所有这些参数会被组合使用 — 这意味着,如果您要在 3D 场景中设置这些参数中的任何一个,则其他两个参数也必须要初始化。 至少应在场景生成阶段完成此操作。 这一点在下面的示例中有所展示,其框架的上边框左右摆动。 摆动是在 Create() 方法中添加以下三行代码来实现的:

   //+------------------------------------------------------------------+
   //| 创建                                                              |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
....       
      //--- 在场景里加入立方体
      m_canvas.ObjectAdd(&m_box);
      //--- 设置场景参数
      m_canvas.ViewUpDirectionSet(DXVector3(0,1,0));  // set the direction vector up, along the Y axis  
      m_canvas.ViewPositionSet(DXVector3(0,0,0));     // set the viewpoint from the center of coordinates
      m_canvas.ViewTargetSet(DXVector3(0,0,6));       // set the gaze point at center of the cube      
      //--- 刷新场景
      Redraw();
      //--- 成功
      return(true);
      }

修改 OnTimer() 方法令顶点左右水平摆动。

   //+------------------------------------------------------------------+
   //| 计时器响应                                                         |
   //+------------------------------------------------------------------+
   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);     // initial direction of the top
      DXMatrix rotation;                        // rotation vector      
      //--- 计算旋转矩阵 
      DXMatrixRotationZ(rotation,float(MathSin(time)*max_angle));
      DXVec3TransformCoord(direction,direction,rotation);
      m_canvas.ViewUpDirectionSet(direction);   // set the new direction of the top
      //--- 重新计算 3D 场景,并在画布上渲染
      Redraw();
      }

将示例另存为 “Step3 ViewUpDirectionSet.mq5” 并运行它。 您将看到一个旋转的立方体图像,尽管它实际上是静止的。 当相机本身左右摆动时,就会得到这种效果。

顶部方向左右摆动

顶部方向左右摆动

请记住,目标、相机和顶部方向的坐标之间存在关联。 所以,为了控制相机的位置,您还必须指定顶部的方向,和目标的坐标,即凝视点。


物体颜色管理

我们来修改代码,然后在移动相机的同时将立方体放在坐标中心。

   //+------------------------------------------------------------------+
   //| 创建                                                              |
   //+------------------------------------------------------------------+
   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();
      //--- 成功
      return(true);
      }

此外,将立方体染成蓝色。 颜色是以含 Alpha 通道的 RGB 格式设置(Alpha 通道在末尾),且数值已被归一化。 因此,数值 1 表示 255,而数值 0.5 表示 127。

添加围绕 X 轴的旋转,并保存修改为 “Step4 Box Color.mq5”

旋转立方体的右上视图。

旋转立方体的右上视图。


旋转与运动

可以一次在三个方向上移动和旋转物体。 物体的所有更改均使用矩阵实现。 它们中的每一个,即旋转、移动和变换,均可分别计算。 我们来修改示例:相机视野现在从顶部到前面。

   //+------------------------------------------------------------------+
   //| 创建                                                              |
   //+------------------------------------------------------------------+
   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();
      //--- 成功
      return(true);
      }

依次创建旋转矩阵和变换矩阵,应用生成的变换矩阵,并渲染立方体。 将修改保存在 "Step5 Translation.mq5",并运行它。

立方体的旋转和运动

立方体的旋转和运动

依旧是相机,从上方稍微指向坐标中心。 立方体在三个方向上旋转,并向右、向下和向场景内偏移。


光照处理

为了获得逼真的三维图像,必须计算物体表面上每个点的光照。 这可利用 Phong 着色模型来完成,该模型计算以下三个光照成份的颜色强度:环境、漫反射和镜面反射。 在此采用以下参数:

  • DirectionLight — 在 CCanvas3D 中设置定向光照的方向
  • AmbientLight — 在 CCanvas3D 中设置环境光照的颜色和强度
  • DiffuseColor — 在 CDXMesh 及其子类中设置计算出的漫射光照成份
  • EmissionColor — 在 CDXMesh 及其子类中设置背景光照成份
  • SpecularColor — 在 CDXMesh 及其子类中设置镜面反射成份

Phong 着色模型
Phong 着色模型


光照模型在标准着色器中实现,模型参数在 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));  // must be specified

下面的 gif 动画展示了添加光照后的图像变化。 该示例的源代码在文件 “Step6 Add Light.mq5” 中提供。

白色立方体,在黄色光源,,蓝色环境光下发出绿光

白色立方体,在黄色光源,,蓝色环境光下发出绿光

尝试屏蔽上面代码中的颜色方法,看看它是如何工作的。


动画

动画意味着场景参数和物体随时间变化。 可以根据时间或事件更改任何可用的属性。 将计时器设置为 10 毫秒 — 此事件将影响场景的刷新:

int OnInit()
  {
...
//--- 创建画布
   ExtAppWindow=new CCanvas3DWindow();
   if(!ExtAppWindow.Create(width,height))
      return(INIT_FAILED);
//--- 设置计时器
   EventSetMillisecondTimer(10);
//---
   return(INIT_SUCCEEDED);
   }

将相应的事件处理程序添加到 CCanvas3DWindow。 我们需要更改物体参数(诸如旋转、移动和缩放)和光照方向:

   //+------------------------------------------------------------------+
   //| 计时器响应                                                         |
   //+------------------------------------------------------------------+
   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();
      //--- 成功
      return(true);
      }

石头材质的立方体

石头材质的立方体。


创建自定义物体

所有物体都包含顶点(DXVector3),这些顶点由索引连接到图元。 最常见的图元是三角形。 基本的 3D 物体是由一系列至少包含坐标的顶点所构建(但也可以包含许多其他数据,例如法线、颜色、等),合成图元的类型,以及合成图元的顶点索引序列。


标准库拥有 DXVertex 顶点类型,其中包含其坐标、计算光照的法线、纹理坐标和颜色。 标准顶点着色器配合顶点类型一同操作。

struct DXVertex
  {
   DXVector4         position;  // 顶点坐标
   DXVector4         normal;    // 法线顶点
   DXVector2         tcoord;    // 施加纹理的表面坐标
   DXColor           vcolor;    // 颜色
  };


MQL5\Include\Canvas\DXDXUtils.mqh 辅助类型包含一套生成基本几何(顶点和索引)图元的方法,以及从 .OBJ 文件里加载 3D 几何图元的方法。

加入创建一个球面和一个圆环,应用相同的石头纹理:

   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();
      //--- 成功
      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 提供了一套便利的图形库,但是该图形库只能构建 2D 图表。

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);
      //--- 成功
      return(true);
      }

将在底面 4x4,高度为 1 的箱体中绘制表面。 纹理维度是 0.25x0.25。

  • SF_TWO_SIDED 表示如果相机在表面的下方移动,则绘制该表面的上、下方。
  • SF_USE_NORMALS 表示计算由定向光源引起的反射时利用法线计算。
  • CS_COLD_TO_HOT 设置表面的热图颜色从蓝色至红色,中间由绿色过渡到黄色。

若要制作表面动画,请在标记符号下方添加时间,并通过计时器对其进行刷新。

   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”,以及 “Remnant 3D” 脚本。 

MQL5 令您无需使用第三方程序包即可解决重要的算法交易任务:

  • 优化包含许多输入参数的复杂交易策略
  • 获得优化结果
  • 以最方便的三维存储将数据可视化
运用最前沿的功能在 MetaTrader 5 中可视化股票数据,并开发交易策略 — 现在拥有了 3D 图形!


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/7708

附加的文件 |
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)
最近评论 | 前往讨论 (2)
okwh
okwh | 5 5月 2023 在 05:41

Sir:

the code in this article cnn not run !  Please check and help me . 

for Canva.mql code:  I add some Print(...) code for trace error, see bellow:

line 328:

bool CCanvas::CreateBitmapLabel(const long chart_id,const int subwin,const string name,
                                const int x,const int y,const int width,const int height,
                                ENUM_COLOR_FORMAT clrfmt)
  {
//--- create canvas
       Print("At CCanvas Create BitmapLabel 1  ",width,"  ",name,"   ",height,"  ",clrfmt);   //I add
       bool xx= Create(name,width,height,clrfmt);     
       Print("CCanvas Create return  ",xx,"   ",GetLastError());   //I add
 //  if(Create(name,width,height,clrfmt))    // I change
   if (xx)
     {
       Print("CCanvas Create  2  ",chart_id,"  ",name,"   ",subwin);
..................

for Canva.mql code:     

xx= Create(name,width,height,clrfmt);    will call function bellow:

line 250:

bool CCanvas::Create(const string name,const int width,const int height,ENUM_COLOR_FORMAT clrfmt)
  {
   Destroy();
//--- prepare data array
   if(width>0 && height>0 && ArrayResize(m_pixels,width*height)>0)
     {
      //--- generate resource name
      m_rcname="::"+name+(string)ChartID()+(string)(GetTickCount()+MathRand());
      
      //--- initialize data with zeros
      ArrayInitialize(m_pixels,0);
      //--- create dynamic resource
       Print("Before CCanvas ResourceCreate 0  ",m_rcname);         //I add
      if(ResourceCreate(m_rcname,m_pixels,width,height,0,0,0,clrfmt))
        {

         //--- successfully created
         //--- complete initialization
         m_width =width;
         m_height=height;
         m_format=clrfmt;
         //--- succeed
       Print("then CCanvas ResourceCreate OK  ",m_rcname);     //I add
         return(true);
        }
     }
.............................


For sample 3D-surface.mq5

line 40:

   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
      //--- save canvas dimensions
      m_width=width;
      m_height=height;
      //--- reset input data
      m_mouse_x=m_mouse_y=-1;
      //--- set default parameters for the camera
      m_camera_distance=10.0f;
      m_camera_angles.x=DX_PI_DIV6;
      m_camera_angles.y=DX_PI_DIV3;
      //--- create a canvas to render a 3D scene
      ResetLastError();
      if(!m_canvas.CreateBitmapLabel("3D Surface",0,0,m_width,m_height,COLOR_FORMAT_ARGB_NORMALIZE))
       { 
         Print("CreateBitmapLabel fail 3D surface ?");   // I add this line
         Print("Error creating canvas: ",GetLastError());
         return(false);
         }
..........................


Run 3D-Surface.mq5, received: 

2023.05.05 11:32:46.180 3D_Surface (EURUSD,M1)  At CCanvas Create BitmapLabel 1  604  3D Surface   392  2
2023.05.05 11:32:46.180 3D_Surface (EURUSD,M1)  Before CCanvas ResourceCreate 0  ::3D Surface1332772457869319469285704
2023.05.05 11:32:46.180 3D_Surface (EURUSD,M1)  then CCanvas ResourceCreate OK  ::3D Surface1332772457869319469285704
2023.05.05 11:32:46.185 3D_Surface (EURUSD,M1)  CCanvas Create return  false   5151
2023.05.05 11:32:46.185 3D_Surface (EURUSD,M1)  CreateBitmapLabel fail 3D surface ?
2023.05.05 11:32:46.185 3D_Surface (EURUSD,M1)  Error creating canvas: 5151
then CCanvas ResourceCreate OK 
BUR BUT But....

    
CCanvas Create return  false   5151

Why? what's wrong?

What is 5151 ??

okwh
okwh | 10 5月 2023 在 11:26

my display adapter is Nivada FX 1700--- an old product.  only support feture-level 10.0


So use DXcpl.exe to set Force WRAP for MT , then all run OK.

应用网络函数,或无需 DLL 的 MySQL:第 I 部分 - 连通器 应用网络函数,或无需 DLL 的 MySQL:第 I 部分 - 连通器
MetaTrader 5 最近已获增网络函数。 这为程序员开发市场所需产品提供了巨大的机遇。 如今,他们能够实现以前需要动态库支持的功能。 在本文中,我们将以 MySQL 为例研究所有的实现。
轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓 轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓
我们继续开发利用延后请求进行交易的函数库功能。 我们已实现了发送开仓和下挂单的条件交易请求。 在本文中,我们将实现条件平仓 – 全部、部分和由逆向仓位平仓。
轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓 轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓
在本文中,我们将完成延后请求交易概念的论述,并创建删除挂单,以及在特定条件下修改挂单和持仓的功能。 由此,我们将拥有完整的功能,令我们能够开发简单的自定义策略,或者根据用户定义的条件激活 EA 行为逻辑。
在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价 在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价
在本文中,我们将继续研讨在交易中运用 OLAP 技术。 我们会扩展前两篇文章中表述的功能。 这次我们将研究报价的操盘分析。 我们还将基于所汇集的历史数据,推导并检验交易策略的设想。 本文推介了基于柱线形态研究和自适应交易的智能交易系统。