如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形(基础篇)
「在 MT5 里直接跑 DirectX 三维图形」
MetaTrader 5 的 MQL5 环境能直接调用 DirectX 函数,在终端窗口内生成可交互的 3D 计算机图形,不需要借助外部程序。平面显示器上要做出三维物体的深度错觉,核心是靠二维图像随物体和观察者位置实时变化——旋转、缩放、光照都得跟着算。 硬前提:显卡必须支持 DX 11 和 Shader Model 5.0,否则相关接口调起来直接无效。这条先查自己机器再写代码,别等渲染黑屏才回头翻驱动。 从建模到出图,完整链路大致是:物体造型 → 场景计算与渲染 → 绕 Z 轴和视角旋转 → 相机位置管理 → 颜色与光照 → 动画与运动 → 鼠标控相机 → 贴纹理 → 自定义物体 → 基于数据的 3D 表面。每一环都有对应的 DX 接口可接,后面小节会逐个拆。 外汇和贵金属行情波动剧烈、杠杆风险高,用 3D 做盘面可视化只是辅助观察,别把它当成信号过滤器。
从多边形网模理解三维建模的代价
想在二维屏幕上画出三维物体,第一步是拿到物体在 X、Y、Z 三个轴上的坐标模型。理想状态是定义表面上无数个点,缩放时不会失真;但实际工程里,3D 建模是用多边形拼成的网模来近似——端点越多网模越细,看着越真,代价是渲染和计算的资源消耗同步上涨。 早期显卡算力弱,把多边形切成三角形成了刚需。三角形能精确标定一小块表面的位置,也方便算光照和反射;一堆小三角形凑起来就是逼真的三维像。后文里多边形和三角形基本同义,因为想三角形比想 N 边形直观得多。 建模型就是按每个三角形顶点坐标来定物体,这样物体挪动或视角变了,坐标还能接着算。顶点、连线、表面之外,靠线性代数能从三角形位置推出切面法线(垂直表面的向量),光照和反射就靠它。 拓扑学管的是多边形怎么织成 3D 网模。好拓扑用最少多边形把对象描清楚,后续移动旋转都轻快。球面模型两种拓扑摆一起,面数差很多。最终 3D 图形的目的很硬核:算准每个点位置、算明光线明暗、丢到屏幕上。外汇贵金属图表里的 3D 辅助视图若跑得卡,多半是网模拓扑没优化,高风险环境下别为好看牺牲帧率。
◍ 用两个对角点拉出一个立方体
在 MT5 里做 3D 可视化,第一步永远是建画布,而不是急着画物体。CCanvas3D 类负责把渲染表面挂到图表上,之后所有投影、裁剪、光照都基于这块画布的参数跑。 投影矩阵定下三件事:视角 30 度(M_PI/6)、宽高比按画布尺寸算、近剪切面 0.1f 远剪切面 100.f。只有落在这两堵虚拟墙之间、且水平张角小于 30 度的几何体才会进管线,坐标绝对值无意义,相对距离才决定显示比例。 立方体由 CDXBox 承载,只需传两个对角顶点就能生成。调试跟踪 DXComputeBox() 能看到:8 个逻辑顶点被展开成 24 个带法线的顶点(每个面独立法线集合供光照用),6 个面切成 12 个三角形,索引数组存 36 项。纹理坐标按面四角绑定,不贴图时可不填。 下面这段代码是窗体骨架与 Create 的前半段,注意 ProjectionMatrixSet 的 0.1f/100.f 和立方体对角 ( -1,-1,5 ) 到 ( 1,1,7 )——z 方向 5~7 正好夹在裁剪面内,改到 101 就整块消失。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 应用窗体 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CCanvas3DWindow { <span class="keyword">class="kw">protected</span>: CCanvas3D m_canvas; <span class="comment">class=class="str">"cmt">//--- 画布尺寸</span> <span class="keyword">class="type">int</span> m_width; <span class="keyword">class="type">int</span> m_height; <span class="comment">class=class="str">"cmt">//--- 立方体</span> CDXBox m_box; <span class="keyword">class="kw">public</span>: CCanvas3DWindow(<span class="keyword">class="type">void</span>) {} ~CCanvas3DWindow(<span class="keyword">class="type">void</span>) {m_box.Shutdown();} <span class="comment">class=class="str">"cmt">//-- 创建场景</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Create(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> width,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> height){} <span class="comment">class=class="str">"cmt">//--- 计算场景</span> <span class="keyword">class="type">void</span> Redraw(){} <span class="comment">class=class="str">"cmt">//--- 响应图表事件</span> <span class="keyword">class="type">void</span> OnChartChange(<span class="keyword">class="type">void</span>) {} }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 创建 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Create(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> width,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> height) { <span class="comment">class=class="str">"cmt">//--- 保存画布维度</span> m_width=width; m_height=height; <span class="comment">class=class="str">"cmt">//--- 为渲染 3D 场景而创建画布</span> <span class="functions">ResetLastError</span>(); <span class="keyword">if</span>(!m_canvas.CreateBitmapLabel(<span class="class="type">class="kw">string">"3D Sample_1"</span>,<span class="number">class="num">0</span>,<span class="number">class="num">0</span>,m_width,m_height,<span class="macro">COLOR_FORMAT_ARGB_NORMALIZE</span>)) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Error creating canvas: "</span>,<span class="functions">GetLastError</span>()); <span class="keyword">class="kw">return</span>(<span class="macro">false</span>); } <span class="comment">class=class="str">"cmt">//--- 设置投影矩阵参数-视角、纵横比、与剪裁面的近端和远端距离</span> m_canvas.ProjectionMatrixSet((<span class="keyword">class="type">float</span>)<span class="macro">M_PI</span>/<span class="number">class="num">6</span>,(<span class="keyword">class="type">float</span>)m_width/m_height,<span class="number">class="num">0.1</span>f,<span class="number">class="num">100.0</span>f); <span class="comment">class=class="str">"cmt">//--- 创建立方体 - 将立方体的两个对角坐标、资源管理器、场景参数传递给它</span> <span class="keyword">if</span>(!m_box.Create(m_canvas.DXDispatcher(),m_canvas.InputScene(),DXVector3(-<span class="number">class="num">1.0</span>,-<span class="number">class="num">1.0</span>,<span class="number">class="num">5.0</span>),DXVector3(<span class="number">class="num">1.0</span>,<span class="number">class="num">1.0</span>,<span class="number">class="num">7.0</span>)))
「立方体法线与纹理坐标的硬编码细节」
这段片段出现在自定义 3D 场景的构建函数尾部,先处理失败回退:若画布对象销毁则直接返回 false,避免空指针继续跑后续逻辑。 成功路径上,m_canvas.ObjectAdd 把立方体实例塞进场景,紧接 Redraw 触发一次重绘,最后返回 true 表示初始化完成。 法线赋值只针对索引 20 到 23 的顶点,统一设成 (0.0,-1.0,0.0,0.0),也就是立方体底面向下的归一化方向,其余面法线应在前面循环里已填好。 纹理坐标按面遍历:每个面 4 个顶点依次映射到 (0,0)、(1,0)、(1,1)、(0,1),覆盖整张贴图,faces 变量决定循环次数,顶点缓冲需按 4 的倍数偏移。 在 MT5 里把这段直接贴进你的 CCube 类,改 faces 数量就能验证不同细分下 UV 是否错位,外汇贵金属行情面板做立体化时这种底模最容易出黑白面。
{
m_canvas.Destroy();
class="kw">return(false);
}
class=class="str">"cmt">//--- 在场景里加入立方体
m_canvas.ObjectAdd(&m_box);
class=class="str">"cmt">//--- 刷新场景
Redraw();
class=class="str">"cmt">//--- 成功
class="kw">return(true);
}
for(class="type">int i=class="num">20; i<class="num">24; i++)
vertices[i].normal=DXVector4(class="num">0.0,-class="num">1.0,class="num">0.0,class="num">0.0);
class=class="str">"cmt">//--- 纹理坐标
for(class="type">int i=class="num">0; i<faces; i++)
{
vertices[i*class="num">4+class="num">0].tcoord=DXVector2(class="num">0.0f,class="num">0.0f);
vertices[i*class="num">4+class="num">1].tcoord=DXVector2(class="num">1.0f,class="num">0.0f);
vertices[i*class="num">4+class="num">2].tcoord=DXVector2(class="num">1.0f,class="num">1.0f);
vertices[i*class="num">4+class="num">3].tcoord=DXVector2(class="num">0.0f,class="num">1.0f);
}3D 场景的逐帧计算与重绘触发
切换 3D 场景时必须把所有计算重跑一遍:先在全局坐标系求每个物体中心,再算各顶点位置,确定像素深度与视野可见性,按顶点插值出多边形上的像素坐标,套纹理上色,算光照方向与反射,叠散射光,最后把全局坐标经相机坐标转到投影矩阵。这一串都在 CCanvas3D 的 Render 方法里完成,之后调 Update 把结果从投影矩阵搬到画布。 示例里的白色立方体只建一次、不再改几何。所以正常情况不用反复渲染,只有图表尺寸变了——比如你拖大拖小窗口——才需要重设画布尺寸、重置投影矩阵并刷新图像。 跑 Step1 Create Box.mq5,黑底上会看到一个白方块,默认物体色是白、光照还没设。坐标系是左手系:X 右、Y 上、Z 向场景里。立方体中心在 (0,0,6),正对默认视点;想挪观察点就用 ViewPositionSet() 显式写坐标。按 Escape 退出。 下面这段是图表变化事件里的关键重绘逻辑,左手系近远裁剪面设的是 0.1 到 100.0,视角 π/6(30°),可照着改参数验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 刷新场景 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Redraw() { class=class="str">"cmt">//--- 计算 3D 场景 m_canvas.Render(DX_CLEAR_COLOR|DX_CLEAR_DEPTH,ColorToARGB(clrBlack)); class=class="str">"cmt">//--- 根据当前场景更新画布上的图片 m_canvas.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理图表变化事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartChange(class="type">void) { class=class="str">"cmt">//--- 获取当前图标尺寸 class="type">int w=(class="type">int)ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); class="type">int h=(class="type">int)ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS); class=class="str">"cmt">//--- 根据图表尺寸更新画布尺寸 if(w!=m_width || h!=m_height) { m_width =w; m_height=h; class=class="str">"cmt">//--- 调整画布尺寸 m_canvas.Resize(w,h); DXContextSetSize(m_canvas.DXContext(),w,h); class=class="str">"cmt">//--- 根据画布尺寸更新投影矩阵 m_canvas.ProjectionMatrixSet((class="type">float)M_PI/class="num">6,(class="type">float)m_width/m_height,class="num">0.1f,class="num">100.0f); class=class="str">"cmt">//--- 重新计算 3D 场景,并在画布上渲染 Redraw(); } }
◍ 用计时器驱动 Z 轴旋转看透视变形
想让 3D 场景里的立方体动起来,靠的是 MT5 的 OnTimer 事件而不是主线程循环。每次计时器触发,就按时间增量累加一个角度,再用 DXMatrixRotationZ() 生成绕 Z 轴的旋转矩阵,交给 TransformMatrixSet() 改写立方体姿态,最后 Redraw() 刷新画布——跑起来能看到一个白色方块在逆时针转。 代码里角度累加用的是秒级 delta:GetMicrosecondCount() 取微秒,除以 1000000.0 转成秒,并钳制在 0.1 秒上限,避免卡顿后跳变。DXMatrixRotationZ(rotation,angle) 这行就是核心,angle 持续增大,立方体就持续绕 Z 轴转。 视角参数设成了 M_PI/5(约 36°),比前一个示例的 M_PI/6(约 30°)更宽。投影矩阵里视角越小物体看起来越大,类似望远镜效果:本例视角变大后,立方体在视觉上反而显小,边框占的画面更少。近端 0.1f、远端 100.0f 的剪裁距离没动,验证时改 m_matrix_view_angle 就能直接看到透视收缩变化。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器响应 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer(class="type">void) { class=class="str">"cmt">//--- 计算旋转角度的变量 class="kw">static class="type">class="kw">ulong last_time=class="num">0; class="kw">static class="type">float angle=class="num">0; class=class="str">"cmt">//--- 获取当前时间 class="type">class="kw">ulong current_time=GetMicrosecondCount(); class=class="str">"cmt">//--- 计算增量 class="type">float deltatime=(current_time-last_time)/class="num">1000000.0f; if(deltatime>class="num">0.1f) deltatime=class="num">0.1f; class=class="str">"cmt">//--- 立方体绕 Z 轴的旋转角度递增 angle+=deltatime; class=class="str">"cmt">//--- 记住时间 last_time=current_time; class=class="str">"cmt">//--- 设置立方体绕 Z 轴的旋转角度 DXMatrix rotation; DXMatrixRotationZ(rotation,angle); m_box.TransformMatrixSet(rotation); class=class="str">"cmt">//--- 重新计算 3D 场景,并在画布上渲染 Redraw(); } class=class="str">"cmt">//--- 设置投影矩阵参数-视角、纵横比、与剪裁面的近端和远端距离 m_matrix_view_angle=(class="type">float)M_PI/class="num">5; m_canvas.ProjectionMatrixSet(m_matrix_view_angle,(class="type">float)m_width/m_height,class="num">0.1f,class="num">100.0f); class=class="str">"cmt">//--- 创建立方体 - 将立方体的两个对角坐标、资源管理器、场景参数传递给它