Создание карандашного эффекта в SVG +60





Моя игра Dragons Abound создаёт карты в векторном графическом формате SVG. Векторная графика имеет множество особенностей (например, зум без потерь), что удобно для карт. Также векторная графика хороша для создания чётких линий, например, чернильных контуров:


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


потребуются десятки тысяч различных элементов. По сути, каждое пятно серого цвета в изображении будет задаваться отдельно. Ещё более проблематичны другие элементы, например, размытые изображения.

Это достаточно серьёзное ограничение векторной графики, поэтому в SVG добавлены хитрости, позволяющие более эффективно воспроизводить некоторые из подобных эффектов текстур. Я исследую некоторые из этих функций SVG для создания эффекта, напоминающего карандашную линию. Разумеется, существует множество более сложных решений для воссоздания карандашных линий. Об этой теме написаны целые научные статьи. Но я просто надеюсь создать довольно простой фильтр, обеспечивающий приемлемый результат.

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

Ранее я уже писал о возможностях Dragons Abound по созданию «рукописных» линий. В основном тогда я рассматривал возможность избегания математически прямых линий и создания линий с незначительными отклонениями, которые выглядят ближе к тому, что могло бы быть нарисовано рукой человека. В начале статьи показаны примеры иллюстраций гор. Для создания стиля чернильной иллюстрации этого вполне достаточно, но не подходит для создания карандашной линии, потому что ей не хватает текстуры карандаша.

Если взглянуть на карандашные линии, показанные выше и ниже:


то можно заметить множество особенностей, отличающих карандашные штрихи от чернильных линий (или линий, нарисованных компьютером). Самое важное отличие заключается в том, что они имеют текстуру, возникающую вследствие способа взаимодействия карандаша с бумагой. Бумага обладает зернистостью, и карандаш обычно оставляет графит на высоких частях зерна, а низкие части при этом остаются белыми. На грубой бумаге текстура получается более заметной; рисовальный картон обладает столь мелкой зернистостью, что почти не создаёт текстуры. Во-вторых, края карандашных линий довольно расплывчаты. Это опять же в большой степени вызвано неровностями бумаги и самого кончика карандаша; это приводит к тому, что вдоль края линии остаётся различное количество графита.

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

Для начала я подготовил простые серые линии:


Я нарисовал «рукописные» линии толщиной 4, 2 и 1 пиксель. К сожалению, SVG-эффекты обычно выглядят по-разному для линий разной длины, поэтому я захотел сравнить эффект на разных размерах.

Основная функция, предоставляемая SVG для добавления эффектов текстур, называется фильтрами. Фильтры накладываются после отрисовки векторного эффекта и изменяют его внешний вид. Обычные фильтры могут выполнять такие действия, как изменение цвета объекта, добавление к нему шума и т.п. Фильтры — это довольно запутанная тема со сложным синтаксисом, поэтому я не буду давать полный туториал по их использованию, но достаточно подробно объясню, чтобы было понятно, что я сделал. А в конце поста я приведу ссылку на Codepen с фильтром, чтобы вы могли поэкспериментировать с ним самостоятельно.

Во-первых, я попробую изменить края линий так, чтобы они не были плавными, а имели неровности, характерные зернистости бумаги. Я сделаю это, перемещая пиксели линии. Выполняющий эту задачу элемент фильтра называется «feDisplacementMap»; он перемещает каждый пиксель на основании значений в другом изображении. Так как мы хотим, чтобы каждый пиксель перемещался случайным, но единым способом, для управления движением нам нужно передать в feDisplacementMap шум. К счастью, в SVG есть ещё один элемент фильтра под названием «feTurbulence», специально предназначенный для создания шума. Так мы сможем скомбинировать два фильтра для огрубления краёв линий.

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


Теперь смещение так велико, что может перемещать линию целиком, а не просто менять её края. Этот эффект ещё заметнее на тонких линиях. В этом примере линия шириной 4 пикселей в основном выглядит так, как будто край грубый, а в линии толщиной 2 пикселя уже очевидно заметны искажения. То есть мне нужно подобрать значение, не создающее искажений в тонких линиях.

При зуме при стандартном увеличении игры эффект выглядит вот так (после подбора параметров для улучшения эффекта):


При таком масштабе многие грубые края становятся пятнами и крапинками. Это не очень неприятный эффект, и он чем-то напоминает карандашную линию. (Однако, в целом, у SVG-фильтров похоже есть проблемы с масштабированием — во многих случаях при близком увеличении они выглядят хорошо, но при отдалении подвергаются плохому алгоритму изменения размера.)

Вот как выглядит эффект при рисовании гор (эффект карандаша слева):


Не так уж ужасно, но линия имеет резкие артефакты, которые выглядят немного странно. А когда эффект применяется к тонким, расположенным близко линиям, они в результате накладываются друг на друга и сливаются:


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

Такое решение добавляет шероховатости контурам карандашного штриха, но не меняет однородного цвета штриха. Внутри реальных карандашных штрихов тоже присутствует текстура, потому что графит оставляет пятна на бумаге неравномерно.

Чтобы добавить текстуру внутрь штриха, я использую SVG-фильтр, а затем комбинирую шум со штрихом:


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


Довольно неплохо, особенно на толстых линиях. Вот как это выглядит при рисовании гор:


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


Но так как цвета линии уже и так довольно тёмные, это в значительной степени разрушает эффект «карандашности». Поэтому если я буду это использовать, нужно подобрать параметры для создания приятного баланса.

Очевидно, оба решения можно скомбинировать. При увеличении результат выглядит довольно неплохо:


От карандашной линии мы ждём и шероховатого края, и внутренней текстуры. При стандартном масштабе всё не так хорошо:


из-за появившихся в этом масштабе резких артефактов.

Хорошим трюком для улучшения этой текстуры будет добавление на фон текстуры бумаги:


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

Вот пример использования этого фильтра на карте (с текстурой бумаги):


В целом всё совсем неплохо, однако при подробном изучении кажется, что повсюду просто добавили шума. При увеличении 200% артефакты становятся ещё более очевидными:


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

В общем случае для реализации такого решения с помощью SVG-фильтров нужно использовать вместе feTurbulence и feDisplacementMap, чтобы создать искажённую версию линии. Однако чтобы сделать это несколько раз и в конце скомбинировать все линии, потребуется набор примитивов feBlend. Если мы, допустим, смешиваем (blend) три копии, то нам нужно соответствующим образом уменьшить непрозрачность линий. (Я не совсем уверен, как конкретно вычислять соответствующую непрозрачность, но думаю, что это может быть кубический корень из яркости (luminosity) линии.)

При этом создаётся такой эффект (три линии с увеличением):


У такого подхода есть пара недостатков. Фильтр имеет фиксированное отклонение, поэтому сильнее влияет на узкие линии, и в некоторых точках видно, что однопиксельная линия полностью разделена. Во-вторых, это довольно сложный фильтр, создающий три отдельных отклонения и объединяющий их; это может быть очень медленно на таких сложных изображениях, как карты Dragons Abound.

Вот как это выглядит при стандартном масштабе:


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

Этот подход можно скомбинировать с описанным выше фильтром внутренней текстуры и добавить текстуру внутрь карандашных линий, а также текстуру бумаги:


Вот как это выглядит после применения к горам:


Этот фильтр имеет тенденцию распределять линии сильнее, чем другой фильтр, по сути, делая их толще. Иногда он генерирует эффект наброска с несколькими линиями, который не так уж плох.

Вот пример использования этого фильтра на карте:


Он лучше сохраняет исходную темноту по сравнению с первым фильтром, и хотя на мой взгляд он не совсем точно похож на карандаш, эффект вполне приятный. При увеличении 200%:


При увеличении этот фильтр не приобретает резких артефактов первого фильтра. Влияние пересекающихся линий на тонкие линии (например, как на изображении леса) начинает выглядеть довольно искусственно, но более широкие линии (горы и реки) по-прежнему выглядят хорошо. Однако в пределах чёрных линий, например, рек, внутренний шум почти полностью теряется.

Я выложил оба этих фильтра на Codepen, чтобы вы могли попробовать их самостоятельно. Первый фильтр находится здесь, второй — здесь. Рекомендую поэкспериментировать с ними и попробовать их улучшить, и если у вас получится что-то лучшее, чем у меня, то сообщите мне об этом! Мне бы хотелось иметь очень хороший карандашный фильтр!

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (9):

  1. rPman
    /#21322894

    как у результата с производительностью, мобильными платформами?

  2. DmitryOlkhovoi
    /#21323376

    Напомнило такую либу https://roughjs.com/

  3. Tiendil
    /#21323822

    А сама игра Dragons Abound где-нибудь доступна? Не могу нагуглить.

  4. sidristij
    /#21325174

    Здорово, не знал, что SVG настолько крут

  5. ivan386
    /#21326762

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

    image


    Отрисовать конечно каждую точку придётся в ручную но не обязательно на каждую использовать новый элемент.


    Делаем так:
    1 создать несколько элементов path
    2 назначить им разные оттенки серого
    3 отрисовать точки в соответствующих элементах


    Мне кажется многие забывают о том что в одном элементе path можно нарисовать больше одной фигуры.

    • ferocactus
      /#21345672

      Но при этом, фильтрами задача решается более естественно, ведь так?

  6. DrOpsik
    /#21326792

    надо попробовать, автору спасибо

  7. Azgar
    /#21326794 / +1

    Dragons Abound это не игра, а название блога и проекта (генератора карт) Скотта Тёрнера. Он многократно писал, что планирует выложить генератор в свободный доступ, но не сейчас.

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

    • Tiendil
      /#21328108

      Расскажите, пожалуйста, про собщество подробнее.