English 中文 Español Deutsch 日本語 Português
preview
Уроки по DirectX (Часть I): Рисуем первый треугольник

Уроки по DirectX (Часть I): Рисуем первый треугольник

MetaTrader 5Интеграция | 2 марта 2022, 07:48
2 774 4
Rorschach
Rorschach

Содержание

  1. Введение
  2. DirectX API
    1. История DirectX
    2. Direct3D
    3. Device
    4. Device Context
    5. Swap Chain
    6. Input Layout
    7. Primitive Topology
    8. HLSL
  3. Графический пайплайн
  4. 3D графика
    1. Примитивы
    2. Вертексы
    3. Цвет
  5. Последовательность действий в MQL
  6. Практика
    1. Обзор класса
    2. Массив вершин
    3. Инициализация
    4. Создание холста для рисования
    5. Инициализация DirectX
    6. Вывод изображения
    7. Освобождение ресурсов
    8. Шейдеры
    9. OnStart
  7. Заключение
  8. Список литературы и ссылки

Введение

Обряд инициации или, как мне больше нравится, обряд инициализации. Именно такое словосочетание приходит на ум, когда видишь, сколько нужно написать кода и заполнить огромных структур в C++, чтобы вывести хотя бы примитивный треугольник с помощью DirectX. Не говоря уже о более сложных вещах, таких как: текстуры, матрицы преобразования, тени и тому подобное. К счастью, в MetaQuotes позаботились об этом: скрыли всю рутину, оставили нам только действительно необходимые функции. Но из-за этого у человека, не знакомого раньше с DirectX, возникают другие проблемы: нет целостности картины, непонятно что, за чем и как происходит. Код, который нужно написать на MQL, все еще обладает избыточностью.

Без понимания происходящего под капотом у DirectX появляется недоумение: "К чему такие сложности, зачем так запутано, неужели нельзя сделать проще?". И это только первый этап. Изучение языка шейдеров HLSL и особенностей программирования видеокарт никто не отменял. Чтобы не возникало всех этих недоумений, предлагаю в статье, без особого фанатизма, рассмотреть внутреннее устройство DirectX, а затем написать на MQL небольшой скрипт, выводящий на экран треугольник.


DirectX API

История DirectX

DirectX — это набор API (программный интерфейс приложения) для работы с мультимедиа и видео на платформах от Microsoft. В первую очередь был разработан для создания игр, но со временем стал использоваться в инженерном и математическом программном обеспечении. DirectX позволяет работать с графикой, звуком, вводом, сетью без необходимости обращаться к низкоуровневым функциям. API появилось как альтернатива кроссплатформенной OpenGL. При создании Windows 95 были внедрены довольно крупные изменения, которые могли повлиять на популярность будущей операционной системы у создателей программ и игр. Чтобы облегчить разработку под новую ОС, был создан DirectX. У основ DirectX стояли Крэйг Айслер, Алекс Сэйнт Джон и Эрик Энгстром.

  • Сентябрь 1995 года. Первый выпуск. Это была довольно примитивная версия, по большей части надстройка над Windows API. Она не получила особого внимания. В тренде был DOS, по сравнению с которым, у новой ОС были повышенные системные требования. К тому же уже существовал OpenGL. Не было уверенности, что Microsoft будет дальше поддерживать DirectX.
  • Июнь 1996 года. Вышла вторая версия.
  • Сентябрь 1996 года. Третья версия.
  • Август 1997 года. Четвертая версия так и не вышла, сразу  появилась пятая. Под нее стало легче писать код, и она получила некоторое внимание со стороны программистов.
  • Август 1998 года. Шестая версия. Работа была еще больше упрощена.
  • Сентябрь 1999 года. Седьмая версия. Появилась возможность создавать вершинные буферы в видеопамяти, что стало большим преимуществом по сравнению с OpenGL.
  • Ноябрь 2000 года. Восьмая версия. Переломный момент. До этого DirectX был в роли догоняющего, но в 8 версии настиг индустрию. Microsoft стала сотрудничать с производителями видеокарт. Появились вершинные и пиксельные шейдеры. Для разработки было достаточно персонального компьютера, в отличии от OpenGL, для которой требовалась рабочая станция.
  • Декабрь 2002 года. Девятая версия. DirectX стал стандартом индустрии. Появился язык шейдеров HLSL. Наверно, самая долгоживущая версия DirectX. Как и 775 сокет... Хотя я отвлекся.
  • Ноябрь 2006 года. Десятая версия. В отличие от девятки, была сделана привязка к операционной системе Vista, которая, в свою очередь, не пользовалась популярностью. Эти факторы плохо сказались на успехе десятки. Был добавлен геометрический шейдер.
  • Октябрь 2009 года. Одиннадцатая версия. Добавлена тесселяция, вычислительный шейдер, улучшена работа с многоядерными процессорами.
  • Июль 2015 года. Двенадцатая версия. Низкоуровневое API. Еще лучшая совместимость с многоядерными процессорами, возможность объединить ресурсы нескольких видеокарт от разных вендоров, трассировка лучей.


Direct3D

Direct3D является одним из множества компонентов более крупного DirectX API, отвечает за графику и является посредником между приложениями и драйвером видеокарты. Direct3D основан на COM (объектная модель компонентов). COM — это стандарт двоичного интерфейса (ABI) для программных компонентов, внедренный Microsoft в 1993 году. Он используется для создания объектов при межпроцессном взаимодействии (IPC) в большом количестве языков программирования. COM возник с целью предоставить независимый от языка способ реализации объектов, которые можно использовать вне среды их создания. СОМ допускает повторное использование объектов без знания их внутренней реализации, поскольку они предоставляют четко определенные интерфейсы, отделенные от реализации. Объекты COM несут ответственность за собственное создание и уничтожение с помощью подсчета ссылок.

interfaces

Интерфейсы


Device

Все в Direct3D начинается с Device (устройство). Создание ресурсов (буферы, текстуры, шейдеры, объекты состояния) и перечисление возможностей графических адаптеров происходят с его помощью. Device представляет собой виртуальный адаптер, расположенный в системе пользователя. Адаптер может быть как реальной видеокартой, так и ее софтверной эмуляцией. Аппаратные устройства используются чаще всего, поскольку они обеспечивают наибольшую производительность. Device предоставляет унифицированный интерфейс для доступа ко всем этим адаптерам и использует их для рендеринга графических изображений на один или несколько выходов.

Direct3D

Device


Device Context

Device Context (контекст устройства) отвечает за все, связанное с рендерингом. Это и конфигурация пайплайна, и создание команд для рендеринга. Device Context появился в одиннадцатой версии DirectX, до этого за рендеринг отвечал Device. Существует два вида контекста: Immediate Context (немедленный контекст) и Deferred Context (отложенный контекст).

Immediate context предоставляет доступ к данным на видеокарте и возможность немедленного выполнения списка команд (command list) на устройстве. Каждый Device имеет только один Immediate Context. Одновременно к нему может иметь доступ только один поток. Для доступа из нескольких потоков нужно использовать синхронизацию.

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


Swap Chain

Swap Chain (цепочка буферов) предназначен для создания одного или более задних (back) буферов. Эти буферы хранят отрендеренные изображения, пока они не будут отображены на дисплее. Работа переднего (front) и одного заднего буферов будет происходить следующим образом. На экране нам показывается передний буфер, в это время идет рендеринг в задний. Затем буферы меняются местами, передний становится задним, задний передним. И весь процесс повторяется по новой. Таким образом, мы все время видим готовую картинку, пока "за кадром" рисуют следующую.

Swapchain

Swap Chain

Device, Device Context и Swap Chain основные компоненты необходимые для рендеринга изображения.


Input Layout

Input layout сообщает пайплайну, какую структуру представляет из себя вершинный буфер. Для наших целей достаточно только координат, поэтому мы можем обойтись без специальной структуры, передав массив вершин типа float4. float4 — это структура, состоящая из четырех переменных типа float.

struct float4
  {
   float x;
   float y;
   float z;
   float w;
  };

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

struct Vertex
  {
   float4 Pos;
   float4 Color0;
   float4 Color1;
  };

Input layout на MQL для нее будет выглядеть следующим образом:

DXVertexLayout layout[3] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT},
                            {"COLOR", 0, DX_FORMAT_R32G32B32A32_FLOAT},
                            {"COLOR", 1, DX_FORMAT_R32G32B32A32_FLOAT}};

Каждый элемент массива layout описывает соответствующий элемент структуры Vertex.

  • Первый элемент структуры DXVertexLayout — это семантическое имя. Оно служит для сопоставления элементов структуры Vertex с элементами структуры в вершинном шейдере. "POSITION" означает, что значение отвечает за координаты, "COLOR" — за цвет.
  • Второй элемент — это семантический индекс. В случае, если нужно передать несколько параметров одного типа, например два значения цвета, мы передаем первый из них с индексом 0, второй с индексом 1.
  • Последний элемент описывает тип, в котором представлено значение в структуре Vertex. DX_FORMAT_R32G32B32A32_FLOAT дословно означает, что это цвет в формате RGBA, представленный 32-битным значением с плавающей точкой для каждой компоненты. Это может сбить с толку. Мы вполне можем использовать этот тип для передачи координат, главное, что он предоставляет информацию о четырех 32-битных значениях с плавающей точкой, как и float4 в структуре Vertex.


Primitive Topology

Вершинный буфер хранит информацию о точках, но мы не знаем как они расположены относительно друг друга в примитиве. Для этого и нужен Primitive Topology. Point List говорит, что в буфере хранятся отдельные точки. Line Strip представляет буфер как последовательно соединенные друг с другом точки, образующие ломаную линию. В Line List каждые две точки описывают одну отдельную линию. Triangle Strip и Triangle List задают порядок точек для треугольников по аналогии с линиями.

Topology

Топология

HLSL

High level shading language (высокоуровневый язык шейдеров) — это С-подобный язык, предназначенный для написания шейдеров. Шейдеры, в свою очередь, программы предназначенные для выполнения на видеокарте. Программирование на всех GPGPU языках очень похоже и имеет свою особенность, связанную с устройством видеокарт. Если у вас есть опыт работы с OpenCL, Cuda или OpenGL вы очень быстро освоите HLSL. Но если вы писали только программы для центральных процессоров, то первое время будет сложно переключиться на новую парадигму. Часто привычные для процессора способы оптимизации не дадут эффекта. Как пример, для процессора будет правильно использовать условный оператор if для избавления от лишних расчетов или для выбора оптимального алгоритма. Но на GPU это может наоборот увеличить время выполнения программы. Чтобы выжать максимум, возможно придется считать количество задействованных регистров. Три главных принципа высокой производительности при программировании видеокарт это: параллельность, пропускная способность и occupancy (нагрузка?).


Графический пайплайн

Пайплайн предназначен для преобразования 3д сцены в 2д представление дисплея. Пайплайн является отражением внутренней структуры видеокарты. Диаграмма ниже показывает как идет поток данных от входа пайплайна к его выходу через все стадии. Овалом обозначены программируемые с помощью языка HLSL стадии — шейдеры, а прямоугольником — фиксированные стадии. Некоторые из них являются опциональными и могут быть пропущены без последствий.

Graphics Pipeline

Графический пайплайн

  • Input Assembler stage — получает данные из вершинного и индексного буферов и подготавливает для вертексного шейдера.

  • Vertex Shader stage — вертексный шейдер. Производит операции над вершинами. Программируемая стадия. Должен обязательно присутствовать в пайплайне.
  • Hull Shader stage — поверхностный шейдер. Отвечает за уровень тесселяции. Программируемая стадия. Не обязателен.
  • Tessellator stage — создает более мелкие примитивы. Фиксированная стадия. Не обязателен.
  • Domain Shader stage — доменный шейдер. Вычисляет конечные значения вершин после тесселяции. Программируемая стадия. Не обязателен.
  • Geometry Shader stage — геометрический шейдер. Применяет различные преобразования к примитивам (точкам, линиям, треугольникам). Программируемая стадия. Не обязателен.
  • Stream Output stage — передает данные в память GPU, откуда они могут быть снова отправлены в пайплайн. Фиксированная стадия. Не обязателен.
  • Rasterizer stage — отсекает все, что не попадает в область видимости, подготавливает данные для пиксельного шейдера. Фиксированная стадия.
  • Pixel Shader stage — пиксельный шейдер. Производит операции с пикселями. Программируемая стадия. Должен обязательно присутствовать в пайплайне.

  • Output Merger stage — формирует окончательное изображение. Фиксированная стадия.

Также стоит упомянуть Compute Shader (DirectCompute), представляющий отдельный пайплайн. Этот шейдер предназначен для вычислений общего назначения, аналог OpenCL и Cuda. Программируемая стадия. Не обязателен.

Реализация DirectX от MetaQuotes не содержит DirectCompute и стадии тесселяции. Таким образом, нам доступны только три шейдера: вершинный, геометрический и пиксельный.


3D графика

Примитивы

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

Mesh

Сетка из треугольников

Вертексы

Для визуализации треугольника в Direct3D необходимо задать три вершины. Может показаться, что вертекс — это положение точки в пространстве, но в Direct3D это нечто большее. Кроме позиции вершин мы можем передать данные о цвете, координаты текстуры, нормали. Забегая вперед, скажу, что обычно используются матричные преобразования для нормализации координат. Но чтобы не усложнять раньше времени, учтем тот факт, что на стадии растеризации по осям X и Y координаты вершин должны быть в пределах [-1;1], по Z от 0 до 1.


Цвет

В компьютерной графике цвет состоит из трех компонент: красный, зеленый и синий. Это обусловлено особенностями строения сетчатки глаза человека. Пиксели монитора так же состоят из трех субпикселей этих цветов. В MQL существует функция ColorToARGB для преобразования веб-цветов в формат ARGB, где кроме цветов хранится информация о прозрачности. Цвет может быть нормализованным, когда компоненты лежат в диапазоне [0;1], и ненормализованным, например, для 32-битного цвета компоненты будут принимать значения от 0 до 255 (2^8-1). Большинство современных мониторов работают именно с 32-битным цветом.


Последовательность действий в MQL

Чтобы вывести изображение с помощью DirectX в MQL нужно следующее:

  1. Создать объект "Графическая метка" или "Рисунок" с помощью ObjectCreate.
  2. Создать динамический графический ресурс с помощью ResourceCreate.
  3. Привязать к объекту ресурс с помощью ObjectSetString с параметром OBJPROP_BMPFILE.
  4. Создать файл для шейдеров (или сохранить шейдеры в переменную типа string).
  5. Написать вершинный и пиксельный шейдеры на языке HLSL.
  6. Подключить файл с шейдерами с помощью #resource "FileName.hlsl" as string variable_name;
  7. Описать формат вершин в массиве типа DXVertexLayout
  8. Создать контекст — DXContextCreate.
  9. Создать вершинный шейдер — DXShaderCreate с параметром DX_SHADER_VERTEX.
  10. Создать пиксельный шейдер — DXShaderCreate с параметром DX_SHADER_PIXEL.
  11. Создать вершинный буфер — DXBufferCreate с параметром DX_BUFFER_VERTEX.
  12. При необходимости создать индексный буфер — DXBufferCreate с параметром DX_BUFFER_INDEX.
  13. Передать информацию о формате вершин — DXShaderSetLayout.
  14. Задать топологию примитивов — DXPrimiveTopologySet.
  15. Привязать вершинный и пиксельный шейдеры — DXShaderSet.
  16. Привязать вершинный (и индексный, если есть) буфер — DXBufferSet.
  17. Очистить буфер глубины — DXContextClearDepth.
  18. При необходимости очистить буфер цвета — DXContextClearColors
  19. Отправить команду на отрисовку — DXDraw (или DXDrawIndexed, если задан индексный буфер)
  20. Передать результат в графический ресурс — DXContextGetColors
  21. Обновить графический ресурс — ResourceCreate
  22. Не забыть обновить график — ChartRedraw
  23. После использования обязательно прибраться за собой — DXRelease
  24. Удалить графический ресурс — ResourceFree
  25. Удалить графический объект — ObjectDelete

Здесь еще остался кто-нибудь? На самом деле все проще, чем кажется. В этом вы убедитесь ниже. И это намного меньше того, что нужно сделать на голых плюсах.


Практика

Обзор класса

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

class DXTutorial
  {
private:
   int               m_width;
   int               m_height;
   uint              m_image[];
   string            m_canvas;
   string            m_resource;

   int               m_dx_context;
   int               m_dx_vertex_shader;
   int               m_dx_pixel_shader;
   int               m_dx_buffer;

   bool              InitCanvas();
   bool              InitDevice(float4 &vertex[]);
   void              Deinit();

public:

   void              DXTutorial() { m_dx_context = 0; m_dx_vertex_shader = 0; m_dx_pixel_shader = 0; m_dx_buffer = 0; }
   void             ~DXTutorial() { Deinit(); }

   bool              Init(float4 &vertex[], int width, int height);
   bool              Draw();
  };

Приватные члены:

  • m_width и m_height — ширина и высота холста. Используются при создании объекта "Графическая метка", динамического графического ресурса и графического контекста. Их значения задаются при инициализации, но также есть возможность установить их значения вручную.
  • m_image — массив, используется при создании графического ресурса. Именно в него передается результат работы DirectX.
  • m_canvas — название графического объекта, m_resource — название графического ресурса. Используются при инициализации и деинициализации.
      Хендлы DirectX:
  • m_dx_context — самый важный, хендл графического контекста. Все операции с DirectX происходят с его участием. Инициализируется при создании графического контекста.
  • m_dx_vertex_shader — хендл вертексного (вершинного) шейдера. Используется при установке разметки вершин, связывании с графическим контекстом, деинициализации. Инициализируется при компиляции вершинного шейдера.
  • m_dx_pixel_shader — хендл пиксельного шейдера. Используется при связывании с графическим контекстом и деинициализации. Инициализируется при компиляции пиксельного шейдера.
  • m_dx_buffer — хендл вершинного буфера. Используется при связывании с графическим контекстом и деинициализации. Инициализируется при создании вершинного буфера.

      Методы инициализации и деинициализации:

  • InitCanvas() — создает холст для вывода изображения. Используются объект "Графическая метка" и динамический графический ресурс. Фон заливается черным цветом. Возвращает статус выполнения операции.
  • InitDevice() — инициализируется DirectX. Создается графический контекст, вертексный и пиксельный шейдеры, вершинный буфер. Задается тип примитивов и разметка вершин. Принимает на вход массив вершин. Возвращает статус выполнения операции.
  • Deinit() — очищает использованные ресурсы. Удаляются графический контекст, вертексный и пиксельный шейдеры, вершинный буфер, объект "Графическая метка", динамический графический ресурс.

Публичные члены:

  • DXTutorial() — конструктор. Устанавливаются в 0 хендлы DirectX.
  • ~DXTutorial() — деструктор. Вызывается метод Deinit().
  • Init() — подготовка к работе. Принимает на вход массив вершин и необязательные высоту и ширину. Проверяет полученные данные на корректность, вызывает InitCanvas() и InitDevice(). Возвращает статус выполнения операции.
  • Draw() — выводит изображение на экран. Очищает буферы цвета и глубины, выводит изображение в графический ресурс. Возвращает статус выполнения операции.


Массив вершин

Так как вертексы содержат только информацию о координатах, то для простоты будем использовать структуру, содержащую 4 переменные float типа. X, Y, Z координаты в трехмерном пространстве, W — вспомогательная константа, должна быть равна 1, нужна для матричных операций.

struct float4
  {
   float             x;
   float             y;
   float             z;
   float             w;
  };

Для треугольника нужно 3 вершины, поэтому используем массив размером 3.

float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}};


Инициализация

Передаем в объект массив вершин и размер холста. Проверяем входные данные. Если переданные ширина или высота меньше единицы, тогда параметр устанавливается в 500 пикселей. Размер массива вершин должен быть равен 3. Далее в цикле проверяем каждую вершину. Координаты X и Y должны быть в диапазоне [-1;1], Z должна быть равна 0, она принудительно сбрасывается в это значение. W должна быть 1, тоже принудительно сбрасывается. Вызываем функции инициализации холста и DirectX.

bool DXTutorial::Init(float4 &vertex[], int width = 500, int height = 500)
  {
   if(width <= 0)
     {
      m_width = 500;
      Print("Предупреждение, ширина изменена на 500");
     }
   else
     {
      m_width = width;
     }

   if(height <= 0)
     {
      m_height = 500;
      Print("Предупреждение, высота изменена на 500");
     }
   else
     {
      m_height = height;
     }

   if(ArraySize(vertex) != 3)
     {
      Print("Ошибка, нужно 3 вершины для треугольника");
      return(false);
     }

   for(int i = 0; i < 3; i++)
     {
      if(vertex[i].w != 1)
        {
         vertex[i].w = 1.0f;
         Print("Предупреждение, vertex.w изменено на 1");
        }

      if(vertex[i].z != 0)
        {
         vertex[i].z = 0.0f;
         Print("Предупреждение, vertex.z изменено на 0");
        }

      if(fabs(vertex[i].x) > 1 || fabs(vertex[i].y) > 1)
        {
         Print("Ошибка, координаты вершин должны быть в диапазоне [-1;1]");
         return(false);
        }
     }

   ResetLastError();

   if(!InitCanvas())
     {
      return(false);
     }

   if(!InitDevice(vertex))
     {
      return(false);
     }

   return(true);
  }


Создание холста для рисования

В функции InitCanvas() создается объект "Графическая метка", координаты которого задаются в пикселях. Затем к этому объекту привязывается динамический графический ресурс, в который будет выводиться изображение из DirectX.

bool DXTutorial::InitCanvas()
  {
   m_canvas = "DXTutorialCanvas";
   m_resource = "::DXTutorialResource";
   int area = m_width * m_height;

   if(!ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0))
     {
      Print("Ошибка, не удалось создать объект для рисования");
      return(false);
     }

   if(!ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100))
     {
      Print("Предупреждение, не удалось сдвинуть объект по горизонтали");
     }

   if(!ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100))
     {
      Print("Предупреждение, не удалось сдвинуть объект по вертикали");
     }

   if(ArrayResize(m_image, area) != area)
     {
      Print("Ошибка, не удалось изменить размер массива для графического ресурса");
      return(false);
     }

   if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area)
     {
      Print("Предупреждение, не удалось инициализировать массив для графического ресурса");
     }

   if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("Ошибка, не удалось создать ресурс для рисования");
      return(false);
     }

   if(!ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource))
     {
      Print("Ошибка, не удалось привязать ресурс к объекту");
      return(false);
     }

   return(true);
  }

Рассмотрим код подробнее.

m_canvas = "DXTutorialCanvas";

Задаем название графическому объекту "DXTutorialCanvas".

m_resource = "::DXTutorialResource";

Задаем название динамическому графическому ресурсу "::DXTutorialResource".

int area = m_width * m_height;

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

ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0)

Создаем объект "Графическая метка" с названием "DXTutorialCanvas".

ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)

Сдвигаем объект на 100 пикселей вправо от верхнего левого угла графика.

ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)

Сдвигаем объект на 100 пикселей вниз от верхнего левого угла графика.

ArrayResize(m_image, area)

Изменяем размер массива для отрисовки.

ArrayInitialize(m_image, ColorToARGB(clrBlack))

Заполняем массив черным цветом. В массиве цвета должны храниться в формате ARGB. Для удобства используем стандартную функцию ColorToARGB для преобразования цвета.

ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)

Создаем динамический графический ресурс с названием "::DXTutorialResource", с шириной m_width и высотой m_height. Указываем на использование цвета с прозрачностью через COLOR_FORMAT_ARGB_NORMALIZE. В качестве источника данных используем массив m_image.

ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)

Связываем объект и ресурс. Ранее мы не указывали размеры объекта, они автоматически подстроятся под размеры ресурса.


Инициализация DirectX

Переходим к самому интересному.

bool DXTutorial::InitDevice(float4 &vertex[])
  {
   DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }};
   string shader_error = "";

   m_dx_context = DXContextCreate(m_width, m_height);
   if(m_dx_context == INVALID_HANDLE)
     {
      Print("Ошибка, не удалось создать графический контекст: ", GetLastError());
      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("Ошибка, не удалось создать вершинный шейдер: ", GetLastError());
      Print("Ошибка компиляции шейдера: ", shader_error);
      return(false);
     }

   m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error);
   if(m_dx_pixel_shader == INVALID_HANDLE)
     {
      Print("Ошибка, не удалось создать пиксельный шейдер: ", GetLastError());
      Print("Ошибка компиляции шейдера: ", shader_error);
      return(false);
     }

   m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);
   if(m_dx_buffer == INVALID_HANDLE)
     {
      Print("Ошибка, не удалось создать вершинный буфер: ", GetLastError());
      return(false);
     }

   if(!DXShaderSetLayout(m_dx_vertex_shader, layout))
     {
      Print("Ошибка, не удалось установить разметку вершин: ", GetLastError());
      return(false);
     }

   if(!DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST))
     {
      Print("Ошибка, не удалось установить тип примитивов: ", GetLastError());
      return(false);
     }

   if(!DXShaderSet(m_dx_context, m_dx_vertex_shader))
     {
      Print("Ошибка, не удалось установить вершинный шейдер: ", GetLastError());
      return(false);
     }

   if(!DXShaderSet(m_dx_context, m_dx_pixel_shader))
     {
      Print("Ошибка, не удалось установить пиксельный шейдер: ", GetLastError());
      return(false);
     }

   if(!DXBufferSet(m_dx_context, m_dx_buffer))
     {
      Print("Ошибка, не удалось установить буфер для отрисовки: ", GetLastError());
      return(false);
     }

   return(true);
  }

Разберем код.

DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }};

Здесь описывается формат вершин. Эта информация нужна, чтобы видеокарта правильно обработала массив вертексов на входе. В данном случае размер массива равен 1, так как вершины хранят информацию только о позиции. Но если мы еще добавим информацию о цвете вершины, то понадобится еще одна ячейка массива. "POSITION" означает, что информация относится к координатам. 0 - это семантический индекс. Если нужно передать две разные координаты в одной вершине, мы можем для первой указать индекс 0, а для второй 1. DX_FORMAT_R32G32B32A32_FLOAT - формат в котором представлена информация. В данном случае четыре 32 битных числа с плавающей запятой.

string shader_error = "";

В этой переменной будут храниться ошибки компиляции шейдера.

m_dx_context = DXContextCreate(m_width, m_height);

Создаем графический контекст с шириной m_width и высотой m_height. Запоминаем хендл.

m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error);

Создаем вершинный шейдер и сохраняем хендл. DX_SHADER_VERTEX указывает на тип шейдера - вертексный. Строка shader хранит исходный код вертексного и пиксельного шейдеров, но рекомендуется хранить их в отдельных файлах и подключать как ресурсы. "VShader" - это название точки входа (функция main в обычных программах). При ошибке компиляции шейдера в shader_error будет записана дополнительная информация. Например, если указать точку входа "VSha", в переменной будет такой текст: "error X3501: 'VSha': entrypoint not found".

m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error);

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

m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);

Создаем буфер и сохраняем хендл. Указываем, что буфер вершинный. Передаем массив вертексов.

DXShaderSetLayout(m_dx_vertex_shader, layout)

Передаем информацию о разметке вершин.

DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)

Устанавливаем тип примитивов "список треугольников".

DXShaderSet(m_dx_context, m_dx_vertex_shader)

Передаем информацию о вершинном шейдере.

DXShaderSet(m_dx_context, m_dx_pixel_shader)

Передаем информацию о пиксельном шейдере.

DXBufferSet(m_dx_context, m_dx_buffer)

Передаем информацию о буфере.


Вывод изображения

DirectX выводит изображение в массив. На основе этого массива создается графический ресурс.

bool DXTutorial::Draw()
  {
   DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f};

   if(!DXContextClearColors(m_dx_context, dx_color))
     {
      Print("Ошибка, не удалось очистить буфер цвета: ", GetLastError());
      return(false);
     }

   if(!DXContextClearDepth(m_dx_context))
     {
      Print("Ошибка, не удалось очистить буфер глубины: ", GetLastError());
      return(false);
     }

   if(!DXDraw(m_dx_context))
     {
      Print("Ошибка, не удалось отрисовать вертексы вершинного буфера: ", GetLastError());
      return(false);
     }

   if(!DXContextGetColors(m_dx_context, m_image))
     {
      Print("Ошибка, не удалось получить изображение из графического контекста: ", GetLastError());
      return(false);
     }

   if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("Ошибка, не удалось создать ресурс для рисования");
      return(false);
     }

   return(true);
  }

Разберем метод подробнее.

DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f};

Создается переменная dx_color типа DXVector. Ей присваивается красный цвет с половинной прозрачностью. Формат RGBA со значениями от 0 до 1 float.

DXContextClearColors(m_dx_context, dx_color)

Заливаем буфер цветом dx_color.

DXContextClearDepth(m_dx_context)

Очищаем буфер глубины.

DXDraw(m_dx_context)

Отправляем в DirectX задание на отрисовку.

DXContextGetColors(m_dx_context, m_image)

Получаем результат работы в массив m_image.

ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)

Обновляем динамический графический ресурс.


Освобождение ресурсов

DirectX требует ручного освобождения ресурсов. Также следует удалить графический объект и ресурс. Проверяется необходимость в освобождении ресурсов, затем происходит вызов функции DXRelease. Динамический графический ресурс удаляется с помощью ResourceFree. Графический объект освобождается с помощью ObjectDelete.

void DXTutorial::Deinit()
  {
   if(m_dx_pixel_shader > 0 && !DXRelease(m_dx_pixel_shader))
     {
      Print("Ошибка, не удалось освободить хендл пиксельного шейдера: ", GetLastError());
     }

   if(m_dx_vertex_shader > 0 && !DXRelease(m_dx_vertex_shader))
     {
      Print("Ошибка, не удалось освободить хендл вершинного шейдера: ", GetLastError());
     }

   if(m_dx_buffer > 0 && !DXRelease(m_dx_buffer))
     {
      Print("Ошибка, не удалось освободить хендл вершинного буфера: ", GetLastError());
     }

   if(m_dx_context > 0 && !DXRelease(m_dx_context))
     {
      Print("Ошибка, не удалось освободить хендл графического контекста: ", GetLastError());
     }

   if(!ResourceFree(m_resource))
     {
      Print("Ошибка, не удалось удалить графический ресурс");
     }

   if(!ObjectDelete(0, m_canvas))
     {
      Print("Ошибка, не удалось удалить графический объект");
     }
  }


Шейдеры

Шейдеры будем хранить в строке shader. Но при больших объемах их лучше вынести в отдельные внешние файлы и подключать как ресурсы.

string shader = "float4 VShader( float4 Pos : POSITION ) : SV_POSITION  \r\n"
                "  {                                                    \r\n"
                "   return Pos;                                         \r\n"
                "  }                                                    \r\n"
                "                                                       \r\n"
                "float4 PShader( float4 Pos : SV_POSITION ) : SV_TARGET \r\n"
                "  {                                                    \r\n"
                "   return float4( 0.0f, 1.0f, 0.0f, 1.0f );            \r\n"
                "  }                                                    \r\n";

Шейдер - это программа для видеокарты. В DirectX пишется на С-подобном языке HLSL. float4 в шейдере - это встроенный тип данных, в отличие от нашей структуры. VShader в данном случае представляет вершинный шейдер, а PShader - пиксельный шейдер. POSITION  - это семантика, указывает, что входные данные представляют координаты, смысл как и в DXVertexLayout. SV_POSITION - так же семантика, но выходного значения. Префикс SV_ говорит, что значение системное. SV_TARGET - семантика, означает, что значение будет записано с текстуру или пиксельный буфер. И так, что же здесь происходит. Вершинный шейдер получает на вход координаты и без изменений передает их на выход. В пиксельный шейдер (со стадии растеризации) поступают интерполированные значения, для которых устанавливается зеленый цвет.


OnStart

В функции создается экземпляр класса DXTutorial. Вызывается функция Init, в которую передается массив вершин. Затем вызывается функция Draw. После этого скрипт завершает выполнение.

void OnStart()
  {
   float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}};
   DXTutorial dx;
   if(!dx.Init(vertex)) return;
   ChartRedraw();
   Sleep(1000);
   if(!dx.Draw()) return;
   ChartRedraw();
   Sleep(1000);
  }


Заключение

В статье мы ознакомились с историей DirectX. Разобрались что это такое и для чего нужно. Рассмотрели внутреннее устройство API. Узнали, что из себя представляет конвейер по преобразованию вершин в пиксели на современных видеокартах. Ознакомились со списком действий, необходимых для работы с DirectX. Рассмотрели небольшой пример на MQL. И наконец, вывели на экран наш первый треугольник! Поздравляю, вы прошли обряд инициации! Но не стоит расслабляться. Впереди еще много нового и интересного, что необходимо узнать для полноценной работы с DirectX. Это и передача дополнительных к вертексам данных, и язык для программирования шейдеров HLSL, и различные преобразования с помощью матриц, текстуры, нормали, многочисленные спецэффекты.


Список литературы и ссылки

  1. Википедия.
  2. Документация Microsoft.


Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Tong Shi Yang
Tong Shi Yang | 29 апр. 2022 в 19:56
Andrey Dik
Andrey Dik | 22 февр. 2024 в 10:49

Очень интересно, спасибо!

Желательно бы продолжение.

Rorschach
Rorschach | 28 февр. 2024 в 19:25
Andrey Dik #:

Очень интересно, спасибо!

Желательно бы продолжение.

Спроса нет
Denis Kirichenko
Denis Kirichenko | 29 февр. 2024 в 12:30
Имхо, тема узкоспециализированная, поэтому наверное у неё свой, немногочисленный читатель. Также думаю, что хороший материал статьи может вызвать интерес у новисов, увеличивая таким образом спрос.
Статья написано добротно, что называется с расстановочкой. Сам далёк от DirectX. Но по мере сил осваиваю...
Автору респект и уважуха. Надеюсь, что будет продолжение!

Советы профессионального программиста (Часть III): Логирование. Подключение к системе сбора и анализа логов Seq Советы профессионального программиста (Часть III): Логирование. Подключение к системе сбора и анализа логов Seq
Реализация класса Logger для унификации (структурирования) сообщений, выводимых в журнал эксперта. Подключение к системе сбора и анализа логов Seq. Наблюдение за сообщениями в онлайн режиме.
Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
В статье рассмотрим реализацию независимого перемещения мышкой любых объектов-форм, а также дополним библиотеку сообщениями об ошибках и новыми свойствами сделок, ранее уже введёнными в терминал и MQL5.
Графика в библиотеке DoEasy (Часть 98): Перемещаем опорные точки расширенных стандартных графических объектов Графика в библиотеке DoEasy (Часть 98): Перемещаем опорные точки расширенных стандартных графических объектов
В статье продолжим развитие расширенных стандартных графических объектов, и создадим функционал перемещения опорных точек составных графических объектов при помощи контрольных точек управления координатами опорных точек графического объекта.
Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5 Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5
В этот раз создадим систему авторизации в торговом терминале MetaTrader 5 на чистом MQL5. Пользователи приложения смогут зарегистрироваться в системе, предоставив свои учётные данные, чтобы впоследствии можно было авторизоваться и получить доступ, к каким-нибудь данным, которые хранятся в серверной части приложения.