![image](https://habrastorage.org/webt/1n/wt/6k/1nwt6kpf53h7tdyd_ez0-j3f5ds.jpeg)
Эта история началась одним морозным весенним вечером, когда в голову пришел вопрос: а есть ли способ определять степень заливки произвольной геометрической фигуры краской (то есть, на сколько процентов она в данный момент закрашена)? Да так, чтобы это не просто не тормозило, а летало на 60 fps на самых слабых мобильных девайсах.
Для тех, кто не сразу понял, о чем речь, поясню: к проблеме возможен как растровый подход, так и… не растровый.
В первом случае все просто, тема flood fill и сопутствующих алгоритмов успешно изучена и реализована на ЯП на любой вкус. Есть массив пикселей, подлежащих заливке, есть их границы. Считаем количество залитых точек, делим на общее количество, и вуаля — имеем заветный процент на выходе. Но — при большом количестве пикселей (а ppi на современных устройствах сами знаете какое), плюс — если таких фигур много, мы упираемся в кучу вычислений в каждом кадре, которые приятно греют девайс, но не душу.
Да и вообще, работать с растром представлялось занятием неспортивным. Взор был обращён в сторону всемогущих полигонов. Несколько волнующих часов расслабленно-упорного кодинга доказали гипотезу: можно воспользоваться такой штукой, как «вершинный цвет» — vertex color.
Немного про vertex color
Нативно дополнительный канал информации, доступный в структуре данных треугольника — тот самый mesh.colors. Теоретически, его можно использовать для любых целей, смотря что прописать в шейдере, но в данном случае заветный байт будет хранить именно текущее значение заполненности цветом для каждой вершины. Его же шейдер использует при отрисовке, и тогда с одним материалом Unity можно создавать неограниченное количество разноцветных мешей с одним материалом на всех. Самое интересное — значения vertex color аппаратно интерполируются между собой, что позволяет делать лёгкие градиенты.
Думаю, стоит упомянуть, для чего понадобился пресловутый процент закраски, с которого началась статья. Основной идеей приложения-раскраски было следующее: конечная картинка состоит из набора многоугольников. Приложение будет последовательно и автоматически подсовывать пользователю элемент за элементом. Соответственно, пока не раскрасишь до конца один кусок, к следующему не перейдёшь. Подобное решение мне казалось весьма элегантным, прельстивым и в свете глобального засилья «пиксельных» раскрасок в сторах — ещё и свежим.
Первые шаги
Само собой, для того, чтобы сделать полноценную раскраску, необходимо было
Первым делом предстояло сделать человеческую тесселяцию (разбиение большого многоугольника, состоящего из набора треугольников, на стохастическую кучу маленьких треугольников). Ведь если мы заведем массив вершин, и будем туда записывать vertex color по мере закрашивания, то сможем обычным проходом по массиву определять, закрашена ли фигура полностью, и какие ещё куски остались незакрашенными – аналогично пиксельному алгоритму, но с куда большей свободой.
![image](https://habrastorage.org/webt/hd/ki/0c/hdki0cypjlbrmfhokpfho7y0qv4.jpeg)
Дальше началось увлекательное путешествие в мир шейдеров. Как вы понимаете, целиком все находки и секреты я открыть не могу, но скажу, что путем взаимодействия с картой шума и олдскульного испускания лучей Unity из пальцев, эффект кисти был достигнут, и даже с некоторым растеканием краски по близлежащим от пальца треугольникам. Использование vertex color предоставило возможность обойтись одним материалом Unity на абсолютно все составные части фигуры и поэтому draw calls в готовой программе не превышает 5-7 (в зависимости от наличия меню и частиц).
Обводка сделана обычным Unity Line Renderer, который предательски глючит на некоторых фигурах, съезжая и демонстрируя изъяны на стыках. Победить это не удалось, поэтому приоритетная задача — переписать компонент с нуля. След за пальцем — это также стандартный Trail Renderer, но в его шейдере используется z-проверка, чтобы элементы следа не накладывались друг на друга, создавая некрасивые артефакты. «Шахматная» текстура фона помогает, в том числе, оценить размер закрашиваемого элемента: чем он больше, тем меньше будет размер клеточек.
![image](https://habrastorage.org/webt/tq/oa/-3/tqoa-3wwdoc-fg23wqk_mcgnpqc.gif)
Функционал, которого не ждали
В ходе тестирования выяснилось, что часто где-то в углах фигуры оставались незаполненные вершины, что было трудно определить визуально. Несмотря на то, что триггер переключения на следующий элемент срабатывал при степени заливки в 97%, ситуации «а что делать дальше?» – при степени заполненности от 90% до 97% — возникали достаточно часто и смущали пользователей (которым в основном было не более 12 лет). Ставить триггер менее 97% не хотелось, потому что тогда возникал эффект «я еще не докрасил, а оно уже перескочило».
Так я неохотно познакомился с мадам Кластеризацией. Представьте: многоугольник, куча точек внутри, есть какие-то «особенные», иногда отдельно, иногда – группами. Нужно найти и обозначить самую большую «группу». Обычная такая математическая задача. Ни один из найденных мной традиционных алгоритмов не подошел по разными причинам, пришлось делать свой. Хак на хаке, но заработало – и недокрашенные области стали выделяться красивым динамическим кругом. В целях оптимизации, этот алгоритм срабатывает раз в 3 секунды, и только после того, как пользователь озадаченно оторвет палец от экрана в стиле «а что делать дальше». Выглядит вполне органично.
![image](https://habrastorage.org/webt/h_/qc/-d/h_qc-dlrdx_2aqol8nbqioqivzq.gif)
После такого мозгового штурма, сделать по требованиям тестеров вариативную «очередь раскраски» — а именно, дать пользователю возможность выбрать, в какой последовательности он хочет раскрашивать элементы – было делом одного вечера. Всего-то нужно определить геометрические центры каждого меша и выстроить их, как нам надо: слева направо, сверху вниз и т. д. Для большей наглядности, были реализованы частицы на фоне, которые показывают направление очереди.
Иллюстрация очереди![image](https://habrastorage.org/webt/vg/kt/ij/vgktijp2haaqb6ij0gi6jemuqjo.gif)
Здесь показана дефолтная очередь (так, как задумал художник). Если включить режим "очередь по направлению" нажатием на одну из кнопок внизу, очередь раскрашивания изменится, и частицы поедут в указанную сторону.
![image](https://habrastorage.org/webt/vg/kt/ij/vgktijp2haaqb6ij0gi6jemuqjo.gif)
Здесь показана дефолтная очередь (так, как задумал художник). Если включить режим "очередь по направлению" нажатием на одну из кнопок внизу, очередь раскрашивания изменится, и частицы поедут в указанную сторону.
UX & UI
Мне вообще импонирует идея контролируемого автоматизма в приложениях, и поэтому каждый элемент центруется и масштабируется так, чтобы его можно было закрасить пальцем без необходимости в прокрутке экрана. Минусом такого подхода стало то, что не всегда понятно, что за часть фигуры сейчас на экране. Как выяснилось, пользователям даже нравится такой небольшой челлендж, так как он тренирует краткосрочную память и соотнесение информации – нужно держать в голове общую картину. Ну а выйти на «обзор фигуры с птичьего полета» можно двумя способами – жестом-щипком (pinch) или нажатием на кнопку zoom.
![image](https://habrastorage.org/webt/9_/ej/ic/9_ejic4dyydfoljrxzxdqjl9xos.gif)
Следуя заветам Apple Interface Guidelines, было принято решение сократить количество кнопок на экране до минимума. Помимо кнопки zoom in/out и очевидной кнопки выхода в меню, еще есть вызов палитры – красить можно как цветом «по умолчанию», установленным художником, так и по собственному выбору.
Кроме того, в режиме «из птичьих глаз» можно поменять градиент фона (рандомно генерируется каждое нажатие) или войти в режим «перекрашивания», который позволяет исправить уже закрашенный элемент. Да, пришлось запрятать этот функционал, но это вполне оправдано — за все тестирование никто ни разу не спросил, как это сделать.
Про палитру
Сама по себе палитра переделывалась два раза. Сначал я просто располагал на экране какое-то количество квадратов с цветами, но пользователи просили больше цветов. Прокрутку в интерфейсе я делать не хотел, и так появилась схема «цвет-оттенок», то есть сначала юзер выбирает нажатием базовый цвет, а затем – один из его оттенков. Палитра убирается кнопкой или вальяжным свайпом вниз. Причем когда она появляется на экране, рабочее пространство художника сокращается на 1/3, что вызывает необходимость в «перемасштабировании» текущей фигуры под изменившийся размер viewport'а.
![image](https://habrastorage.org/webt/-b/7l/mc/-b7lmcif2fozsmqquhrifrisyqo.gif)
На сладкое
Ключевым нехватающим звеном всей картины был reward – некая визуально-психологическая награда, которую получает пользователь по завершению процесса раскраски. Идея
![image](https://habrastorage.org/webt/1m/q7/dg/1mq7dg-rgkjl0r5hamxlci7vq94.gif)
Разумеется, timelapse при проигрывании записывается в видеофайл, и после визуальной феерии пользователю предлагается сохранить/поделиться свежесозданным шедевром. К счастью, как раз весной в Asset Store появился плагин, который позволяет полноценно и мультиплатформенно захватывать видео с экрана (после некоторой настройки), потому что написание такого инструмента с нуля далеко выходит за рамки моей программерской квалификации
Вместо заключения
На этом тысяча слов, отведенных мной на первый опус, заканчивается. В следующих частях планируется поведать про героические сражения с Unity UI при разработке второй части приложения — меню выбора картинок, а также посчитать набитые шишки в нелегком деле ASO.