что такое отрисовка изображения
Отрисовка векторной графики — триангуляция, растеризация, сглаживание и новые варианты развития событий
В далёком 2013м году вышла игра Tiny Thief, которая наделала много шуму в среде мобильной Flash (AIR) разработки из-за отказа от растровой графики в билдах, включая атласы анимации и прочего — всё что было в сборке хранилось в векторном формате прямиком из Flash редактора.
Это позволило использовать огромное количество уникального контента и сохранить размер установочного файла до
70 мегабайт (*.apk-файл из Google Play). Совсем недавно снова возник интерес к теме отрисовки векторной графики на мобильных устройствах (и вообще к теме отрисовки вектора с аппаратной поддержкой), и меня удивило отсутствие информации «начального» уровня по этой теме. Это обзорно-справочная статья по возможным способам отрисовки вектора и уже существующим решениям, а так же о том, как подобные вещи можно сделать самостоятельно.
Основной способ
К отрисовке вектора чаще всего подходят так — берут все фигуры, кривые и прочие вещи, проходятся по ним алгоритмом триангуляции (разделение замкнутых контуров на треугольники), считая разного рода обводки и линии такими же закрашенными объектами, и получают какое-то приближённое представление описанной математической формулой фигуры.
То есть векторный круг, отрисованный таким образом, будет на самом деле многоугольником. Критерием качества в данном случае будет являться количество и размер треугольников, полученных в итоге:
Причина таких обходных путей простая — графическая карта умеет эффективно работать только с вершинами, треугольниками и пикселями (про GPGPU немного другая история, но в данном контексте стоит только упоминания вскользь). Если же рисовать математически верные представления моделей с помощью CPU, то это будет занимать намного больше времени. Поэтому просто триангулируем и отправляем графической карте на отрисовку в сыром виде, как есть.
Подводные камни
Подобная сырая отрисовка треугольников приводит к появлению эффекта алиасинга — ступенчатости краёв изображения (это хорошо заметно на скриншоте выше). Эта проблема присуща любой непрозрачной геометрии, представленной в виде треугольников.
Если же посмотреть на скриншот Tiny Thief, то сразу видно, что игра лишена этого недостатка — края объектов красиво сглажены.
Field study
Все описанные ниже вещи я проверял с помощью Adreno Profiler, NVIDIA PerfHUD ES и Unity (проверка работоспособности предложенных вариантов решения).
Вот что покажет Adreno Profiler если включить режим цветной сетки:
То есть отрисовка тем самым методом триангуляции. Вершины при этом красятся напрямую, без текстур (параметр color у вершин).
Вот что находится в альфа буфере (очевидно у Adreno GPU есть такое понятие как «альфа-буфер»):
По краям объектов тонкая однопиксельная полоска. Интересно, что на гранях между соседними объектами (белый фон-цветная буква) альфа канал «белый», то есть весь логотип рисуется одним проходом, а сглаживание внутри подобных объектов реализовано немного другим способом.
Суть сглаживания более или менее ясна — при триангуляции добавляем тонкий набор треугольников вдоль края объекта.
Сколько бы я ни пробовал приближать картинку — не смог разглядеть эти хитрые тонкие треугольники. Но, к счастью, Adreno Profiler, в отличие от PerfHUD, позволяет экспортировать геометрию в текстовом виде.
Сборка по кускам
Написав простой парсер, получилось восстановить исходный меш в Unity. Но меня ждала странная картина:
Рамочки без сглаживания. В режиме просмотра сетки тоже не видно заполняющих треугольников:
Долго не мог понять в чём дело. Оказалось, что заполняющие треугольники вывернуты в другую сторону. Это становится видно, если посмотреть на логотип с другой стороны:
Также заметно, что между элементами логотипа присутствуют пустые линии, которые заполнены градиентами (градиент сделан с помощью закрашивания вершин в соответствующие цвета), и при этом нет сглаживания.
Если убрать backface culling в шейдере, то получим то, что хотели получить в итоге:
Но возникает интересная особенность — если приближать этот объект к камере, то сглаживание становится слишком заметным и выглядит как размытие:
То есть триангуляция происходит каждый раз по месту, в зависимости от размеров экрана, и не предполагает изменения размеров этого объекта. Размер треугольников рассчитывается таким образом, чтобы общая ширина составляла один экранный пиксель или меньше.
Практически все объекты на экране отрисовываются таким же образом. Исключение составляет задний фон, который рендерится один раз в текстуру.
Статистика
Достаточно интересно посмотреть сводную информацию об отрисовке персонажей, в среднем каждый герой (стражник, повар) в триангулированном виде составляет около 3-4 тысяч треугольников. Это примерно как хорошего качества лоу-поли 3Д модель. Сетка настолько плотная, что кажется что объект отрисован текстурой:
Логотип занимает почти 9 тысяч треугольников. Общее количество draw calls в среднем около сотни (было бы намного больше, если бы задний фон не рисовался в виде текстуры), но ФПС стабильно максимальный даже на стареньком ZTE V811 (Билайн смарт 2).
В общем пока берём в копилку первый (и основной) способ отрисовки векторной графики:
триангулируем нашу векторную картинку, делаем вдоль стыков тонкую границу с промежуточными цветами, а по краям делаем тонкую полупрозрачную полоску.
С ног на голову
Если выставить ограничение на количество цветов векторного изображения, то можно пойти по совершенно другому пути. Допустим, что у нас есть простая векторная одноцветная иконка:
Её можно «сжать» почти без потери качества с помощью Signed Distance Field. Суть заключается в том, что мы храним не саму текстуру, а информацию о расстоянии пикселей до границы иконки. Значение на границе обычно считается равным 0.5. Всё что больше считается «внутри» иконки. Всё что меньше — «снаружи». По факту неважно в какую сторону перевешивается граница — иногда можно сделать меньше 0.5 внутри и больше 0.5 снаружи. Для наглядности (чёрные иконки на белом фоне) я покажу именно такой вариант.
Размазанный таким образом игральный кубик выглядит вот так:
Отличие от обычного размытия заключается в том, что находя минимальное расстояние между текущим пикселем и границей, у нас в любом случае получится расчёт расстояния по нормали (минимальное расстояние от точки до прямой всегда определяется перпендикуляром). То есть градиенты на текстуре описывают направление нормали к ближайшей границе.
В интернете и, в частности, на хабре достаточно много статей про SDF, я приведу их в конце статьи.
На картинке хорошо видно различие в качестве между обычной текстурой и двумя вариантами SDF. При увеличении обычной картинки становятся явно видны размытости. Увеличивая же SDF текстуру, мы в любом случае получим резкие границы. Причём даже уменьшив размер текстуры в два раза, наличие артефактов остаётся почти незаметным (про увеличение качества сырой SDF текстуры можно написать отдельную статью). Артефакты, в отличие от обычной текстуры, проявляются в виде сглаженной лесенки на скошенных краях иконки. Это обусловлено тем, что пиксели идут ровно по горизонтали и вертикали, и при уменьшении размера изображения также уменьшается точность аппроксимации косой прямой с помощью двух перпендикулярных (напомню что мы аппроксимируем вектор нормали к границе).
Шейдер для отрисовки будет самую малость сложнее, чем простое чтение текстуры. В экспериментах я попробовал много разных вариантов, в т.ч. и вариант из статьи [2], в общем виде это выглядит примерно так:
Стоит отметить, что в данном случае RBG канал текстуры выбрасывается и в расчётах не участвует (к этому мы ещё потом вернёмся). Настраивать _Smoothing можно либо вручную под текущий размер текстуры на экране (но тогда будет та же проблема при увеличении, что и была в случае с отрисовкой через меши), либо использовать cg функцию fwidth, которая примерно оценивает размер текущего фрагмента относительно экрана и «подстраивает» сглаживание под относительный размер иконки на экране.
Поскольку основным ограничением метода SDF является необходимость «бинарности» (одноцветность) исходного символа, наиболее часто его применяют при отрисовке текста — настраивая и видоизменяя варианты обработки одной и той же SDF текстуры, можно создавать обводку, тень, размытие и т.д. [1]. Менее популярным способом использования SDF является отрисовка одноцветных иконок (как в случае с изображением игральной кости), но по большей части это просто частный случай текстового символа.
Ещё одним минусом подобного подхода является потеря резких граней и углов:
Слева — оригинальная текстура. Справа — SDF уменьшенного размера
И ещё пример с текстом из статьи [3]:
Решение проблем по мере поступления
Есть примеры реализации подобного алгоритма, сохраняющего острые углы [4][5]:
Краткая суть алгоритма заключается в следующем:
«сырой» SDF скругляет углы из-за того, что чем дальше от него пиксели, тем сильнее он скругляется. Происходит это из-за того, что к углу нельзя провести перпендикуляр (производная функции в этой точке не существует) — много пикселей будут считать расстояние по радиусу окружности, центром которой как раз и является угол. Этого можно избежать, если отследить все углы символа, проверив разрывы плавно идущей кривой. А затем, используя таблицу истинности, определить, должен ли быть закрашен квадрант угла или нет. То есть углы обычно закрашиваются пересечёнными SDF картами, записанными в разные каналы, а конечное значение пикселя считается по медиане векторов из трёх каналов.
Разумеется я не могу вместить всю статью на 90 страниц в один абзац, поэтому советую посмотреть её полностью [5].
Были и более ранние попытки сделать что-то подобное с пересечением разных полей раскиданных по каналам, но некоторые варианты не предполагают наличие хитрых возможностей добавления тени, обводки или увеличения толщины символа, в отличие от описанного примера (из-за того что фактически там не остаётся поля расстояний как такового).
Меньше каналов — больше точности
В твиттере есть товарищ, который всеми правдами и неправдами делает что-то подобное, но с одним каналом:
Если посмотреть разные ссылки в его твиттере, то можно наткнуться на некоторые варианты реализации. Насколько я понял, подход отличается от стандартного SDF тем, что не используется фактическое расстояние до границ (чтобы избежать того самого округления вокруг угла-центра), а используется немного переинтерпретированная фигура, углы которой продолжаются дальше. Это позволяет избавиться и от скругления углов и от нескольких каналов, упрощая шейдер, и уменьшая общее количество информации, требуемое на представление подобных фигур.
Также у этого товарища есть шейдер, который в реальном времени считает distance field для кривой безье на GPU, но там требуются десктопные вычислительные мощности даже для одной кривой (которая задаётся параметрически и формула её лежит «прямо в шейдере»). Если покрутить настройки и код, то можно посмотреть само поле расстояний без закрашивания и модуляции:
Общая суть у этих методов заключается в анализе кривой, определяющей границы символов.
Возвращаемся к истокам
Также можно пойти по третьему пути — не хранить растровую информацию о каком-либо символе, а рисовать, так скажем, «из печки» — напрямую из векторного представления кривых. Проблема в том, что относительно сложно передать информацию о кривых на графическую карту без потери производительности. Есть несколько статей, описывающих подобные методы:
Если кратко, то суть в следующем:
Разделяем символ на ячейки, для каждой ячейки делаем карту пересечений кривых с лучами, пущенными под разными углами и пересекающими эту ячейку. Смотрим количество пересечений и расстояние, на которых возникли эти пересечения. Данные о кривых хранятся в виде скомканной текстуры, в которой заданы координаты кривых безье. Одна кривая безье — это 3 или 4 параметра в зависимости от степени этой кривой. Выше 4го параметра кривые обычно не берут. Шейдер занимается тем, что в зависимости от текущей отрисовываемой ячейки и параметров текстуры, присутствующих на этой ячейке, считывает необходимые пиксели из референсной текстуры и использует их для восстановления аналитического вида кривой на GPU.
Не всё так радужно.
Минусом подобных подходов является использование относительно большого количества операций чтения текстуры. Я как-то имел дело с реалтаймовым рендерингом теней с tap blur размытием на мобильных устройствах, и любые Dependent Texture Reads (DTS — я не нашёл общепринятого аналога на русском) существенно ухудшали производительность. Если очень грубо — DTS возникают, когда координаты чтения текстуры становятся известны только во fragment shader, то есть непосредственно при отрисовке пикселя. Обычно высокая скорость чтения текстуры во fragment shader обуславливается тем, что конкретная интерполированная текстурная координата пикселя становится известна сразу после работы vertex shader, то есть видеокарта заранее читает нужный пиксель текстуры и отдаёт значение пикселя «бесплатно». Алгоритмы, поведение и степень трудозатрат определяется в первую очередь железом, на котором выполняются эти шейдеры. В OpenGL ES 3.0+ вроде как по большей части решается проблема производительности DTS, но на данный момент около половины мобильных устройств работает на OpenGL ES 2.0, поэтому пока надеяться на хорошее железо не стоит.
(источник от 6 февраля 2017)
Стоит отметить, что запатентованный Microsoft подход позволяет, используя 4 канала, кодировать цвет пикселей, находящихся в ячейке. И именно отрисовкой цветных векторных изображений я заинтересовался изначально.
Как жить дальше
У описанных выше методов есть следующие недостатки:
Поэтому у меня возникло желание предложить немного другой способ отрисовки разноцветной векторной графики, основанный на всё том же принципе SDF.
Вторая жизнь для SDF
SDF стал синонимом одноцветной отрисовки символов текста. Но если представить векторную картинку в виде набора одноцветных слоёв, то с помощью той же SDF текстуры, можно отрисовать векторную картинку любой сложности и цветности. То есть мы просто разделяем начальную картинку на набор одноцветных слоёв.
Пример — ящик из популярного набора от Kenney, разрезанный на слои, выглядит так:
Это внешний вид SVG файла. Можно заметить, что слои не перекрывают друг друга, а «стыкуются» между собой. При просмотре подобных векторных изображений через Inkscape становятся явно видны артефакты, присущие подобной стыковке этих слоёв:
Присутствие артефактов зависит от способа создания векторной графики, но пока возьмём именно этот вариант.
К действиям
Для каждого слоя сделаем свою SDF текстуру и выставим слои друг поверх друга в том же порядке, в котором они идут в SVG файле.
Слева направо — SVG Importer с включённым antialiasing, «слоёный» SDF, увеличенная начальная текстура. SVG Importer не смог нормально распарсить SVG из Inkscape, но суть не в этом.
Если сильно приблизить оба объекта, то различия выглядят примерно так:
Основным недостатком этого метода по сравнению со всеми другими является существенный ovedraw. Чтобы нарисовать эту коробку требуется 4 полноразмерных слоя, помещённых друг на друга, плюс небольшой квад для пятого слоя (маленькая чёрточка). В худшем случае overdraw будет прямопропорционален количеству слоёв векторной картинки. Чем выше разрешение устройства, тем медленнее будет работать рендеринг.
Но в отличие от большинства пакетов по парсингу SVG файлов в меши, заранее подготовленные текстуры занимают существенно меньше места. Scaleform в этом плане пошли дальше — они генерили все меши на лету при загрузке сцены, не забивая архив приложения заранее созданными файлами. Для сравнения, начальный размер коробки составляет 4 Кб текста, то есть заранее собранный со сглаживанием меш векторной картинки занимает в 11 раз больше места, чем сырой текст, описывающий эту векторную фигуру.
Варианты — тысячи их
Я также наткнулся на другой способ конвертации цветного изображения в SDF вид. [6] Суть заключается в использовании bit planes (бит-карт) изображения для цветов. Бит-карты раскладывают яркости цвета по битам.
То есть берутся по порядку биты яркости и кладутся в отдельную бинарную текстуру. Всего на один канал изображения нужно 8 текстур. То есть 24 текстуры на цветное изображение без прозрачности.
Если пойти дальше и представить каждую такую двоичную текстуру как 8ми битную SDF текстуру, то получается что для полноценного представления начального изображения потребуется 24 восьмибитных текстуры (а не 24 однобитных, которые получаются сразу после разложения на бит-карты).
Процесс восстановления начального цветного изображения из обработанных с помощью SDF бит-карт выглядит следующим образом:
Хотя алгоритм этот достаточно хитрый, на деле качество оставляет желать лучшего:
Артефакты появляются из-за того, что проблема потери точности при хранении уменьшенной копии SDF текстуры ухудшается разрезанием каналов цветов на побитовую составляющую. На мой взгляд, этот метод не особо применим уже по этой причине. Но ещё одним минусом является необходимость хранить 24 восьмибитных SDF текстуры на одну исходную цветную картинку.
Итоги
Нового полноценного решения «из коробки» пока предложить не могу, но есть идеи и попытки сделать кодировку SDF по палитрам с маркировкой контуров, что, возможно, позволит избавиться от хранения большого количества разных текстур для разных каналов и уменьшить overdraw.
Статья уже получилась очень большой, и мне пришлось существенно обрезать контент. Из того, что не рассказал:
Отрисовки. Марафон — день №3
Здравствуйте! Сегодня третий день марафона — создание детской фотокниги. И мы начинаем учиться создавать элементы для детских фотокниг.
Сегодня мы научимся создавать элементы, которые в нашей условной квалификации мы определи, как «Нарисованные с помощью пера и других инструментов фотошоп».
Конечно, часто для скрап-наборов подобные элементы создаются авторами на основе собственных эскизов, сделанных на бумаге. От этого подобные скрап-наборы становятся по-настоящему авторскими и неповторимыми. Но, не все имеют талант к рисованию, а очень хочется нарисовать что-нибудь красивое. Для примера был взят львенок из мультфильма «Львенок и черепаха».
Как вы видите, рисунок у меня маленького разрешения, но это поправимо. Нажимаем горячие клавиши трансформации Ctrl+T и тянем рисунок за любой уголок, удерживая клавиши Shift+Alt (чтобы не потерять пропорции), до нужного нам размера. Растянем по-максимуму, чтобы качество нашей отрисовки было хорошее, а уменьшить мы всегда успеем. Затем Enter.
Чтобы нам не настраивать постоянно данные параметры, можно сохранить эту кисть.
Сохраненная кисть отразится в конце списка кистей.
Не забудьте убедиться, что вы рисуете на новом слое.
Желательно контур каждого фрагмента создавать на отдельном слое, сразу переименовывать все слои и сгруппировывать в отдельные папки. Так вам будет легче находить элементы для редактирования.
Затем нам необходимо выполнить обводку контура той кистью, которую мы создали. Кликаем по полю правой кнопной мыши.
Я Слой 1 залила инструментом «Заливка», предварительно выбрав в «Палитре цветов» белый цвет. Отключила глазик на слое с львенком на панели слоев и вот что у меня получилось.
Вот результат!
Урок подготовлен командой сайта «Фотокниги для души». Исполнитель — Светлана.
Дополнение к уроку.
У некоторых может возникнуть вопрос зачем столько времени тратить на отрисовку, если можно найти картинки в интернете, сделать из них клипарт и создавать спокойно развороты фотокниги. Теоретически это можно сделать, но на практике, в интернете не всегда можно найти картинку необходимого вам персонажа (и чего угодно), нужного качества и размера. Связано это в первую очередь с тем, что изображения делятся на растровые и векторные. И во-вторую, с тем, что авторы изображений просто не выкладывают в интернет оригиналы своих работ (работы в хорошем качестве).
Чем же отличаются векторные изображения от растровых?
Растровые изображения состоят из сетки цветовых точек, называемых пикселями. Каждый пиксель имеет своё расположение и цвет. Растровые изображения используются для представления изображений с плавным переходом цветов, таких, как картины или фотографии, так как могут наиболее точно показывать все оттенки цветов. Главный недостаток — при увеличении изображение кажется созданым из квадратов. Чем больше увеличение — тем больше квадраты. Поэтому имеет большое значение какие размеры и качество документа были заданы изначально.
Векторная графика состоит из линий или кривых, которые описываются математическими объектами, называемыми векторами. Например, колесо автомобиля описывается формулой эллипса. При изменении размеров рисунка качество его не меняется (Изображение можно растянуть хоть на баннер). Векторные форматы: AI, CDR, EPS. Самая популярная программа для создания векторных изображений — illustrator.
Размеры разворотов фотокниги нельзя сравнить, к примеру, с рекламным уличным баннером, поэтому отрисовки для фотокниг легко можно делать в программе фотошоп ))). Но, как следствие, из всего вышесказанного при работе со слоями могут возникнуть всякие артифакты, вылезать непонятные «штуковины» и т.д. и т.п. Поэтому, после завершения всех операций по отрисовке, вам необходимо слить все слои Ctrl+Alt+Shift+E. Таким образом, вы объедините все слои (с которыми работали) в один общий слой (самый верхний в слоях).
После того, того как вы объединили все слои, вы можете преобразовать его в смарт-объект. В палитре инструментов нажимаем на слой с объектом, нажимаем правую клавишу мыши — Преобразовать в смарт-объект. Смарт-объекты — это слои, содержащие данные изображения из растровых или векторных изображений, таких как файлы Photoshop или Illustrator. Смарт-объекты сохраняют первоначальное содержимое изображения со всеми исходными характеристиками, позволяя производить обратимое редактирование слоя. В этом случае вы можете работать с объектом и трансформировать его до первоначального размера (без потери качества).
Девушки, только прежде чем сливать все в один слой, не забывайте отключать фоновый слой и слой с оригинальным рисунком, чтобы ваша отрисовка оказалась на прозрачном фоне )))
А теперь попытаюсь показать, все что сказано выше на примерах.
Вот так выглядит отрисовка в первоначальном виде (ее размеры 3000*3000 пикс. 300 dpi)
Вот что происходит с объектом, если уменьшить и увеличить его несколько раз без преобразования в смарт-объект. Это происходит, когда вы выбираете инструмент трансформирование — масштабирование и увеличиваете объект, потом по-каким то причинам уменьшаете объект, потом снова увеличиваете. И при каждой операции объект теряет в качестве.
А вот результат после тех же «манипуляций», но с преобразованием в смарт-объект.
Но, это работает только в том случае, если вы увеличиваете объект не больше, чем до первоначальных размеров, т.к. март-объекты сохраняют первоначальное содержимое изображения со всеми исходными характеристиками, позволяя производить обратимое редактирование слоя.
Если же вам потребуется увеличить объект во много раз (например, для баннера), то преобразование в смарт-объект вам к сожалению не поможет. Я увеличила первоначальную картинку всего в три раза, и вот что получилось.Хоть и незначительно, но качество все же ухудшилось.
А если бы вы изначально работали в редакторах для создания векторных изображений, то смогли бы растянуть объект до любых размеров. Надеюсь теперь вам понятна разница между растровой и векторной графикой.
Участницы марафона и читатели сайта, не забывайте комментировать статьи. Нам интересно ваше мнение!