Эта история началась одним морозным весенним вечером, когда в голову пришел вопрос: а есть ли способ определять степень заливки произвольной геометрической фигуры краской (то есть, на сколько процентов она в данный момент закрашена)? Да так, чтобы это не просто не тормозило, а летало на 60 fps на самых слабых мобильных девайсах.
Для тех, кто не сразу понял, о чем речь, поясню: к проблеме возможен как растровый подход, так и… не растровый.
В первом случае все просто, тема flood fill и сопутствующих алгоритмов успешно изучена и реализована на ЯП на любой вкус. Есть массив пикселей, подлежащих заливке, есть их границы. Считаем количество залитых точек, дальше пропорция к общему количеству, и вуаля — имеем заветный процент на выходе. Но — при большом количестве пикселей (а ppi на современных устройствах сами знаете какое), плюс — если таких фигур много, мы упираемся в кучу вычислений в каждом кадре, которые приятно греют девайс, но не душу.
Да и вообще, работать с растром представлялось занятием неспортивным. Взор был обращён в сторону всемогущих полигонов. Несколько волнующих часов расслабленно-упорного кодинга доказали гипотезу: можно воспользоваться такой штукой, как «вершинный цвет» — vertex color.
Думаю, стоит упомянуть, для чего мне понадобился пресловутый процент закраски, с которого началась статья. Основной идеей приложения-раскраски было следующее: конечная картинка состоит из набора многоугольников. Приложение будет последовательно и автоматически подсовывать пользователю элемент за элементом. Соответственно, пока не раскрасишь до конца один кусок, к следующему не перейдёшь. Подобное решение мне казалось весьма элегантным, прельстивым и в свете глобального засилья «пиксельных» раскрасок в сторах — ещё и свежим.
Первые шаги
Само собой, для того, чтобы сделать полноценную раскраску, необходимо было наколхозить накреативить еще много интригующих решений. Во-первых, я хотел, чтобы при всей полигональной природе приложения, раскрашивание воспринималось как самое что ни на есть растровое, то есть краска должна была растекаться под пальцем, и иметь более-менее реалистичный вид. Изначальное требование по максимальной производительности при этом никуда не исчезало и продолжало висеть грозным кучевым облаком над всем техпроцессом.
Первым делом предстояло сделать человеческую тесселяцию (разбиение большого многоугольника, состоящего из набора треугольников, на стохастическую кучу маленьких треугольников). Ведь если мы заведем массив вершин, и будем туда записывать vertex color по мере закрашивания, то сможем обычным проходом по массиву определять, закрашена ли фигура полностью, и какие ещё куски остались незакрашенными – аналогично пиксельному алгоритму, но с куда большей свободой.
Дальше началось увлекательное путешествие в мир шейдеров. Как вы понимаете, целиком все находки и секреты я открыть не могу, но скажу, что путем взаимодействия с картой шума и олдскульного испускания лучей Unity из пальцев, эффект кисти был достигнут, и даже с некоторым растеканием краски по близлежащим от пальца треугольникам. Использование vertex color предоставило возможность обойтись одним материалом Unity на абсолютно все составные части фигуры и поэтому draw calls в готовой программе не превышает 5-7 (в зависимости от наличия меню и частиц).
Обводка сделана обычным Unity Line Renderer, который предательски глючит на некоторых фигурах, съезжая и демонстрируя изъяны на стыках. Победить это не удалось, поэтому приоритетная задача — переписать компонент с нуля. След за пальцем — это также стандартный Trail Renderer, но в его шейдере используется z-проверка, чтобы элементы следа не накладывались друг на друга, создавая некрасивые артефакты. «Шахматная» текстура фона помогает, в том числе, оценить размер закрашиваемого элемента: чем он больше, тем меньше будет размер клеточек.
Функционал, которого не ждали
В ходе тестирования выяснилось, что часто где-то в углах фигуры оставались незаполненные вершины, что было трудно определить визуально. Несмотря на то, что триггер переключения на следующий элемент срабатывал при степени заливки в 97%, ситуации «а что делать дальше?» – при степени заполненности от 90% до 97% — возникали достаточно часто и смущали пользователей (которым в основном было не более 12 лет). Ставить триггер менее 97% не хотелось, потому что тогда возникал эффект «я еще не докрасил, а оно уже перескочило».
Так я неохотно познакомился с мадам Кластеризацией. Представьте: многоугольник, куча точек внутри, есть какие-то «особенные», иногда отдельно, иногда – группами. Нужно найти и обозначить самую большую «группу». Обычная такая математическая задача. Ни один из найденных мной традиционных алгоритмов не подошел по разными причинам, пришлось делать свой. Хак на хаке, но заработало – и недокрашенные области стали выделяться красивым динамическим кругом. В целях оптимизации, этот алгоритм срабатывает раз в 3 секунды, и только после того, как пользователь озадаченно оторвет палец от экрана в стиле «а что делать дальше». Выглядит вполне органично.
После такого мозгового штурма, сделать по требованиям тестеров вариативную «очередь раскраски» — а именно, дать пользователю возможность выбрать, в какой последовательности он хочет раскрашивать элементы – было делом одного вечера. Всего-то нужно определить геометрические центры каждого меша и выстроить их, как нам надо: слева направо, сверху вниз и т. д. Для большей наглядности, были реализованы частицы на фоне, которые показывают направление очереди.
Здесь показана дефолтная очередь (так, как задумал художник). Если включить режим "очередь по направлению" нажатием на одну из кнопок внизу, очередь раскрашивания изменится, и частицы поедут в указанную сторону.
UX & UI
Мне вообще импонирует идея контролируемого автоматизма в приложениях, и поэтому каждый элемент центруется и масштабируется так, чтобы его можно было закрасить пальцем без необходимости в прокрутке экрана. Минусом такого подхода стало то, что не всегда понятно, что за часть фигуры сейчас на экране. Как выяснилось, пользователям даже нравится такой небольшой челлендж, так как он тренирует краткосрочную память и соотнесение информации – нужно держать в голове общую картину. Ну а выйти на «обзор фигуры с птичьего полета» можно двумя способами – жестом-щипком (pinch) или нажатием на кнопку zoom.
Следуя заветам Apple Interface Guidelines, было принято решение сократить количество кнопок на экране до минимума. Помимо кнопки zoom in/out и очевидной кнопки выхода в меню, еще есть вызов палитры – красить можно как цветом «по умолчанию», установленным художником, так и по собственному выбору.
Кроме того, в режиме «из птичьих глаз» можно поменять градиент фона (рандомно генерируется каждое нажатие) или войти в режим «перекрашивания», который позволяет исправить уже закрашенный элемент. Да, пришлось запрятать этот функционал, но это вполне оправданно — за все тестирование никто ни разу не спросил, как это сделать…
Про палитру
Сама по себе палитра переделывалась два раза. Сначал я просто располагал на экране какое-то количество квадратов с цветами, но пользователи просили больше цветов. Прокрутку в интерфейсе я делать не хотел, и так появилась схема «цвет-оттенок», то есть сначала юзер выбирает нажатием базовый цвет, а затем – один из его оттенков. Палитра убирается кнопкой или вальяжным свайпом вниз.
На сладкое
Ключевым нехватающим звеном всей картины был reward – некая визуально-психологическая награда, которую получает пользователь по завершению процесса раскраски. Идея была подсмотрена лежала на поверхности: фигура раскрашивалась автоматически и заново, в ускоренном режиме, причём именно так, как это делал пользователь – проще говоря, timelapse на 15-20 секунд. Записать последовательность, в которой юзер касался вершин фигур, и затем ее проиграть, оказалось делом нехитрым. Намного сложнее было настроить всё так, чтобы это выглядело выигрышно с визуальной точки зрения.
Разумеется, timelapse при проигрывании записывается в видеофайл, и после визуальной феерии пользователю предлагается сохранить/поделиться свежесозданным шедевром.
Автор: FullOn