Под капотом Graveyard Keeper: Как реализованы графические эффекты

Всем привет! Целых 4 года я не писал на Хабр. Последняя моя серия постов была о различных инструментах и приемах, которые мы применяли на нашей прошлой игре (разрабатывая ее на Unity). С тех пор игру ту мы благополучно выпустили, а также выпустили и новую. Так что теперь можно немного выдохнуть и написать несколько новых статей, которые могут быть кому-то полезны.

wbqcnc2xkeiepxqphi3iphwnoo0.gif


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

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

Для начала, кратко перечислю из чего собирается картинка в нашей игре:

  1. Изменяемый ambient light — банальное изменение освещенности в зависимости от времени дня.
  2. LUT-цветокоррекция — отвечает за изменение тона картинки в зависимости от времени суток (или типа зоны).
  3. Динамические источники света — факела, печки, лампы.
  4. Карты нормалей — отвечают за придание объема объектами, особенно при движении источников света.
  5. Математика 3D-распределения света — отвечает за то, чтобы источник света по центру экрана корректно освещал объект, который находится выше, но не освещал объект, который находится ниже (т.е. повернут к камере неосвещенной стороной).
  6. Тени — сделаны спрайтами, вращаются и реагируют на положение источников света.
  7. Имитация высоты объектов — для корректного отображения тумана.
  8. Прочие украшалки: дождь, ветер, анимации (в т.ч. шейдерныая анимация листвы и травы) и т.п.


Теперь — подробнее.

Изменяемый ambient light


Тут, в принципе, ничего особенного. Ночью — темнее, днем — светлее. Цвет света задан градиентом по времени суток. К ночи источник света не просто становится темнее, а приобретает синий оттенок.

Выглядит это вот так:

kzucz6juapnzkaxln72k6h6o5pk.png


LUT-цветокоррекция


LUT (Look-up table) — таблицы замены цветов. Грубо говоря, это трехмерный массив RGB где в каждом узле находится значение цвета, на которое следует заменить соответствующий. То есть если по координатам (1, 1, 1) находится красная точка, это значит что весь белый цвет на картинке будет заменен на красный. Если же по координатам (1, 1, 1) находится белый цвет (R=1, G=1, B=1), то изменения не происходит. Соответственно, LUT без изменений имеет по каждым координатам цвет, соответствующий этим самым координатам. Т.е. в точке (0.4, 0.5, 0.8) находится цвет (R=0.4, G=0.5, B=0.8).

Ну и стоит отметить, что для удобства представляют 3D текстуру в качестве двумерной. Например, вот так выглядит «дефолтный» LUT (никак не изменяющий цветопередачу):

evdpa_vrtx8qpwetg0a1lsbnvv4.png

Реализуется элементарно, работает быстро и удобно.

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

В нашем случае художник немного упоролся и создал аж 10 разных LUT’ов для разного времени суток (ночь, сумерки, вечер и т.п.). Вот так выглядит их настройка:

rarwlem4o5aoufvhb8vmficb-za.png


В результате в зависимости от времени суток одна и та же локация выглядит по-разному:

k2fz1wmhn9yw-rcgvticox4_bh4.png

Здесь еще изменяется прозрачность спрайтов света из окон в зависимости от времени суток.

Динамические источники света и карты нормалей


Источники света используются абсолютно обыкновенные, от Unity. Кроме того, каждому спрайту нарисованы карты нормалей, что позволяет получить ощущение объема.

hydvvfxlld52gux9o0mnz2gavbe.gif


Рисуются такие нормали довольно просто. Художник грубо кисточкой прорисовывает свет с 4х сторон:

0omvjvscrffh1o8exichnqylnia.png


А потом это скриптом собирается уже в карту нормалей:

_4mdoakkf1puyp5lyvwjn6zmm-g.png


Если ищете шейдер (да и софт), который это делает, можете посмотреть в сторону Sprite Lamp.

3D-имитация света


Тут немного сложнее. Нельзя просто так взять и осветить спрайты. Нам нужно учитывать, стоит ли спрайт «за» источником света или «перед».

Обратите внимание на эту картинку:

k1tr51sgx_4fmnzjkhtkv176ye0.png


Оба дерева находятся на одинаковом расстоянии от источника света, однако дальнее дерево освещено, а ближнее — нет (т.к. к камере повернута его неосвещенная часть).

Эту проблему я решил довольно просто. Шейдер высчитывает расстояние по вертикальной оси y между источником света и спрайтом. И если оно положительное (источник света перед спрайтом), то мы освещаем спрайт как обычно, а вот если отрицательное (спрайт перекрывает источник света), но интенсивность освещения очень затухает от расстояния с очень большим коэффициентом. Сделан именно коэффициент, а не просто «не освещать», чтобы когда источник света двигается и оказывается вдруг за спрайтом, то спрайт не моментально становился черным, а постепенно. Но все-таки довольно быстро.

vyd7cwsfbrkbe4majwq81fynkck.gif


Тени


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

Всего каждый объект может иметь максимум 4 тени. Одна — от Солнца, и три — от динамических источников света. На картинке ниже показан принцип:

8r8_u21cgbiicmqpdvpjywl5rb4.png

Задачу «найти ближайшие 3 источника света и рассчитать расстояние/угол теней до них» решает скрипт, который крутится в Update’е. Да, получается не очень быстро, т.к. приходится выполнять много математики. Если бы писал сейчас, то воспользовался бы новомодными системами параллельных job’ов в Unity. Но тогда этого еще не было, так что просто максимально оптимизировал обычные скрипты.

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

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

Второй недостаток в том, что иногда трудно сделать тень для объекта, у которого пятно контакта с землей очень вытянуто. Например, посмотрите тень от забора:

moi2drffv8hi9ynls99ve-gqjnc.gif


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

7j0tfz3lovxbfy2d2zu1afhtl_e.gif


Тут, правда, стоит отметить, что спрайт еще очень сильно деформирован по вертикали (оригинал теневого спрайта выглядит почти что как круг). Именно по-этому его поворот выглядит не столько как поворот, сколько как искажение.

Туман и имитация высоты


Еще в игре есть туман. Выглядит он вот так (сверху — нормальный вариант, снизу — экстремальный 100% туман, для демонстрации эффекта).

4i2rc__pzbgrqecd4oq83piemsu.png

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

43gdgrxqrdisek50lynbramf4bc.png

Ветер


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

Мы выбрали вариант искажений с помощью шейдера. Выглядит это так:

cbuhnnzyx14un9chz96mma0qmyq.gif


Если применить этот шейдер к текстуре с шашечками, становится понятно, что происходит:

uoagn5vbuxpeee_71sn-frkklyk.gif


Стоит также отметить, что анимируем мы не всю крону, а только отдельные листики:

2siumyhll6pl84c_hn6ursn-cpe.png


Еще мы шатаем на ветру пшеницу, но тут все просто — вертексный шейдер деформирует x-координаты, при чем учитывает y-составляющую. Чем выше точка, тем сильнее шатать. Это сделано, чтобы шаталась только верхушка, а корень — нет. Плюс — фаза шатания меняется от x/y координат, чтобы разные спрайты на экране качались вразнобой.

gyksz6p4hodfgv4e-shli_gelyg.gif


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

szooq97kmlmd9wmmlqnjohyyqdm.gif


Наверное, на этом пока все. Я намерено не затрагивал вопрос построения сцены и ее геометрии, т.к. это материал для отдельной статьи. В остальном — рассказал об основных решениях, которые применяли в разработке.

PS: Если кому-то интересны какие-то технические аспекты, пишите в комментах. Возможно, расскажу в отдельной статье. Если, конечно, нужно.

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

© Habrahabr.ru