请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3195047 新帖:252

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

螺罗丝发表于:4 月 29 日 20:00回复(1)

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

  • 物体建模
  • 造型
  • 场景计算与渲染
  • 围绕 Z 轴和视角的物体旋转
  • 相机位置管理
  • 物体颜色管理
  • 旋转与运动
  • 光照处理
  • 动画
  • 用鼠标控制相机位置
  • 应用纹理
  • 创建自定义物体
  • 基于数据的 3D 表面


物体建模

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

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

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

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


由三角形组成的立方体。

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


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

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

两种拓扑中的球面模型

两种拓扑中的球面模型。

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

造型

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

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

//+------------------------------------------------------------------+
//| Application window                                               |
//+------------------------------------------------------------------+
class CCanvas3DWindow
  {
protected:
   CCanvas3D         m_canvas;
   //--- canvas size
   int               m_width;
   int               m_height;
   //--- the Cube object
   CDXBox            m_box;

public:
                     CCanvas3DWindow(void) {}
                    ~CCanvas3DWindow(void) {m_box.Shutdown();}
   //-- create a scene
   virtual bool      Create(const int width,const int height){}
   //--- calculate the scene
   void              Redraw(){}
   //--- handle chart events
   void              OnChartChange(void) {}
  };

场景的创建要从创建画布开始。 然后为投影矩阵设置以下参数:

  1. 30 度视角(M_PI/6),从这处我们观看 3D 场景
  2. 纵横比(宽度与高度比率)
  3. 距剪切面近处(0.1f)到远处(100.f)的距离

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

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
      //--- save canvas dimensions
      m_width=width;
      m_height=height;
      //--- create a canvas to render a 3D scene
      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);
         }
      //--- set projection matrix parameters - angle of view, aspect ratio, distance to the near and far clip planes
      m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
      //--- create cube - pass to it the resource manager, scene parameters and coordinates of two opposite corners of the cube
      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);
         }
      //--- add the cube to the scene
      m_canvas.ObjectAdd(&m_box);
      //--- redraw the scene
      Redraw();
      //--- succeed
      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);

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

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

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

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

   //+------------------------------------------------------------------+
   //| Process chart change event                                       |
   //+------------------------------------------------------------------+
   void              OnChartChange(void)
     {
      //--- get current chart sizes
      int w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
      int h=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS);
      //--- update canvas dimensions in accordance with the chart size
      if(w!=m_width || h!=m_height)
        {
         m_width =w;
         m_height=h;
         //--- resize canvas
         m_canvas.Resize(w,h);
         DXContextSetSize(m_canvas.DXContext(),w,h);
         //--- update projection matrix in accordance with the canvas sizes
         m_canvas.ProjectionMatrixSet((float)M_PI/6,(float)m_width/m_height,0.1f,100.0f);
         //--- recalculate 3D scene and render it onto the canvas
         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() 刷新画布上的图像。

   //+------------------------------------------------------------------+
   //| Timer handler                                                    |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- variables for calculating the rotation angle
      static ulong last_time=0;
      static float angle=0;
      //--- get the current time
      ulong current_time=GetMicrosecondCount();
      //--- calculate the delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- increase the angle of rotation of the cube around the Z axis
      angle+=deltatime;
      //--- remember the time
      last_time=current_time;
      //--- set the angle of rotation of the cube around the Z axis
      DXMatrix rotation;
      DXMatrixRotationZ(rotation,angle);
      m_box.TransformMatrixSet(rotation);
      //--- recalculate 3D scene and render it onto the canvas
      Redraw();
      }

启动后,您会看到一个旋转的白色正方形。

立方体绕 Z 轴逆时针旋转

该示例的源代码在文件 “Step2 Rotation Z.mq5” 中提供。 请注意,创建场景时现已指定了角度 M_PI/5,该角度大于前一个示例的角度 M_PI/6。 

      //--- set projection matrix parameters - angle of view, aspect ratio, distance to the near and far clip planes
      m_matrix_view_angle=(float)M_PI/5;
      m_canvas.ProjectionMatrixSet(m_matrix_view_angle,(float)m_width/m_height,0.1f,100.0f);
      //--- create cube - pass to it the resource manager, scene parameters and coordinates of two opposite corners of the cube

然而,场景里的立方体维度在视觉上较小。 设置投影矩阵时指定的视角越小,则物体边框占据的部分越大。 这好比用望远镜观看物体:尽管视角较小,但物体较大。


相机位置管理

CCanvas3D 类拥有三个设置重要 3D 场景参数的方法,这些方法相互关联:

  • ViewPositionSet 设置 3D 场景的视点
  • ViewTargetSet 设置凝视点的坐标
  • ViewUpDirectionSet 设置框架上边框在 3D 空间中的方向

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

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
....       
      //--- add the cube to the scene
      m_canvas.ObjectAdd(&m_box);
      //--- set the scene parameters
      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 the scene
      Redraw();
      //--- succeed
      return(true);
      }

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

   //+------------------------------------------------------------------+
   //| Timer handler                                                    |
   //+------------------------------------------------------------------+
   void              OnTimer(void)
     {
      //--- variables for calculating the rotation angle
      static ulong last_time=0;
      static float max_angle=(float)M_PI/30;
      static float time=0;
      //--- get the current time
      ulong current_time=GetMicrosecondCount();
      //--- calculate the delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- increase the angle of rotation of the cube around the Z axis
      time+=deltatime;
      //--- remember the time
      last_time=current_time;
      //--- set the rotation angle around the Z axis
      DXVector3 direction=DXVector3(0,1,0);     // initial direction of the top
      DXMatrix rotation;                        // rotation vector      
      //--- calculate the rotation matrix 
      DXMatrixRotationZ(rotation,float(MathSin(time)*max_angle));
      DXVec3TransformCoord(direction,direction,rotation);
      m_canvas.ViewUpDirectionSet(direction);   // set the new direction of the top
      //--- recalculate 3D scene and render it onto the canvas
      Redraw();
      }

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

顶部方向左右摆动

顶部方向左右摆动

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


物体颜色管理

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

   //+------------------------------------------------------------------+
   //| Create                                                           |
   //+------------------------------------------------------------------+
   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- create cube - pass to it the resource manager, scene parameters and coordinates of two opposite corners of the cube
      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);
         }
      //--- set the color 
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- add the cube to the scene
      m_canvas.ObjectAdd(&m_box);
      //--- set positions for camera, gaze and direction of the top
      m_canvas.ViewUpDirectionSet(DXVector3(0.0,1.0,0.0));  // set the direction vector up, along the Y axis
      m_canvas.ViewPositionSet(DXVector3(3.0,2.0,-5.0));    // set camera on the right, on top and in front of the cube
      m_canvas.ViewTargetSet(DXVector3(0,0,0));             // set the gaze direction at center of the cube
      //--- redraw the scene
      Redraw();
      //--- succeed
      return(true);
      }

此外,将立方体染成蓝色。 颜色是以含 Alpha 通道的 RGB 格式设置(Alpha 通道在末尾),且数值已被归一化。 因此,数值 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);
      //--- position the camera in top and in front of the center of coordinates
      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));      
      //--- create cube - pass to it the resource manager, scene parameters and coordinates of two opposite corners of the cube
      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);
         }
      //--- set the cube color
      m_box.DiffuseColorSet(DXColor(0.0,0.5,1.0,1.0));        
      //--- calculate the cube position and the transfer matrix
      DXMatrix rotation,translation;
      //--- rotate the cube sequentially along the X, Y and Z axes
      DXMatrixRotationYawPitchRoll(rotation,(float)M_PI/4,(float)M_PI/3,(float)M_PI/6);
      //-- move the cube to the right/downward/inward
      DXMatrixTranslation(translation,1.0,-2.0,5.0);
      //--- get the transformation matrix as a product of rotation and transfer
      DXMatrix transform;
      DXMatrixMultiply(transform,rotation,translation);
      //--- set the transformation matrix 
      m_box.TransformMatrixSet(transform);      
      //--- add the cube to the scene
      m_canvas.ObjectAdd(&m_box);    
      //--- redraw the scene
      Redraw();
      //--- succeed
      return(true);
      }

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

立方体的旋转和运动

立方体的旋转和运动

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


光照处理

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

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

Phong 着色模型
Phong 着色模型


光照模型在标准着色器中实现,模型参数在 CCanvas3D 中设置,物体参数在 CDXMesh 及其子类中设置。 如下所示修改示例:

  1. 将立方体返回到坐标中心。
  2. 为其设置白色。
  3. 添加黄色定向光源,从上到下照亮场景。
  4. 非定向光照设置为蓝色。
      //--- set yellow color for the source and direct it from above downwards
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- set the blue color for the ambient light 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));          
      //--- create cube - pass to it the resource manager, scene parameters and coordinates of two opposite corners of the cube
      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);
         }
      //--- set the white color for the cube
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0)); 
      //--- add green glow for the cube (emission)
      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)。 默认情况下,环境光的颜色设置为强度最大的白色,因此所有阴影均为白色。 这意味着场景中的物体会被环境光染成白色泛光,而定向光源将被白光打断。

      //--- set yellow color for the source and direct it from above downwards
      m_canvas.LightColorSet(DXColor(1.0,1.0,0.0,0.8f));
      m_canvas.LightDirectionSet(DXVector3(0.0,-1.0,0.0));
      //--- set the blue color for the ambient light 
      m_canvas.AmbientColorSet(DXColor(0.0,0.0,1.0,0.4f));  // must be specified

下面的 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;       
      //--- get the current time
      ulong current_time=GetMicrosecondCount();
      //--- calculate the delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- increase the elapsed time value
      time+=deltatime;
      //--- remember the time
      last_time=current_time;
      //--- calculate the cube position and the rotation matrix
      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);
      //--- calculate the cube compression/extension along the axes
      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));
      //--- multiply the matrices to obtain the final transformation
      DXMatrix transform;
      DXMatrixMultiply(transform,scale,rotation);
      DXMatrixMultiply(transform,transform,translation);
      //--- set the transformation matrix
      m_box.TransformMatrixSet(transform);
      //--- calculate the rotation of the light source around the Z axis
      DXMatrixRotationZ(rotation,deltatime);
      DXVector3 light_direction;
      //--- get the current direction of the light source
      m_canvas.LightDirectionGet(light_direction);
      //--- calculate the new direction of the light source and set it
      DXVec3TransformCoord(light_direction,light_direction,rotation);
      m_canvas.LightDirectionSet(light_direction);
      //--- recalculate the 3D scene and draw it in the canvas
      Redraw();
      }

请注意,物体变化将应用于初始值,就像我们始终在处理立方体状态,并从草创开始相对于旋转/移动/缩放等应用所有操作一样,意即立方体的当前状态不会被保存。 不过,光源方向从当前值开始以 deltatime 为增量进行更改。

动态光照的旋转立方体

动态光源方向变化的旋转立方体。

结果是一个非常复杂的 3D 动画。 示例代码在文件 “Step7 Animation.mq5” 中提供。


用鼠标控制相机位置

我们来研究 3D 图形的最后一个动画元素,反馈用户的动作。 在我们的示例中,加入利用鼠标管理相机。 首先,订阅鼠标事件,并创建相应的处理程序:

int OnInit()
  {
...
//--- set the timer
   EventSetMillisecondTimer(10);
//--- enable receiving of mouse events: moving and button clicks
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,1);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,1)
//---
   return(INIT_SUCCEEDED);
   }
void OnDeinit(const int reason)
  {
//--- Deleting the timer
   EventKillTimer();
//--- disable the receiving of mouse events
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,0);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,0);
//--- delete the object
   delete ExtAppWindow;
//--- return chart to the usual display mode with price charts
   ChartSetInteger(0,CHART_SHOW,true);
   }
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- chart change event
   if(id==CHARTEVENT_CHART_CHANGE)
      ExtAppWindow.OnChartChange();
//--- mouse movement event
   if(id==CHARTEVENT_MOUSE_MOVE)
      ExtAppWindow.OnMouseMove((int)lparam,(int)dparam,(uint)sparam);
//--- mouse wheel scroll event
   if(id==CHARTEVENT_MOUSE_WHEEL)
      ExtAppWindow.OnMouseWheel(dparam);

在 CCanvas3DWindow 中,创建鼠标移动事件处理程序。 按住鼠标左键并移动鼠标时,它会改变相机的方向角度:

   //+------------------------------------------------------------------+
   //| Handle mouse movements                                           |
   //+------------------------------------------------------------------+
   void              OnMouseMove(int x,int y,uint flags)
     {
      //--- left mouse button
      if((flags&1)==1)
        {
         //--- there is no information about the previous mouse position
         if(m_mouse_x!=-1)
           {
            //--- update the camera angle upon change of position
            m_camera_angles.y+=(x-m_mouse_x)/300.0f;
            m_camera_angles.x+=(y-m_mouse_y)/300.0f;
            //--- set the vertical angle in the range between (-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;
            //--- update camera position
            UpdateCameraPosition();
            }
         //--- save mouse position
         m_mouse_x=x;
         m_mouse_y=y;
         }
      else
        {
         //--- reset the saved position if the left mouse button is not pressed
         m_mouse_x=-1;
         m_mouse_y=-1;
         }
      }

这是鼠标滚轮事件的处理程序,其内更改相机和场景中心之间的距离:

   //+------------------------------------------------------------------+
   //| Handling mouse wheel events                                      |
   //+------------------------------------------------------------------+
   void              OnMouseWheel(double delta)
     {
      //--- update the distance between the camera and the center upon a mouse scroll
      m_camera_distance*=1.0-delta*0.001;
      //--- set the distance in the range between [3,50]
      if(m_camera_distance>50.0)
         m_camera_distance=50.0;
      if(m_camera_distance<3.0)
         m_camera_distance=3.0;
      //--- update camera position
      UpdateCameraPosition();
      }

两个处理程序均调用 UpdateCameraPosition() 方法,从而根据更新后的参数变更相机位置:

   //+------------------------------------------------------------------+
   //| Updates the camera position                                      |
   //+------------------------------------------------------------------+
   void              UpdateCameraPosition(void)
     {
      //--- the position of the camera taking into account the distance to the center of coordinates
      DXVector4 camera=DXVector4(0.0f,0.0f,-(float)m_camera_distance,1.0f);
      //--- camera rotation around the X axis
      DXMatrix rotation;
      DXMatrixRotationX(rotation,m_camera_angles.x);
      DXVec4Transform(camera,camera,rotation);
      //--- camera rotation around the Y axis
      DXMatrixRotationY(rotation,m_camera_angles.y);
      DXVec4Transform(camera,camera,rotation);
      //--- set camera to position
      m_canvas.ViewPositionSet(DXVector3(camera));
      }

源代码位于下面的 “Step8 Mouse Control.mq5” 文件之中。

用鼠标控制相机位置

利用鼠标控制相机位置。


应用纹理

纹理是应用于多边形表面,呈现图案或材质的位图图像。 利用纹理可在表面上复现小物体,若是我们用多边形来创建它们,则会需要更多资源。 例如,这可以是对石头、木材、土壤和其他材质的模仿。

CDXMesh 及其子类允许指定纹理。 在标准像素着色器中,纹理会与 DiffuseColor 一起使用。 删除对象动画,并应用石头纹理。 它应该位于终端工作目录的 MQL5\Files 文件夹当中:

   virtual bool      Create(const int width,const int height)
     {
  ...
      //--- set the white color for the non-directional lighting
      m_box.DiffuseColorSet(DXColor(1.0,1.0,1.0,1.0));

      //--- add texture to draw the cube faces
      m_box.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- add the cube to the scene
      m_canvas.ObjectAdd(&m_box);
      //--- redraw the scene
      Redraw();
      //--- succeed
      return(true);
      }

石头材质的立方体

石头材质的立方体。


创建自定义物体

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


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

struct DXVertex
  {
   DXVector4         position;  // vertex coordinates
   DXVector4         normal;    // normal vector
   DXVector2         tcoord;    // face coordinate to apply the texture
   DXColor           vcolor;    // color
  };


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

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

   virtual bool      Create(const int width,const int height)
     {
 ...     
      // --- vertices and indexes for manually created objects
      DXVertex vertices[];
      uint indices[];
      //--- prepare vertices and indices for the sphere
      if(!DXComputeSphere(0.3f,50,vertices,indices))
         return(false);
      //--- set white color for the vertices
      DXColor white=DXColor(1.0f,1.0f,1.0f,1.0f);
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- create the sphere object
      if(!m_sphere.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- set diffuse color for the sphere
      m_sphere.DiffuseColorSet(DXColor(0.0,1.0,0.0,1.0));
      //--- set white specular color
      m_sphere.SpecularColorSet(white);
      m_sphere.TextureSet(m_canvas.DXDispatcher(),"stone.bmp");
      //--- add the sphere to a scene
      m_canvas.ObjectAdd(&m_sphere);
      //--- prepare vertices and indices for the torus
      if(!DXComputeTorus(0.3f,0.1f,50,vertices,indices))
         return(false);
      //--- set white color for the vertices
      for(int i=0; i<ArraySize(vertices); i++)
         vertices[i].vcolor=white;
      //--- create the torus object
      if(!m_torus.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),vertices,indices))
        {
         m_canvas.Destroy();
         return(false);
         }
      //--- set diffuse color for the 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");
      //--- add the torus to a scene
      m_canvas.ObjectAdd(&m_torus);      
      //--- redraw the scene
      Redraw();
      //--- succeed
      return(true);
      }

为新物体添加动画:

   void              OnTimer(void)
     {
...
      m_canvas.LightDirectionSet(light_direction);
      //--- sphere orbit
      DXMatrix translation;
      DXMatrixTranslation(translation,1.1f,0,0);
      DXMatrixRotationY(rotation,time);
      DXMatrix transform;
      DXMatrixMultiply(transform,translation,rotation);
      m_sphere.TransformMatrixSet(transform);
      //--- torus orbit with rotation around its axis
      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);           
      //--- recalculate the 3D scene and draw it in the canvas
      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)
     {
...
      //--- prepare an array to store data
      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;
      //--- create a surface object
      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);
         }
      //--- create texture and reflection
      m_surface.SpecularColorSet(DXColor(1.0,1.0,1.0,1.0));
      m_surface.TextureSet(m_canvas.DXDispatcher(),"checker.bmp");
      //--- add the surface to the scene
      m_canvas.ObjectAdd(&m_surface);
      //--- succeed
      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;
      //--- get the current time
      ulong current_time=GetMicrosecondCount();
      //--- calculate the delta
      float deltatime=(current_time-last_time)/1000000.0f;
      if(deltatime>0.1f)
         deltatime=0.1f;
      //--- increase the elapsed time value
      time+=deltatime;
      //--- remember the time
      last_time=current_time;
      //--- calculate surface values taking into account time changes
      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);
            }
         }
      //--- update data to draw the surface
      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))
        {
         //--- recalculate the 3D scene and draw it in the canvas
         Redraw();
         }
      }

源代码在 3D Surface.mq5 中提供,该程序的示例展示在视频当中。




在本文中,我们研究为了进行直观数据分析,利用 DirectX 函数创建简单几何图形和 3D 动画的功能。 可以在 MetaTrader 5 终端安装目录中找到更复杂的示例:智能交易系统 “Correlation Matrix 3D” 和 “Math 3D Morpher”,以及 “Remnant 3D” 脚本。 

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

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


全部回复

0/140

量化课程

    移动端课程