Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 4): Сглаживание, покрытие и композиция
Содержание
- Введение
- Точное определение покрытия
- Config.mqh — один параметр, настраиваемый во время выполнения
- CairoBlendOver — наложение одного цвета на другой
- Вторая версия растеризатора
- Демонстрационный пример
- Структура проекта
- Заключение и дальнейшие шаги
Введение
Вы рисуете фигуры в буфере и выводите их на график или в пользовательский интерфейс. Все выглядит хорошо — пока наклонные ребра и полупрозрачность не выявят ограничения такого подхода. Любое ребро, которое не является строго горизонтальным или вертикальным, превращается в заметную лесенку, узкие элементы могут исчезать, а полупрозрачная заливка может перезаписывать то, что уже было под ней, вместо корректного смешивания с ним. Это не независимые ошибки: у них одна и та же первопричина. Растеризатор из третьей части отвечал на вопрос "находится ли центр пикселя внутри фигуры?" только "да" или "нет" (0 или 1). Нам нужен количественный ответ на вопрос "какая доля этого пикселя находится внутри фигуры?" — значение покрытия от 0 до 1.
В этой части мы введем значение покрытия и используем его на этапе композиции, чтобы сглаживание и альфа-смешивание выполнялись с помощью одних и тех же вычислений. Покрытие вычисляется точно по оси X (вычитанием интервалов) и приближенно по оси Y — с помощью N горизонтальных линий сканирования внутри строки пикселей (CairoAaSamples). Изменения в реализации невелики и локальны: в растеризатор добавляются накопитель покрытия и AddSpan для учета частично покрытых пикселей, в Color.mqh — CairoBlendOver для умножения альфа-значения источника на покрытие, а в Config.mqh — настройка качества, доступная во время выполнения. Измеримые цели: ребра должны стать гладкими без отдельного прохода сглаживания, перекрывающиеся полупрозрачные фигуры — корректно смешиваться, а настройка CairoAaSamples — обеспечивать понятное и измеримое соотношение качества и скорости.
В третьей части мы создали растеризатор, который заливает любую фигуру, заданную обводкой, с помощью одной функции. Он работает, нигде не содержит кода для отдельных типов фигур, и именно с его помощью нам впервые в этой серии удалось получить сплошные залитые фигуры. Но у него есть два недостатка, и оба видны на последнем рисунке третьей части:
- любое ребро, которое не является строго горизонтальным или вертикальным, отображается лесенкой;
- полупрозрачный цвет заменяет уже записанный цвет пикселя вместо смешивания с ним.
На первый взгляд это две разные проблемы, и в большинстве рендереров их решают по отдельности, но причина у них одна. Обе проблемы сводятся к одному недостающему числу — "сколько". Как только оно появляется, обе решаются одними и теми же тремя строчками вычислений.
Всю идею можно выразить одним предложением. В третьей части задавался вопрос с ответом "да" или "нет":
Находится ли центр пикселя внутри фигуры? -> 0 или 1
В четвертой части вопрос требует количественного ответа:
какая доля этого пикселя находится внутри? -> любое значение от 0 до 1
Эта доля называется покрытием. Если синяя фигура покрывает пиксель на 30%, его итоговый цвет должен сместиться от текущего к синему на 30%. Если пиксель полностью покрыт синим цветом с непрозрачностью 30%, его итоговый цвет должен быть точно таким же. В вычислениях покрытие и альфа-значение перемножаются, поэтому оба учитываются в одной функции — CairoBlendOver. Отдельный проход для сглаживания при этом вообще не нужен.
Это самое заметное визуальное изменение во всей серии. Пути не меняются, геометрия демонстрационного примера не меняется, четыре шага построчного прохода не меняются. Меняется только вопрос — и лесенки исчезают.
В этой части изменения затронут три файла. В Color.mqh добавляется оператор композиции. В Raster.mqh добавляется накопитель покрытия и убирается округление. А в новом файле Config.mqh хранится единственное число, определяющее соотношение качества и скорости.
Color.mqh Часть 1 тип цвета ARGB
Часть 4 + CairoBlendOver <- сегодня
Surface.mqh Часть 1 пиксельный буфер, связанный с одним графическим объектом
Path.mqh Части 2/3 точки, контуры, ребра
Config.mqh Часть 4 CairoAaSamples — настройка качества <- сегодня
Raster.mqh Часть 3 построчная заливка
Часть 4 покрытие вместо проверки "да/нет" <- сегодня
Точное определение покрытия
Начнем с одного пикселя и одного прямого ребра, пересекающего его.
Пиксель — это не точка. Это квадрат шириной и высотой в одну единицу: столбец пикселей 10 занимает диапазон x от 10,0 до 11,0, а строка 7 — диапазон y от 7,0 до 8,0. Ребро, проходящее через этот квадрат, делит его на две части: одна находится внутри фигуры, другая — снаружи. Покрытие — это площадь части пикселя, находящейся внутри фигуры. Поскольку площадь всего квадрата равна 1, покрытие принимает значения от 0 до 1.
Вот и все определение. Все остальное в этой статье посвящено тому, как быстро вычислять покрытие.
Вернемся к разобранному примеру из первой части и доведем его до конца. Синяя (31, 111, 235) фигура на белом (255, 255, 255) фоне. Граничный пиксель на 30% находится внутри фигуры, поэтому его покрытие равно 0,30. Итоговый цвет пикселя — не синий и не белый. В каждом канале его значение смещается от белого к синему на 30%:
red = 255 + ( 31 - 255 ) * 0.30 = 187.8 -> 188
green = 255 + ( 111 - 255 ) * 0.30 = 212
blue = 255 + ( 235 - 255 ) * 0.30 = 249
result = (188, 212, 249) a very pale blue Повторите это для каждого пикселя вдоль границы — и лесенка исчезнет. Ступенька высотой в целый пиксель сглаживается за счет двух или трех соседних пикселей: вклад цвета фигуры в каждый из них пропорционален доле площади пикселя, фактически занятой фигурой. Глаз, который и так усредняет мелкие детали, воспринимает результат как прямую линию.
Стоит четко объяснить, чем сглаживание не является: распространенное заблуждение на этот счет приводит к ошибкам в реализации. Это не размытие. Это не фильтр, применяемый к готовому изображению. Ничего не размывается: значение покрытия вычисляется по геометрии, а не усреднением соседних пикселей. Покрытие дает количественный ответ на вопрос, на который проверка центра пикселя отвечала слишком грубо. Поскольку оно вычисляется во время растеризации, а не после нее, линия шириной в один пиксель сохраняет визуальную толщину, а не превращается в размытую полосу шириной в три пикселя.
Откуда берется эта доляПо двум осям растеризатор вычисляет покрытие разными способами, и эта асимметрия намеренна:
- По оси X — аналитически. Интервал от x = 10,3 до x = 13,6 дает покрытие ровно 0,7 для столбца 10, ровно 1,0 для столбцов 11 и 12 и ровно 0,6 для столбца 13. Никаких выборок и приближений — точная площадь, вычисленная двумя вычитаниями.
- По оси Y — по выборкам. Строка пикселей делится на CairoAaSamples горизонтальных полос, вклад каждой из которых в общее покрытие учитывается с весом 1/N.
Вычислить точную площадь с учетом обеих осей возможно — именно это делает полностью аналитический растеризатор. Но для этого нужен значительно более сложный алгоритм, затраты на каждый пиксель выше, а при экранном разрешении результат трудно отличить от результата с четырьмя выборками по оси Y. Это единственное место в серии, где мы сознательно используем приближение, и о его цене стоит знать: при N выборках покрытие по вертикали квантуется с шагом 1/N. Поэтому при значении по умолчанию 4 покрытие по оси Y может составлять 0, 0,25, 0,5, 0,75 или 1.
Config.mqh — один параметр, настраиваемый во время выполнения
Прежде чем менять растеризатор, нужно определить, где будет храниться управляющий им параметр.
#ifndef CAIRO5_CONFIG_MQH #define CAIRO5_CONFIG_MQH //--- library identity #define CAIRO5_VERSION "1.0" //+------------------------------------------------------------------+ //| CairoAaSamples | //| | //| The number of horizontal sample lines taken per pixel row. | //+------------------------------------------------------------------+ int CairoAaSamples = 4; #endif // CAIRO5_CONFIG_MQH
Отныне все числовые параметры, которые пользователю может понадобиться настроить, хранятся здесь, а не разбросаны по файлам, в которых используются. Это правило кажется бюрократическим ровно до того момента, когда вам впервые понадобится выяснить, почему фигура отрисовывается по-разному в двух местах.
CairoAaSamples — это число горизонтальных линий сканирования, на которых берутся выборки в каждой строке пикселей. Оно определяет качество сглаживания по вертикали, поскольку покрытие по оси X в любом случае вычисляется точно. Затраты на заливку почти линейно зависят от этого значения:
| Значение | Результат |
|---|---|
| 1 | Сглаживания по вертикали нет; вертикальные ребра по-прежнему отображаются точно, но на горизонтальных и пологих видны полосы |
| 2 | Почти весь выигрыш в качестве при вдвое меньших затратах, чем при значении 4 |
| 4 | Значение по умолчанию, после которого дальнейшее улучшение изображения на экране становится все менее заметным |
| 8 | Заметное улучшение только на почти горизонтальных ребрах крупных фигур |
| 16 | Для отрисовки в высоком разрешении при экспорте |
Обратите внимание, что это обычная глобальная переменная, а не #define. Это осознанный выбор, который стоит обосновать: для константы времени компиляции привычнее было бы использовать #define.
Приложение вполне может менять качество отрисовки во время выполнения. Панель, которая отрисовывается один раз и затем остается статичной, следует отрисовывать с максимальным качеством. Но при перетаскивании той же панели мышью важнее скорость: сорок кадров в секунду важнее идеально сглаженного ребра движущейся фигуры. Если используется глобальная переменная, обработчик перетаскивания задает CairoAaSamples = 1 при нажатии кнопки мыши и восстанавливает прежнее значение при ее отпускании. При использовании #define так изменить значение во время выполнения невозможно.
Демонстрационный пример использует именно эту возможность: рисует одну и ту же звезду четыре раза с четырьмя разными значениями параметра, а затем восстанавливает исходное значение.
CairoBlendOver — наложение одного цвета на другой
Эта функция занимает вторую половину Color.mqh и обеспечивает оба исправления, рассматриваемые в этой части.
Эта операция называется over: цвет источника накладывается на цвет назначения с учетом прозрачности. Она есть в любой системе композиции, и это единственная операция, которая нужна этой библиотеке.
//+------------------------------------------------------------------+ //| CairoBlendOver - put `src` on top of `dst`, at `coverage`. | //+------------------------------------------------------------------+ uint CairoBlendOver(const uint dst, const uint src, const double coverage) { //--- fold coverage straight into the source alpha const double sa = (CAIRO_A(src) / 255.0) * coverage; //--- nothing to add: leave the destination exactly as it was if(sa <= 0.0) return dst; const double da = CAIRO_A(dst) / 255.0; const double ia = 1.0 - sa; const double oa = sa + da * ia; //--- transparent over transparent stays transparent if(oa <= 0.0) return CAIRO_TRANSPARENT; //--- weight each color by how much of the result it owns, then //--- divide through the output alpha to get back to straight ARGB const double inv = 1.0 / oa; const double r = (CAIRO_R(src) * sa + CAIRO_R(dst) * da * ia) * inv; const double g = (CAIRO_G(src) * sa + CAIRO_G(dst) * da * ia) * inv; const double b = (CAIRO_B(src) * sa + CAIRO_B(dst) * da * ia) * inv; return CairoArgb((uint)(oa * 255.0 + 0.5), (uint)(r + 0.5), (uint)(g + 0.5), (uint)(b + 0.5)); }
Самая важная строчка — первая, но при чтении ее легко пропустить. Альфа-значение источника непосредственно умножается на покрытие. Это и есть вся реализация сглаживания в этой библиотеке. Здесь нет ни функции сглаживания, ни отдельного прохода для сглаживания ребер, ни фильтра. Пиксель, наполовину покрытый непрозрачным цветом, и пиксель, полностью покрытый цветом с прозрачностью 50%, дают одно и то же значение sa, а значит, и один и тот же результат. Это правильно: для такого расчета ситуации эквивалентны.

Рис. 1. Один граничный пиксель — наложение синего цвета на белый при покрытии 0,30.
На рисунке 1 показан результат приведенных выше вычислений. Цвет назначения — белый, цвет источника — синий из нашей палитры, покрытие — 0,30. В результате получается бледно-синий цвет, как и ожидалось в первой части. Обратите внимание: средняя панель показывает не цвет, а числовую величину — долю пикселя, занятую фигурой.
Остальные вычисленияТри переменные после защитной проверки соответствуют стандартным формулам операции over:
- sa — доля вклада источника с учетом покрытия;
- da — исходное альфа-значение цвета назначения;
- ia — 1 - sa, доля, остающаяся для цвета назначения;
- oa — итоговое альфа-значение, равное sa + da * ia. Вклад источника учитывается целиком, а альфа-значение цвета назначения — только в оставшейся доле, на которую не претендовал источник.
Затем значение каждого цветового канала вычисляется как сумма двух вкладов — компонента источника с весом sa, и компонента назначения с весом da * ia, — деленная на oa.
Именно об этом делении часто забывают. Без него результат остается предумноженным, и полупрозрачные пиксели отображаются слишком темными. Поскольку ResourceCreate с COLOR_FORMAT_ARGB_NORMALIZE ожидает ARGB с без предумножения, передача предумноженных значений делает каждый полупрозрачный пиксель темнее, чем нужно, — словно всю панель окунули в черную краску. Деление на oa отменяет предумножение, возвращая цвет в формат, используемый остальной частью библиотеки.В первой части мы отметили это различие, но отложили его разбор. Теперь вернемся к нему. В формате без предумножения цветовые компоненты и альфа-значение хранятся независимо, поэтому красный цвет с прозрачностью 50% записывается как (128, 255, 0, 0). В формате с предумножением цветовые компоненты хранятся уже умноженными на нормированное альфа-значение, поэтому тот же красный цвет записывается как (128, 128, 0, 0). Оба способа представления широко используются и оба корректны; проблемы возникают при их смешении.
Два ранних выхода нужны не только для оптимизации. Возврат dst без изменений при sa <= 0 позволяет растеризатору пропускать пиксели, не покрытые фигурой, без отдельной проверки. А проверка oa <= 0 предотвращает деление на ноль: при наложении полностью прозрачного цвета на полностью прозрачный цвет восстанавливать цветовые компоненты делением не требуется. Математически правильный результат — "по-прежнему прозрачно".
Прибавление + 0.5 перед каждым приведением типа обеспечивает округление по причине, указанной в первой части: при преобразовании в целое число язык MQL5 отбрасывает дробную часть. Поэтому без такого прибавления значение канала 187,9 превращается в 187, а повторное смешивание постепенно затемняет результат.
Вторая версия растеризатора
Структура Fill осталась такой же, как в третьей части: определение ограничивающего прямоугольника, перебор строк, поиск пересечений, их сортировка и проход с учетом числа обхода. Изменились только две вещи. Вклады интервалов накапливаются в массиве покрытия вместо непосредственного закрашивания пикселей, а в каждой строке берутся N выборок вместо одного.
//+------------------------------------------------------------------+ //| CCairoRasterizer | //+------------------------------------------------------------------+ class CCairoRasterizer { private: CCairoEdgeList m_edges; // edges of the path being filled double m_coverage[]; // NEW: one accumulator per pixel column double m_cross_x[]; // where this scanline crosses each edge int m_cross_dir[]; // and in which direction bool EnsureCapacity(const int width, const int edge_count); void AddSpan(const double xa, const double xb, const double weight, const int lo, const int hi); void SortCrossings(const int n); public: CCairoRasterizer(); void Fill(CCairoPath &path, const uint argb, uint &buffer[], const int width, const int height); };
В m_coverage хранится по одному значению типа double на столбец пикселей, а не на каждый пиксель изображения. Этот прием позволяет экономить память: в каждый момент мы обрабатываем только одну строку, поэтому и накопители покрытия нужны только для нее. Для панели шириной 920 пикселей нужен массив из 920 элементов объемом около 7 КБ. Он повторно используется для каждой строки каждой фигуры на протяжении всей работы программы.
//+------------------------------------------------------------------+ //| Grow the scratch arrays if needed. They are never shrunk. | //| | //| Checked, for the reason Part 2 and Part 3 check their own: if | //| the memory is refused and the code carries on regardless, the | //| sweep below writes past the end of an array instead of failing | //| where the mistake actually is. | //+------------------------------------------------------------------+ bool CCairoRasterizer::EnsureCapacity(const int width, const int edge_count) { //--- the coverage accumulator needs one slot per pixel column if(ArraySize(m_coverage) < width) if(ArrayResize(m_coverage, width) < width) return false; if(ArraySize(m_cross_x) < edge_count) { if(ArrayResize(m_cross_x, edge_count) < edge_count) return false; if(ArrayResize(m_cross_dir, edge_count) < edge_count) return false; } return true; }
Возвращаемое значение появилось в этой части в соответствии с правилом, заданным во второй и третьей частях: если ArrayResize завершается неудачно, об этом нужно сообщить, а не продолжать работу в расчете на успех операции. Без этой проверки при неудачном изменении размера последующий проход мог бы записать данные за пределы m_coverage. Запись за пределы массива покрытия возможна так же, как и за пределы массивов точек и ребер, поэтому нет причин защищать те два массива, но оставлять без защиты этот. Обратите внимание, как устроена проверка: оба массива пересечений расширяются вместе, поэтому условие раннего выхода проверяет только m_cross_x, предполагая, что m_cross_dir тоже был расширен.
AddSpan — точный расчет доли покрытияЭто новая функция, в которой и заключается основная идея четвертой части.
//+------------------------------------------------------------------+ //| THE NEW FUNCTION, AND THE WHOLE POINT OF PART 4. | //+------------------------------------------------------------------+ void CCairoRasterizer::AddSpan(const double xa, const double xb, const double weight, const int lo, const int hi) { double a = xa; double b = xb; if(b <= a) return; //--- clip to the range of columns this shape can touch if(a < lo) a = lo; if(b > hi + 1) b = hi + 1; if(b <= a) return; const int ia = (int)MathFloor(a); //--- THE EPSILON MATTERS. A span ending exactly on a pixel //--- boundary, say at x = 14.0, covers NOTHING of column 14. //--- Without the nudge, MathFloor(14.0) = 14 and we would add a //--- zero-width contribution to a column the shape never reached //--- - visible as a faint one-pixel fringe on every axis-aligned //--- right-hand edge. const int ib = (int)MathFloor(b - 1e-9); //--- the whole span lies inside a single pixel if(ia == ib) { m_coverage[ia] += weight * (b - a); return; } m_coverage[ia] += weight * ((ia + 1) - a); // leading partial pixel for(int x = ia + 1; x < ib; x++) m_coverage[x] += weight; // fully covered interior m_coverage[ib] += weight * (b - ib); // trailing partial pixel }
Интервал задается вещественными координатами, а не диапазоном пикселей. В этом предложении и заключается отличие этой статьи от предыдущей. В третьей части границы интервала округлялись до целых пикселей с помощью MathRound, а информация о точном положении ребра терялась. Здесь вклад первого и последнего пикселей, затронутых интервалом, учитывается пропорционально их покрытию, а каждый пиксель, полностью находящийся внутри интервала, получает полный вес.

Рис. 2. Интервал от 10,3 до 13,6 и точная доля покрытия для каждого столбца.
На рисунке 2 показан разобранный пример. В столбце 10 интервал занимает участок от 10,3 до конца столбца в точке 11,0, поэтому покрытие равно 0,7. Столбцы 11 и 12 полностью находятся внутри интервала, поэтому покрытие каждого равно 1,0. В столбце 13 интервал занимает участок от начала столбца до точки 13,6, поэтому покрытие равно 0,6. Три вычитания и цикл — без каких-либо выборок.
В этой функции две детали критически важны.
Эпсилон. Использование MathFloor( b - 1e-9 ) вместо MathFloor( b ) может показаться суеверием, но это не так. Рассмотрим интервал, который заканчивается точно на границе пикселя, при x = 14,0. Это обычная ситуация: прямоугольники со сторонами, параллельными осям координат, — самая распространенная фигура в любом интерфейсе. MathFloor( 14.0 ) возвращает 14, поэтому в последней строчке выполнилось бы m_coverage[14] += weight * ( 14.0 - 14 ) — добавление вклада в столбец, которого фигура не касается. Вклад равен нулю, поэтому внешне все выглядит нормально — пока интервал не заканчивается на последнем столбце, которого может касаться фигура. Выше в коде b ограничивается значением hi + 1. Поэтому без этой поправки ib становится равным hi + 1, а при hi равном width - 1, последняя строчка записывает данные в m_coverage[width] — элемент сразу за концом массива покрытия. В языке MQL5 это вызывает ошибку выхода за пределы массива и останавливает программу. Благодаря поправке ib указывает на последний фактически затронутый столбец, как и требуется.
Параметр weight. Его значение равно 1 / CairoAaSamples, поэтому суммарный вклад N линий сканирования внутри одной строки не превышает 1,0. Во всех N проходах по строке используется один и тот же накопитель покрытия. Поэтому усреднение по оси Y не требует отдельного буфера или дополнительного прохода: N вкладов просто суммируются в одних и тех же ячейках.
Fill — две защитные проверки, затем проходСама функция Fill начинается с двух проверок. Далее в этом разделе предполагается, что они уже пройдены:
//+------------------------------------------------------------------+ //| Fill `path` with the color `argb` into an ARGB buffer. | //+------------------------------------------------------------------+ void CCairoRasterizer::Fill(CCairoPath &path, const uint argb, uint &buffer[], const int width, const int height) { //--- a path that lost an allocation is not geometry we can trust if(!path.IsValid()) return; path.BuildEdges(m_edges); const int edge_count = m_edges.m_count; if(edge_count == 0) return; if(!EnsureCapacity(width, edge_count)) return; // no scratch room - draw nothing
Обе проверки приводят к отказу от рисования, а не к обработке ошибок в обычном смысле: функции Fill() некуда сообщить о проблеме. Поэтому при поврежденном пути или неудачном расширении буфера правильнее ничего не рисовать, чем нарисовать неверную фигуру. path.IsValid() проверяет тот самый флаг, который был введен во второй части: объект CCairoPath запоминает отказ выделения памяти при построении. Благодаря этому любой код, использующий путь, может выполнить одну проверку, а не пытаться определить его корректность по числу вершин, которое может оказаться неполным. EnsureCapacity(), возвращающая значение типа bool, реализует ту же идею уровнем ниже — для собственной рабочей памяти растеризатора. Функция Fill() не проверяет размер буфера, поэтому вызывающий код должен передать буфер как минимум из width * height элементов.
Ни одна из этих проверок не меняет результат отрисовки правильно построенного пути. Обе проверки предназначены для случая, когда путь был поврежден еще до передачи в эту функцию — из-за деления на ноль, некорректных входных данных или нехватки памяти. Если просто продолжить работу, можно нарисовать внешне правдоподобную, но неверную фигуру.
Ограничивающий прямоугольник — теперь по обеим осям//--- BOUNDING BOX, now in BOTH axes. //--- //--- Part 3 only needed the vertical extent, to know which rows to //--- walk. Now we also need the horizontal extent, because the //--- coverage accumulator has to be cleared before each row - and //--- clearing the whole buffer width for a 20-pixel-wide shape //--- would cost far more than drawing it. //--- //--- An edge is a straight segment, so its x extremes are at its //--- two ends; there is no need to walk along it. double ymin = 1e18, ymax = -1e18; double xmin = 1e18, xmax = -1e18; for(int i = 0; i < edge_count; i++) { const double etop = m_edges.m_edges[i].ytop; const double ebot = m_edges.m_edges[i].ybot; if(etop < ymin) ymin = etop; if(ebot > ymax) ymax = ebot; const double xa = m_edges.m_edges[i].xtop; const double xb = m_edges.m_edges[i].XAt(ebot); if(xa < xmin) xmin = xa; if(xb < xmin) xmin = xb; if(xa > xmax) xmax = xa; if(xb > xmax) xmax = xb; }
В третьей части нужны были только границы по вертикали, чтобы определить, какие строки обрабатывать. Теперь нужны и границы по горизонтали: перед обработкой каждой строки необходимо обнулять накопитель покрытия, а обнуление 920 столбцов ради фигуры шириной 20 пикселей потребовало бы гораздо больше затрат, чем сама отрисовка.
Ребро — это прямой отрезок, поэтому минимальное и максимальное значения x достигаются на его концах; проходить вдоль него не нужно. xtop уже хранит координату x одного конца, а XAt( ebot ) возвращает координату x другого.
int iy0 = (int)MathFloor(ymin); int iy1 = (int)MathCeil(ymax); int ix0 = (int)MathFloor(xmin); int ix1 = (int)MathCeil(xmax); if(iy0 < 0) iy0 = 0; if(iy1 > height) iy1 = height; if(ix0 < 0) ix0 = 0; if(ix1 > width - 1) ix1 = width - 1; //--- entirely off the buffer if(ix1 < ix0 || iy1 <= iy0) return;
Обратите внимание на различие верхних ограничений: iy1 ограничивается значением height, поскольку цикл по строкам выполняется при iy < iy1, а ix1 — значением width - 1, поскольку циклы по столбцам включают конечный индекс: x <= ix1. Ошибка в этих ограничениях приводит к записи пикселя сразу за концом строки, что в языке MQL5 вызывает ошибку выхода за пределы массива и останавливает программу.
N линий сканирования внутри каждой строки пикселей//--- How finely do we sample the row vertically? int samples = CairoAaSamples; if(samples < 1) samples = 1; const double weight = 1.0 / samples; //--- WALK THE ROWS for(int iy = iy0; iy < iy1; iy++) { //--- clear only the columns this shape can possibly touch for(int x = ix0; x <= ix1; x++) m_coverage[x] = 0.0; //--- N SUB-SCANLINES PER ROW. //--- //--- Part 3 took one sample, at iy + 0.5. Now we take N, spread //--- evenly through the row, each contributing 1/N of the //--- coverage. //--- //--- This is what fixes a NEAR-HORIZONTAL edge. Consider a line //--- at a very shallow angle crossing a row: with one sample //--- the row is either fully in or fully out, so the edge //--- advances in whole-row jumps. With four, a row can be 25%, //--- 50% or 75% covered, and the jump is smoothed away. for(int s = 0; s < samples; s++) { //--- center of each sub-row, never its boundary const double yc = iy + (s + 0.5) / samples;
Ограничение samples защищает от случая, когда вызывающий код задает глобальной переменной нулевое или отрицательное значение: иначе weight стал бы бесконечным либо цикл вообще не выполнился бы. Верхнего ограничения нет, поэтому при запросе тысячи выборок заливка будет выполняться в тысячу раз медленнее. В библиотеке, предоставляющей глобальную переменную для настройки, нужно учитывать, что рано или поздно ей могут задать неразумное значение.
Выражение ( s + 0.5 ) / samples располагает каждую линию сканирования в центре соответствующей горизонтальной полосы, а не на ее границе. Здесь используется тот же принцип, что и в iy + 0.5 из третьей части, только уровнем ниже. При samples = 4 линии сканирования проходят на отметках 0,125, 0,375, 0,625 и 0,875 высоты строки от ее верхнего края.
Именно это устраняет ступенчатость почти горизонтального ребра. Рассмотрим линию, пересекающую строку под очень малым углом к горизонтали: при одной выборке строка считается либо полностью внутри фигуры, либо полностью снаружи. Поэтому ребро отображается скачками на целые строки, и на фигуре видны полосы. При четырех выборках покрытие строки может составлять 25%, 50% или 75%, и скачок сглаживается.

Рис. 3. То же пологое ребро при одном, двух и восьми выборках на строку.
На рисунке 3 отдельно показан этот случай. Изображение получено с помощью тех же вычислений, которые выполняет растеризатор. При одной выборке пологое ребро отображается скачками на целые строки, и полосы хорошо заметны. При двух выборках эффект уменьшается вдвое. При восьми выборках вдоль границы образуется плавный градиент значений частичного покрытия. Бледные пунктирные линии — это линии сканирования внутри каждой строки пикселей. Посчитайте их — и настройка перестанет быть абстрактной.
Этот рисунок также показывает, почему значение по умолчанию — 4, а не 16. Переход от 1 к 2 дает резкое улучшение, от 2 к 4 — заметное, а от 4 к 8 — едва различимое при экранном разрешении. При этом затраты продолжают расти линейно.
Поиск пересечений и накопление покрытияint n = 0; for(int i = 0; i < edge_count; i++) { //--- half-open [ytop, ybot), exactly as in Part 3: a //--- vertex shared by two edges counts exactly once if(yc >= m_edges.m_edges[i].ytop && yc < m_edges.m_edges[i].ybot) { m_cross_x[n] = m_edges.m_edges[i].XAt(yc); m_cross_dir[n] = m_edges.m_edges[i].dir; n++; } } if(n < 2) continue; SortCrossings(n); //--- sweep with the winding number, as before - but a span //--- is now ACCUMULATED rather than painted int winding = 0; for(int k = 0; k < n - 1; k++) { winding += m_cross_dir[k]; if(winding != 0) AddSpan(m_cross_x[k], m_cross_x[k + 1], weight, ix0, ix1); } }
Сравните этот код с кодом из третьей части — изменилась только последняя строчка. Проверка пересечения с полуоткрытым интервалом, сортировка вставками, число обхода — все осталось прежним и используется так же, как раньше. AddSpan заменяет округление и прямую запись; остальной код прохода менять не потребовалось.
На этом стоит остановиться: это показывает, что архитектура, заложенная в третьей части, была выбрана удачно. Для столь заметного изменения — все ребра, отрисовываемые библиотекой, стали сглаженными вместо ступенчатых — понадобилось изменить одну строчку алгоритма и добавить одну функцию.
Композиция готовой строки//--- COMPOSITE THE FINISHED ROW. //--- //--- Every column now holds a number from 0 to 1: how much of //--- that pixel the shape covers. Hand it to CairoBlendOver and //--- the anti-aliasing happens for free, because coverage and //--- alpha are the same thing. const int row_base = iy * width; for(int x = ix0; x <= ix1; x++) { double a = m_coverage[x]; //--- untouched pixel: leave the destination alone. This is //--- what stops a shape disturbing anything outside itself, //--- even within its own bounding box. if(a <= 0.0) continue; //--- Sub-spans that overlap within one sub-scanline can //--- push the sum slightly past 1.0 - a self-overlapping //--- path does exactly that. Clamp, rather than letting a //--- coverage of 1.3 brighten the pixel. if(a > 1.0) a = 1.0; const int idx = row_base + x; buffer[idx] = CairoBlendOver(buffer[idx], argb, a); } } }
Теперь для каждого столбца в накопителе хранится число от 0 до 1 — доля соответствующего пикселя, покрытая фигурой. Передайте это значение в CairoBlendOver() — и сглаживание выполнится автоматически.
Пропуск пикселей при a <= 0.0 оставляет неизменными пиксели вне фигуры, даже если они находятся внутри ее ограничивающего прямоугольника. У тонкой наклонной линии большой ограничивающий прямоугольник, но сама она затрагивает лишь немногие пиксели внутри него. Без этой проверки для каждого незатронутого пикселя выполнялось бы смешивание с нулевым покрытием — результат был бы верным, но время тратилось бы впустую.
Ограничение покрытия сверху значением 1,0 защищает от арифметических погрешностей, а не от ошибок геометрии. Интервалы на одной линии сканирования формируются по отсортированным пересечениям, поэтому идут последовательно и никогда не перекрываются — независимо от числа контуров в пути и их взаимных пересечений. Все пересечения попадают в один отсортированный список, а проход формирует непересекающиеся интервалы. Поэтому вклад каждой линии сканирования в покрытие столбца не превышает weight, а сумма N таких вкладов — 1,0. Ограничение корректирует сумму, которая после N сложений с плавающей запятой оказывается равной 1,0000000002. Иначе это значение попало бы в CairoBlendOver как покрытие больше 1, а функция использует переданное покрытие без проверки.
Демонстрационный пример
Демонстрационный пример Demo04_AntiAliasing.mq5 выводит шесть панелей, каждая из которых отдельно показывает один из результатов внесенных изменений.
A — заново отрисованный веер из третьей части. Геометрические данные побайтно совпадают с данными ряда ступенчатых фигур из третьей части:
//+------------------------------------------------------------------+ //| A. THE PART 3 FAN, REDRAWN | //+------------------------------------------------------------------+ void PanelFan(const double ox, const double oy) { for(int i = 0; i < 7; i++) { const double a = -M_PI * 0.5 + M_PI * 0.5 * i / 6.0; CCairoPath t; t.AddTriangle(ox, oy + 120, ox + 150 * MathCos(a), oy + 120 + 150 * MathSin(a), ox + 150 * MathCos(a + 0.09), oy + 120 + 150 * MathSin(a + 0.09)); Fill(t, CairoLerp(C_BLUE, C_ORANGE, i / 6.0)); } }
В этой функции не изменилось ни одно число. Изменился только растеризатор, который она использует. Сравните этот скриншот со скриншотом из третьей части при одинаковом увеличении — и основная мысль статьи станет очевидной.
B — регулятор качества. Та же звезда при четырех значениях параметра качества, с замером времени. Длина интервала на каждой линии сканирования вычисляется точно по оси X при любом значении параметра. Поэтому видимые различия проявляются только на более пологих ребрах звезды; именно этот случай отдельно показан на панели D. Эта панель показывает затраты на повышение качества:
//+------------------------------------------------------------------+ //| B. THE QUALITY DIAL | //+------------------------------------------------------------------+ void PanelSamples(const double ox, const double oy) { const int saved = CairoAaSamples; int levels[4] = { 1, 2, 4, 8 }; for(int k = 0; k < 4; k++) { CairoAaSamples = levels[k]; const double cx = ox + k * 150; double xs[10], ys[10]; for(int i = 0; i < 10; i++) { const double r = (i % 2 == 0) ? 58.0 : 24.0; const double a = -M_PI * 0.5 + M_PI * i / 5.0; xs[i] = cx + 62 + r * MathCos(a); ys[i] = oy + 62 + r * MathSin(a); } CCairoPath star; star.AddPolygon(xs, ys, 10); //--- the one fill the reader sees Fill(star, C_ORANGE); //--- an untimed warm-up so the scratch arrays are already at //--- their high-water mark, then 40 timed repeats - all of //--- them invisible, see the note above Fill(star, C_NONE); const ulong t0 = GetMicrosecondCount(); for(int r2 = 0; r2 < 40; r2++) Fill(star, C_NONE); const ulong t1 = GetMicrosecondCount(); PrintFormat("B. CairoAaSamples = %d -> %.1f us per fill", levels[k], (double)(t1 - t0) / 40.0); } CairoAaSamples = saved; }
Предварительная заливка важна. При первой заливке фигуры рабочие массивы расширяются до требуемого максимального размера. Поэтому замер этого запуска отражал бы затраты на выделение памяти, а не на растеризацию. Затем результаты сорока повторений усредняются, чтобы уменьшить влияние других задач, которые в это время выполняет терминал.
Еще важнее использовать C_NONE в этих повторениях. На этом стоит остановиться: ошибка здесь создает впечатление, будто неверно работает растеризатор, хотя причина не в нем. Fill выполняет смешивание цветов, а не просто перезаписывает пиксели. Поэтому, если ради замера времени нарисовать одну и ту же непрозрачную звезду сорок раз, результат не останется одинаковым: при каждом повторении она накладывается сама на себя. После n заливок эффективная непрозрачность пикселя с покрытием a достигает 1 - (1 - a)^n, причем это значение очень быстро приближается к 1:
покрытие после 1 заливки покрытие после 41 заливки
0.94 1.000
0.67 1.000
0.40 1.000
0.14 0.998
0.004 0.148 Из-за повторных заливок при замере каждый частично покрытый пиксель вдоль каждого ребра — именно такие пиксели мы и рассматриваем в этой статье — становится практически непрозрачным. В итоге панель выглядит так же, как при заливке из третьей части, где пиксель либо закрашивался целиком, либо не закрашивался вовсе. Такой замер производительности уничтожал бы именно тот эффект, который должен продемонстрировать.
C_NONE — это CairoArgb( 0, 0, 0, 0 ): альфа-значение равно нулю. При sa <= 0 CairoBlendOver сразу возвращает dst без изменений. Поэтому при заливке этим цветом строится список ребер, обрабатываются строки, берутся все выборки внутри каждой строки и накапливается покрытие — но в итоге ни один пиксель не меняется. Важно понимать, что остается за рамками замера: при этом раннем выходе пропускаются и вычисления смешивания. Поэтому заливка видимым цветом требует немного больше времени, чем показывают эти результаты. Измеряются все операции, объем которых растет с числом выборок; итоговое смешивание цветов для каждого пикселя в замер не входит.
Код панели сохраняет и восстанавливает исходное значение глобальной переменной, а не оставляет его измененным:
const int saved = CairoAaSamples; ... CairoAaSamples = saved;
C — теперь субпиксельное позиционирование действительно работает. Десять одинаковых полос, каждая следующая сдвинута еще на одну десятую пикселя:
//+------------------------------------------------------------------+ //| C. SUB-PIXEL POSITIONING IS REAL NOW | //+------------------------------------------------------------------+ void PanelSubPixel(const double ox, const double oy) { for(int i = 0; i < 10; i++) { CCairoPath bar; bar.AddRect(ox + i * 34 + i / 10.0, oy, 22, 90); Fill(bar, C_GREEN); }
Мы проводим этот эксперимент уже в третий раз за цикл статей, и впервые он дает нужный результат. Во второй части путь точно сохранял смещения, но примитивный отрисовщик терял их при округлении. В третьей части эти смещения терялись при округлении в растеризаторе. Здесь смещения сохраняются вплоть до вывода на экран: интенсивность цвета вдоль левого ребра у каждой полосы немного отличается, а сами полосы смещаются вправо с шагом в одну десятую пикселя, заметным глазу.
Именно это и позволяет создавать плавную анимацию. Виджет, перемещающийся на 0,1 пикселя за кадр, теперь движется плавно, а не перескакивает между целочисленными позициями.
Затем на панели рисуются тонкие линии с дробными значениями ширины:
//--- and a single hairline at a series of fractional widths, to //--- show that a shape THINNER than a pixel still renders - it //--- simply comes out fainter, which is exactly right for(int i = 0; i < 10; i++) { CCairoPath hair; hair.AddRect(ox + i * 34 + 22, oy + 100, i / 10.0, 60); Fill(hair, C_BLUE); } }
Даже прямоугольник шириной 0,1 пикселя отображается. Он выглядит бледным, а не исчезает: его покрытие равно 0,1, и CairoBlendOver() учитывает его цвет с соответствующим весом. Именно так и должен работать полноценный рендерер. В третьей части первые девять таких линий вообще не были бы нарисованы.
D — где важны выборки по оси Y. Клинья с очень пологими, почти горизонтальными ребрами:
//+------------------------------------------------------------------+ //| D. WHERE SAMPLING IN Y ACTUALLY MATTERS | //+------------------------------------------------------------------+ void PanelShallow(const double ox, const double oy) { for(int i = 0; i < 5; i++) { const double y = oy + i * 30; CCairoPath wedge; wedge.AddTriangle(ox, y, ox + 380, y + 2.0 + i * 1.5, ox + 380, y + 16); Fill(wedge, CairoLerp(C_PURPLE, C_GREEN, i / 4.0)); } }
При CairoAaSamples, равном 1, на этой панели заметны полосы, а при 8 изображение станет гладким. Все остальное в демонстрации при обоих значениях выглядит почти одинаково. Отсюда практический вывод: увеличение числа выборок дает заметный выигрыш только на почти горизонтальных ребрах.
E — композиция. Три перекрывающихся полупрозрачных диска:
//+------------------------------------------------------------------+ //| E. COMPOSITING | //+------------------------------------------------------------------+ void PanelBlending(const double ox, const double oy) { const int n = 120; double cx[3], cy[3]; uint cols[3]; cx[0] = ox + 70; cy[0] = oy + 60; cols[0] = C_RED; cx[1] = ox + 130; cy[1] = oy + 60; cols[1] = C_GREEN; cx[2] = ox + 100; cy[2] = oy + 110; cols[2] = C_BLUE; for(int k = 0; k < 3; k++) { double xs[], ys[]; ArrayResize(xs, n); ArrayResize(ys, n); for(int i = 0; i < n; i++) { const double a = 2.0 * M_PI * i / n; xs[i] = cx[k] + 52 * MathCos(a); ys[i] = cy[k] + 52 * MathSin(a); } CCairoPath disc; disc.AddPolygon(xs, ys, n); Fill(disc, CairoWithOpacity(cols[k], 0.55)); } }
В третьей части были бы нарисованы три однотонных диска, каждый из которых перезаписывал бы предыдущий в области перекрытия. Теперь в области перекрытия двух дисков цвет получается корректным смешиванием обоих цветов, а в области перекрытия трех — всех трех. Внимательно посмотрите на места пересечения сглаженных границ: там нет ни искажений, ни темного шва. Это результат корректного расчета альфа-значения без предумножения в CairoBlendOver(). Если бы этап отмены предумножения отсутствовал, появился бы темный шов.
F — шкала покрытия. Одна и та же полоса с десятью значениями непрозрачности на шахматном фоне, нарисованном самим растеризатором:
//+------------------------------------------------------------------+ //| F. A COVERAGE RAMP | //+------------------------------------------------------------------+ void PanelRamp(const double ox, const double oy) { //--- checkerboard background for(int r = 0; r < 8; r++) for(int c = 0; c < 40; c++) { if((r + c) % 2 != 0) continue; CCairoPath sq; sq.AddRect(ox + c * 9, oy + r * 9, 9, 9); Fill(sq, CairoRgb(231, 235, 239)); } for(int i = 0; i < 10; i++) { CCairoPath bar; bar.AddRect(ox + i * 36, oy, 30, 72); Fill(bar, CairoWithOpacity(C_ORANGE, 1.0 - i / 10.0)); } }
В CairoBlendOver() покрытие умножается на альфа-значение. Поэтому пиксель, покрытый непрозрачным цветом на 50%, и пиксель, полностью покрытый цветом с непрозрачностью 50%, дают одинаковый результат. Эта панель позволяет проверить утверждение: бледные полосы показывают те же значения цвета, которые появляются вдоль сглаженных ребер на остальных панелях демонстрации.
На что следует обратить внимание 
Рис. 4. Demo04_AntiAliasing на графике с цветовой схемой Black On White — шесть панелей, один вызов.

Рис. 5. Один и тот же веер в третьей и четвертой частях, увеличенный без интерполяции.
Самое важное показано на рисунке 5. Уточним, на что именно следует обратить внимание. Не обращайте внимания на фигуры — они одинаковы. Обратите внимание на граничные пиксели. На изображении слева только два значения цвета — цвет фигуры и цвет фона. Справа их около дюжины; промежуточные значения появляются вдоль ребра, а внутри фигуры и за ее пределами цвета не меняются. Именно там и появляются новые значения: сглаживание дает промежуточные цвета только в пикселях, которые пересекает граница фигуры. У типичной панели таких пикселей лишь небольшая доля от общего числа. Но вычисления не ограничиваются пикселями с промежуточными цветами: в каждой строке, затронутой фигурой, берутся N выборок, а смешивание выполняется для каждого покрытого пикселя.

Рис. 6. Сравнение результатов при разных значениях CairoAaSamples
На рисунке 6 хорошо видно, как CairoAaSamples влияет на сглаживание фигур. Как видно, на почти горизонтальных ребрах эффект выражен сильнее; на более крутых ребрах изменение числа выборок влияет меньше.
В журнале показаны затраты времени при разных значениях параметра качества:
=== Part 4: coverage instead of a yes/no test === B. CairoAaSamples = 1 -> 194.2 us per fill B. CairoAaSamples = 2 -> 227.8 us per fill B. CairoAaSamples = 4 -> 305.5 us per fill B. CairoAaSamples = 8 -> 440.5 us per fill whole 940x700 frame in 62849 us at CairoAaSamples = 4
У вас результаты будут другими, но характер зависимости сохранится: затраты почти линейно растут с числом выборок. При этом есть постоянные накладные расходы — построение ребер, обнуление накопителя покрытия и композиция строки, — которые от числа выборок не зависят. Именно из-за этой постоянной части затраты при двух выборках меньше удвоенных затрат при одном.
Структура проекта
Распакуйте архив в MQL5/, сохранив структуру папок, и скомпилируйте Demo04_AntiAliasing.mq5. Редактировать пути не требуется.
MQL5/
├── Include/
│ └── CairoG2D/
│ ├── Color.mqh // the ARGB color type, and now blending
│ ├── Config.mqh // CairoAaSamples - the quality dial
│ ├── Surface.mqh // pixel buffer bound to one chart object
│ ├── Path.mqh // the path and the edges - unchanged this part
│ ├── Raster.mqh // the scanline fill, now with coverage
│ └── DemoTheme.mqh // the demos' shared palette - NOT library code
└── Experts/
└── CairoG2D/
└── Article 04/
└── Demo04_AntiAliasing.mq5 // six panels, one Fill() | # | Файл | Каталог | Содержимое |
|---|---|---|---|
| 1 | Color.mqh | MQL5/Include/CairoG2D | Тип цвета ARGB, а теперь и CairoBlendOver |
| 2 | Config.mqh | MQL5/Include/CairoG2D | CairoAaSamples и строка версии |
| 3 | Surface.mqh | MQL5/Include/CairoG2D | CCairoSurface — вывод пиксельного буфера на график в виде растрового изображения |
| 4 | Path.mqh | MQL5/Include/CairoG2D | Путь, SCairoEdge и BuildEdges |
| 5 | Raster.mqh | MQL5/Include/CairoG2D | CCairoRasterizer — покрытие и композиция |
| 6 | DemoTheme.mqh | MQL5/Include/CairoG2D | Палитра, настройка графика, подписи — не входят в состав библиотеки |
| 7 | Demo04_AntiAliasing.mq5 | MQL5/Experts/CairoG2D/Article 04 | Шесть панелей и замеры времени при разных значениях параметра качества |
| 8 | Cairo-Style Library - Part 04.zip | Архив, содержащий все прикрепленные файлы и пути к ним относительно корневой папки терминала. |
В этой части изменились два уже существовавших файла. В конец Color.mqh добавлен CairoBlendOver; код выше не изменился. В Raster.mqh добавлены m_coverage, AddSpan, расчет границ по горизонтали, цикл по линиям сканирования внутри строки и проверка результата EnsureCapacity. Структура из четырех этапов осталась такой же, как в третьей части. Файлы Path.mqh и Surface.mqh вообще не изменялись.
Заключение и дальнейшие шаги
Теперь движок выполняет отрисовку так же, как современная 2D-библиотека. Ребра сглаживаются под любым углом, фигуры могут располагаться в позициях с дробными координатами и перемещаться на доли пикселя, полупрозрачные цвета корректно смешиваются с цветами под ними, а фигура шириной меньше пикселя отображается бледной, а не исчезает. Отрисовка фигуры, тонкой в другом направлении, то есть высотой меньше пикселя, зависит от числа выборок: фигура не отображается, если внутри нее не проходит ни одна линия сканирования.
Все это стало результатом замены одного вопроса другим. Составляющие:
- покрытие — доля пикселя, фактически занятая фигурой, которая вычисляется точно по оси X с помощью вычитания и приближенно по оси Y с помощью N линий сканирования внутри строки;
- AddSpan — функция, которая учитывает частичное покрытие пикселей на обоих концах интервала вместо округления его границ до целых пикселей;
- CairoBlendOver — реализация операции over для ARGB без предумножения, в которой альфа-значение умножается на покрытие, благодаря чему сглаживание реализуется без отдельной функции или прохода, специально посвященного ему;
- CairoAaSamples — один параметр качества, изменяемый во время выполнения, с измеренными затратами на повышение качества.
Пока еще не хватает способа задать, что считать внутренней областью фигуры. Сейчас используется только правило ненулевого числа обхода, унаследованное из третьей части. Именно поэтому буква A в демонстрации из третьей части закрашивалась целиком, если вызывающий код не задавал обратный порядок вершин внутреннего контура вручную. Если библиотека заставляет каждого, кто ее использует, задумываться о направлении обхода, это уже не библиотека, а ловушка.
Позже мы добавим правило заливки "чет-нечет", чтобы вызывающий код мог выбирать правило. С этим правилом путь, содержащий два вложенных контура, дает кольцо, рамку или букву с нужным отверстием — без необходимости учитывать направление построения внутренней обводки. Мы рассмотрим, в каких именно случаях эти два правила дают разные результаты и почему в шрифтах и SVG предпочтение отдается разным правилам.
Затем, научившись оставлять отверстия, мы построим обводки с помощью заливки: исходная обводка вместе со своей уменьшенной копией, смещенной внутрь, образует рамку любой толщины. Тот же механизм позволит рисовать кольца, кольцевые диаграммы и рамки, из которых строится каждая панель. На этом этапе библиотека уже начинает выглядеть как инструмент для создания интерфейсов.
Проект поддерживается на MQL5 Algo Forge. Для каждой части серии подготовлен отдельный релиз с кодом, зафиксированным именно в том виде, который разбирается в этой статье. Поэтому скачивайте релиз, соответствующий той статье, которую читаете.
Часть 4 находится здесь: https://forge.mql5.io/SandroBegashvil/CairoG2D/releases/tag/part-04
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/24259
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
За рамками REST и ZeroMQ: Создание моста Protocol Buffers между MetaTrader 5 и Python с выходом на gRPC для инференса в реальном времени
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 3): Ребра и первая фигура с заливкой
Интеграция ИИ в три концепции Smart Money (SMC): Ордер-блок (OB), слом структуры (BOS) и гэп справедливой стоимости (FVG)
Автоматизация торговых стратегий в MQL5 (Часть 53): Разворотные модели "двойная вершина" и "двойное дно"
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования