Direct2D. Контур текста. Эффект тени

Direct2D

Direct2D — это загадочное и малоизученное мной существо семейства Microsoft, обитающее в недрах DirectX. Давно хотел познакомиться с ним поближе.

Плюсом Direct2D является, что он шустрый и запросто взаимодействует с GDI, GDI+. Минусом, то что он аппаратно-зависимый. Последнее утверждение — мое субъективное мнение. Аппаратное ускорение — это супер, но ведь не на всех аппаратах и не все аппараты адекватны. Делая коммерческий софт на Direct2D, надо сразу обзавестись службой психически устойчивой поддержки.

Для нормальной работы требуется DirectX 11 и выше, Windows 7 и выше. Есть конечно DirectX 10 и Vista, именно там зародился Direct2D, но ведь и то, и другое, врагу не пожелаешь.

Цели

Вообще, все это дико интересно. Появилось время, решил покопаться в этой теме. Для начала, хотелось разобраться со следующим:

  1. Оценить скорость Direct2D в условиях прикладной задачи.
  2. Попробовать всякие интересности, типа эффектов и «новый» контекст.
  3. Понять, как рисовать на битмапе, когда он постоянно пересоздается.
  4. Понять, как работать с текстом сверх тех возможностей, что дает Delphi. Для начала — вывести контур текста, потому что это красиво и стильно.
  5. Интересует работа с градиентом. Понятно, что тут, как и везде есть градиентные и радиальные кисти, а также текстурные. Пощупать вживую было б очень интересно.
  6. Разобраться как работать с изображением. Отображение, масштаб, гауссово размытие, аффинные преобразования.
  7. Попробовать эффект тени.

Что получилось, видно на заставке к статье.

Подготовка

Direct2D в Delphi представлен классом TDirect2DCanvas (Vcl.Direct2D) и заголовочным модулем Winapi.D2D1. Имеющийся в Delphi набор — это базовый интерфейс версии 1.0, которая ныне даже не упоминается в википедии.

По большому счету, стандартный набор методов TDirect2DCanvas, наследника TCustomCanvas, не особо интересен. Он давно знаком, делает то, что ожидаешь, и не делает того, что нужно мне. Интересны плюшки Direct2D от версии 1.1. С этим возникает проблема, т.к. заголовочных файлов для этой версии в Delphi нет.

Гугл поломался и таки позволил мне найти две альтернативы:

DirectX 12: Набор заголовочных файлов FreePascal/Delphi

SVGIconImageList. Работа с SVG и прочие прелести. Классная библиотека.

Первый неудобно использовать одновременно со стандартным Direct2D Delphi, т.к. типы имеют ожидаемо одинаковое название, и надо постоянно указывать — какой тип имеется ввиду. Зато это наиболее полная коллекция заголовочных файлов. Поэтому буду брать оттуда нужные мне описания.

У второго взял заголовочный файл Winapi.D2DMissing, который является замечательным расширением стандартного набора Delphi и отлично с ним ладит.

Контур текста

IDWriteTextLayout — ключевой интерфейс при отрисовке текста. Помимо текста, он содержит массу другой необходимой информации, включая форматирование, пользовательские эффекты, одним словом много всего. В обычном режиме, IDWriteTextLayout используется как параметр функции ID2D1RenderTarget.DrawTextLayout.

IDWriteTextRenderer — интерфейс, предоставляющий набор обратных вызовов, которые выполняют отрисовку текста, встроенных объектов и украшений, таких как подчеркивание и зачеркивание. IDWriteTextRenderer является параметром метода Draw интерфейса IDWriteTextLayout.

Чтобы осуществить собственную отрисовку текста, необходимо сделать наследника IDWriteTextRenderer, который будет рисовать текст как вам угодно, допустим, с контуром. Затем вызывать IDWriteTextLayout.Draw, указав его в качестве параметра.

IDWriteTextRenderer

Имеет на борту методы:

DrawGlyphRun
IDWriteTextLayout.Draw вызывает эту функцию, чтобы нарисовать серию глифов.
DrawInlineObject
IDWriteTextLayout.Draw вызывает эту функцию, когда ему нужно нарисовать встроенный объект.
DrawStrikethrough
IDWriteTextLayout.Draw вызывает эту функцию, когда надо нарисовать зачеркивание.
DrawUnderline
IDWriteTextLayout.Draw вызывает эту функцию, чтобы нарисовать подчеркивание.

Глиф «по жизни» — это векторная форма символа/части символа. Символ может состоять из нескольких глифов. По глифам и не только — статья «Практический взгляд на базовые термины и анатомию шрифтов».

Фрагмент статьи «Практический взгляд на базовые термины и анатомию шрифтов»

Здесь же под глифом понимается 16-разрядный беззнаковый индекс в CMAP таблице. Чтобы его (их) получить для набора символов, надо использовать функцию IDWriteFontFace.GetGlyphIndices. В этой статье функция задействована не будет, но информация не лишняя.

Чтобы получить геометрию контура глифа(ов) надо использовать IDWriteFontFace.GetGlyphRunOutline. Вот этим сейчас и займемся.

Берем за основу статью MSDN. Это готовая реализация на С++ того, что нужно. А нужно нам нарисовать контур текста. Переписываем все на Delphi.

Для начала описание нового интерфейсного типа. Наследуем от IDWriteTextRenderer.

Класс наследник IDWriteTextRenderer

[свернуть]

Нам сейчас нужен только DrawGlyphRun. Именно в нем магия. Остальные методы можно дописывать по мере необходимости.

IDWriteTextRenderer.DrawGlyphRun

Метод DrawGlyphRun вызывается из IDWriteTextLayout.Draw для группы подряд идущих символов с одинаковыми свойствами, находящихся на одной базовой линии. Параметры baselineOriginX, baselineOriginY: Single указывают на начало такой группы по X и позицию по Y. Параметр clientDrawingContext : Pointer может содержать указатель на контекст, на котором требуется рисовать. Он может быть задан первым параметром функции IDWriteTextLayout.Draw. Параметр glyphRun: TDwriteGlyphRun — искомая группа глифов.

Алгоритм работы DrawGlyphRun таков:

  1. Создать геометрию ID2D1PathGeometry, открыть ее описатель ID2D1GeometrySink.
  2. Извлечь и поместить геометрию контура серии глифов glyphRun в описатель с помощью вышеупомянутой GetGlyphRunOutline.
  3. Закрыть описатель.
  4. Полученную геометрию необходимо перенести в baselineOriginX, baselineOriginY. Для этого инициализируется матрица переноса.
  5. Создать с помощью метода ID2D1Factory.CreateTransformedGeometry новую геометрию на основе ранее полученных геометрии и матрицы переноса .
  6. Нарисовать контур и залить новую геометрию.

Собственно, реализация

Метод DrawGlyphRun

[свернуть]

В реальности чуть по другому, чуть больше. Но, чтобы не растекаться мыслью по древу — здесь так, близко к тексту MSDN. Этот шаблон всегда можно модифицировать и дополнить. Допустим, в реальности, во-первых, можно оставить только контур, без заливки. Во-вторых, ну градиент, конечно. Все в исходниках по ссылке ниже.

За скобками

Группа методов от IDWritePixelSnapping стандартна, подвержена безболезненному копипасту. В той же статье от MSDN говориться, что лучше писать так, и все.

IDWritePixelSnapping часть реализации

[свернуть]

Остальные методы имеют только эту фразу: Result := E_NOTIMPL;

Это не означает, что так и надо. Просто они здесь не реализованы, о чем и сообщают.

Конструктор почти такой же, как в статье MSDN. Добавил только толщину контура.

Конструктор TOutlineTextRenderer

[свернуть]

Внутри класса ничего не создается. Все передается в конструктор. Если что-то поменялось, надо пересоздать. Это безболезненно, надежно и, главное, быстро.

Например, поменялась у меня ширина контура в главной форме. Будет вызван следующий метод:

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

Эффекты

В версии 1.1. Direct2D обзаводится новым контекстом для рисования ID2D1DeviceContext, который на самом деле наследник ID2D1RenderTarget. Появляется новое понятие — эффект. Представлен интерфейсом ID2D1Effect.

Эффект — это алгоритм преобразования одного и более входных изображений в одно. В частности, нам понадобятся размытие, тень и аффинное преобразование. Эффект может быть нарисован/воспринят, как изображение.

Описаний этих интерфейсов в Delphi нет, но они есть в Winapi.D2DMissing, про который говорилось вначале статьи.

Для создания эффекта требуется экземпляр ID2D1DeviceContext. На самом деле он у нас уже есть, это RenderTarget канвы. Просто контекст надо получить таким образом:

или

Далее, вызываем метод ID2D1DeviceContext.CreateEffect. В методе указываем, какой эффект хотим получить, и получаем его. После этого инициализируем. Даем на вход изображение. Задаем свойства.

Хватит теорий. Рассмотрим, как «заблюрить» изображение.

GaussianBlur: Размыть изображение

Нам потребуется изображение FBitmap типа ID2D1Bitmap. Получить его из обычного TBitmap очень просто. За обычный битмап отвечает FSource . Должен быть предварительно созданный FWICImage типа TWICImage.

Займемся блюром. Степень размытия находится в переменной valBlur: single. За контекст ID2D1DeviceContext отвечает переменная context.

Что тут происходит. При удачном создании эффекта, даем на вход изображение FBitmap. Затем устанавливаем свойство размытия valBlur.

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

Обратите внимание, сейчас эффект выступает как изображение.

Блюр с параметром valBlur = 10

Отдельно эффект Гауссова размытия можно скачать из телеги. Там только блюр и больше ничего:

Direct2D. Эффект Гауссова размывания

[свернуть]

Shadow: Эффект тени

Это интересный эффект. Он не делает тень «под», он превращает в тень, все, что ему будет скормлено. Классный эффект, одним словом. Вот во что он превращает надпись

Очевидно, что ему надо скормить надпись, нарисовать, и потом поверх еще раз нарисовать эту надпись.

Но, об особенностях применения поговорим позже. Сейчас сам эффект.

Что тут происходит. На вход эффекту скармливаем изображение bitmapTarget типа ID2D1Bitmap, в котором находится картинка надписи. Тень строится по всему, что не альфа(0), т.е., что непрозрачно. Далее задается свойство размытия значением valBlur. И, наконец, свойство цвета тени clrShadow, тип TD2D1_VECTOR_4F.

AffineTransform: Аффинные преобразования на плоскости

Куда ж без них. Этот эффект нужен здесь, чтобы сместить тень.

Вначале инициализируем матрицу трансформации. Задавая тем самым любую из возможных трансформаций или композицию их. Затем устанавливаем матрицу в эффект. И потом на вход подаем уже созданный эффект тени. На самом деле подается результат работы эффекта, но все равно, все очень просто и со вкусом. Мощно и красиво.

Компоновка

Теперь надо все аккуратно увязать друг с другом.

Рисовать будем в обработчике OnPaint компонента pb: TPaintBox. Считаем, что у нас уже есть FCanvas типа TDirect2DCanvas, который пересоздавать мы не собираемся, т.к. это занимает приличное время. Шаблон таков.

Здесь каждый раз создается новый битмап. Надо заставить холст безболезненно рисовать на каждом таком битмапе.

Почему так. Рисовать ведь можно и без создания промежуточных битмап.

Вообще, когда технология умеет рисовать на любой поверхности, это весьма расширяет горизонты. Если для рисования не нужно hwnd-окно или визуальный TControl, когда достаточно только DC:HDC, это позволит выводить рисунок куда угодно. Лично мне нужно даже не столько скорость интерфейса, сколько графические возможности доступные сразу из коробки.

Есть столько интересных задач, связанных с обработкой фотографий, например.

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

Создавать холст предлагается такой процедурой:

Для рисования текста нужен экземпляр IDWriteTextFormat. Менять значения шрифта и выравниваний не предполагается, поэтому он также в глобальном единственном экземпляре. Дело в том, что при всяком обращении к свойству Font.Handle, будет создаваться новый объект, и хорошо если бы вместе с выравниванием, но ведь нет.

Одним словом, имеет смысл получить его один раз и все.

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

Проблема, как увязать единственный экземпляр FCanvas со всякий раз новым объектом bmp решается так.

При создании холста передаем любую канву.

А в обработчике в секции инициализации (1) уже назначаем вновь созданный битмап.

Теперь все рисунки пойдут в битмап bmp. И поэтому до момента отрисовки нам глубоко фиолетово, куда сейчас смотрит FCanvas.

Для рисования текста нужен IDWriteTextLayout. Смысла выносить в глобальные нет, это локальная переменная TextLayout обработчика. Создается так:

Видим, задействован ранее созданный глобальный FTextFormat.

Получаем контекст и начинаем творить.

Все еще находясь в секции инициализации(1) обработчика вначале блюрим изображение с требуемыми параметрами. Это будет фон. Теперь надо сформировать тень.

Код указан выше. Есть нюанс. Перед тем, как что-то скормить тени, надо это что-то создать. В обычном варианте нужно было бы создать какой-то промежуточный битмап, в нем нарисовать текст и т.д. Для этих целей в Direct2D служит интерфейс ID2D1BitmapRenderTarget. Он необходим для промежуточного закадрового рисования. Его можно получить через ID2D1RenderTarget.CreateCompatibleRenderTarget. Далее просто рисуем текст в этом контексте, получаем на руки битмап с рисунком текста, освобождаем контекст.

Это тот самый битмап, которым кормим тень.

При рисовании текста используется наш класс TOutlineTextRenderer. Это глобальная переменная (поле формы) FTextRenderer: IDWriteTextRenderer. Создает и пересоздает экземпляр ранее описанный метод UpdateTextRenderer.

Рисование

Таким образом все готово. Компоновка такова:

  1. Пересоздать, если требуется, FTextRenderer через UpdateTextRenderer.
  2. Связать FCanvas с новой битмап bmp.
  3. Создать TextLayout.
  4. Получить context: ID2D1DeviceContext.
  5. Заблюрить фон.
  6. Получить изображение тени текста.
  7. Сместить тень эффектом аффинного преобразования.
  8. Рисуем фон.
  9. Рисуем тень.
  10. Рисуем текст.
  11. Завершение работы с ресурсами и освобождение.
Обработчик отрисовки

[свернуть]

Полностью текст обработчика лучше копировать из исходников. Представленный листинг сильно сокращен. Если скопировать этот листинг, на Window 7 будет жесткий AV, т.к. экземпляр context не будет создан. Поэтому лучше скачать рабочий исходник.

Без промежуточных битмапов

Давайте посмотрим, как будет работать без промежуточных битмапов. Для этого у нас есть пункт контекстного меню в «рабочей» области pmiWithoutBitmap, который определяет режим «рисования». Если он с галочкой, значит рисуем прямо на канву pb.

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

Во-вторых, перенаправить холст либо на pb, либо на битмап

В-третьих, чуть подправить завершение

Скорость ожидаемо возросла. Нет создания битмапов, нет дополнительной отрисовки средствами GDI на канву pb.

Также, скорость практически не меняется от размера окна. Даже растянутый на два монитора, Direct2D отлично держит скорость где-то около 10 миллисекунд (понятно, что на другой машине будут другие значения).

Копировать в буфер

Теперь обещанный пример. Зачастую надо скопировать изображение в буфер обмена для какой-то последующей обработки. Предлагается очень простой способ.

Есть пункт контекстного меню pmiCopy. Его обработчик:

И еще раз меняем перенаправление холста.

Теперь при выборе пункта меню Copy рендер рисует в битмап, который будет потом скопирован в буфер.

Таким образом, если создавая ID2D1HwndRenderTarget рендер, мы можем работать только с этим окном, и только в интерфейсе, то ID2D1DCRenderTarget позволяет рисовать не только на элементах интерфейса, но и на закадровых битмап.

Вылечить мерцание

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

В секции private формы пропишем следующее:

В событии OnCreate формы добавим такие строки:

И реализация метода NewWindowProc:

Если FBuffer присутствует, то при очистке фона панели, на которой происходит рисование, на ее поверхность копируется ранее сохраненный битмап.

Сохранение буфера происходит в секции finally обработчика OnPaint:

Результат

Получилось такое небольшое приложение.

В заголовке время отрисовки в миллисекундах. Видно, что текущая отрисовка заняла 10.6 миллисекунд. Что неплохо при наличии изображения + размытия + масштаб + градиент + размытие на тень + контур.

Двойной клик по тексту откроет слева панель настроек, можно поменять значения свойств, посмотреть, как меняется время отрисовки. Чтобы вызвать отрисовку можно поиграть размерами окна, оценить время.

Пара слов, о моей субъективности насчет аппаратной зависимости.

Intel HD Graphics

На рисунке вид приложения, запущенного на тестовом ноуте 2014 года, при включенном Intel HD Graphics. Драйвера установлены самые последние. Это именно то, что назвал «аппаратной неадекватностью» в начале статьи. Понятно, что тут может быть масса причин, к железке отношения мало имеющих.

Все волшебным образом меняется, если на том же ноуте переключить на NVIDIA 710M.

NVIDIA 710M

Пользователю наплевать на причины, он купил софт и хочет видеть чудо, а не оправдания.

К слову сказать, больше ни один из трех мне доступных Intel HD Graphics’ов так себя не ведет. Но вот один такой нашелся. Правда, он самый древний из них.

Полезные ссылки

Render Using a Custom Text Renderer
Та самая статья MSDN, на которую постоянно ссылаюсь в тексте. Посвящена созданию пользовательского рендерера.
DirectWrite: CustomTextRenderer, Hit-Test
Шикарная статья на русском. Рассказано не только о пользовательской отрисовке методом DrawGlyphRun, но и как сделать подчеркивание и hit-test’ы.
Outline Text With DirectWrite
Другой взгляд на отрисовку контура текста. Рендерера нет вообще, все в одной процедуре.
Shadow effect
Статья MSDN об эффекте тени.
DirectX 12
Упоминается в начале статьи. Большая библиотека заголовочных файлов FreePascal/Delphi
SVGIconImageList.
Упоминается в начале статьи. Классная библиотека для работы с SVG.
Общие сведения о геометрических контурах
Статья MSDN об использовании объектов Direct2D для создания сложных рисунков
Рисование с Direct2D
Большая статья MSDN про рисование с Direct2D скажем так, «вообще». Линии, цвета, градиенты, фигуры и т.д.
Геометрические элементы Direct2D и манипуляции над ними
Очень классная статья на русском о геометрических путях и связанных с ними темами.
Встроенные эффекты
Справка MSDN. Список встроенных эффектов с описанием и ссылками.

Про эффекты толковое смог найти только на MSDN. Вообще, информации по Direct2D в инете оказалось не так много, как хотелось бы.

В итоге штудировал MSDN.


Друзья, спасибо за внимание!

Надеюсь, на этом Direct2D для меня не заканчивается. Как только время позволит, обязательно покопаюсь еще. 66 эффектов, шутка ли! SVG опять же.

Да и Microsoft рекомендует работать с 2D графикой через Direct2D. Советует забывать про GDI и GDI+. Говорит, устарели…

Что узнаю интересного, обязательно расскажу.

Не пропустите, подписывайтесь на телегу. )))


Скачать

Исходники (Delphi XE 7-10) 200 Кб

Исполняемый файл 1.13 Мб

Если при запуске будет такой вид, без теней и блюра, это означает, что у Вас Windows 7 и DirectX 10. Следовательно, нет ID2D1DeviceContext, нет D2D1Effect. О чем свидетельствует надпись в левом нижнем углу окна программы. То есть, из доступного, только контур текста и градиент.

Необходимо накатить обновления из Windows Update. Есть инструкция для установки DirectX 11 под Windows 7. Должен быть предварительно установлен ServicePack 1.

Direct2D версии 1.0

5 7 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

5 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Nikolay

Доброго дня. Не могу понять почему при заливки геометрии(FillGeometry), не работает кисть прозрачности(opacityBrush). Нет ли у вас примера работающего приложения применяющего этот метод?

Nikolay

не совсем. в MSDN есть статья https://learn.microsoft.com/en-us/windows/win32/direct2d/opacity-masks-overview
суть в том что задаются две кисти одна с рисунком другая с маской прозрачности. Вот почему-то маска прозрачности не применяться.
В интернете есть пример но он тоже не работает почему-то.
Может дело в Win7 или Delphi10.3 rio

Vit

Спасибо тебе дорогой за науку

5
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x