DirectX 教程(第一部分):绘制第一个三角形·进阶篇
(2/3)· 从 3D 图形概念到 MQL 动作序列,拆解 MT5 隐藏例程后的真实绘制链路
很多交易者想在 MT5 图表上画点自定义图形,一碰到 DirectX 就卡在初始化和结构填充上。MetaQuotes 把底层例程藏了,反而让人看不清三角形到底怎么跑到屏幕上的。这篇接着基础篇,直接进图形管道和 MQL 调用序列。
GPU 只认三角形和顶点属性
现代显卡的强项就是快速光栅化海量三角形。3D 建模软件里画的是矩形面,但到了图形 API 这一层,全部被拆成三角形——因为三点定义一个平面,硬件流水线只吃这套图元。 在 Direct3D 里,一个三角形必须给满三个顶点。顶点不只是空间坐标,还能附带颜色、纹理坐标和法线;光栅化时 XY 坐标必须落在 [-1,1],Z 在 [0,1],矩阵变换通常就为了把这个规范掉。 颜色本质是 RGB 三分量,对应人眼视锥细胞和屏幕子像素的结构。MQL5 里用 ColorToARGB 把 web 色转成带透明通道的 ARGB;32 位色下每个分量取 0–255(即 2^8-1),现今多数显示器就是 32 位色深。想验证?在 MT5 里调一下 ColorToARGB 看返回值的高字节是不是透明度。
◍ DirectX 显图的动作链怎么排
在 MQL5 里走 DirectX 渲染,不是调一个函数就出图,而是一条固定顺序的动作链。先 ObjectCreate 建位图标签,再 ResourceCreate 开动态资源,用 ObjectSetString 带 OBJPROP_BMPFILE 把资源和对象绑上,这一步漏了图表上什么都不会显示。 着色器侧要自己写 HLSL 顶点和像素着色器,用 #resource 引进来,按 DXVertexLayout 数组描述顶点格式。随后 DXContextCreate 建上下文,分别用 DXShaderCreate 配 DX_SHADER_VERTEX / DX_SHADER_PIXEL 建两种着色器,DXBufferCreate 配 DX_BUFFER_VERTEX(索引可选 DX_BUFFER_INDEX)开缓冲区。 真正下命令前还有几道绑定:DXShaderSetLayout 传顶点格式、DXPrimiveTopologySet 设拓扑、DXShaderSet 绑着色器、DXBufferSet 绑缓冲。清完深度缓冲(必要时清颜色缓冲)后,DXDraw 或 DXDrawIndexed 发渲染,DXContextGetColors 取回结果,再 ResourceCreate 更新资源,ChartRedraw 刷新图表。 收尾容易被忽略:DXRelease 释放、ResourceFree 删资源、ObjectDelete 删对象,不清理会吃显存。整条链 20 步左右,顺序错了 MT5 大概率黑屏或报错,建议开 MT5 按这个次序抄一遍验证。
「在 MT5 里搭一个 DirectX 画布类」
用 DirectX 在 MT5 做自定义渲染,核心是把「画布 — 设备 — 着色器 — 资源释放」封装进一个类。下面这个类把画布宽高、图像数组、图形上下文句柄和三个 DirectX 句柄(顶点着色器、像素着色器、顶点缓冲区)都收在私有成员里,初始化时若宽度或高度小于 1 会被强制设为 500 像素,顶点数组长度必须为 3,且每个顶点的 X/Y 须在 [-1,1]、Z 须为 0、W 须为 1,否则当场重置。 构造器只把四个 DirectX 句柄置 0,析构器直接调 Deinit() 释放;对外只暴露 Init() 和 Draw()。Init() 收顶点数组和可选宽高,校验完后依次调 InitCanvas() 与 InitDevice(),后者需要一张含 3 个 float4 顶点的数组作为输入,返回 bool 表示进度。 InitCanvas() 会建一个名为 DXTutorialCanvas 的位图标签对象,从左上角右移、下移各 100 像素,再绑一个叫 ::DXTutorialResource 的动态图形资源,数组用黑色(ARGB)填充。InitDevice() 才真正建图形上下文、编译两个着色器、建顶点缓冲区,并把图元类型设成三角形列表。 DirectX 不自动回收,Deinit() 里要手动 DXRelease 四个句柄,再用 ResourceFree 删动态资源、ObjectDelete 删标签对象。开 MT5 新建脚本把下面类贴进去,配一份 3 顶点数组就能跑通最薄的一层渲染管线。
class DXTutorial { class="kw">private: class="type">int m_width; class="type">int m_height; class="type">uint m_image[]; class="type">class="kw">string m_canvas; class="type">class="kw">string m_resource; class="type">int m_dx_context; class="type">int m_dx_vertex_shader; class="type">int m_dx_pixel_shader; class="type">int m_dx_buffer; class="type">bool InitCanvas(); class="type">bool InitDevice(float4 &vertex[]); class="type">void Deinit(); class="kw">public: class="type">void DXTutorial() { m_dx_context = class="num">0; m_dx_vertex_shader = class="num">0; m_dx_pixel_shader = class="num">0; m_dx_buffer = class="num">0; } class="type">void ~DXTutorial() { Deinit(); } class="type">bool Init(float4 &vertex[], class="type">int width, class="type">int height); class="type">bool Draw(); };
三角形顶点与画布的初始化校验
在 MT5 里用 DirectX 画一个基础三角形,第一步是把顶点数据塞进 float4 结构数组。下面这段定义了 3 个顶点,x/y 落在 [-1,1] 的裁剪空间,z 固定为 0,w 统一为 1,正好构成一个居中等边三角形雏形。 Init 函数先兜底画布尺寸:width 或 height 传进来 ≤0 时,强制回退成 500×500 并打印警告,避免后续创建位图标签时因零面积崩掉。顶点数组长度必须严格等于 3,否则直接返回 false,因为三角形就靠这 3 个点。 循环里逐点修正:w 不是 1 就改成 1.0f,z 不是 0 就改成 0.0f,并各自打印警告;一旦 x 或 y 绝对值超过 1,立即报错返回 false——裁剪空间坐标越界 GPU 不会给你面子。 InitCanvas 负责挂 OBJ_BITMAP_LABEL 对象,名字写死成 DXTutorialCanvas,资源路径指向 ::DXTutorialResource。面积用 m_width*m_height 算,若 ObjectCreate 返回失败就打印错误并退出。开 MT5 新建 EA 把这段抄进去,改改顶点坐标就能肉眼验证画布有没有起来。
class="kw">struct float4 { class="type">float x; class="type">float y; class="type">float z; class="type">float w; }; float4 vertex[class="num">3] = {{-class="num">0.5f, -class="num">0.5f, class="num">0.0f, class="num">1.0f}, {class="num">0.0f, class="num">0.5f, class="num">0.0f, class="num">1.0f}, {class="num">0.5f, -class="num">0.5f, class="num">0.0f, class="num">1.0f}}; class="type">bool DXTutorial::Init(float4 &vertex[], class="type">int width = class="num">500, class="type">int height = class="num">500) { if(width <= class="num">0) { m_width = class="num">500; Print("Warning: width changed to class="num">500"); } else { m_width = width; } if(height <= class="num">0) { m_height = class="num">500; Print("Warning: height changed to class="num">500"); } else { m_height = height; } if(ArraySize(vertex) != class="num">3) { Print("Error: class="num">3 vertex are needed for a triangle"); class="kw">return(false); } for(class="type">int i = class="num">0; i < class="num">3; i++) { if(vertex[i].w != class="num">1) { vertex[i].w = class="num">1.0f; Print("Warning: vertex.w changed to class="num">1"); } if(vertex[i].z != class="num">0) { vertex[i].z = class="num">0.0f; Print("Warning: vertex.z changed to class="num">0"); } if(fabs(vertex[i].x) > class="num">1 || fabs(vertex[i].y) > class="num">1) { Print("Error: vertex coordinates must be in the range [-class="num">1;class="num">1]"); class="kw">return(false); } } ResetLastError(); if(!InitCanvas()) { class="kw">return(false); } if(!InitDevice(vertex)) { class="kw">return(false); } class="kw">return(true); } class="type">bool DXTutorial::InitCanvas() { m_canvas = "DXTutorialCanvas"; m_resource = "::DXTutorialResource"; class="type">int area = m_width * m_height; if(!ObjectCreate(class="num">0, m_canvas, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0)) { Print("Error: failed to create an object to draw"); class="kw">return(false); }
◍ 把画布与顶点缓冲真正挂上显卡
在 MT5 里用 DirectX 画自定义图形,第一步是把 OBJ_BITMAP_LABEL 对象摆到图表坐标 (100,100) 的位置,再把一块 ARGB 像素数组绑成位图资源。下面这段把对象位移、数组尺寸、资源创建串成一条链,任何一步返回 false 都会中断初始化。 if(!ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)) { Print("Warning: failed to move the object horizontally"); } if(!ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)) { Print("Warning: failed to move the object vertically"); } if(ArrayResize(m_image, area) != area) { Print("Error: failed to resize the array for the graphical resource"); return(false); } if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area) { Print("Warning: failed to initialize array for graphical resource"); } if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Error: failed to create a resource to draw"); return(false); } if(!ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)) { Print("Error: failed to bind resource to object"); return(false); } return(true); } m_canvas = "DXTutorialCanvas"; m_resource = "::DXTutorialResource"; int area = m_width * m_height; ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0) ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100) ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100) ArrayResize(m_image, area) ArrayInitialize(m_image, ColorToARGB(clrBlack)) ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE) ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource) 逐行拆一下:前两个 if 把画布标签推到距图表左上角各 100 像素处,失败仅打印警告不中断;ArrayResize 按 area=m_width*m_height 开像素缓冲,尺寸不符直接 return(false);ArrayInitialize 填黑底,失败也只是警告。ResourceCreate 用 COLOR_FORMAT_ARGB_NORMALIZE 把数组注册成名称为 ::DXTutorialResource 的位图资源,最后 ObjectSetString 把资源和标签绑定,任一步失败都退出。 顶点布局与上下文则要另起炉灶。InitDevice 里先声明只有 POSITION 语义、用 R32G32B32A32_FLOAT 的单属性布局,再 DXContextCreate 拿上下文句柄,无效就带 GetLastError 退场;接着编译顶点着色器 "VShader",若句柄无效除了报错还会把 shader_error 文本打出来,方便你定位 HLSL 拼写。外汇与贵金属图表叠加这类自绘层属高风险试验,上下文创建失败往往意味着显卡特性不支持,先在模拟环境跑通再上真仓。
if(!ObjectSetInteger(class="num">0, m_canvas, OBJPROP_XDISTANCE, class="num">100)) { Print("Warning: failed to move the object horizontally"); } if(!ObjectSetInteger(class="num">0, m_canvas, OBJPROP_YDISTANCE, class="num">100)) { Print("Warning: failed to move the object vertically"); } if(ArrayResize(m_image, area) != area) { Print("Error: failed to resize the array for the graphical resource"); class="kw">return(false); } if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area) { Print("Warning: failed to initialize array for graphical resource"); } if(!ResourceCreate(m_resource, m_image, m_width, m_height, class="num">0, class="num">0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Error: failed to create a resource to draw"); class="kw">return(false); } if(!ObjectSetString(class="num">0, m_canvas, OBJPROP_BMPFILE, m_resource)) { Print("Error: failed to bind resource to object"); class="kw">return(false); } class="kw">return(true); } m_canvas = "DXTutorialCanvas"; m_resource = "::DXTutorialResource"; class="type">int area = m_width * m_height; ObjectCreate(class="num">0, m_canvas, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0) ObjectSetInteger(class="num">0, m_canvas, OBJPROP_XDISTANCE, class="num">100) ObjectSetInteger(class="num">0, m_canvas, OBJPROP_YDISTANCE, class="num">100) ArrayResize(m_image, area) ArrayInitialize(m_image, ColorToARGB(clrBlack)) ResourceCreate(m_resource, m_image, m_width, m_height, class="num">0, class="num">0, m_width, COLOR_FORMAT_ARGB_NORMALIZE) ObjectSetString(class="num">0, m_canvas, OBJPROP_BMPFILE, m_resource) class="type">bool DXTutorial::InitDevice(float4 &vertex[]) { DXVertexLayout layout[class="num">1] = {{"POSITION", class="num">0, DX_FORMAT_R32G32B32A32_FLOAT }}; class="type">class="kw">string shader_error = ""; m_dx_context = DXContextCreate(m_width, m_height); if(m_dx_context == INVALID_HANDLE) { Print("Error: failed to create graphics context: ", GetLastError()); class="kw">return(false); } m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error); if(m_dx_vertex_shader == INVALID_HANDLE) { Print("Error: failed to create vertex shader: ", GetLastError()); Print("Shader compilation error: ", shader_error); class="kw">return(false); }